-module(verl_parser).

-export([parse_requirement/1, parse_version/1, parse_version/2]).

-type operator() :: '!=' | '&&' | '<' | '<=' | '==' | '>' | '>=' | '||' | '~>' | bitstring().

-spec parse_version(verl:version()) ->
    {ok, {verl:major(), verl:minor(), verl:patch(), verl:pre(), [verl:build()]}}
    | {error, invalid_version}.
parse_version(Str) -> parse_version(Str, false).

-spec parse_version(verl:version(), boolean()) ->
    {ok, {verl:major(), verl:minor(), verl:patch(), verl:pre(), [verl:build()]}}
    | {error, invalid_version}.
parse_version(Str, Approximate) when is_binary(Str) ->
    try parse_and_convert(Str, Approximate) of
        {ok, {_, _, undefined, _, _}} ->
            {error, invalid_version};
        {ok, _} = V ->
            V;
        {error, invalid_version} ->
            {error, invalid_version}
    catch
        error:{badmatch, {error, T}} when
            T =:= invalid_version orelse
                T =:= nan orelse
                T =:= bad_part orelse
                T =:= leading_zero
        ->
            {error, invalid_version}
    end.

-spec parse_requirement(verl:requirement()) ->
    {ok, ets:match_spec()} | {error, invalid_requirement}.
parse_requirement(Source) ->
    Lexed = lexer(Source, []),
    to_matchspec(Lexed).

%% @private
-spec lexer(binary(), [operator()]) -> [operator()].
lexer(<<">=", Rest/binary>>, Acc) ->
    lexer(Rest, ['>=' | Acc]);
lexer(<<"<=", Rest/binary>>, Acc) ->
    lexer(Rest, ['<=' | Acc]);
lexer(<<"~>", Rest/binary>>, Acc) ->
    lexer(Rest, ['~>' | Acc]);
lexer(<<">", Rest/binary>>, Acc) ->
    lexer(Rest, ['>' | Acc]);
lexer(<<"<", Rest/binary>>, Acc) ->
    lexer(Rest, ['<' | Acc]);
lexer(<<"==", Rest/binary>>, Acc) ->
    lexer(Rest, ['==' | Acc]);
lexer(<<"!=", Rest/binary>>, Acc) ->
    lexer(Rest, ['!=' | Acc]);
lexer(<<"!", Rest/binary>>, Acc) ->
    lexer(Rest, ['!=' | Acc]);
lexer(<<" or ", Rest/binary>>, Acc) ->
    lexer(Rest, ['||' | Acc]);
lexer(<<" and ", Rest/binary>>, Acc) ->
    lexer(Rest, ['&&' | Acc]);
lexer(<<" ", Rest/binary>>, Acc) ->
    lexer(Rest, Acc);
lexer(<<Char/utf8, Rest/binary>>, []) ->
    lexer(Rest, [<<Char/utf8>>, '==']);
lexer(<<Char/utf8, Body/binary>>, [Head | Acc]) ->
    Acc1 =
        case Head of
            Head when is_binary(Head) ->
                [<<Head/binary, Char/utf8>> | Acc];
            Head when Head =:= '&&' orelse Head =:= '||' ->
                [<<Char/utf8>>, '==', Head | Acc];
            _Other ->
                [<<Char/utf8>>, Head | Acc]
        end,
    lexer(Body, Acc1);
lexer(<<>>, Acc) ->
    lists:reverse(Acc).

%% @private
-spec parse_condition(verl:version()) ->
    {integer(), integer(), 'undefined' | integer(), [binary() | integer()]}.
parse_condition(Version) -> parse_condition(Version, false).

%% @private
-spec parse_condition(verl:version(), boolean()) ->
    {integer(), integer(), 'undefined' | integer(), [binary() | integer()]}.
parse_condition(Version, Approximate) ->
    try
        case parse_and_convert(Version, Approximate) of
            {ok, {Major, Minor, Patch, Pre, _Bld}} ->
                {Major, Minor, Patch, Pre};
            _ ->
                throw(invalid_matchspec)
        end
    catch
        error:{badmatch, {error, T}} when
            T =:= invalid_version orelse
                T =:= nan orelse
                T =:= bad_part orelse
                T =:= leading_zero
        ->
            throw(invalid_matchspec)
    end.

