src/automata@fsevent@diff.erl

-module(automata@fsevent@diff).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/automata/fsevent/diff.gleam").
-export([compare_entry/2, diff/3]).

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

-file("src/automata/fsevent/diff.gleam", 70).
-spec ops_for_creation(automata@fsevent@entry:entry()) -> gleam@set:set(automata@fsevent@ast:op()).
ops_for_creation(E) ->
    case automata@fsevent@entry:entry_kind(E) of
        file ->
            case automata@fsevent@entry:entry_size(E) > 0 of
                true ->
                    automata@fsevent@op:ops_from_list([create, write]);

                false ->
                    automata@fsevent@op:single_op(create)
            end;

        _ ->
            automata@fsevent@op:single_op(create)
    end.

-file("src/automata/fsevent/diff.gleam", 92).
-spec content_changed(
    automata@fsevent@entry:entry(),
    automata@fsevent@entry:entry()
) -> boolean().
content_changed(Old, New) ->
    case automata@fsevent@entry:entries_have_both_hashes(Old, New) of
        true ->
            not automata@fsevent@entry:entries_content_hash_equal(Old, New);

        false ->
            (automata@fsevent@entry:entry_size(Old) /= automata@fsevent@entry:entry_size(
                New
            ))
            orelse (automata@fsevent@entry:entry_mtime(Old) /= automata@fsevent@entry:entry_mtime(
                New
            ))
    end.

-file("src/automata/fsevent/diff.gleam", 81).
-spec ops_for_modification(
    automata@fsevent@entry:entry(),
    automata@fsevent@entry:entry()
) -> gleam@set:set(automata@fsevent@ast:op()).
ops_for_modification(Old, New) ->
    Mode_changed = automata@fsevent@entry:entry_mode(Old) /= automata@fsevent@entry:entry_mode(
        New
    ),
    Content_changed = content_changed(Old, New),
    case {Content_changed, Mode_changed} of
        {false, false} ->
            automata@fsevent@op:empty_ops();

        {false, true} ->
            automata@fsevent@op:single_op(chmod);

        {true, false} ->
            automata@fsevent@op:single_op(write);

        {true, true} ->
            automata@fsevent@op:ops_from_list([write, chmod])
    end.

-file("src/automata/fsevent/diff.gleam", 57).
?DOC(false).
-spec compare_entry(
    gleam@option:option(automata@fsevent@entry:entry()),
    gleam@option:option(automata@fsevent@entry:entry())
) -> gleam@set:set(automata@fsevent@ast:op()).
compare_entry(Old, New) ->
    case {Old, New} of
        {none, none} ->
            automata@fsevent@op:empty_ops();

        {none, {some, E}} ->
            ops_for_creation(E);

        {{some, _}, none} ->
            automata@fsevent@op:single_op(remove);

        {{some, O}, {some, N}} ->
            case automata@fsevent@entry:entry_kind(O) =:= automata@fsevent@entry:entry_kind(
                N
            ) of
                false ->
                    automata@fsevent@op:ops_from_list([remove, create]);

                true ->
                    ops_for_modification(O, N)
            end
    end.

-file("src/automata/fsevent/diff.gleam", 130).
-spec apply_rename(
    binary(),
    gleam@set:set(automata@fsevent@ast:op()),
    automata@fsevent@snapshot:snapshot(),
    gleam@option:option(automata@fsevent@entry:entry()),
    gleam@dict:dict(binary(), automata@fsevent@path:normalized_path())
) -> {gleam@set:set(automata@fsevent@ast:op()),
    gleam@option:option(automata@fsevent@path:normalized_path())}.
apply_rename(Key, Base_ops, Prev, Curr_entry, Renames) ->
    case Curr_entry of
        none ->
            {Base_ops, none};

        {some, Curr} ->
            case gleam_stdlib:map_get(Renames, Key) of
                {error, _} ->
                    {Base_ops, none};

                {ok, From} ->
                    From_key = automata@fsevent@path:path_to_string(From),
                    Prev_at_from = automata@fsevent@snapshot:lookup_by_key(
                        Prev,
                        From_key
                    ),
                    Modification_ops = case Prev_at_from of
                        {some, Old} ->
                            ops_for_modification(Old, Curr);

                        none ->
                            gleam@set:delete(Base_ops, create)
                    end,
                    With_rename = gleam@set:insert(Modification_ops, rename),
                    {With_rename, {some, From}}
            end
    end.

