-module(elixir_map).
-export([expand_map/3, translate_map/3, expand_struct/4, translate_struct/4]).
-import(elixir_errors, [compile_error/4]).
-include("elixir.hrl").

expand_map(Meta, [{'|', UpdateMeta, [Left, Right]}], E) ->
  {[ELeft | ERight], EA} = elixir_exp:expand_args([Left | Right], E),
  validate_kv(Meta, ERight, Right, E),
  {{'%{}', Meta, [{'|', UpdateMeta, [ELeft, ERight]}]}, EA};
expand_map(Meta, Args, E) ->
  {EArgs, EA} = elixir_exp:expand_args(Args, E),
  validate_kv(Meta, EArgs, Args, E),
  {{'%{}', Meta, EArgs}, EA}.

expand_struct(Meta, Left, Right, #{context := Context} = E) ->
  {[ELeft, ERight], EE} = elixir_exp:expand_args([Left, Right], E),

  case validate_struct(ELeft, Context) of
    true when is_atom(ELeft) ->
      %% We always record structs when they are expanded
      %% as they expect the reference at compile time.
      elixir_lexical:record_remote(ELeft, '__struct__', 1, nil, ?line(Meta), ?m(E, lexical_tracker));
    true ->
      ok;
    false when Context == match ->
      compile_error(Meta, ?m(E, file), "expected struct name in a match to be a compile "
        "time atom, alias or a variable, got: ~ts", ['Elixir.Macro':to_string(ELeft)]);
    false ->
      compile_error(Meta, ?m(E, file), "expected struct name to be a compile "
        "time atom or alias, got: ~ts", ['Elixir.Macro':to_string(ELeft)])
  end,

  EMeta =
    %% We also include the current module because it won't be present
    %% in context module in case the module name is defined dynamically.
    case lists:member(ELeft, [?m(E, module) | ?m(E, context_modules)]) of
      true  -> [{struct, context} | Meta];
      false -> Meta
    end,

  case ERight of
    {'%{}', _, _} -> ok;
    _ -> compile_error(Meta, ?m(E, file),
           "expected struct to be followed by a map, got: ~ts",
           ['Elixir.Macro':to_string(ERight)])
  end,

  {{'%', EMeta, [ELeft, ERight]}, EE}.

validate_struct({'^', _, [{Var, _, Ctx}]}, match) when is_atom(Var), is_atom(Ctx) -> true;
validate_struct({Var, _Meta, Ctx}, match) when is_atom(Var), is_atom(Ctx) -> true;
validate_struct(Atom, _) when is_atom(Atom) -> true;
validate_struct(_, _) -> false.

validate_kv(Meta, KV, Original, E) ->
  lists:foldl(fun
    ({_K, _V}, Acc) -> Acc + 1;
    (_, Acc) ->
      compile_error(Meta, ?m(E, file),
        "expected key-value pairs in a map, got: ~ts",
        ['Elixir.Macro':to_string(lists:nth(Acc, Original))])
  end, 1, KV).

translate_map(Meta, Args, S) ->
  {Assocs, TUpdate, US} = extract_assoc_update(Args, S),
  translate_map(Meta, Assocs, TUpdate, US).

translate_struct(_Meta, Name, {'%{}', MapMeta, Assocs}, S) when is_tuple(Name) ->
  translate_map(MapMeta, Assocs ++ [{'__struct__', Name}], nil, S);

translate_struct(Meta, Name, {'%{}', MapMeta, Args}, S) ->
  {Assocs, TUpdate, US} = extract_assoc_update(Args, S),
  Operation = operation(TUpdate, S),

  Struct = case Operation of
    expand -> load_struct(Meta, Name, [Args], S);
    _ -> load_struct(Meta, Name, [], S)
  end,

  assert_struct_keys(Meta, Name, Struct, Assocs, S),

  case Operation of
    update ->
      Ann = ?ann(Meta),
      {VarName, _, VS} = elixir_scope:build_var('_', US),

      Var = {var, Ann, VarName},
      Map = {map, Ann, [{map_field_exact, Ann, {atom, Ann, '__struct__'}, {atom, Ann, Name}}]},

      Match = {match, Ann, Var, Map},
      Error = {tuple, Ann, [{atom, Ann, badstruct}, {atom, Ann, Name}, Var]},

      {TMap, TS} = translate_map(MapMeta, Assocs, Var, VS),

      {{'case', ?generated, TUpdate, [
        {clause, Ann, [Match], [], [TMap]},
        {clause, ?generated, [Var], [], [elixir_utils:erl_call(Ann, erlang, error, [Error])]}
      ]}, TS};
    match ->
      translate_map(MapMeta, Assocs ++ [{'__struct__', Name}], nil, US);
    expand ->
      Keys = [K || {K, _} <- Assocs],
      {StructAssocs, _} = elixir_quote:escape(maps:to_list(maps:without(Keys, Struct)), false),
      translate_map(MapMeta, StructAssocs ++ Assocs ++ [{'__struct__', Name}], nil, US)
  end.

%% Helpers

operation(nil, #elixir_scope{context=match}) -> match;
operation(nil, _) -> expand;
operation(_, _) -> update.

load_struct(Meta, Name, Args, S) ->
  Context = lists:keyfind(struct, 1, Meta) == {struct, context},
  Arity = length(Args),

  Local =
    not(ensure_loaded(Name)) andalso
      (Context orelse wait_for_struct(Name)),

  try
    case Local of
      true ->
        try
          apply(elixir_locals:local_for(Name, '__struct__', Arity, def), Args)
        catch
          error:undef  -> apply(Name, '__struct__', Args);
          error:badarg -> apply(Name, '__struct__', Args)
        end;
      false ->
        apply(Name, '__struct__', Args)
    end
  of
    #{} = Struct ->
      Struct;
    Other ->
      compile_error(Meta, S#elixir_scope.file, "expected ~ts.__struct__/~p to "
        "return a map, got: ~ts", [elixir_aliases:inspect(Name), Arity, 'Elixir.Kernel':inspect(Other)])
  catch
    error:undef ->
      Inspected = elixir_aliases:inspect(Name),

      case Context andalso (S#elixir_scope.function == nil) of
        true ->
          compile_error(Meta, S#elixir_scope.file,
            "cannot access struct ~ts, the struct was not yet defined or the struct is being "
            "accessed in the same context that defines it", [Inspected]);
        false ->
          compile_error(Meta, S#elixir_scope.file, "~ts.__struct__/~p is undefined, "
            "cannot expand struct ~ts", [Inspected, Arity, Inspected])
      end;

    Kind:Reason ->
      Info = [{Name, '__struct__', Arity, [{file, "expanding struct"}]},
              elixir_utils:caller(?line(Meta), S#elixir_scope.file,
                                  S#elixir_scope.module, S#elixir_scope.function)],
      erlang:raise(Kind, Reason, prune_stacktrace(erlang:get_stacktrace(), Name, Arity) ++ Info)
  end.

prune_stacktrace([{Module, '__struct__', Arity, _} | _], Module, Arity) ->
  [];
prune_stacktrace([H | T], Module, Arity) ->
  [H | prune_stacktrace(T, Module, Arity)];
prune_stacktrace([], _Module, _Arity) ->
  [].

ensure_loaded(Module) ->
  code:ensure_loaded(Module) == {module, Module}.

wait_for_struct(Module) ->
  is_pid(erlang:get(elixir_compiler_pid)) andalso
    'Elixir.Kernel.ErrorHandler':ensure_compiled(Module, struct).

translate_map(Meta, Assocs, TUpdate, #elixir_scope{extra=Extra} = S) ->
  {Op, KeyFun, ValFun} = extract_key_val_op(TUpdate, S),

  Ann = ?ann(Meta),

  {TArgs, SA} = lists:mapfoldl(fun({Key, Value}, Acc) ->
    {TKey, Acc1}   = KeyFun(Key, Acc),
    {TValue, Acc2} = ValFun(Value, Acc1#elixir_scope{extra=Extra}),
    {{Op, ?ann(Meta), TKey, TValue}, Acc2}
  end, S, Assocs),

  build_map(Ann, TUpdate, TArgs, SA).

extract_assoc_update([{'|', _Meta, [Update, Args]}], S) ->
  {TArg, SA} = elixir_translator:translate_arg(Update, S, S),
  {Args, TArg, SA};
extract_assoc_update(Args, SA) -> {Args, nil, SA}.

extract_key_val_op(_TUpdate, #elixir_scope{context=match}) ->
  {map_field_exact,
    fun(X, Acc) -> elixir_translator:translate(X, Acc#elixir_scope{extra=map_key}) end,
    fun elixir_translator:translate/2};
extract_key_val_op(TUpdate, S) ->
  KS = S#elixir_scope{extra=map_key},
  Op = if TUpdate == nil -> map_field_assoc; true -> map_field_exact end,
  {Op,
    fun(X, Acc) -> elixir_translator:translate_arg(X, Acc, KS) end,
    fun(X, Acc) -> elixir_translator:translate_arg(X, Acc, S) end}.

build_map(Ann, nil, TArgs, SA) -> {{map, Ann, TArgs}, SA};
build_map(Ann, TUpdate, TArgs, SA) -> {{map, Ann, TUpdate, TArgs}, SA}.

assert_struct_keys(Meta, Name, Struct, Assocs, S) ->
  [begin
     compile_error(Meta, S#elixir_scope.file, "unknown key ~ts for struct ~ts",
                   ['Elixir.Kernel':inspect(Key), elixir_aliases:inspect(Name)])
   end || {Key, _} <- Assocs, not maps:is_key(Key, Struct)].
