-module(elixir_env).
-include("elixir.hrl").
-export([
  new/0, linify/1, with_vars/2, reset_vars/1,
  env_to_scope/1,
  reset_unused_vars/1, check_unused_vars/1,
  merge_and_check_unused_vars/2,
  trace/2, format_error/1,
  reset_read/2, prepare_write/1, close_write/2
]).

new() ->
  #{
    '__struct__' => 'Elixir.Macro.Env',
    module => nil,                                    %% the current module
    file => <<"nofile">>,                             %% the current filename
    line => 1,                                        %% the current line
    function => nil,                                  %% the current function
    context => nil,                                   %% can be match, guard or nil
    aliases => [],                                    %% a list of aliases by new -> old names
    requires => elixir_dispatch:default_requires(),   %% a set with modules required
    functions => elixir_dispatch:default_functions(), %% a list with functions imported from module
    macros => elixir_dispatch:default_macros(),       %% a list with macros imported from module
    macro_aliases => [],                              %% keep aliases defined inside a macro
    context_modules => [],                            %% modules defined in the current context
    vars => [],                                       %% a set of defined variables
    current_vars => {#{}, false},                     %% a tuple with maps of read and optional write current vars
    unused_vars => {#{}, 0},                          %% a map of unused vars and a version counter for vars
    prematch_vars => warn,                            %% controls behaviour outside and inside matches
    lexical_tracker => nil,                           %% lexical tracker PID
    contextual_vars => [],                            %% available contextual variables
    tracers => []                                     %% available compilation tracers
  }.

trace(Event, #{tracers := Tracers} = E) ->
  [ok = Tracer:trace(Event, E) || Tracer <- Tracers],
  ok.

linify({Line, Env}) ->
  Env#{line := Line};
linify(#{} = Env) ->
  Env.

with_vars(Env, Vars) ->
  NumVars = length(Vars),
  VarVersions = lists:zip(Vars, lists:seq(0, NumVars - 1)),
  Read = maps:from_list(VarVersions),
  Env#{vars := Vars, current_vars := {Read, false}, unused_vars := {#{}, NumVars}}.

env_to_scope(#{context := Context, current_vars := {Read, _}}) ->
  {VarsList, _Counter} = lists:mapfoldl(fun to_scope_var/2, 0, maps:values(Read)),
  VarsMap = maps:from_list(VarsList),
  Scope = #elixir_erl{
    context=Context,
    var_names=VarsMap,
    counter=#{'_' => map_size(VarsMap)}
  },
  {VarsList, Scope}.

to_scope_var(Version, Counter) ->
  {{Version, list_to_atom("_@" ++ integer_to_list(Counter))}, Counter + 1}.

reset_vars(Env) ->
  Env#{vars := [], current_vars := {#{}, false}, unused_vars := {#{}, 0}}.

%% SCOPE MERGING

reset_read(#{current_vars := {_, Write}} = E, #{current_vars := {Read, _}}) ->
  E#{current_vars := {Read, Write}}.

prepare_write(#{current_vars := {Read, _}} = E) ->
  E#{current_vars := {Read, Read}}.

close_write(#{current_vars := {_Read, Write}} = E, #{current_vars := {_, false}}) ->
  E#{current_vars := {Write, false}};
close_write(#{current_vars := {_Read, Write}} = E, #{current_vars := {_, UpperWrite}}) ->
  E#{current_vars := {Write, merge_vars(UpperWrite, Write)}}.

merge_vars(V, V) ->
  V;
merge_vars(V1, V2) ->
  maps:fold(fun(K, M2, Acc) ->
    case Acc of
      #{K := M1} when M1 >= M2 -> Acc;
      _ -> Acc#{K => M2}
    end
  end, V1, V2).

%% UNUSED VARS

reset_unused_vars(#{unused_vars := {_Unused, Version}} = E) ->
  E#{unused_vars := {#{}, Version}}.

check_unused_vars(#{unused_vars := {Unused, _Version}} = E) ->
  [elixir_errors:form_warn([{line, Line}], E, ?MODULE, {unused_var, Name, Overridden}) ||
    {{Name, _}, {Line, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
  E.

merge_and_check_unused_vars(E, #{unused_vars := {ClauseUnused, Version}}) ->
  #{current_vars := {Read, _}, unused_vars := {Unused, _Version}} = E,
  E#{unused_vars := {merge_and_check_unused_vars(Read, Unused, ClauseUnused, E), Version}}.

merge_and_check_unused_vars(Current, Unused, ClauseUnused, E) ->
  maps:fold(fun
    ({Name, Count} = Key, false, Acc) ->
      Var = {Name, nil},

      %% The parent knows it, so we have to propagate it was used up.
      case Current of
        #{Var := CurrentCount} when Count =< CurrentCount ->
          Acc#{Key => false};

        #{} ->
          Acc
      end;

    ({Name, _Count}, {Line, Overridden}, Acc) ->
      case is_unused_var(Name) of
        true ->
          Warn = {unused_var, Name, Overridden},
          elixir_errors:form_warn([{line, Line}], E, ?MODULE, Warn);

        false ->
          ok
      end,

      Acc
  end, Unused, ClauseUnused).

is_unused_var(Name) ->
  case atom_to_list(Name) of
    "_" ++ Rest -> is_compiler_var(Rest);
    _ -> true
  end.

is_compiler_var([$_]) -> true;
is_compiler_var([Var | Rest]) when Var =:= $_; Var >= $A, Var =< $Z -> is_compiler_var(Rest);
is_compiler_var(_) -> false.

format_error({unused_var, Name, Overridden}) ->
  case atom_to_list(Name) of
    "_" ++ _ ->
      io_lib:format("unknown compiler variable \"~ts\" (expected one of __MODULE__, __ENV__, __DIR__, __CALLER__, __STACKTRACE__)", [Name]);
    _ when Overridden ->
      io_lib:format("variable \"~ts\" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)", [Name]);
    _ ->
      io_lib:format("variable \"~ts\" is unused (if the variable is not meant to be used, prefix it with an underscore)", [Name])
  end.