-file("src/automata/fsevent/diff.gleam", 160).
-spec apply_op_mask(
    {ok, automata@fsevent@event:watch_event()} | {error, nil},
    automata@fsevent@watch:watch()
) -> {ok, automata@fsevent@event:watch_event()} | {error, nil}.
apply_op_mask(Result, Watch) ->
    case Result of
        {error, _} ->
            Result;

        {ok, Ev} ->
            Masked = gleam@set:intersection(
                automata@fsevent@event:event_ops(Ev),
                automata@fsevent@watch:watch_op_mask(Watch)
            ),
            case gleam@set:size(Masked) =:= 0 of
                true ->
                    {error, nil};

                false ->
                    case automata@fsevent@event:multi_op(
                        automata@fsevent@event:event_path(Ev),
                        Masked,
                        case gleam@set:contains(Masked, rename) of
                            true ->
                                automata@fsevent@event:event_renamed_from(Ev);

                            false ->
                                none
                        end
                    ) of
                        {ok, Masked_ev} ->
                            {ok, Masked_ev};

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

-file("src/automata/fsevent/diff.gleam", 190).
-spec primary_path(
    gleam@option:option(automata@fsevent@entry:entry()),
    gleam@option:option(automata@fsevent@entry:entry())
) -> gleam@option:option(automata@fsevent@path:normalized_path()).
primary_path(Curr_entry, Prev_entry) ->
    case Curr_entry of
        {some, E} ->
            {some, automata@fsevent@entry:entry_path(E)};

        none ->
            case Prev_entry of
                {some, E@1} ->
                    {some, automata@fsevent@entry:entry_path(E@1)};

                none ->
                    none
            end
    end.

-file("src/automata/fsevent/diff.gleam", 204).
-spec path_union(
    automata@fsevent@snapshot:snapshot(),
    automata@fsevent@snapshot:snapshot()
) -> list(binary()).
path_union(Prev, Curr) ->
    Prev_keys = gleam@set:from_list(automata@fsevent@snapshot:paths(Prev)),
    Curr_keys = gleam@set:from_list(automata@fsevent@snapshot:paths(Curr)),
    _pipe = gleam@set:union(Prev_keys, Curr_keys),
    gleam@set:to_list(_pipe).

-file("src/automata/fsevent/diff.gleam", 211).
-spec lookup_by_key(automata@fsevent@snapshot:snapshot(), binary()) -> gleam@option:option(automata@fsevent@entry:entry()).
lookup_by_key(S, Key) ->
    automata@fsevent@snapshot:lookup_by_key(S, Key).

-file("src/automata/fsevent/diff.gleam", 101).
-spec build_event_for(
    binary(),
    automata@fsevent@snapshot:snapshot(),
    automata@fsevent@snapshot:snapshot(),
    gleam@dict:dict(binary(), automata@fsevent@path:normalized_path())
) -> {ok, automata@fsevent@event:watch_event()} | {error, nil}.
build_event_for(Key, Prev, Curr, Renames) ->
    Prev_entry = lookup_by_key(Prev, Key),
    Curr_entry = lookup_by_key(Curr, Key),
    Base_ops = compare_entry(Prev_entry, Curr_entry),
    {Final_ops, Renamed_from} = apply_rename(
        Key,
        Base_ops,
        Prev,
        Curr_entry,
        Renames
    ),
    case gleam@set:size(Final_ops) =:= 0 of
        true ->
            {error, nil};

        false ->
            Target_path = primary_path(Curr_entry, Prev_entry),
            case Target_path of
                none ->
                    {error, nil};

                {some, P} ->
                    case automata@fsevent@event:multi_op(
                        P,
                        Final_ops,
                        Renamed_from
                    ) of
                        {ok, Ev} ->
                            {ok, Ev};

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

-file("src/automata/fsevent/diff.gleam", 242).
-spec match_disappearance_to_appearance(
    list(automata@fsevent@entry:entry()),
    list(automata@fsevent@entry:entry()),
    gleam@set:set(binary()),
    gleam@set:set(binary()),
    gleam@dict:dict(binary(), automata@fsevent@path:normalized_path())
) -> gleam@dict:dict(binary(), automata@fsevent@path:normalized_path()).
match_disappearance_to_appearance(
    Prev_entries,
    Curr_entries,
    Curr_paths,
    Prev_paths,
    Acc
) ->
    Disappeared = begin
        _pipe = Prev_entries,
        gleam@list:filter(
            _pipe,
            fun(E) ->
                not gleam@set:contains(
                    Curr_paths,
                    automata@fsevent@snapshot:entry_key(E)
                )
            end
        )
    end,
    Appeared = begin
        _pipe@1 = Curr_entries,
        gleam@list:filter(
            _pipe@1,
            fun(E@1) ->
                not gleam@set:contains(
                    Prev_paths,
                    automata@fsevent@snapshot:entry_key(E@1)
                )
            end
        )
    end,
    case {Disappeared, Appeared} of
        {[Old], [New]} ->
            gleam@dict:insert(
                Acc,
                automata@fsevent@snapshot:entry_key(New),
                automata@fsevent@entry:entry_path(Old)
            );

        {_, _} ->
            Acc
    end.

