-module(birdie@internal@titles).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).

-export([new/0, literals/1, prefixes/1, find/2, from_module/3, from_test_directory/0]).
-export_type([titles/0, test_info/0, match/0, error/0, birdie_import/0, snap_title/0]).

-opaque titles() :: {titles,
        gleam@dict:dict(binary(), test_info()),
        trie:trie(binary(), test_info())}.

-type test_info() :: {test_info, binary(), binary()}.

-type match() :: {literal, test_info()} | {prefix, test_info(), binary()}.

-type error() :: {cannot_find_project_root, simplifile:file_error()} |
    {cannot_read_test_directory, simplifile:file_error()} |
    {cannot_read_test_file, simplifile:file_error(), binary()} |
    {duplicate_literal_titles, binary(), test_info(), test_info()} |
    {overlapping_prefixes, binary(), test_info(), binary(), test_info()} |
    {prefix_overlapping_with_literal_title, binary(), test_info(), test_info()}.

-type birdie_import() :: {unqualified, binary()} |
    {qualified, binary(), binary()} |
    {discarded, binary()}.

-type snap_title() :: {literal_title, binary()} | {prefix_title, binary()}.

-spec new() -> titles().
new() ->
    {titles, gleam@dict:new(), trie:new()}.

-spec add_literal_title(titles(), binary(), test_info()) -> titles().
add_literal_title(Titles, Title, Info) ->
    Literals = erlang:element(2, Titles),
    New_literals = gleam@dict:insert(Literals, Title, Info),
    erlang:setelement(2, Titles, New_literals).

-spec literals(titles()) -> gleam@dict:dict(binary(), test_info()).
literals(Titles) ->
    {titles, Literals, _} = Titles,
    Literals.

-spec prefixes(titles()) -> gleam@dict:dict(binary(), test_info()).
prefixes(Titles) ->
    {titles, _, Prefixes} = Titles,
    _pipe = Prefixes,
    _pipe@1 = trie:to_list(_pipe),
    _pipe@2 = gleam@list:map(
        _pipe@1,
        fun(Pair) ->
            {Prefix, Info} = Pair,
            {gleam@string:join(Prefix, <<""/utf8>>), Info}
        end
    ),
    maps:from_list(_pipe@2).

-spec find(titles(), binary()) -> {ok, match()} | {error, nil}.
find(Titles, Title) ->
    Literal_match = gleam@result:map(
        gleam@dict:get(erlang:element(2, Titles), Title),
        fun(Field@0) -> {literal, Field@0} end
    ),
    gleam@result:lazy_or(
        Literal_match,
        fun() ->
            Letters = gleam@string:to_graphemes(Title),
            gleam@result:'try'(
                trie:subtrie(erlang:element(3, Titles), Letters),
                fun(Matching_prefixes) ->
                    case trie:to_list(Matching_prefixes) of
                        [{Prefix, Info} | _] ->
                            {ok,
                                {prefix,
                                    Info,
                                    gleam@string:join(Prefix, <<""/utf8>>)}};

                        _ ->
                            {error, nil}
                    end
                end
            )
        end
    ).

-spec imported_snap(list(glance:unqualified_import())) -> {ok, binary()} |
    {error, nil}.
imported_snap(Values) ->
    gleam@list:fold_until(Values, {error, nil}, fun(Nil, Value) -> case Value of
                {unqualified_import, <<"snap"/utf8>>, none} ->
                    {stop, {ok, <<"snap"/utf8>>}};

                {unqualified_import, <<"snap"/utf8>>, {some, Alias}} ->
                    {stop, {ok, Alias}};

                _ ->
                    {continue, Nil}
            end end).