%% @private
-spec approximate_upper({integer(), integer(), 'undefined' | integer(), [binary() | integer()]}) ->
    {integer(), integer(), 0, [0, ...]}.
approximate_upper(Version) ->
    case Version of
        {Major, _Minor, undefined, _} ->
            {Major + 1, 0, 0, [0]};
        {Major, Minor, _Patch, _Pre} ->
            {Major, Minor + 1, 0, [0]}
    end.

%% @private
-spec matchable_to_string(
    {integer(), integer(), 'undefined' | integer(), [binary() | integer()]}
) -> binary().
matchable_to_string({Major, Minor, Patch, Pre}) ->
    Patch1 =
        case Patch of
            P when P =:= undefined orelse P =:= false ->
                <<"0">>;
            _ ->
                maybe_to_string(Patch)
        end,
    Pre1 =
        case Pre == [] of
            true ->
                <<>>;
            false ->
                case Pre of
                    [0] ->
                        <<"-0">>;
                    _ ->
                        Pre0 = maybe_to_string(Pre),
                        <<<<"-">>/binary, Pre0/binary>>
                end
        end,
    Major1 = maybe_to_string(Major),
    Minor1 = maybe_to_string(Minor),
    Patch2 = maybe_to_string(Patch1),
    Joined = join_bins([Major1, Minor1, Patch2], <<".">>),
    <<Joined/binary, Pre1/binary>>.

%% @private
-spec pre_condition('<' | '>', [binary() | integer()]) -> tuple().
pre_condition('>', Pre) ->
    PreLength = length(Pre),
    {'orelse', {'andalso', {'==', {length, '$4'}, 0}, {const, PreLength /= 0}},
        {'andalso', {const, PreLength /= 0},
            {'orelse', {'>', {length, '$4'}, PreLength},
                {'andalso', {'==', {length, '$4'}, PreLength}, {'>', '$4', {const, Pre}}}}}};
pre_condition('<', Pre) ->
    PreLength = length(Pre),
    {'orelse', {'andalso', {'/=', {length, '$4'}, 0}, {const, PreLength == 0}},
        {'andalso', {'/=', {length, '$4'}, 0},
            {'orelse', {'<', {length, '$4'}, PreLength},
                {'andalso', {'==', {length, '$4'}, PreLength}, {'<', '$4', {const, Pre}}}}}}.

%% @private
-spec no_pre_condition([binary() | integer()]) -> tuple().
no_pre_condition([]) ->
    {'orelse', '$5', {'==', {length, '$4'}, 0}};
no_pre_condition(_) ->
    {const, true}.

%% @private
-spec to_matchspec([operator(), ...]) -> {error, invalid_requirement} | {ok, ets:match_spec()}.
to_matchspec(Lexed) ->
    try
        case is_valid_requirement(Lexed) of
            true ->
                First = to_condition(Lexed),
                Rest = lists:nthtail(2, Lexed),
                {ok, [{{'$1', '$2', '$3', '$4', '$5'}, [to_condition(First, Rest)], ['$_']}]};
            false ->
                {error, invalid_requirement}
        end
    catch
        invalid_matchspec -> {error, invalid_requirement}
    end.

%% @private
-spec to_condition([iodata(), ...]) -> tuple().
to_condition(['==', Version | _]) ->
    Matchable = parse_condition(Version),
    main_condition('==', Matchable);
to_condition(['!=', Version | _]) ->
    Matchable = parse_condition(Version),
    main_condition('/=', Matchable);
to_condition(['~>', Version | _]) ->
    From = parse_condition(Version, true),
    To = approximate_upper(From),
    {'andalso', to_condition(['>=', matchable_to_string(From)]),
        to_condition(['<', matchable_to_string(To)])};
to_condition(['>', Version | _]) ->
    {Major, Minor, Patch, Pre} =
        parse_condition(Version),
    {'andalso',
        {'orelse', main_condition('>', {Major, Minor, Patch}),
            {'andalso', main_condition('==', {Major, Minor, Patch}), pre_condition('>', Pre)}},
        no_pre_condition(Pre)};
to_condition(['>=', Version | _]) ->
    Matchable = parse_condition(Version),
    {'orelse', main_condition('==', Matchable), to_condition(['>', Version])};
