-module(gleam@time@duration).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gleam/time/duration.gleam").
-export([approximate/1, compare/2, difference/2, add/2, seconds/1, minutes/1, hours/1, milliseconds/1, nanoseconds/1, to_seconds/1, to_seconds_and_nanoseconds/1, to_iso8601_string/1]).
-export_type([duration/0, unit/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.

-opaque duration() :: {duration, integer(), integer()}.

-type unit() :: nanosecond |
    microsecond |
    millisecond |
    second |
    minute |
    hour |
    day |
    week |
    month |
    year.

-file("src/gleam/time/duration.gleam", 110).
?DOC(
    " Ensure the duration is represented with `nanoseconds` being positive and\n"
    " less than 1 second.\n"
    "\n"
    " This function does not change the amount of time that the duratoin refers\n"
    " to, it only adjusts the values used to represent the time.\n"
).
-spec normalise(duration()) -> duration().
normalise(Duration) ->
    Multiplier = 1000000000,
    Nanoseconds = case Multiplier of
        0 -> 0;
        Gleam@denominator -> erlang:element(3, Duration) rem Gleam@denominator
    end,
    Overflow = erlang:element(3, Duration) - Nanoseconds,
    Seconds = erlang:element(2, Duration) + (case Multiplier of
        0 -> 0;
        Gleam@denominator@1 -> Overflow div Gleam@denominator@1
    end),
    case Nanoseconds >= 0 of
        true ->
            {duration, Seconds, Nanoseconds};

        false ->
            {duration, Seconds - 1, Multiplier + Nanoseconds}
    end.

-file("src/gleam/time/duration.gleam", 76).
?DOC(
    " Convert a duration to a number of the largest number of a unit, serving as\n"
    " a rough description of the duration that a human can understand.\n"
    "\n"
    " The size used for each unit are described in the documentation for the\n"
    " `Unit` type.\n"
    "\n"
    " ```gleam\n"
    " seconds(125)\n"
    " |> approximate\n"
    " // -> #(2, Minute)\n"
    " ```\n"
    "\n"
    " This function rounds _towards zero_. This means that if a duration is just\n"
    " short of 2 days then it will approximate to 1 day.\n"
    "\n"
    " ```gleam\n"
    " hours(47)\n"
    " |> approximate\n"
    " // -> #(1, Day)\n"
    " ```\n"
).
-spec approximate(duration()) -> {integer(), unit()}.
approximate(Duration) ->
    {duration, S, Ns} = Duration,
    Minute = 60,
    Hour = Minute * 60,
    Day = Hour * 24,
    Week = Day * 7,
    Year = (Day * 365) + (Hour * 6),
    Month = Year div 12,
    Microsecond = 1000,
    Millisecond = Microsecond * 1000,
    case nil of
        _ when S < 0 ->
            {Amount, Unit} = begin
                _pipe = {duration, - S, - Ns},
                _pipe@1 = normalise(_pipe),
                approximate(_pipe@1)
            end,
            {- Amount, Unit};

        _ when S >= Year ->
            {case Year of
                    0 -> 0;
                    Gleam@denominator -> S div Gleam@denominator
                end, year};

        _ when S >= Month ->
            {case Month of
                    0 -> 0;
                    Gleam@denominator@1 -> S div Gleam@denominator@1
                end, month};

        _ when S >= Week ->
            {case Week of
                    0 -> 0;
                    Gleam@denominator@2 -> S div Gleam@denominator@2
                end, week};

        _ when S >= Day ->
            {case Day of
                    0 -> 0;
                    Gleam@denominator@3 -> S div Gleam@denominator@3
                end, day};

        _ when S >= Hour ->
            {case Hour of
                    0 -> 0;
                    Gleam@denominator@4 -> S div Gleam@denominator@4
                end, hour};

        _ when S >= Minute ->
            {case Minute of
                    0 -> 0;
                    Gleam@denominator@5 -> S div Gleam@denominator@5
                end, minute};

        _ when S > 0 ->
            {S, second};

        _ when Ns >= Millisecond ->
            {case Millisecond of
                    0 -> 0;
                    Gleam@denominator@6 -> Ns div Gleam@denominator@6
                end, millisecond};

        _ when Ns >= Microsecond ->
            {case Microsecond of
                    0 -> 0;
                    Gleam@denominator@7 -> Ns div Gleam@denominator@7
                end, microsecond};

        _ ->
            {Ns, nanosecond}
    end.

-file("src/gleam/time/duration.gleam", 140).
?DOC(
    " Compare one duration to another, indicating whether the first spans a\n"
    " larger amount of time (and so is greater) or smaller amount of time (and so\n"
    " is lesser) than the second.\n"
    "\n"
    " # Examples\n"
    "\n"
    " ```gleam\n"
    " compare(seconds(1), seconds(2))\n"
    " // -> order.Lt\n"
    " ```\n"
    "\n"
    " Whether a duration is negative or positive doesn't matter for comparing\n"
    " them, only the amount of time spanned matters.\n"
    "\n"
    " ```gleam\n"
    " compare(seconds(-2), seconds(1))\n"
    " // -> order.Gt\n"
    " ```\n"
).
-spec compare(duration(), duration()) -> gleam@order:order().
compare(Left, Right) ->
    Parts = fun(X) -> case erlang:element(2, X) >= 0 of
            true ->
                {erlang:element(2, X), erlang:element(3, X)};

            false ->
                {(erlang:element(2, X) * -1) - 1,
                    1000000000 - erlang:element(3, X)}
        end end,
    {Ls, Lns} = Parts(Left),
    {Rs, Rns} = Parts(Right),
    _pipe = gleam@int:compare(Ls, Rs),
    gleam@order:break_tie(_pipe, gleam@int:compare(Lns, Rns)).

-file("src/gleam/time/duration.gleam", 164).
?DOC(
    " Calculate the difference between two durations.\n"
    "\n"
    " This is effectively substracting the first duration from the second.\n"
    "\n"
    " # Examples\n"
    "\n"
    " ```gleam\n"
    " difference(seconds(1), seconds(5))\n"
    " // -> seconds(4)\n"
    " ```\n"
).
-spec difference(duration(), duration()) -> duration().
difference(Left, Right) ->
    _pipe = {duration,
        erlang:element(2, Right) - erlang:element(2, Left),
        erlang:element(3, Right) - erlang:element(3, Left)},
    normalise(_pipe).

-file("src/gleam/time/duration.gleam", 178).
?DOC(
    " Add two durations together.\n"
    "\n"
    " # Examples\n"
    "\n"
    " ```gleam\n"
    " add(seconds(1), seconds(5))\n"
    " // -> seconds(6)\n"
    " ```\n"
).
-spec add(duration(), duration()) -> duration().
add(Left, Right) ->
    _pipe = {duration,
        erlang:element(2, Left) + erlang:element(2, Right),
        erlang:element(3, Left) + erlang:element(3, Right)},
    normalise(_pipe).

-file("src/gleam/time/duration.gleam", 225).
-spec nanosecond_digits(integer(), integer(), binary()) -> binary().
nanosecond_digits(N, Position, Acc) ->
    case Position of
        9 ->
            Acc;

        _ when (Acc =:= <<""/utf8>>) andalso ((N rem 10) =:= 0) ->
            nanosecond_digits(N div 10, Position + 1, Acc);

        _ ->
            Acc@1 = <<(erlang:integer_to_binary(N rem 10))/binary, Acc/binary>>,
            nanosecond_digits(N div 10, Position + 1, Acc@1)
    end.

-file("src/gleam/time/duration.gleam", 239).
?DOC(" Create a duration of a number of seconds.\n").
-spec seconds(integer()) -> duration().
seconds(Amount) ->
    {duration, Amount, 0}.

-file("src/gleam/time/duration.gleam", 244).
?DOC(" Create a duration of a number of minutes.\n").
-spec minutes(integer()) -> duration().
minutes(Amount) ->
    seconds(Amount * 60).

-file("src/gleam/time/duration.gleam", 249).
?DOC(" Create a duration of a number of hours.\n").
-spec hours(integer()) -> duration().
hours(Amount) ->
    seconds((Amount * 60) * 60).

-file("src/gleam/time/duration.gleam", 254).
?DOC(" Create a duration of a number of milliseconds.\n").
-spec milliseconds(integer()) -> duration().
milliseconds(Amount) ->
    Remainder = Amount rem 1000,
    Overflow = Amount - Remainder,
    Nanoseconds = Remainder * 1000000,
    Seconds = Overflow div 1000,
    _pipe = {duration, Seconds, Nanoseconds},
    normalise(_pipe).

-file("src/gleam/time/duration.gleam", 273).
?DOC(
    " Create a duration of a number of nanoseconds.\n"
    "\n"
    " # JavaScript int limitations\n"
    "\n"
    " Remember that JavaScript can only perfectly represent ints between positive\n"
    " and negative 9,007,199,254,740,991! If you use a single call to this\n"
    " function to create durations larger than that number of nanoseconds then\n"
    " you will likely not get exactly the value you expect. Use `seconds` and\n"
    " `milliseconds` as much as possible for large durations.\n"
).
-spec nanoseconds(integer()) -> duration().
nanoseconds(Amount) ->
    _pipe = {duration, 0, Amount},
    normalise(_pipe).

-file("src/gleam/time/duration.gleam", 283).
?DOC(
    " Convert the duration to a number of seconds.\n"
    "\n"
    " There may be some small loss of precision due to `Duration` being\n"
    " nanosecond accurate and `Float` not being able to represent this.\n"
).
-spec to_seconds(duration()) -> float().
to_seconds(Duration) ->
    Seconds = erlang:float(erlang:element(2, Duration)),
    Nanoseconds = erlang:float(erlang:element(3, Duration)),
    Seconds + (Nanoseconds / 1000000000.0).

-file("src/gleam/time/duration.gleam", 292).
?DOC(
    " Convert the duration to a number of seconds and nanoseconds. There is no\n"
    " loss of precision with this conversion on any target.\n"
).
-spec to_seconds_and_nanoseconds(duration()) -> {integer(), integer()}.
to_seconds_and_nanoseconds(Duration) ->
    {erlang:element(2, Duration), erlang:element(3, Duration)}.

-file("src/gleam/time/duration.gleam", 192).
?DOC(
    " Convert the duration to an [ISO8601][1] formatted duration string.\n"
    "\n"
    " The ISO8601 duration format is ambiguous without context due to months and\n"
    " years having different lengths, and because of leap seconds. This function\n"
    " encodes the duration as days, hours, and seconds without any leap seconds.\n"
    " Be sure to take this into account when using the duration strings.\n"
    "\n"
    " [1]: https://en.wikipedia.org/wiki/ISO_8601#Durations\n"
).
-spec to_iso8601_string(duration()) -> binary().
to_iso8601_string(Duration) ->
    gleam@bool:guard(
        Duration =:= {duration, 0, 0},
        <<"PT0S"/utf8>>,
        fun() ->
            Split = fun(Total, Limit) ->
                Amount = case Limit of
                    0 -> 0;
                    Gleam@denominator -> Total rem Gleam@denominator
                end,
                Remainder = case Limit of
                    0 -> 0;
                    Gleam@denominator@1 -> (Total - Amount) div Gleam@denominator@1
                end,
                {Amount, Remainder}
            end,
            {Seconds, Rest} = Split(erlang:element(2, Duration), 60),
            {Minutes, Rest@1} = Split(Rest, 60),
            {Hours, Rest@2} = Split(Rest@1, 24),
            Days = Rest@2,
            Add = fun(Out, Value, Unit) -> case Value of
                    0 ->
                        Out;

                    _ ->
                        <<<<Out/binary,
                                (erlang:integer_to_binary(Value))/binary>>/binary,
                            Unit/binary>>
                end end,
            Output = begin
                _pipe = <<"P"/utf8>>,
                _pipe@1 = Add(_pipe, Days, <<"D"/utf8>>),
                _pipe@2 = gleam@string:append(_pipe@1, <<"T"/utf8>>),
                _pipe@3 = Add(_pipe@2, Hours, <<"H"/utf8>>),
                Add(_pipe@3, Minutes, <<"M"/utf8>>)
            end,
            case {Seconds, erlang:element(3, Duration)} of
                {0, 0} ->
                    Output;

                {_, 0} ->
                    <<<<Output/binary,
                            (erlang:integer_to_binary(Seconds))/binary>>/binary,
                        "S"/utf8>>;

                {_, _} ->
                    F = nanosecond_digits(
                        erlang:element(3, Duration),
                        0,
                        <<""/utf8>>
                    ),
                    <<<<<<<<Output/binary,
                                    (erlang:integer_to_binary(Seconds))/binary>>/binary,
                                "."/utf8>>/binary,
                            F/binary>>/binary,
                        "S"/utf8>>
            end
        end
    ).