-file("src/automata/fsevent/diff.gleam", 262).
-spec group_by_file_id(list(automata@fsevent@entry:entry())) -> gleam@dict:dict(binary(), list(automata@fsevent@entry:entry())).
group_by_file_id(Entries) ->
    gleam@list:fold(
        Entries,
        maps:new(),
        fun(Acc, E) -> case automata@fsevent@entry:entry_file_id(E) of
                none ->
                    Acc;

                {some, Id} ->
                    case gleam_stdlib:map_get(Acc, Id) of
                        {ok, Existing} ->
                            gleam@dict:insert(Acc, Id, [E | Existing]);

                        {error, _} ->
                            gleam@dict:insert(Acc, Id, [E])
                    end
            end end
    ).

-file("src/automata/fsevent/diff.gleam", 215).
-spec detect_renames(
    automata@fsevent@snapshot:snapshot(),
    automata@fsevent@snapshot:snapshot()
) -> gleam@dict:dict(binary(), automata@fsevent@path:normalized_path()).
detect_renames(Prev, Curr) ->
    Prev_groups = group_by_file_id(
        automata@fsevent@snapshot:entries_unsorted(Prev)
    ),
    Curr_groups = group_by_file_id(
        automata@fsevent@snapshot:entries_unsorted(Curr)
    ),
    Prev_paths = gleam@set:from_list(automata@fsevent@snapshot:paths(Prev)),
    Curr_paths = gleam@set:from_list(automata@fsevent@snapshot:paths(Curr)),
    _pipe = Prev_groups,
    _pipe@1 = maps:to_list(_pipe),
    gleam@list:fold(
        _pipe@1,
        maps:new(),
        fun(Acc, Pair) ->
            {File_id, Prev_entries} = Pair,
            case gleam_stdlib:map_get(Curr_groups, File_id) of
                {error, _} ->
                    Acc;

                {ok, Curr_entries} ->
                    match_disappearance_to_appearance(
                        Prev_entries,
                        Curr_entries,
                        Curr_paths,
                        Prev_paths,
                        Acc
                    )
            end
        end
    ).

-file("src/automata/fsevent/diff.gleam", 275).
-spec renames_suppressed_paths(
    gleam@dict:dict(binary(), automata@fsevent@path:normalized_path())
) -> gleam@set:set(binary()).
renames_suppressed_paths(Renames) ->
    _pipe = Renames,
    _pipe@1 = maps:to_list(_pipe),
    _pipe@2 = gleam@list:map(
        _pipe@1,
        fun(Pair) ->
            automata@fsevent@path:path_to_string(erlang:element(2, Pair))
        end
    ),
    gleam@set:from_list(_pipe@2).

-file("src/automata/fsevent/diff.gleam", 27).
?DOC(
    " Compute the events emitted when `prev` is replaced by `curr` under\n"
    " the subscription described by `watch`.\n"
    "\n"
    " The result list is sorted by canonical path so the output is\n"
    " deterministic across the BEAM and JavaScript targets.\n"
    "\n"
    " Rename detection: when the same `file_id` is observed at two\n"
    " different paths across `prev` and `curr` and the disappearance and\n"
    " appearance pair up one-to-one, the differ folds the would-be\n"
    " `Remove` (at the old path) and `Create` (at the new path) into a\n"
    " single `Rename` event whose `renamed_from` carries the old path.\n"
    " If no `file_id` is available, or the pairing is ambiguous, the\n"
    " rename is reported as a `Remove`/`Create` pair instead.\n"
).
-spec diff(
    automata@fsevent@snapshot:snapshot(),
    automata@fsevent@snapshot:snapshot(),
    automata@fsevent@watch:watch()
) -> list(automata@fsevent@event:watch_event()).
diff(Prev, Curr, Watch) ->
    Renames = detect_renames(Prev, Curr),
    Suppressed_old_paths = renames_suppressed_paths(Renames),
    Union = path_union(Prev, Curr),
    _pipe = Union,
    _pipe@2 = gleam@list:filter_map(
        _pipe,
        fun(Key) -> case gleam@set:contains(Suppressed_old_paths, Key) of
                true ->
                    {error, nil};

                false ->
                    _pipe@1 = build_event_for(Key, Prev, Curr, Renames),
                    apply_op_mask(_pipe@1, Watch)
            end end
    ),
    gleam@list:sort(
        _pipe@2,
        fun(A, B) ->
            gleam@string:compare(
                automata@fsevent@path:path_to_string(
                    automata@fsevent@event:event_path(A)
                ),
                automata@fsevent@path:path_to_string(
                    automata@fsevent@event:event_path(B)
                )
            )
        end
    ).