to_condition(['<', Version | _]) ->
    {Major, Minor, Patch, Pre} =
        parse_condition(Version),
    {'orelse', main_condition('<', {Major, Minor, Patch}),
        {'andalso', main_condition('==', {Major, Minor, Patch}), pre_condition('<', Pre)}};
to_condition(['<=', Version | _]) ->
    Matchable = parse_condition(Version),
    {'orelse', main_condition('==', Matchable), to_condition(['<', Version])}.

%% @private
-spec to_condition(tuple(), list()) -> tuple().
to_condition(Current, []) ->
    Current;
to_condition(
    Current,
    ['&&', Operator, Version | Rest]
) ->
    to_condition(
        {'andalso', Current, to_condition([Operator, Version])},
        Rest
    );
to_condition(
    Current,
    ['||', Operator, Version | Rest]
) ->
    to_condition(
        {'orelse', Current, to_condition([Operator, Version])},
        Rest
    ).

%% @private
-spec main_condition(any(), tuple()) -> tuple().
main_condition(Op, Version) when tuple_size(Version) == 3 ->
    {Op, {{'$1', '$2', '$3'}}, {const, Version}};
main_condition(Op, Version) when tuple_size(Version) == 4 ->
    {Op, {{'$1', '$2', '$3', '$4'}}, {const, Version}}.

%% @private
-spec bisect(binary(), binary(), list()) -> [binary() | undefined, ...].
bisect(Str, Delim, Opts) ->
    [First | Rest] = binary:split(Str, [Delim], Opts),
    Rest1 =
        case Rest of
            [] ->
                undefined;
            _ ->
                join_bins(Rest, Delim)
        end,
    [First, Rest1].

%% @private
-spec has_leading_zero(error | undefined | binary() | [binary()]) -> boolean().
has_leading_zero(<<48/integer, _/integer, _/binary>>) ->
    true;
has_leading_zero(_) ->
    false.

%% @private
-spec is_valid_identifier(any()) -> boolean().
is_valid_identifier(<<Char/integer, Rest/binary>>) when
    is_integer(Char) andalso
        Char >= 48 andalso Char =< 57;
    is_integer(Char) andalso
        Char >= 97 andalso Char =< 122;
    is_integer(Char) andalso
        Char >= 65 andalso Char =< 90;
    Char == 45
->
    is_valid_identifier(Rest);
is_valid_identifier(<<>>) ->
    true;
is_valid_identifier(_) ->
    false.

%% @private
-spec join_bins([binary(), ...], binary()) -> binary().
join_bins(List, Delim) ->
    lists:foldl(
        fun(Bin, Acc) ->
            case bit_size(Acc) of
                N when N > 0 ->
                    <<Acc/binary, Delim/binary, Bin/binary>>;
                _ ->
                    Bin
            end
        end,
        <<>>,
        List
    ).

%% @private
-spec maybe_patch(undefined | binary() | integer(), boolean()) -> {ok, undefined | integer()}.
maybe_patch(undefined, true) ->
    {ok, undefined};
maybe_patch(Patch, _) ->
    to_digits(Patch).

%% @private
-spec parse_and_convert(verl:version(), boolean()) ->
    {error, invalid_version}
    | {ok,
        {
            integer(),
            integer(),
            'undefined'
            | integer(),
            [
                binary()
                | integer()
            ],
            [binary()]
        }}.
parse_and_convert(Str, Approx) ->
    [VerPre, Build] = bisect(Str, <<"+">>, [global]),
    [Ver, Pre] = bisect(VerPre, <<"-">>, []),
    [Maj1, Min1, Patch1, Other] = split_ver(Ver),
    case Other of
        undefined ->
            {ok, Maj2} = to_digits(Maj1),
            {ok, Min2} = to_digits(Min1),
            {ok, Patch2} = maybe_patch(Patch1, Approx),
            {ok, PreParts} = opt_dot_separated(Pre),
            {ok, PreParts1} = parts_to_integers(PreParts, []),
            {ok, Build2} = opt_dot_separated(Build),
            {ok, {Maj2, Min2, Patch2, PreParts1, Build2}};
        _ ->
            {error, invalid_version}
    end.