-spec birdie_import(glance:module_()) -> {ok, birdie_import()} | {error, nil}.
birdie_import(Module) ->
    gleam@list:fold_until(
        erlang:element(2, Module),
        {error, nil},
        fun(Nil, Import_) -> case erlang:element(3, Import_) of
                {import, <<"birdie"/utf8>>, Birdie_alias, _, Unqualified_values} ->
                    case Birdie_alias of
                        {some, {discarded, _}} ->
                            case imported_snap(Unqualified_values) of
                                {ok, Snap_alias} ->
                                    {stop, {ok, {discarded, Snap_alias}}};

                                {error, _} ->
                                    {stop, {error, nil}}
                            end;

                        {some, {named, Module_name}} ->
                            case imported_snap(Unqualified_values) of
                                {ok, Snap_alias@1} ->
                                    {stop,
                                        {ok,
                                            {qualified,
                                                Module_name,
                                                Snap_alias@1}}};

                                {error, _} ->
                                    {stop, {ok, {unqualified, Module_name}}}
                            end;

                        none ->
                            case imported_snap(Unqualified_values) of
                                {ok, Snap_alias@2} ->
                                    {stop,
                                        {ok,
                                            {qualified,
                                                <<"birdie"/utf8>>,
                                                Snap_alias@2}}};

                                {error, _} ->
                                    {stop,
                                        {ok, {unqualified, <<"birdie"/utf8>>}}}
                            end
                    end;

                _ ->
                    {continue, Nil}
            end end
    ).

-spec is_snap_function(glance:expression(), birdie_import()) -> boolean().
is_snap_function(Expression, Birdie_import) ->
    Is_a_call_to_snap = fun(Module, Name) -> case {Module, Birdie_import} of
            {none, {unqualified, _}} ->
                false;

            {none, {qualified, _, Snap}} ->
                Snap =:= Name;

            {none, {discarded, Snap@1}} ->
                Snap@1 =:= Name;

            {{some, Module@1}, {qualified, Birdie, Snap@2}} ->
                (<<<<Module@1/binary, "."/utf8>>/binary, Name/binary>>) =:= (<<<<Birdie/binary,
                        "."/utf8>>/binary,
                    Snap@2/binary>>);

            {{some, Module@2}, {unqualified, Birdie@1}} ->
                (<<<<Module@2/binary, "."/utf8>>/binary, Name/binary>>) =:= (<<Birdie@1/binary,
                    ".snap"/utf8>>);

            {{some, _}, {discarded, _}} ->
                false
        end end,
    case Expression of
        {variable, Name@1} ->
            Is_a_call_to_snap(none, Name@1);

        {field_access, {variable, Module@3}, Name@2} ->
            Is_a_call_to_snap({some, Module@3}, Name@2);

        _ ->
            false
    end.

-spec expression_to_snap_title(glance:expression()) -> {ok, snap_title()} |
    {error, nil}.
expression_to_snap_title(Expression) ->
    case Expression of
        {string, Title} ->
            {ok, {literal_title, Title}};

        {binary_operator, concatenate, {string, Prefix}, _} ->
            {ok, {prefix_title, Prefix}};

        _ ->
            {error, nil}
    end.

-spec snap_call(birdie_import(), glance:expression()) -> {ok, snap_title()} |
    {error, nil}.
