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

%% Convenience functions used to manipulate scope and its variables.
-module(elixir_erl_var).
-export([
  translate/4, assign/2, build/2,
  load_binding/2, dump_binding/4,
  from_env/1, from_env/2
]).
-include("elixir.hrl").

%% VAR HANDLING

translate(Meta, '_', _Kind, S) ->
  {{var, ?ann(Meta), '_'}, S};

translate(Meta, Name, Kind, #elixir_erl{var_names=VarNames} = S) ->
  {version, Version} = lists:keyfind(version, 1, Meta),

  case VarNames of
    #{Version :=  ErlName} -> {{var, ?ann(Meta), ErlName}, S};
    #{} when Kind /= nil -> assign(Meta, '_', Version, S);
    #{} -> assign(Meta, Name, Version, S)
  end.

assign(Meta, #elixir_erl{var_names=VarNames} = S) ->
  Version = -(map_size(VarNames)+1),
  ExVar = {var, [{version, Version} | Meta], ?var_context},
  {ErlVar, SV} = assign(Meta, '_', Version, S),
  {ExVar, ErlVar, SV}.

assign(Meta, Name, Version, #elixir_erl{var_names=VarNames} = S) ->
  {NewVar, NS} = build(Name, S),
  NewVarNames = VarNames#{Version => NewVar},
  {{var, ?ann(Meta), NewVar}, NS#elixir_erl{var_names=NewVarNames}}.

build(Key, #elixir_erl{counter=Counter} = S) ->
  Count =
    case Counter of
      #{Key := Val} -> Val + 1;
      _ -> 1
    end,
  {build_name(Key, Count),
   S#elixir_erl{counter=Counter#{Key => Count}}}.

build_name('_', Count) -> list_to_atom("_" ++ integer_to_list(Count));
build_name(Name, Count) -> list_to_atom("_" ++ atom_to_list(Name) ++ "@" ++ integer_to_list(Count)).

%% BINDINGS

from_env(#{versioned_vars := Read} = Env) ->
  VarsList = to_erl_vars(maps:values(Read), 0),
  {VarsList, from_env(Env, maps:from_list(VarsList))}.

from_env(#{context := Context}, VarsMap) ->
  #elixir_erl{
    context=Context,
    var_names=VarsMap,
    counter=#{'_' => map_size(VarsMap)}
  }.

to_erl_vars([Version | Versions], Counter) ->
  [{Version, to_erl_var(Counter)} | to_erl_vars(Versions, Counter + 1)];
to_erl_vars([], _Counter) ->
  [].

to_erl_var(Counter) ->
  list_to_atom("_@" ++ integer_to_list(Counter)).

load_binding(Binding, Prune) ->
  load_binding(Binding, #{}, [], [], 0, Prune).

load_binding([Binding | NextBindings], ExVars, ErlVars, Normalized, Counter, Prune) ->
  {Pair, Value} = load_pair(Binding),

  case ExVars of
    #{Pair := VarCounter} ->
      ErlVar = to_erl_var(VarCounter),
      load_binding(NextBindings, ExVars, ErlVars, [{ErlVar, Value} | Normalized], Counter, Prune);

    #{} ->
      ErlVar = to_erl_var(Counter),

      load_binding(
        NextBindings,
        ExVars#{Pair => Counter},
        [{Counter, ErlVar} | ErlVars],
        [{ErlVar, Value} | Normalized],
        Counter + 1,
        Prune
      )
  end;
load_binding([], ExVars, ErlVars, Normalized, Counter, true) ->
  load_binding([{{elixir, prune_binding}, true}], ExVars, ErlVars, Normalized, Counter, false);
load_binding([], ExVars, ErlVars, Normalized, _Counter, false) ->
  {ExVars, maps:from_list(ErlVars), maps:from_list(lists:reverse(Normalized))}.

load_pair({Key, Value}) when is_atom(Key) -> {{Key, nil}, Value};
load_pair({Pair, Value}) -> {Pair, Value}.

dump_binding(Binding, ErlS, ExS, PruneBefore) ->
  #elixir_erl{var_names=ErlVars} = ErlS,
  #elixir_ex{vars={ExVars, _}, unused=Unused} = ExS,

  maps:fold(fun
    ({Var, Kind} = Pair, Version, {B, V})
    when is_atom(Kind),
         %% If the variable is part of the pruning (usually the input binding)
         %% and is unused, we removed it from vars.
         Version > PruneBefore orelse is_map_key({Pair, Version}, Unused) ->
      Key = case Kind of
        nil -> Var;
        _ -> Pair
      end,

      ErlName = maps:get(Version, ErlVars),
      Value = maps:get(ErlName, Binding, nil),
      {[{Key, Value} | B], V};

    (Pair, _, {B, V}) when PruneBefore >= 0 ->
      {B, maps:remove(Pair, V)};

    (_, _, Acc) ->
      Acc
  end, {[], ExVars}, ExVars).
