%% Module responsible for handling imports and conflicts
%% between local functions and imports.
%% For imports dispatch, please check elixir_dispatch.
-module(elixir_import).
-export([import/4, special_form/2, format_error/1]).
-include("elixir.hrl").

import(Meta, Ref, Opts, E) ->
  {Functions, Macros, Added} =
    case keyfind(only, Opts) of
      {only, functions} ->
        {Added1, _Used1, Funs} = import_functions(Meta, Ref, Opts, E),
        {Funs, keydelete(Ref, ?key(E, macros)), Added1};
      {only, macros} ->
        {Added2, _Used2, Macs} = import_macros(true, Meta, Ref, Opts, E),
        {keydelete(Ref, ?key(E, functions)), Macs, Added2};
      {only, sigils} ->
        {Added1, _Used1, Funs} = import_sigil_functions(Meta, Ref, Opts, E),
        {Added2, _Used2, Macs} = import_sigil_macros(Meta, Ref, Opts, E),
        {Funs, Macs, Added1 or Added2};
      {only, List} when is_list(List) ->
        {Added1, Used1, Funs} = import_functions(Meta, Ref, Opts, E),
        {Added2, Used2, Macs} = import_macros(false, Meta, Ref, Opts, E),
        [elixir_errors:file_warn(Meta, ?key(E, file), ?MODULE, {invalid_import, {Ref, Name, Arity}}) ||
         {Name, Arity} <- (List -- Used1) -- Used2],
        {Funs, Macs, Added1 or Added2};
      {only, Other} ->
        elixir_errors:file_error(Meta, E, ?MODULE, {invalid_option, only, Other});
      false ->
        {Added1, _Used1, Funs} = import_functions(Meta, Ref, Opts, E),
        {Added2, _Used2, Macs} = import_macros(false, Meta, Ref, Opts, E),
        {Funs, Macs, Added1 or Added2}
    end,

  elixir_env:trace({import, [{imported, Added} | Meta], Ref, Opts}, E),
  {Functions, Macros}.

import_functions(Meta, Ref, Opts, E) ->
  calculate(Meta, Ref, Opts, ?key(E, functions), ?key(E, file), fun() ->
    get_functions(Ref)
  end).

import_macros(Force, Meta, Ref, Opts, E) ->
  calculate(Meta, Ref, Opts, ?key(E, macros), ?key(E, file), fun() ->
    case fetch_macros(Ref) of
      {ok, Macros} ->
        Macros;
      error when Force ->
        elixir_errors:file_error(Meta, E, ?MODULE, {no_macros, Ref});
      error ->
        []
    end
  end).

import_sigil_functions(Meta, Ref, Opts, E) ->
  calculate(Meta, Ref, Opts, ?key(E, functions), ?key(E, file), fun() ->
    filter_sigils(get_functions(Ref))
  end).

import_sigil_macros(Meta, Ref, Opts, E) ->
  calculate(Meta, Ref, Opts, ?key(E, macros), ?key(E, file), fun() ->
    case fetch_macros(Ref) of
      {ok, Macros} ->
        filter_sigils(Macros);
      error ->
        []
    end
  end).

filter_sigils(Funs) ->
  lists:filter(fun is_sigil/1, Funs).

is_sigil({Name, 2}) ->
  case atom_to_list(Name) of
    "sigil_" ++ Letters ->
      case Letters of
        [L] when L >= $a, L =< $z -> true;
        [] -> false;
        Letters -> lists:all(fun(L) -> L >= $A andalso L =< $Z end, Letters)
      end;
    _ ->
      false
  end;
is_sigil(_) ->
  false.

%% Calculates the imports based on only and except

calculate(Meta, Key, Opts, Old, File, Existing) ->
  New = case keyfind(only, Opts) of
    {only, DupOnly} when is_list(DupOnly) ->
      ensure_keyword_list(Meta, File, DupOnly, only),
      Only = ensure_no_duplicates(Meta, File, DupOnly, only),

      case keyfind(except, Opts) of
        false -> ok;
        _ -> elixir_errors:file_error(Meta, File, ?MODULE, only_and_except_given)
      end,

      intersection(Only, Existing());

    _ ->
      case keyfind(except, Opts) of
        false ->
          remove_underscored(Existing());

        {except, DupExcept} when is_list(DupExcept) ->
          ensure_keyword_list(Meta, File, DupExcept, except),
          Except = ensure_no_duplicates(Meta, File, DupExcept, except),
          %% We are not checking existence of exports listed in :except option
          %% on purpose: to support backwards compatible code.
          %% For example, "import String, except: [trim: 1]"
          %% should work across all Elixir versions.
          case keyfind(Key, Old) of
            false -> remove_underscored(Existing()) -- Except;
            {Key, OldImports} -> OldImports -- Except
          end;

        {except, Other} ->
          elixir_errors:file_error(Meta, File, ?MODULE, {invalid_option, except, Other})
      end
  end,

  %% Normalize the data before storing it
  case ordsets:from_list(New) of
    [] ->
      {false, [], keydelete(Key, Old)};
    Set  ->
      ensure_no_special_form_conflict(Meta, File, Key, Set),
      {true, Set, [{Key, Set} | keydelete(Key, Old)]}
  end.

%% Retrieve functions and macros from modules

