%% SPDX-License-Identifier: Apache-2.0
%% SPDX-FileCopyrightText: 2021 The Elixir Team
%% SPDX-FileCopyrightText: 2012 Plataformatec

% Holds the logic responsible for defining overridable functions and handling super.
-module(elixir_overridable).
-export([overridables_for/1, overridable_for/2,
         record_overridable/3, super/4,
         store_not_overridden/1, format_error/1]).
-include("elixir.hrl").
-define(overridden_pos, 4).

overridables_for(Module) ->
  {_, Bag} = elixir_module:data_tables(Module),
  try
    ets:lookup_element(Bag, overridables, 2)
  catch
    _:_ -> []
  end.

overridable_for(Module, Tuple) ->
  {Set, _} = elixir_module:data_tables(Module),

  case ets:lookup(Set, {overridable, Tuple}) of
    [Overridable] -> Overridable;
    [] -> not_overridable
  end.

record_overridable(Module, Tuple, Def) ->
  {Set, Bag} = elixir_module:data_tables(Module),

  case ets:insert_new(Set, {{overridable, Tuple}, 1, Def, false}) of
    true ->
      ets:insert(Bag, {overridables, Tuple});
    false ->
      [{_, Count, PreviousDef, _}] = ets:lookup(Set, {overridable, Tuple}),
      {{_, Kind, Meta, File, _, _}, _} = Def,
      {{_, PreviousKind, _, _, _, _}, _} = PreviousDef,

      case is_valid_kind(Kind, PreviousKind) of
        true ->
          ets:insert(Set, {{overridable, Tuple}, Count + 1, Def, false});
        false ->
          elixir_errors:file_error(Meta, File, ?MODULE, {bad_kind, Module, Tuple, Kind})
      end
  end,

  ok.

super(Meta, Module, Tuple, E) ->
  {Set, _} = elixir_module:data_tables(Module),

  case ets:lookup(Set, {overridable, Tuple}) of
    [Overridable] ->
      store(Set, Module, Tuple, Overridable, true);
    [] ->
      elixir_errors:file_error(Meta, E, ?MODULE, {no_super, Module, Tuple})
  end.

store_not_overridden(Module) ->
  {Set, Bag} = elixir_module:data_tables(Module),

  lists:foreach(fun({_, Tuple}) ->
    [Overridable] = ets:lookup(Set, {overridable, Tuple}),

    case ets:lookup(Set, {def, Tuple}) of
      [] ->
        store(Set, Module, Tuple, Overridable, false);
      [{_, Kind, Meta, File, _, _}] ->
        {{_, OverridableKind, _, _, _, _}, _} = element(3, Overridable),

        case is_valid_kind(Kind, OverridableKind) of
          true -> ok;
          false -> elixir_errors:file_error(Meta, File, ?MODULE, {bad_kind, Module, Tuple, Kind})
        end
    end
  end, ets:lookup(Bag, overridables)).

%% Private

store(Set, Module, Tuple, {_, Count, Def, Overridden}, Hidden) ->
  {{{def, {Name, Arity}}, Kind, BaseMeta, File, _Check,
   {Defaults, _HasBody, _LastDefaults}}, Clauses} = Def,
  Meta = [{from_super, Hidden} | BaseMeta],

  {FinalKind, FinalName, FinalArity, FinalClauses} =
    case Hidden of
      false ->
        {Kind, Name, Arity, Clauses};
      true when Kind == defmacro; Kind == defmacrop ->
        {defmacrop, name(Name, Count), Arity, Clauses};
      true ->
        {defp, name(Name, Count), Arity, Clauses}
    end,

  case Overridden of
    false ->
      ets:update_element(Set, {overridable, Tuple}, {?overridden_pos, true}),
      elixir_def:store_definition(none, FinalKind, Meta, FinalName, FinalArity,
                                  File, Module, Defaults, FinalClauses);
    true ->
      ok
  end,

  {FinalKind, FinalName, Meta}.

name(Name, Count) when is_integer(Count) ->
  list_to_atom(atom_to_list(Name) ++ " (overridable " ++ integer_to_list(Count) ++ ")").

is_valid_kind(NewKind, PreviousKind) ->
  is_macro(NewKind) =:= is_macro(PreviousKind).

is_macro(defmacro) -> true;
is_macro(defmacrop) -> true;
is_macro(_) -> false.

%% Error handling
format_error({bad_kind, Module, {Name, Arity}, Kind}) ->
  case is_macro(Kind) of
    true ->
      io_lib:format("cannot override function (def, defp) ~ts/~B in module ~ts as a macro (defmacro, defmacrop)",
        [Name, Arity, elixir_aliases:inspect(Module)]);
    false ->
      io_lib:format("cannot override macro (defmacro, defmacrop) ~ts/~B in module ~ts as a function (def, defp)",
        [Name, Arity, elixir_aliases:inspect(Module)])
  end;

format_error({no_super, Module, {Name, Arity}}) ->
  Bins   = [format_fa(Tuple) || Tuple <- overridables_for(Module)],
  Joined = 'Elixir.Enum':join(Bins, <<", ">>),
  io_lib:format("no super defined for ~ts/~B in module ~ts. Overridable functions available are: ~ts",
    [Name, Arity, elixir_aliases:inspect(Module), Joined]).

format_fa({Name, Arity}) ->
  A = 'Elixir.Macro':inspect_atom(remote_call, Name),
  B = integer_to_binary(Arity),
  <<A/binary, $/, B/binary>>.