snap_call(Birdie_import, Expression) ->
    case Expression of
        {call,
            Function,
            [{field, none, Title}, {field, {some, <<"content"/utf8>>}, _}]} ->
            Is_snap_function = is_snap_function(Function, Birdie_import),
            gleam@bool:guard(
                not Is_snap_function,
                {error, nil},
                fun() -> expression_to_snap_title(Title) end
            );

        {call, Function, [{field, none, _}, {field, _, Title}]} ->
            Is_snap_function = is_snap_function(Function, Birdie_import),
            gleam@bool:guard(
                not Is_snap_function,
                {error, nil},
                fun() -> expression_to_snap_title(Title) end
            );

        {call,
            Function,
            [{field, {some, <<"content"/utf8>>}, _}, {field, _, Title}]} ->
            Is_snap_function = is_snap_function(Function, Birdie_import),
            gleam@bool:guard(
                not Is_snap_function,
                {error, nil},
                fun() -> expression_to_snap_title(Title) end
            );

        {call,
            Function,
            [{field, {some, <<"title"/utf8>>}, Title}, {field, _, _}]} ->
            Is_snap_function = is_snap_function(Function, Birdie_import),
            gleam@bool:guard(
                not Is_snap_function,
                {error, nil},
                fun() -> expression_to_snap_title(Title) end
            );

        {binary_operator,
            pipe,
            Title,
            {call, Function, [{field, {some, <<"content"/utf8>>}, _}]}} ->
            Is_snap_function = is_snap_function(Function, Birdie_import),
            gleam@bool:guard(
                not Is_snap_function,
                {error, nil},
                fun() -> expression_to_snap_title(Title) end
            );

        {binary_operator, pipe, _, {call, Function, [{field, _, Title}]}} ->
            Is_snap_function = is_snap_function(Function, Birdie_import),
            gleam@bool:guard(
                not Is_snap_function,
                {error, nil},
                fun() -> expression_to_snap_title(Title) end
            );

        {binary_operator,
            pipe,
            Title,
            {fn_capture, {some, <<"title"/utf8>>}, Function, _, _}} ->
            Is_snap_function = is_snap_function(Function, Birdie_import),
            gleam@bool:guard(
                not Is_snap_function,
                {error, nil},
                fun() -> expression_to_snap_title(Title) end
            );

        {binary_operator,
            pipe,
            Title,
            {fn_capture, _, Function, [{field, none, _}], []}} ->
            Is_snap_function = is_snap_function(Function, Birdie_import),
            gleam@bool:guard(
                not Is_snap_function,
                {error, nil},
                fun() -> expression_to_snap_title(Title) end
            );

        _ ->
            {error, nil}
    end.

-spec try_or({ok, KTY} | {error, any()}, KUC, fun((KTY) -> KUC)) -> KUC.
try_or(Result, Default, Fun) ->
    case Result of
        {ok, A} ->
            Fun(A);

        {error, _} ->
            Default
    end.

-spec 'try'(
    {ok, KUD} | {error, KUE},
    fun((KUE) -> KUH),
    fun((KUD) -> {ok, KUI} | {error, KUH})
) -> {ok, KUI} | {error, KUH}.
'try'(Result, Map_error, Fun) ->
    case Result of
        {ok, A} ->
            Fun(A);

        {error, E} ->
            {error, Map_error(E)}
    end.