get_functions(Module) ->
  try
    Module:'__info__'(functions)
  catch
    error:undef -> remove_internals(Module:module_info(exports))
  end.

fetch_macros(Module) ->
  try
    {ok, Module:'__info__'(macros)}
  catch
    error:undef -> error
  end.

%% VALIDATION HELPERS

ensure_no_special_form_conflict(Meta, File, Key, [{Name, Arity} | T]) ->
  case special_form(Name, Arity) of
    true  ->
      elixir_errors:file_error(Meta, File, ?MODULE, {special_form_conflict, {Key, Name, Arity}});
    false ->
      ensure_no_special_form_conflict(Meta, File, Key, T)
  end;

ensure_no_special_form_conflict(_Meta, _File, _Key, []) -> ok.

ensure_keyword_list(_Meta, _File, [], _Kind) -> ok;

ensure_keyword_list(Meta, File, [{Key, Value} | Rest], Kind) when is_atom(Key), is_integer(Value) ->
  ensure_keyword_list(Meta, File, Rest, Kind);

ensure_keyword_list(Meta, File, _Other, Kind) ->
  elixir_errors:file_error(Meta, File, ?MODULE, {invalid_option, Kind}).

ensure_no_duplicates(Meta, File, Option, Kind) ->
  lists:foldl(fun({Name, Arity}, Acc) ->
    case lists:member({Name, Arity}, Acc) of
      true ->
        elixir_errors:file_warn(Meta, File, ?MODULE, {duplicated_import, {Kind, Name, Arity}}),
        Acc;
      false ->
        [{Name, Arity} | Acc]
    end
  end, [], Option).

%% ERROR HANDLING

format_error(only_and_except_given) ->
  ":only and :except can only be given together to import "
  "when :only is :functions, :macros, or :sigils";

format_error({duplicated_import, {Option, Name, Arity}}) ->
  io_lib:format("invalid :~s option for import, ~ts/~B is duplicated", [Option, Name, Arity]);

format_error({invalid_import, {Receiver, Name, Arity}}) ->
  io_lib:format("cannot import ~ts.~ts/~B because it is undefined or private",
    [elixir_aliases:inspect(Receiver), Name, Arity]);

format_error({invalid_option, Option}) ->
  Message = "invalid :~s option for import, expected a keyword list with integer values",
  io_lib:format(Message, [Option]);

format_error({invalid_option, only, Value}) ->
  Message = "invalid :only option for import, expected value to be an atom :functions, :macros"
  ", or a list literal, got: ~s",
  io_lib:format(Message, ['Elixir.Macro':to_string(Value)]);

format_error({invalid_option, except, Value}) ->
  Message = "invalid :except option for import, expected value to be a list literal, got: ~s",
  io_lib:format(Message, ['Elixir.Macro':to_string(Value)]);

format_error({special_form_conflict, {Receiver, Name, Arity}}) ->
  io_lib:format("cannot import ~ts.~ts/~B because it conflicts with Elixir special forms",
    [elixir_aliases:inspect(Receiver), Name, Arity]);

format_error({no_macros, Module}) ->
  io_lib:format("could not load macros from module ~ts", [elixir_aliases:inspect(Module)]).

%% LIST HELPERS

keyfind(Key, List) ->
  lists:keyfind(Key, 1, List).

keydelete(Key, List) ->
  lists:keydelete(Key, 1, List).

intersection([H | T], All) ->
  case lists:member(H, All) of
    true  -> [H | intersection(T, All)];
    false -> intersection(T, All)
  end;

intersection([], _All) -> [].

%% Internal funs that are never imported, and the like

remove_underscored(List) ->
  lists:filter(fun({Name, _}) ->
    case atom_to_list(Name) of
      "_" ++ _ -> false;
      _ -> true
    end
  end, List).

remove_internals(Set) ->
  Set -- [{behaviour_info, 1}, {module_info, 1}, {module_info, 0}].

%% Special forms

special_form('&', 1) -> true;
special_form('^', 1) -> true;
special_form('=', 2) -> true;
special_form('%', 2) -> true;
special_form('|', 2) -> true;
special_form('.', 2) -> true;
special_form('::', 2) -> true;
special_form('__block__', _) -> true;
special_form('->', _) -> true;
special_form('<<>>', _) -> true;
special_form('{}', _) -> true;
special_form('%{}', _) -> true;
special_form('alias', 1) -> true;
special_form('alias', 2) -> true;
special_form('require', 1) -> true;
special_form('require', 2) -> true;
special_form('import', 1) -> true;
special_form('import', 2) -> true;
special_form('__ENV__', 0) -> true;
special_form('__CALLER__', 0) -> true;
special_form('__STACKTRACE__', 0) -> true;
special_form('__MODULE__', 0) -> true;
special_form('__DIR__', 0) -> true;
special_form('__aliases__', _) -> true;
special_form('quote', 1) -> true;
special_form('quote', 2) -> true;
special_form('unquote', 1) -> true;
special_form('unquote_splicing', 1) -> true;
special_form('fn', _) -> true;
special_form('super', _) -> true;
special_form('for', _) -> true;
special_form('with', _) -> true;
special_form('cond', 1) -> true;
special_form('case', 2) -> true;
special_form('try', 1) -> true;
special_form('receive', 1) -> true;
special_form(_, _) -> false.
