%%%-------------------------------------------------------------------
%%% @copyright (C) 2025, QPQ AG
%%% @copyright (C) 2018, Aeternity Anstalt
%%% @doc
%%%     Sophia syntax utilities.
%%% @end
%%%-------------------------------------------------------------------
-module(so_syntax_utils).
-vsn("9.0.0").

-export([used_ids/1, used_ids/2, used_types/2, used/1]).

-record(alg, {zero, plus, scoped}).

-type alg(A) :: #alg{ zero   :: A
                    , plus   :: fun((A, A) -> A)
                    , scoped :: fun((A, A) -> A) }.

-type kind() :: decl | type | bind_type | expr | bind_expr.

-spec fold(alg(A), fun((kind(), _) -> A), kind(), E | [E]) -> A
    when E :: so_syntax:decl()
            | so_syntax:typedef()
            | so_syntax:field_t()
            | so_syntax:constructor_t()
            | so_syntax:type()
            | so_syntax:expr()
            | so_syntax:pat()
            | so_syntax:arg()
            | so_syntax:alt()
            | so_syntax:elim()
            | so_syntax:arg_expr()
            | so_syntax:field(so_syntax:expr())
            | so_syntax:stmt().
fold(Alg = #alg{zero = Zero, plus = Plus, scoped = Scoped}, Fun, K, X) ->
    ExprKind = if K == bind_expr -> bind_expr; true -> expr end,
    TypeKind = if K == bind_type -> bind_type; true -> type end,
    Sum  = fun(Xs) -> lists:foldl(Plus, Zero, Xs) end,
    Same = fun(A) -> fold(Alg, Fun, K, A) end,
    Decl = fun(D) -> fold(Alg, Fun, decl, D) end,
    Type = fun(T) -> fold(Alg, Fun, TypeKind, T) end,
    Expr = fun(E) -> fold(Alg, Fun, ExprKind, E) end,
    BindExpr = fun(P) -> fold(Alg, Fun, bind_expr, P) end,
    BindType = fun(T) -> fold(Alg, Fun, bind_type, T) end,
    Top = Fun(K, X),
    Rec = case X of
            %% lists (bound things in head scope over tail)
            [A | As]                      -> Scoped(Same(A), Same(As));
            %% decl()
            {contract, _, _, Ds}          -> Decl(Ds);
            {namespace, _, _, Ds}         -> Decl(Ds);
            {type_def, _, I, _, D}        -> Plus(BindType(I), Decl(D));
            {fun_decl, _, _, T}           -> Type(T);
            {letval, _, P, E}             -> Scoped(BindExpr(P), Expr(E));
            {letfun, _, F, Xs, T, GEs} -> Sum([BindExpr(F), Type(T), Expr(Xs ++ GEs)]);
            {fun_clauses, _, _, T, Cs}    -> Sum([Type(T) | [Decl(C) || C <- Cs]]);
            %% typedef()
            {alias_t, T}    -> Type(T);
            {record_t, Fs}  -> Type(Fs);
            {variant_t, Cs} -> Type(Cs);
            %% field_t() and constructor_t()
            {field_t, _, _, T}   -> Type(T);
            {constr_t, _, _, Ts} -> Type(Ts);
            %% type()
            {fun_t, _, Named, Args, Ret} -> Type([Named, Args, Ret]);
            {app_t, _, T, Ts}            -> Type([T | Ts]);
            {tuple_t, _, Ts}             -> Type(Ts);
            %% named_arg_t()
            {named_arg_t, _, _, T, E} -> Plus(Type(T), Expr(E));
            %% expr()
            {lam, _, Args, E}      -> Scoped(BindExpr(Args), Expr(E));
            {'if', _, A, B, C}     -> Expr([A, B, C]);
            {switch, _, E, Alts}   -> Expr([E, Alts]);
            {app, _, A, As}        -> Expr([A | As]);
            {proj, _, E, _}        -> Expr(E);
            {tuple, _, As}         -> Expr(As);
            {list, _, As}          -> Expr(As);
            {list_comp, _, Y, []}  -> Expr(Y);
            {list_comp, A, Y, [{comprehension_bind, I, E}|R]} ->
                Plus(Expr(E), Scoped(BindExpr(I), Expr({list_comp, A, Y, R})));
            {list_comp, A, Y, [{comprehension_if, _, E}|R]} ->
                Plus(Expr(E), Expr({list_comp, A, Y, R}));
            {list_comp, A, Y, [D = {letval, _, Pat, _} | R]} ->
                Plus(Decl(D), Scoped(BindExpr(Pat), Expr({list_comp, A, Y, R})));
            {list_comp, A, Y, [D = {letfun, _, F, _, _, _} | R]} ->
                Plus(Decl(D), Scoped(BindExpr(F), Expr({list_comp, A, Y, R})));
            {typed, _, E, T}       -> Plus(Expr(E), Type(T));
            {record, _, Fs}        -> Expr(Fs);
            {record, _, E, Fs}     -> Expr([E | Fs]);
            {map, _, E, Fs}        -> Expr([E | Fs]);
            {map, _, KVs}          -> Sum([Expr([Key, Val]) || {Key, Val} <- KVs]);
            {map_get, _, A, B}     -> Expr([A, B]);
            {map_get, _, A, B, C}  -> Expr([A, B, C]);
            {block, _, Ss}         -> Expr(Ss);
            {letpat, _, X, P}      -> Plus(BindExpr(X), Expr(P));
            {guarded, _, Gs, E}    -> Expr([E | Gs]);
            %% field()
            {field, _, LV, E}    -> Expr([LV, E]);
            {field, _, LV, _, E} -> Expr([LV, E]);
            %% arg()
            {arg, _, Y, T} -> Plus(BindExpr(Y), Type(T));
            %% alt()
            {'case', _, P, GEs} -> Scoped(BindExpr(P), Expr(GEs));
            %% elim()
            {proj, _, _}    -> Zero;
            {map_get, _, E} -> Expr(E);
            %% arg_expr()
            {named_arg, _, _, E} -> Expr(E);
            _ -> Alg#alg.zero
          end,
    (Alg#alg.plus)(Top, Rec).

%% Name dependencies

%% Used ids, top level
used_ids(E) ->
    used_ids([], E).

%% Used ids, top level or in (current) namespace
used_ids(Ns, E) ->
    [ lists:last(Xs) || {{term, Xs}, _} <- used(E), in_ns(Xs, Ns) ].

in_ns([_], _) -> true;
in_ns(Xs, Ns) -> lists:droplast(Xs) == Ns.

used_types([Top] = _CurrentNS, T) ->
    F = fun({{type, [X]}, _}) -> [X];
           ({{type, [Top1, X]}, _}) when Top1 == Top -> [X];
           (_) -> []
        end,
    lists:flatmap(F, used(T)).

-type entity() :: {term, [string()]}
                | {type, [string()]}
                | {namespace, [string()]}.

-spec entity_alg() -> alg(#{entity() => so_syntax:ann()}).
entity_alg() ->
    IsBound = fun({K, _}) -> lists:member(K, [bound_term, bound_type]) end,
    Unbind  = fun(bound_term) -> term; (bound_type) -> type end,
    Remove  = fun(Keys, Map) -> maps:without(Keys, Map) end,
    Scoped = fun(Xs, Ys) ->
                Bound  = [E || E <- maps:keys(Xs), IsBound(E)],
                Bound1 = [ {Unbind(Tag), X} || {Tag, X} <- Bound ],
                Others = Remove(Bound1, Ys),
                maps:merge(Remove(Bound, Xs), Others)
            end,
    #alg{ zero   = #{}
        , plus   = fun maps:merge/2
        , scoped = Scoped }.

-spec used(_) -> [{entity(), so_syntax:ann()}].
used(D) ->
    Kind = fun(expr)      -> term;
              (bind_expr) -> bound_term;
              (type)      -> type;
              (bind_type) -> bound_type
           end,
    NS = fun(Xs) -> {namespace, lists:droplast(Xs)} end,
    NotBound = fun({{Tag, _}, _}) -> not lists:member(Tag, [bound_term, bound_type]) end,
    Xs =
        maps:to_list(fold(entity_alg(),
            fun(K, {id,   Ann, X})  -> #{{Kind(K), [X]} => Ann};
               (K, {qid,  Ann, Xs}) -> #{{Kind(K), Xs}  => Ann, NS(Xs) => Ann};
               (K, {con,  Ann, X})  -> #{{Kind(K), [X]} => Ann};
               (K, {qcon, Ann, Xs}) -> #{{Kind(K), Xs}  => Ann, NS(Xs) => Ann};
               (_, _)             -> #{}
            end, decl, D)),
    lists:filter(NotBound, Xs).