-spec try_fold_expression(
    glance:expression(),
    KSW,
    fun((KSW, glance:expression()) -> {ok, KSW} | {error, KSX})
) -> {ok, KSW} | {error, KSX}.
try_fold_expression(Expression, Acc, Fun) ->
    gleam@result:'try'(Fun(Acc, Expression), fun(Acc@1) -> case Expression of
                {int, _} ->
                    {ok, Acc@1};

                {float, _} ->
                    {ok, Acc@1};

                {string, _} ->
                    {ok, Acc@1};

                {variable, _} ->
                    {ok, Acc@1};

                {panic, _} ->
                    {ok, Acc@1};

                {todo, _} ->
                    {ok, Acc@1};

                {negate_int, Expression@1} ->
                    try_fold_expression(Expression@1, Acc@1, Fun);

                {negate_bool, Expression@1} ->
                    try_fold_expression(Expression@1, Acc@1, Fun);

                {field_access, Expression@1, _} ->
                    try_fold_expression(Expression@1, Acc@1, Fun);

                {tuple_index, Expression@1, _} ->
                    try_fold_expression(Expression@1, Acc@1, Fun);

                {block, Statements} ->
                    try_fold_statements(Statements, Acc@1, Fun);

                {tuple, Expressions} ->
                    try_fold_expressions(Expressions, Acc@1, Fun);

                {list, Expressions, none} ->
                    try_fold_expressions(Expressions, Acc@1, Fun);

                {list, Elements, {some, Rest}} ->
                    gleam@result:'try'(
                        try_fold_expressions(Elements, Acc@1, Fun),
                        fun(Acc@2) -> try_fold_expression(Rest, Acc@2, Fun) end
                    );

                {fn, _, _, Statements@1} ->
                    try_fold_statements(Statements@1, Acc@1, Fun);

                {record_update, _, _, Record, Fields} ->
                    gleam@result:'try'(
                        try_fold_expression(Record, Acc@1, Fun),
                        fun(Acc@3) ->
                            gleam@list:try_fold(
                                Fields,
                                Acc@3,
                                fun(Acc@4, _use1) ->
                                    {_, Expression@2} = _use1,
                                    try_fold_expression(
                                        Expression@2,
                                        Acc@4,
                                        Fun
                                    )
                                end
                            )
                        end
                    );

                {call, Function, Arguments} ->
                    gleam@result:'try'(
                        try_fold_expression(Function, Acc@1, Fun),
                        fun(Acc@5) -> try_fold_fields(Arguments, Acc@5, Fun) end
                    );

                {fn_capture, _, Function@1, Arguments_before, Arguments_after} ->
                    gleam@result:'try'(
                        try_fold_expression(Function@1, Acc@1, Fun),
                        fun(Acc@6) ->
                            gleam@result:'try'(
                                try_fold_fields(Arguments_before, Acc@6, Fun),
                                fun(Acc@7) ->
                                    try_fold_fields(Arguments_after, Acc@7, Fun)
                                end
                            )
                        end
                    );

                {bit_string, Segments} ->
                    gleam@list:try_fold(
                        Segments,
                        Acc@1,
                        fun(Acc@8, _use1@1) ->
                            {Segment, Options} = _use1@1,
                            gleam@result:'try'(
                                try_fold_expression(Segment, Acc@8, Fun),
                                fun(Acc@9) ->
                                    gleam@list:try_fold(
                                        Options,
                                        Acc@9,
                                        fun(Acc@10, Option) -> case Option of
                                                {size_value_option,
                                                    Expression@3} ->
                                                    try_fold_expression(
                                                        Expression@3,
                                                        Acc@10,
                                                        Fun
                                                    );

                                                _ ->
                                                    {ok, Acc@10}
                                            end end
                                    )
                                end
                            )
                        end
                    );

                {'case', Subjects, Clauses} ->
                    gleam@result:'try'(
                        try_fold_expressions(Subjects, Acc@1, Fun),
                        fun(Acc@11) ->
                            try_fold_clauses(Clauses, Acc@11, Fun)
                        end
                    );

                {binary_operator, _, Left, Right} ->
                    gleam@result:'try'(
                        try_fold_expression(Left, Acc@1, Fun),
                        fun(Acc@12) ->
                            try_fold_expression(Right, Acc@12, Fun)
                        end
                    )
            end end).

-spec try_fold_statements(
    list(glance:statement()),
    KSQ,
    fun((KSQ, glance:expression()) -> {ok, KSQ} | {error, KSR})
) -> {ok, KSQ} | {error, KSR}.
try_fold_statements(Statements, Acc, Fun) ->
    gleam@list:try_fold(
        Statements,
        Acc,
        fun(Acc@1, Statement) -> case Statement of
                {use, _, Expression} ->
                    try_fold_expression(Expression, Acc@1, Fun);

                {assignment, _, _, _, Expression} ->
                    try_fold_expression(Expression, Acc@1, Fun);

                {expression, Expression} ->
                    try_fold_expression(Expression, Acc@1, Fun)
            end end
    ).

-spec from_module(titles(), binary(), glance:module_()) -> {ok, titles()} |
    {error, error()}.
