src/lightspeed@stream.erl

-module(lightspeed@stream).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/stream.gleam").
-export([new/1, item/2, options/0, with_at/2, with_limit/2, with_reset/2, target/1, entries/1, stream/3, stream_insert/4, stream_delete/2]).
-export_type([stream_item/0, stream/0, stream_options/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 stream compatibility helpers.\n").

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

-opaque stream() :: {stream, binary(), list(stream_item())}.

-type stream_options() :: {stream_options,
        integer(),
        gleam@option:option(integer()),
        boolean()}.

-file("src/lightspeed/stream.gleam", 23).
?DOC(" Create a new empty stream state for a target container.\n").
-spec new(binary()) -> stream().
new(Target) ->
    {stream, Target, []}.

-file("src/lightspeed/stream.gleam", 28).
?DOC(" Build one stream item.\n").
-spec item(binary(), binary()) -> stream_item().
item(Id, Html) ->
    {stream_item, Id, Html}.

-file("src/lightspeed/stream.gleam", 36).
?DOC(
    " Default stream options:\n"
    " - at: -1 (append)\n"
    " - limit: none\n"
    " - reset: false\n"
).
-spec options() -> stream_options().
options() ->
    {stream_options, -1, none, false}.

-file("src/lightspeed/stream.gleam", 41).
?DOC(" Override insertion position.\n").
-spec with_at(stream_options(), integer()) -> stream_options().
with_at(Options, At) ->
    {stream_options, At, erlang:element(3, Options), erlang:element(4, Options)}.

-file("src/lightspeed/stream.gleam", 47).
?DOC(
    " Override stream limit.\n"
    " Positive keeps first N; negative keeps last N.\n"
).
-spec with_limit(stream_options(), integer()) -> stream_options().
with_limit(Options, Limit) ->
    {stream_options,
        erlang:element(2, Options),
        {some, Limit},
        erlang:element(4, Options)}.

-file("src/lightspeed/stream.gleam", 52).
?DOC(" Override reset behavior.\n").
-spec with_reset(stream_options(), boolean()) -> stream_options().
with_reset(Options, Reset) ->
    {stream_options,
        erlang:element(2, Options),
        erlang:element(3, Options),
        Reset}.

-file("src/lightspeed/stream.gleam", 57).
?DOC(" Current target selector.\n").
-spec target(stream()) -> binary().
target(Stream_state) ->
    erlang:element(2, Stream_state).

-file("src/lightspeed/stream.gleam", 62).
?DOC(" Current entries in render order.\n").
-spec entries(stream()) -> list(stream_item()).
entries(Stream_state) ->
    erlang:element(3, Stream_state).

-file("src/lightspeed/stream.gleam", 301).
-spec key_order(list(stream_item()), list(binary())) -> list(binary()).
key_order(Current, Keys_rev) ->
    case Current of
        [] ->
            lists:reverse(Keys_rev);

        [{stream_item, Id, _} | Rest] ->
            key_order(Rest, [Id | Keys_rev])
    end.

-file("src/lightspeed/stream.gleam", 354).
-spec concat(list(lightspeed@diff:patch()), list(lightspeed@diff:patch())) -> list(lightspeed@diff:patch()).
concat(Left, Right) ->
    case Left of
        [] ->
            Right;

        [Head | Rest] ->
            [Head | concat(Rest, Right)]
    end.

-file("src/lightspeed/stream.gleam", 155).
-spec keyed_reorder_patch(
    binary(),
    list(stream_item()),
    list(lightspeed@diff:patch())
) -> list(lightspeed@diff:patch()).
keyed_reorder_patch(Target, Current, Keyed) ->
    concat(Keyed, [{reorder_keyed, Target, key_order(Current, [])}]).

-file("src/lightspeed/stream.gleam", 332).
-spec contains_id(list(stream_item()), binary()) -> boolean().
contains_id(Entries, Id) ->
    case Entries of
        [] ->
            false;

        [Entry | Rest] ->
            case erlang:element(2, Entry) =:= Id of
                true ->
                    true;

                false ->
                    contains_id(Rest, Id)
            end
    end.

-file("src/lightspeed/stream.gleam", 311).
-spec new_ids_only_at_tail(list(stream_item()), list(stream_item()), boolean()) -> boolean().
new_ids_only_at_tail(Current, Previous, Saw_new) ->
    case Current of
        [] ->
            true;

        [{stream_item, Id, _} | Rest] ->
            case contains_id(Previous, Id) of
                true ->
                    case Saw_new of
                        true ->
                            false;

                        false ->
                            new_ids_only_at_tail(Rest, Previous, false)
                    end;

                false ->
                    new_ids_only_at_tail(Rest, Previous, true)
            end
    end.

-file("src/lightspeed/stream.gleam", 285).
-spec collect_existing_ids(
    list(stream_item()),
    list(stream_item()),
    list(binary())
) -> list(binary()).
collect_existing_ids(Left, Right, Ids_rev) ->
    case Left of
        [] ->
            lists:reverse(Ids_rev);

        [{stream_item, Id, _} | Rest] ->
            case contains_id(Right, Id) of
                true ->
                    collect_existing_ids(Rest, Right, [Id | Ids_rev]);

                false ->
                    collect_existing_ids(Rest, Right, Ids_rev)
            end
    end.

-file("src/lightspeed/stream.gleam", 146).
-spec supports_incremental(list(stream_item()), list(stream_item())) -> boolean().
supports_incremental(Previous, Current) ->
    (collect_existing_ids(Current, Previous, []) =:= collect_existing_ids(
        Previous,
        Current,
        []
    ))
    andalso new_ids_only_at_tail(Current, Previous, false).

-file("src/lightspeed/stream.gleam", 344).
-spec to_keyed_nodes(list(stream_item())) -> list(lightspeed@diff:keyed_node()).
to_keyed_nodes(Entries) ->
    case Entries of
        [] ->
            [];

        [{stream_item, Id, Html} | Rest] ->
            [lightspeed@diff:keyed_node(Id, Html) | to_keyed_nodes(Rest)]
    end.

-file("src/lightspeed/stream.gleam", 123).
-spec patch_plan(binary(), list(stream_item()), list(stream_item())) -> list(lightspeed@diff:patch()).
patch_plan(Target, Previous, Current) ->
    case Previous =:= Current of
        true ->
            [];

        false ->
            Keyed = lightspeed@diff:keyed_patch_plan(
                Target,
                to_keyed_nodes(Previous),
                to_keyed_nodes(Current)
            ),
            case supports_incremental(Previous, Current) of
                true ->
                    Keyed;

                false ->
                    keyed_reorder_patch(Target, Current, Keyed)
            end
    end.

-file("src/lightspeed/stream.gleam", 274).
-spec drop(list(stream_item()), integer()) -> list(stream_item()).
drop(Entries, Count) ->
    case Count =< 0 of
        true ->
            Entries;

        false ->
            case Entries of
                [] ->
                    [];

                [_ | Rest] ->
                    drop(Rest, Count - 1)
            end
    end.

-file("src/lightspeed/stream.gleam", 265).
-spec take_last(list(stream_item()), integer()) -> list(stream_item()).
take_last(Entries, Count) ->
    Length = erlang:length(Entries),
    case Count >= Length of
        true ->
            Entries;

        false ->
            drop(Entries, Length - Count)
    end.

-file("src/lightspeed/stream.gleam", 254).
-spec take(list(stream_item()), integer()) -> list(stream_item()).
take(Entries, Count) ->
    case Count =< 0 of
        true ->
            [];

        false ->
            case Entries of
                [] ->
                    [];

                [Head | Rest] ->
                    [Head | take(Rest, Count - 1)]
            end
    end.

-file("src/lightspeed/stream.gleam", 235).
-spec apply_limit(list(stream_item()), gleam@option:option(integer())) -> list(stream_item()).
apply_limit(Entries, Limit) ->
    case Limit of
        none ->
            Entries;

        {some, Value} ->
            case Value of
                0 ->
                    [];

                _ ->
                    case Value > 0 of
                        true ->
                            take(Entries, Value);

                        false ->
                            take_last(Entries, 0 - Value)
                    end
            end
    end.

-file("src/lightspeed/stream.gleam", 205).
-spec insert_at(list(stream_item()), stream_item(), integer()) -> list(stream_item()).
insert_at(Entries, Entry, Index) ->
    case Index =< 0 of
        true ->
            [Entry | Entries];

        false ->
            case Entries of
                [] ->
                    [Entry];

                [Head | Rest] ->
                    [Head | insert_at(Rest, Entry, Index - 1)]
            end
    end.

-file("src/lightspeed/stream.gleam", 194).
-spec resolve_insert_index(integer(), integer()) -> integer().
resolve_insert_index(At, Length) ->
    case At < 0 of
        true ->
            Length;

        false ->
            case At > Length of
                true ->
                    Length;

                false ->
                    At
            end
    end.

-file("src/lightspeed/stream.gleam", 220).
-spec remove_entry(list(stream_item()), binary()) -> list(stream_item()).
remove_entry(Entries, Entry_id) ->
    case Entries of
        [] ->
            [];

        [Entry | Rest] ->
            case erlang:element(2, Entry) =:= Entry_id of
                true ->
                    remove_entry(Rest, Entry_id);

                false ->
                    [Entry | remove_entry(Rest, Entry_id)]
            end
    end.

-file("src/lightspeed/stream.gleam", 184).
-spec insert_one(list(stream_item()), stream_item(), integer()) -> list(stream_item()).
insert_one(Entries, Entry, At) ->
    Without_existing = remove_entry(Entries, erlang:element(2, Entry)),
    Index = resolve_insert_index(At, erlang:length(Without_existing)),
    insert_at(Without_existing, Entry, Index).

-file("src/lightspeed/stream.gleam", 165).
-spec insert_many(list(stream_item()), list(stream_item()), integer()) -> list(stream_item()).
insert_many(Entries, Incoming, At) ->
    case Incoming of
        [] ->
            Entries;

        [Entry | Rest] ->
            Next = insert_one(Entries, Entry, At),
            Next_at = case At < 0 of
                true ->
                    At;

                false ->
                    At + 1
            end,
            insert_many(Next, Rest, Next_at)
    end.

-file("src/lightspeed/stream.gleam", 67).
?DOC(" Apply `stream/4` semantics.\n").
-spec stream(stream(), list(stream_item()), stream_options()) -> {stream(),
    list(lightspeed@diff:patch())}.
stream(Stream_state, Incoming, Options) ->
    Previous = erlang:element(3, Stream_state),
    Base = case erlang:element(4, Options) of
        true ->
            [];

        false ->
            Previous
    end,
    Merged = insert_many(Base, Incoming, erlang:element(2, Options)),
    Limited = apply_limit(Merged, erlang:element(3, Options)),
    Next = {stream, erlang:element(2, Stream_state), Limited},
    Patches = patch_plan(erlang:element(2, Stream_state), Previous, Limited),
    {Next, Patches}.

-file("src/lightspeed/stream.gleam", 88).
?DOC(" Apply `stream_insert/4` semantics.\n").
-spec stream_insert(
    stream(),
    stream_item(),
    integer(),
    gleam@option:option(integer())
) -> {stream(), list(lightspeed@diff:patch())}.
stream_insert(Stream_state, Entry, At, Limit) ->
    stream(Stream_state, [Entry], {stream_options, At, Limit, false}).

-file("src/lightspeed/stream.gleam", 102).
?DOC(" Apply `stream_delete/2` semantics.\n").
-spec stream_delete(stream(), binary()) -> {stream(),
    list(lightspeed@diff:patch())}.
stream_delete(Stream_state, Entry_id) ->
    Previous = erlang:element(3, Stream_state),
    Current = remove_entry(Previous, Entry_id),
    Next = {stream, erlang:element(2, Stream_state), Current},
    Patches = case Previous =:= Current of
        true ->
            [];

        false ->
            lightspeed@diff:keyed_patch_plan(
                erlang:element(2, Stream_state),
                to_keyed_nodes(Previous),
                to_keyed_nodes(Current)
            )
    end,
    {Next, Patches}.