%% @private
-spec parse_digits('error' | 'undefined' | binary() | [binary()], bitstring()) ->
    {'error', 'nan'} | {'ok', integer()}.
parse_digits(<<Char/integer, Rest/binary>>, Acc) when
    is_integer(Char) andalso Char >= 48 andalso Char =< 57
->
    parse_digits(Rest, <<Acc/binary, Char/integer>>);
parse_digits(<<>>, Acc) when byte_size(Acc) > 0 ->
    {ok, binary_to_integer(Acc)};
parse_digits(_, _) ->
    {error, nan}.

%% @private
-spec parts_to_integers([binary()], [binary() | integer()]) ->
    {'error', 'nan'} | {'ok', [binary() | integer()]}.
parts_to_integers([Part | Rest], Acc) ->
    case parse_digits(Part, <<>>) of
        {ok, Int} ->
            case has_leading_zero(Part) of
                P when P =:= undefined orelse P =:= false ->
                    parts_to_integers(Rest, [Int | Acc]);
                _ ->
                    {error, nan}
            end;
        {error, nan} ->
            parts_to_integers(Rest, [Part | Acc])
    end;
parts_to_integers([], Acc) ->
    {ok, lists:reverse(Acc)}.

%% @private
-spec opt_dot_separated('undefined' | binary()) -> {'error', 'bad_part'} | {'ok', [binary()]}.
opt_dot_separated(undefined) ->
    {ok, []};
opt_dot_separated(Str) ->
    Parts = binary:split(Str, <<".">>, [global]),
    Fun = fun(P) ->
        case P /= <<>> of
            false -> false;
            true -> is_valid_identifier(P)
        end
    end,
    case lists:all(Fun, Parts) of
        P when P =:= undefined orelse P =:= false ->
            {error, bad_part};
        _ ->
            {ok, Parts}
    end.

%% @private
-spec split_ver(binary()) -> ['error' | 'undefined' | binary() | [binary()], ...].
split_ver(Str) ->
    case binary:split(Str, [<<".">>], [global]) of
        [Maj0, Min0] ->
            [Maj0, Min0, undefined, undefined];
        [Maj, Min, P] ->
            [Maj, Min, P, undefined];
        [Major, Minor, Patch | Rest] ->
            [Major, Minor, Patch, Rest];
        _ ->
            [error, error, error, error]
    end.

%% @private
-spec to_digits('error' | 'undefined' | binary() | [binary()]) ->
    {'error', 'leading_zero' | 'nan'} | {'ok', integer()}.
to_digits(Str) ->
    case has_leading_zero(Str) of
        S when S =:= undefined orelse S =:= false ->
            parse_digits(Str, <<>>);
        true ->
            {error, leading_zero}
    end.

%% @private
-spec maybe_to_string(binary() | [binary() | byte()] | integer()) -> binary().
maybe_to_string(Part) ->
    case Part of
        Rewrite when is_binary(Rewrite) ->
            Rewrite;
        Int when is_integer(Int) ->
            integer_to_binary(Int);
        Rewrite when is_list(Rewrite) ->
            list_to_binary(Rewrite)
    end.

%% @private
-spec is_valid_requirement([operator(), ...]) -> boolean().
is_valid_requirement([]) -> false;
is_valid_requirement([A | Next]) -> is_valid_requirement(A, Next).

%% @private
-spec is_valid_requirement(operator(), [operator()]) -> boolean().
is_valid_requirement(A, []) when is_binary(A) ->
    true;
is_valid_requirement(A, [B | Next]) when
    (is_atom(A) andalso
        is_atom(B)) andalso
        (A =:= '&&' orelse A =:= '||')
->
    is_valid_requirement(B, Next);
is_valid_requirement(A, [B | Next]) when
    (is_binary(A) andalso
        is_atom(B)) andalso
        (B =:= '&&' orelse B =:= '||')
->
    is_valid_requirement(B, Next);
is_valid_requirement(A, [B | Next]) when
    (is_atom(A) andalso
        is_binary(B)) andalso
        (A =:= '&&' orelse A =:= '||')
->
    is_valid_requirement(B, Next);
is_valid_requirement(A, [B | Next]) when
    is_atom(A) andalso
        is_binary(B)
->
    is_valid_requirement(B, Next);
is_valid_requirement(_, _) ->
    false.
