src/lightspeed@form_compat.erl

-module(lightspeed@form_compat).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/form_compat.gleam").
-export([new/1, change/4, blur/3, tick/2, submit/3, dispatch_label/1, has_pending/2, mode/2]).
-export_type([change_mode/0, input_config/0, dispatch/0, input_state/0, runtime/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(" LiveView-style form change/submit compatibility semantics.\n").

-type change_mode() :: immediate |
    {debounce, integer()} |
    debounce_blur |
    {throttle, integer()}.

-type input_config() :: {input_config, binary(), change_mode()}.

-type dispatch() :: {dispatch_change, binary(), lightspeed@form:form_data()} |
    {dispatch_submit, lightspeed@form:form_data()} |
    {queued, binary()} |
    {suppressed, binary(), binary()} |
    no_dispatch.

-type input_state() :: {input_state,
        binary(),
        change_mode(),
        gleam@option:option(integer()),
        gleam@option:option(lightspeed@form:form_data()),
        gleam@option:option(integer())}.

-opaque runtime() :: {runtime, list(input_state())}.

-file("src/lightspeed/form_compat.gleam", 126).
-spec from_configs(list(input_config()), list(input_state())) -> list(input_state()).
from_configs(Configs, States_rev) ->
    case Configs of
        [] ->
            lists:reverse(States_rev);

        [{input_config, Name, Mode} | Rest] ->
            from_configs(
                Rest,
                [{input_state, Name, Mode, none, none, none} | States_rev]
            )
    end.

-file("src/lightspeed/form_compat.gleam", 45).
?DOC(" Build a runtime from known inputs.\n").
-spec new(list(input_config())) -> runtime().
new(Configs) ->
    {runtime, from_configs(Configs, [])}.

-file("src/lightspeed/form_compat.gleam", 400).
-spec reverse_into(list(input_state()), list(input_state())) -> list(input_state()).
reverse_into(Left, Right) ->
    case Left of
        [] ->
            Right;

        [Entry | Rest] ->
            reverse_into(Rest, [Entry | Right])
    end.

-file("src/lightspeed/form_compat.gleam", 392).
-spec rebuild(list(input_state()), input_state(), list(input_state())) -> list(input_state()).
rebuild(States_rev, Updated, Rest) ->
    reverse_into([Updated | States_rev], Rest).

-file("src/lightspeed/form_compat.gleam", 365).
-spec should_dispatch_throttle(
    gleam@option:option(integer()),
    integer(),
    integer()
) -> boolean().
should_dispatch_throttle(Last_dispatch, Now_ms, Window_ms) ->
    case Window_ms =< 0 of
        true ->
            true;

        false ->
            case Last_dispatch of
                none ->
                    true;

                {some, Last} ->
                    (Now_ms - Last) >= Window_ms
            end
    end.

-file("src/lightspeed/form_compat.gleam", 147).
-spec apply_change(
    list(input_state()),
    binary(),
    lightspeed@form:form_data(),
    integer(),
    list(input_state())
) -> {list(input_state()), dispatch()}.
apply_change(States, Input, Payload, At_ms, States_rev) ->
    case States of
        [] ->
            {lists:reverse(States_rev),
                {suppressed, Input, <<"unknown_input"/utf8>>}};

        [State | Rest] ->
            case erlang:element(2, State) =:= Input of
                false ->
                    apply_change(
                        Rest,
                        Input,
                        Payload,
                        At_ms,
                        [State | States_rev]
                    );

                true ->
                    case erlang:element(3, State) of
                        immediate ->
                            Next = {input_state,
                                erlang:element(2, State),
                                erlang:element(3, State),
                                {some, At_ms},
                                none,
                                none},
                            {rebuild(States_rev, Next, Rest),
                                {dispatch_change, Input, Payload}};

                        {debounce, Wait_ms} ->
                            case Wait_ms =< 0 of
                                true ->
                                    Next@1 = {input_state,
                                        erlang:element(2, State),
                                        erlang:element(3, State),
                                        {some, At_ms},
                                        none,
                                        none},
                                    {rebuild(States_rev, Next@1, Rest),
                                        {dispatch_change, Input, Payload}};

                                false ->
                                    Next@2 = {input_state,
                                        erlang:element(2, State),
                                        erlang:element(3, State),
                                        erlang:element(4, State),
                                        {some, Payload},
                                        {some, At_ms}},
                                    {rebuild(States_rev, Next@2, Rest),
                                        {queued, Input}}
                            end;

                        debounce_blur ->
                            Next@3 = {input_state,
                                erlang:element(2, State),
                                erlang:element(3, State),
                                erlang:element(4, State),
                                {some, Payload},
                                {some, At_ms}},
                            {rebuild(States_rev, Next@3, Rest), {queued, Input}};

                        {throttle, Window_ms} ->
                            case should_dispatch_throttle(
                                erlang:element(4, State),
                                At_ms,
                                Window_ms
                            ) of
                                true ->
                                    Next@4 = {input_state,
                                        erlang:element(2, State),
                                        erlang:element(3, State),
                                        {some, At_ms},
                                        none,
                                        none},
                                    {rebuild(States_rev, Next@4, Rest),
                                        {dispatch_change, Input, Payload}};

                                false ->
                                    {rebuild(States_rev, State, Rest),
                                        {suppressed,
                                            Input,
                                            <<"throttled"/utf8>>}}
                            end
                    end
            end
    end.

-file("src/lightspeed/form_compat.gleam", 356).
-spec clear_timers(input_state()) -> input_state().
clear_timers(State) ->
    {input_state,
        erlang:element(2, State),
        erlang:element(3, State),
        none,
        none,
        none}.

-file("src/lightspeed/form_compat.gleam", 325).
-spec reset_other_timers(list(input_state()), binary(), list(input_state())) -> list(input_state()).
reset_other_timers(States, Selected_input, States_rev) ->
    case States of
        [] ->
            lists:reverse(States_rev);

        [State | Rest] ->
            case erlang:element(2, State) =:= Selected_input of
                true ->
                    reset_other_timers(
                        Rest,
                        Selected_input,
                        [State | States_rev]
                    );

                false ->
                    reset_other_timers(
                        Rest,
                        Selected_input,
                        [clear_timers(State) | States_rev]
                    )
            end
    end.

-file("src/lightspeed/form_compat.gleam", 56).
?DOC(
    " Handle one `phx-change`-style event.\n"
    "\n"
    " Semantics:\n"
    " - debounce stores pending payload\n"
    " - debounce blur stores pending payload until blur\n"
    " - throttle emits immediately then suppresses inside window\n"
    " - change for one input resets timers for all other inputs\n"
).
-spec change(runtime(), binary(), binary(), integer()) -> {runtime(),
    dispatch()}.
change(Runtime, Input, Payload, At_ms) ->
    Parsed = lightspeed@form:parse_payload(Payload),
    Inputs = reset_other_timers(erlang:element(2, Runtime), Input, []),
    {Inputs@1, Dispatch} = apply_change(Inputs, Input, Parsed, At_ms, []),
    {{runtime, Inputs@1}, Dispatch}.

-file("src/lightspeed/form_compat.gleam", 250).
-spec apply_blur(list(input_state()), binary(), integer(), list(input_state())) -> {list(input_state()),
    dispatch()}.
apply_blur(States, Input, At_ms, States_rev) ->
    case States of
        [] ->
            {lists:reverse(States_rev),
                {suppressed, Input, <<"unknown_input"/utf8>>}};

        [State | Rest] ->
            case erlang:element(2, State) =:= Input of
                false ->
                    apply_blur(Rest, Input, At_ms, [State | States_rev]);

                true ->
                    case {erlang:element(3, State), erlang:element(5, State)} of
                        {debounce_blur, {some, Payload}} ->
                            Next = {input_state,
                                erlang:element(2, State),
                                erlang:element(3, State),
                                {some, At_ms},
                                none,
                                none},
                            {rebuild(States_rev, Next, Rest),
                                {dispatch_change, Input, Payload}};

                        {_, _} ->
                            {rebuild(States_rev, State, Rest), no_dispatch}
                    end
            end
    end.

-file("src/lightspeed/form_compat.gleam", 69).
?DOC(" Handle one blur event used by `phx-debounce=\"blur\"`.\n").
-spec blur(runtime(), binary(), integer()) -> {runtime(), dispatch()}.
blur(Runtime, Input, At_ms) ->
    {Inputs, Dispatch} = apply_blur(
        erlang:element(2, Runtime),
        Input,
        At_ms,
        []
    ),
    {{runtime, Inputs}, Dispatch}.

-file("src/lightspeed/form_compat.gleam", 288).
-spec flush_debounce(
    list(input_state()),
    integer(),
    list(input_state()),
    list(dispatch())
) -> {list(input_state()), list(dispatch())}.
flush_debounce(States, At_ms, States_rev, Dispatches_rev) ->
    case States of
        [] ->
            {lists:reverse(States_rev), Dispatches_rev};

        [State | Rest] ->
            case {erlang:element(3, State),
                erlang:element(5, State),
                erlang:element(6, State)} of
                {{debounce, Wait_ms}, {some, Payload}, {some, Pending_since}} ->
                    case (At_ms - Pending_since) >= Wait_ms of
                        true ->
                            Next = {input_state,
                                erlang:element(2, State),
                                erlang:element(3, State),
                                {some, At_ms},
                                none,
                                none},
                            flush_debounce(
                                Rest,
                                At_ms,
                                [Next | States_rev],
                                [{dispatch_change,
                                        erlang:element(2, State),
                                        Payload} |
                                    Dispatches_rev]
                            );

                        false ->
                            flush_debounce(
                                Rest,
                                At_ms,
                                [State | States_rev],
                                Dispatches_rev
                            )
                    end;

                {_, _, _} ->
                    flush_debounce(
                        Rest,
                        At_ms,
                        [State | States_rev],
                        Dispatches_rev
                    )
            end
    end.

-file("src/lightspeed/form_compat.gleam", 79).
?DOC(" Flush due debounce timers.\n").
-spec tick(runtime(), integer()) -> {runtime(), list(dispatch())}.
tick(Runtime, At_ms) ->
    {Inputs, Dispatches_rev} = flush_debounce(
        erlang:element(2, Runtime),
        At_ms,
        [],
        []
    ),
    {{runtime, Inputs}, lists:reverse(Dispatches_rev)}.

-file("src/lightspeed/form_compat.gleam", 345).
-spec clear_all_timers(list(input_state()), list(input_state())) -> list(input_state()).
clear_all_timers(States, States_rev) ->
    case States of
        [] ->
            lists:reverse(States_rev);

        [State | Rest] ->
            clear_all_timers(Rest, [clear_timers(State) | States_rev])
    end.

-file("src/lightspeed/form_compat.gleam", 87).
?DOC(
    " Handle one `phx-submit`-style event.\n"
    "\n"
    " Submit resets existing change timers.\n"
).
-spec submit(runtime(), binary(), integer()) -> {runtime(), dispatch()}.
submit(Runtime, Payload, _) ->
    Inputs = clear_all_timers(erlang:element(2, Runtime), []),
    {{runtime, Inputs},
        {dispatch_submit, lightspeed@form:parse_payload(Payload)}}.

-file("src/lightspeed/form_compat.gleam", 100).
?DOC(" Stable dispatch label for tests and logs.\n").
-spec dispatch_label(dispatch()) -> binary().
dispatch_label(Dispatch) ->
    case Dispatch of
        {dispatch_change, _, _} ->
            <<"change"/utf8>>;

        {dispatch_submit, _} ->
            <<"submit"/utf8>>;

        {queued, _} ->
            <<"queued"/utf8>>;

        {suppressed, _, Reason} ->
            <<"suppressed:"/utf8, Reason/binary>>;

        no_dispatch ->
            <<"none"/utf8>>
    end.

-file("src/lightspeed/form_compat.gleam", 381).
-spec find_input(list(input_state()), binary()) -> gleam@option:option(input_state()).
find_input(States, Name) ->
    case States of
        [] ->
            none;

        [State | Rest] ->
            case erlang:element(2, State) =:= Name of
                true ->
                    {some, State};

                false ->
                    find_input(Rest, Name)
            end
    end.

-file("src/lightspeed/form_compat.gleam", 111).
?DOC(" Return pending status for one input.\n").
-spec has_pending(runtime(), binary()) -> boolean().
has_pending(Runtime, Input) ->
    case find_input(erlang:element(2, Runtime), Input) of
        {some, {input_state, _, _, _, {some, _}, _}} ->
            true;

        _ ->
            false
    end.

-file("src/lightspeed/form_compat.gleam", 119).
?DOC(" Return the current mode for one input.\n").
-spec mode(runtime(), binary()) -> gleam@option:option(change_mode()).
mode(Runtime, Input) ->
    case find_input(erlang:element(2, Runtime), Input) of
        {some, {input_state, _, Mode, _, _, _}} ->
            {some, Mode};

        none ->
            none
    end.