from_module(Titles, Name, Module) ->
    try_or(
        birdie_import(Module),
        {ok, Titles},
        fun(Birdie_import) ->
            gleam@list:try_fold(
                erlang:element(6, Module),
                Titles,
                fun(Titles@1, Function) ->
                    Body = erlang:element(6, erlang:element(3, Function)),
                    try_fold_statements(
                        Body,
                        Titles@1,
                        fun(Titles@2, Expression) ->
                            case snap_call(Birdie_import, Expression) of
                                {ok, {literal_title, Title}} ->
                                    Info = {test_info,
                                        Name,
                                        erlang:element(
                                            2,
                                            erlang:element(3, Function)
                                        )},
                                    case find(Titles@2, Title) of
                                        {error, nil} ->
                                            {ok,
                                                add_literal_title(
                                                    Titles@2,
                                                    Title,
                                                    Info
                                                )};

                                        {ok, {prefix, Prefix_info, Prefix}} ->
                                            {error,
                                                {prefix_overlapping_with_literal_title,
                                                    Prefix,
                                                    Prefix_info,
                                                    Info}};

                                        {ok, {literal, Other_info}} ->
                                            {error,
                                                {duplicate_literal_titles,
                                                    Title,
                                                    Info,
                                                    Other_info}}
                                    end;

                                {ok, {prefix_title, _}} ->
                                    {ok, Titles@2};

                                {error, nil} ->
                                    {ok, Titles@2}
                            end
                        end
                    )
                end
            )
        end
    ).

-spec from_test_directory() -> {ok, titles()} | {error, error()}.
from_test_directory() ->
    'try'(
        birdie@internal@project:find_root(),
        fun(Field@0) -> {cannot_find_project_root, Field@0} end,
        fun(Root) ->
            Test_directory = filepath:join(Root, <<"test"/utf8>>),
            Get_files = simplifile:get_files(Test_directory),
            'try'(
                Get_files,
                fun(Field@0) -> {cannot_read_test_directory, Field@0} end,
                fun(Files) ->
                    gleam@list:try_fold(
                        Files,
                        new(),
                        fun(Titles, File) ->
                            Is_gleam_file = filepath:extension(File) =:= {ok,
                                <<"gleam"/utf8>>},
                            gleam@bool:guard(
                                not Is_gleam_file,
                                {ok, Titles},
                                fun() ->
                                    'try'(
                                        simplifile:read(File),
                                        fun(_capture) ->
                                            {cannot_read_test_file,
                                                _capture,
                                                File}
                                        end,
                                        fun(Raw_module) ->
                                            case glance:module(Raw_module) of
                                                {ok, Module} ->
                                                    from_module(
                                                        Titles,
                                                        File,
                                                        Module
                                                    );

                                                {error, _} ->
                                                    {ok, Titles}
                                            end
                                        end
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-spec try_fold_fields(
    list(glance:field(glance:expression())),
    KTE,
    fun((KTE, glance:expression()) -> {ok, KTE} | {error, KTF})
) -> {ok, KTE} | {error, KTF}.
try_fold_fields(Fields, Acc, Fun) ->
    gleam@list:try_fold(
        Fields,
        Acc,
        fun(Acc@1, Field) ->
            {field, _, Expression} = Field,
            try_fold_expression(Expression, Acc@1, Fun)
        end
    ).

-spec try_fold_clauses(
    list(glance:clause()),
    KTL,
    fun((KTL, glance:expression()) -> {ok, KTL} | {error, KTM})
) -> {ok, KTL} | {error, KTM}.
try_fold_clauses(Clauses, Acc, Fun) ->
    gleam@list:try_fold(Clauses, Acc, fun(Acc@1, Clause) -> case Clause of
                {clause, _, none, Body} ->
                    try_fold_expression(Body, Acc@1, Fun);

                {clause, _, {some, Guard}, Body@1} ->
                    gleam@result:'try'(
                        try_fold_expression(Guard, Acc@1, Fun),
                        fun(Acc@2) ->
                            try_fold_expression(Body@1, Acc@2, Fun)
                        end
                    )
            end end).

-spec try_fold_expressions(
    list(glance:expression()),
    KTS,
    fun((KTS, glance:expression()) -> {ok, KTS} | {error, KTT})
) -> {ok, KTS} | {error, KTT}.
try_fold_expressions(Expressions, Acc, Fun) ->
    gleam@list:try_fold(
        Expressions,
        Acc,
        fun(Acc@1, Expression) ->
            try_fold_expression(Expression, Acc@1, Fun)
        end
    ).
