src/lightspeed@upload.erl

-module(lightspeed@upload).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/upload.gleam").
-export([allow/5, name/1, mode/1, entries/1, errors/1, add_entry/5, entry_status_label/1, prepare_external/3, start_upload/2, progress/3, complete/3, fail/3, cancel/3, error_to_string/1]).
-export_type([upload_mode/0, entry_status/0, upload_entry/0, upload_error/0, upload/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 upload compatibility model.\n").

-type upload_mode() :: direct_server | {external_client, binary()}.

-type entry_status() :: selected |
    {external_prepared, binary()} |
    {uploading, integer()} |
    {completed, binary()} |
    {cancelled, binary()} |
    {failed, binary()}.

-type upload_entry() :: {upload_entry,
        binary(),
        binary(),
        integer(),
        binary(),
        entry_status()}.

-type upload_error() :: {too_many_files, integer()} |
    {too_large, binary(), integer(), integer()} |
    {not_accepted, binary(), list(binary())} |
    {duplicate_ref, binary()} |
    {unknown_ref, binary()} |
    {invalid_progress, binary(), integer()} |
    {invalid_transition, binary(), binary(), binary()}.

-opaque upload() :: {upload,
        binary(),
        upload_mode(),
        list(binary()),
        integer(),
        integer(),
        list(upload_entry()),
        list(upload_error())}.

-file("src/lightspeed/upload.gleam", 59).
?DOC(" Build one upload configuration.\n").
-spec allow(binary(), upload_mode(), list(binary()), integer(), integer()) -> upload().
allow(Name, Mode, Accept, Max_entries, Max_file_size) ->
    {upload, Name, Mode, Accept, Max_entries, Max_file_size, [], []}.

-file("src/lightspeed/upload.gleam", 78).
?DOC(" Upload name.\n").
-spec name(upload()) -> binary().
name(Upload) ->
    erlang:element(2, Upload).

-file("src/lightspeed/upload.gleam", 83).
?DOC(" Upload mode.\n").
-spec mode(upload()) -> upload_mode().
mode(Upload) ->
    erlang:element(3, Upload).

-file("src/lightspeed/upload.gleam", 88).
?DOC(" Entries in selection order.\n").
-spec entries(upload()) -> list(upload_entry()).
entries(Upload) ->
    lists:reverse(erlang:element(7, Upload)).

-file("src/lightspeed/upload.gleam", 93).
?DOC(" Errors in occurrence order.\n").
-spec errors(upload()) -> list(upload_error()).
errors(Upload) ->
    lists:reverse(erlang:element(8, Upload)).

-file("src/lightspeed/upload.gleam", 370).
-spec reject(upload(), upload_error()) -> {upload(),
    {ok, upload_entry()} | {error, upload_error()}}.
reject(Upload, Error) ->
    {{upload,
            erlang:element(2, Upload),
            erlang:element(3, Upload),
            erlang:element(4, Upload),
            erlang:element(5, Upload),
            erlang:element(6, Upload),
            erlang:element(7, Upload),
            [Error | erlang:element(8, Upload)]},
        {error, Error}}.

-file("src/lightspeed/upload.gleam", 421).
-spec accepted_pattern(binary(), binary(), binary()) -> boolean().
accepted_pattern(Pattern, Client_name, Client_type) ->
    case Pattern of
        <<"*/*"/utf8>> ->
            true;

        _ ->
            case gleam_stdlib:string_starts_with(Pattern, <<"."/utf8>>) of
                true ->
                    gleam_stdlib:string_ends_with(Client_name, Pattern);

                false ->
                    case gleam_stdlib:string_ends_with(Pattern, <<"/*"/utf8>>) of
                        true ->
                            gleam_stdlib:string_starts_with(
                                Client_type,
                                gleam@string:replace(
                                    Pattern,
                                    <<"*"/utf8>>,
                                    <<""/utf8>>
                                )
                            );

                        false ->
                            Client_type =:= Pattern
                    end
            end
    end.

-file("src/lightspeed/upload.gleam", 404).
-spec accepted_any(list(binary()), binary(), binary()) -> boolean().
accepted_any(Patterns, Client_name, Client_type) ->
    case Patterns of
        [] ->
            false;

        [Pattern | Rest] ->
            case accepted_pattern(
                string:lowercase(Pattern),
                Client_name,
                Client_type
            ) of
                true ->
                    true;

                false ->
                    accepted_any(Rest, Client_name, Client_type)
            end
    end.

-file("src/lightspeed/upload.gleam", 388).
-spec accepted(list(binary()), binary(), binary()) -> boolean().
accepted(Accept, Client_name, Client_type) ->
    case Accept of
        [] ->
            true;

        _ ->
            accepted_any(
                Accept,
                string:lowercase(Client_name),
                string:lowercase(Client_type)
            )
    end.

-file("src/lightspeed/upload.gleam", 377).
-spec has_ref(list(upload_entry()), binary()) -> boolean().
has_ref(Entries_rev, Ref) ->
    case Entries_rev of
        [] ->
            false;

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

                false ->
                    has_ref(Rest, Ref)
            end
    end.

-file("src/lightspeed/upload.gleam", 98).
?DOC(" Add one selected file entry with validation.\n").
-spec add_entry(upload(), binary(), binary(), integer(), binary()) -> {upload(),
    {ok, upload_entry()} | {error, upload_error()}}.
add_entry(Upload, Ref, Client_name, Client_size, Client_type) ->
    case erlang:length(erlang:element(7, Upload)) >= erlang:element(5, Upload) of
        true ->
            reject(Upload, {too_many_files, erlang:element(5, Upload)});

        false ->
            case has_ref(erlang:element(7, Upload), Ref) of
                true ->
                    reject(Upload, {duplicate_ref, Ref});

                false ->
                    case Client_size > erlang:element(6, Upload) of
                        true ->
                            reject(
                                Upload,
                                {too_large,
                                    Ref,
                                    Client_size,
                                    erlang:element(6, Upload)}
                            );

                        false ->
                            case accepted(
                                erlang:element(4, Upload),
                                Client_name,
                                Client_type
                            ) of
                                false ->
                                    reject(
                                        Upload,
                                        {not_accepted,
                                            Ref,
                                            erlang:element(4, Upload)}
                                    );

                                true ->
                                    Entry = {upload_entry,
                                        Ref,
                                        Client_name,
                                        Client_size,
                                        Client_type,
                                        selected},
                                    {{upload,
                                            erlang:element(2, Upload),
                                            erlang:element(3, Upload),
                                            erlang:element(4, Upload),
                                            erlang:element(5, Upload),
                                            erlang:element(6, Upload),
                                            [Entry | erlang:element(7, Upload)],
                                            erlang:element(8, Upload)},
                                        {ok, Entry}}
                            end
                    end
            end
    end.

-file("src/lightspeed/upload.gleam", 300).
?DOC(" Stable status label for assertions and logs.\n").
-spec entry_status_label(entry_status()) -> binary().
entry_status_label(Status) ->
    case Status of
        selected ->
            <<"selected"/utf8>>;

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

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

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

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

        {failed, _} ->
            <<"failed"/utf8>>
    end.

-file("src/lightspeed/upload.gleam", 441).
-spec reverse_into(list(UOA), list(UOA)) -> list(UOA).
reverse_into(Left, Right) ->
    case Left of
        [] ->
            Right;

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

-file("src/lightspeed/upload.gleam", 347).
-spec update_entry(
    list(upload_entry()),
    binary(),
    fun((upload_entry()) -> {ok, upload_entry()} | {error, upload_error()}),
    list(upload_entry())
) -> {ok, {list(upload_entry()), upload_entry()}} | {error, upload_error()}.
update_entry(Entries_rev, Ref, With, Seen_rev) ->
    case Entries_rev of
        [] ->
            {error, {unknown_ref, Ref}};

        [Entry | Rest] ->
            case erlang:element(2, Entry) =:= Ref of
                true ->
                    case With(Entry) of
                        {error, Error} ->
                            {error, Error};

                        {ok, Updated} ->
                            {ok,
                                {reverse_into(Seen_rev, [Updated | Rest]),
                                    Updated}}
                    end;

                false ->
                    update_entry(Rest, Ref, With, [Entry | Seen_rev])
            end
    end.

-file("src/lightspeed/upload.gleam", 332).
-spec transition(
    upload(),
    binary(),
    binary(),
    fun((upload_entry()) -> {ok, upload_entry()} | {error, upload_error()})
) -> {upload(), {ok, upload_entry()} | {error, upload_error()}}.
transition(Upload, Ref, _, With) ->
    case update_entry(erlang:element(7, Upload), Ref, With, []) of
        {error, Error} ->
            reject(Upload, Error);

        {ok, {Entries_rev, Entry}} ->
            {{upload,
                    erlang:element(2, Upload),
                    erlang:element(3, Upload),
                    erlang:element(4, Upload),
                    erlang:element(5, Upload),
                    erlang:element(6, Upload),
                    Entries_rev,
                    erlang:element(8, Upload)},
                {ok, Entry}}
    end.

-file("src/lightspeed/upload.gleam", 150).
?DOC(" Prepare one external upload entry with a preflight URL.\n").
-spec prepare_external(upload(), binary(), binary()) -> {upload(),
    {ok, upload_entry()} | {error, upload_error()}}.
prepare_external(Upload, Ref, Upload_url) ->
    case erlang:element(3, Upload) of
        direct_server ->
            reject(
                Upload,
                {invalid_transition,
                    Ref,
                    <<"direct_mode"/utf8>>,
                    <<"prepare_external"/utf8>>}
            );

        {external_client, _} ->
            transition(
                Upload,
                Ref,
                <<"prepare_external"/utf8>>,
                fun(Entry) -> case erlang:element(6, Entry) of
                        selected ->
                            {ok,
                                {upload_entry,
                                    erlang:element(2, Entry),
                                    erlang:element(3, Entry),
                                    erlang:element(4, Entry),
                                    erlang:element(5, Entry),
                                    {external_prepared, Upload_url}}};

                        _ ->
                            {error,
                                {invalid_transition,
                                    Ref,
                                    entry_status_label(erlang:element(6, Entry)),
                                    <<"prepare_external"/utf8>>}}
                    end end
            )
    end.

-file("src/lightspeed/upload.gleam", 189).
?DOC(" Start uploading one entry.\n").
-spec start_upload(upload(), binary()) -> {upload(),
    {ok, upload_entry()} | {error, upload_error()}}.
start_upload(Upload, Ref) ->
    transition(
        Upload,
        Ref,
        <<"start_upload"/utf8>>,
        fun(Entry) ->
            case {erlang:element(3, Upload), erlang:element(6, Entry)} of
                {direct_server, selected} ->
                    {ok,
                        {upload_entry,
                            erlang:element(2, Entry),
                            erlang:element(3, Entry),
                            erlang:element(4, Entry),
                            erlang:element(5, Entry),
                            {uploading, 0}}};

                {{external_client, _}, {external_prepared, _}} ->
                    {ok,
                        {upload_entry,
                            erlang:element(2, Entry),
                            erlang:element(3, Entry),
                            erlang:element(4, Entry),
                            erlang:element(5, Entry),
                            {uploading, 0}}};

                {_, _} ->
                    {error,
                        {invalid_transition,
                            Ref,
                            entry_status_label(erlang:element(6, Entry)),
                            <<"start_upload"/utf8>>}}
            end
        end
    ).

-file("src/lightspeed/upload.gleam", 212).
?DOC(" Update upload progress.\n").
-spec progress(upload(), binary(), integer()) -> {upload(),
    {ok, upload_entry()} | {error, upload_error()}}.
progress(Upload, Ref, Progress) ->
    case (Progress < 0) orelse (Progress > 100) of
        true ->
            reject(Upload, {invalid_progress, Ref, Progress});

        false ->
            transition(
                Upload,
                Ref,
                <<"progress"/utf8>>,
                fun(Entry) -> case erlang:element(6, Entry) of
                        {external_prepared, _} ->
                            {ok,
                                {upload_entry,
                                    erlang:element(2, Entry),
                                    erlang:element(3, Entry),
                                    erlang:element(4, Entry),
                                    erlang:element(5, Entry),
                                    {uploading, Progress}}};

                        {uploading, _} ->
                            {ok,
                                {upload_entry,
                                    erlang:element(2, Entry),
                                    erlang:element(3, Entry),
                                    erlang:element(4, Entry),
                                    erlang:element(5, Entry),
                                    {uploading, Progress}}};

                        _ ->
                            {error,
                                {invalid_transition,
                                    Ref,
                                    entry_status_label(erlang:element(6, Entry)),
                                    <<"progress"/utf8>>}}
                    end end
            )
    end.

-file("src/lightspeed/upload.gleam", 238).
?DOC(" Complete one upload entry.\n").
-spec complete(upload(), binary(), binary()) -> {upload(),
    {ok, upload_entry()} | {error, upload_error()}}.
complete(Upload, Ref, Location) ->
    transition(
        Upload,
        Ref,
        <<"complete"/utf8>>,
        fun(Entry) -> case erlang:element(6, Entry) of
                {uploading, _} ->
                    {ok,
                        {upload_entry,
                            erlang:element(2, Entry),
                            erlang:element(3, Entry),
                            erlang:element(4, Entry),
                            erlang:element(5, Entry),
                            {completed, Location}}};

                _ ->
                    {error,
                        {invalid_transition,
                            Ref,
                            entry_status_label(erlang:element(6, Entry)),
                            <<"complete"/utf8>>}}
            end end
    ).

-file("src/lightspeed/upload.gleam", 258).
?DOC(" Mark one upload entry as failed.\n").
-spec fail(upload(), binary(), binary()) -> {upload(),
    {ok, upload_entry()} | {error, upload_error()}}.
fail(Upload, Ref, Reason) ->
    transition(
        Upload,
        Ref,
        <<"fail"/utf8>>,
        fun(Entry) -> case erlang:element(6, Entry) of
                {uploading, _} ->
                    {ok,
                        {upload_entry,
                            erlang:element(2, Entry),
                            erlang:element(3, Entry),
                            erlang:element(4, Entry),
                            erlang:element(5, Entry),
                            {failed, Reason}}};

                _ ->
                    {error,
                        {invalid_transition,
                            Ref,
                            entry_status_label(erlang:element(6, Entry)),
                            <<"fail"/utf8>>}}
            end end
    ).

-file("src/lightspeed/upload.gleam", 277).
?DOC(" Cancel one upload entry.\n").
-spec cancel(upload(), binary(), binary()) -> {upload(),
    {ok, upload_entry()} | {error, upload_error()}}.
cancel(Upload, Ref, Reason) ->
    transition(
        Upload,
        Ref,
        <<"cancel"/utf8>>,
        fun(Entry) -> case erlang:element(6, Entry) of
                selected ->
                    {ok,
                        {upload_entry,
                            erlang:element(2, Entry),
                            erlang:element(3, Entry),
                            erlang:element(4, Entry),
                            erlang:element(5, Entry),
                            {cancelled, Reason}}};

                {external_prepared, _} ->
                    {ok,
                        {upload_entry,
                            erlang:element(2, Entry),
                            erlang:element(3, Entry),
                            erlang:element(4, Entry),
                            erlang:element(5, Entry),
                            {cancelled, Reason}}};

                {uploading, _} ->
                    {ok,
                        {upload_entry,
                            erlang:element(2, Entry),
                            erlang:element(3, Entry),
                            erlang:element(4, Entry),
                            erlang:element(5, Entry),
                            {cancelled, Reason}}};

                _ ->
                    {error,
                        {invalid_transition,
                            Ref,
                            entry_status_label(erlang:element(6, Entry)),
                            <<"cancel"/utf8>>}}
            end end
    ).

-file("src/lightspeed/upload.gleam", 312).
?DOC(" Stable error label for assertions and logs.\n").
-spec error_to_string(upload_error()) -> binary().
error_to_string(Error) ->
    case Error of
        {too_many_files, Max_entries} ->
            <<"too_many_files:"/utf8,
                (erlang:integer_to_binary(Max_entries))/binary>>;

        {too_large, Ref, Size_bytes, Max_bytes} ->
            <<<<<<<<<<"too_large:"/utf8, Ref/binary>>/binary, ":"/utf8>>/binary,
                        (erlang:integer_to_binary(Size_bytes))/binary>>/binary,
                    ":"/utf8>>/binary,
                (erlang:integer_to_binary(Max_bytes))/binary>>;

        {not_accepted, Ref@1, _} ->
            <<"not_accepted:"/utf8, Ref@1/binary>>;

        {duplicate_ref, Ref@2} ->
            <<"duplicate_ref:"/utf8, Ref@2/binary>>;

        {unknown_ref, Ref@3} ->
            <<"unknown_ref:"/utf8, Ref@3/binary>>;

        {invalid_progress, Ref@4, Progress} ->
            <<<<<<"invalid_progress:"/utf8, Ref@4/binary>>/binary, ":"/utf8>>/binary,
                (erlang:integer_to_binary(Progress))/binary>>;

        {invalid_transition, Ref@5, Status, Action} ->
            <<<<<<<<<<"invalid_transition:"/utf8, Ref@5/binary>>/binary,
                            ":"/utf8>>/binary,
                        Status/binary>>/binary,
                    ":"/utf8>>/binary,
                Action/binary>>
    end.