% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.

-module(mango_util).

-export([
    open_doc/2,
    open_ddocs/1,
    load_ddoc/2,
    load_ddoc/3,

    defer/3,
    do_defer/3,

    assert_ejson/1,

    to_lower/1,

    enc_dbname/1,
    dec_dbname/1,

    enc_hex/1,
    dec_hex/1,

    lucene_escape_field/1,
    lucene_escape_query_value/1,
    lucene_escape_user/1,
    is_number_string/1,

    has_suffix/2,

    join/2,

    parse_field/1,

    cached_re/2
]).

-include_lib("couch/include/couch_db.hrl").
-include("mango.hrl").

-define(DIGITS, "(\\p{N}+)").
-define(HEXDIGITS, "([0-9a-fA-F]+)").
-define(EXP, "[eE][+-]?" ++ ?DIGITS).
-define(NUMSTRING,
    "[\\x00-\\x20]*" ++ "[+-]?(" ++ "NaN|" ++
        "Infinity|" ++ "(((" ++
        ?DIGITS ++
        "(\\.)?(" ++
        ?DIGITS ++
        "?)(" ++
        ?EXP ++
        ")?)|" ++
        "(\\.(" ++
        ?DIGITS ++
        ")(" ++
        ?EXP ++
        ")?)|" ++
        "((" ++
        "(0[xX]" ++
        ?HEXDIGITS ++
        "(\\.)?)|" ++
        "(0[xX]" ++
        ?HEXDIGITS ++
        "?(\\.)" ++
        ?HEXDIGITS ++
        ")" ++
        ")[pP][+-]?" ++ ?DIGITS ++ "))" ++ "[fFdD]?))" ++ "[\\x00-\\x20]*"
).

open_doc(Db, DocId) ->
    open_doc(Db, DocId, [deleted, ejson_body]).

open_doc(Db, DocId, Options) ->
    case mango_util:defer(fabric, open_doc, [Db, DocId, Options]) of
        {ok, Doc} ->
            {ok, Doc};
        {not_found, _} ->
            not_found;
        _ ->
            ?MANGO_ERROR({error_loading_doc, DocId})
    end.

open_ddocs(Db) ->
    case mango_util:defer(fabric, design_docs, [Db]) of
        {ok, Docs} ->
            {ok, Docs};
        _ ->
            ?MANGO_ERROR(error_loading_ddocs)
    end.

load_ddoc(Db, DDocId) ->
    load_ddoc(Db, DDocId, [deleted, ejson_body]).

load_ddoc(Db, DDocId, DbOpts) ->
    case open_doc(Db, DDocId, DbOpts) of
        {ok, Doc} ->
            {ok, check_lang(Doc)};
        not_found ->
            Body =
                {[
                    {<<"language">>, <<"query">>}
                ]},
            {ok, #doc{id = DDocId, body = Body}}
    end.

defer(Mod, Fun, Args) ->
    {Pid, Ref} = spawn_monitor(?MODULE, do_defer, [Mod, Fun, Args]),
    receive
        {'DOWN', Ref, process, Pid, {mango_defer_ok, Value}} ->
            Value;
        {'DOWN', Ref, process, Pid, {mango_defer_throw, Value}} ->
            erlang:throw(Value);
        {'DOWN', Ref, process, Pid, {mango_defer_error, Value}} ->
            error(Value);
        {'DOWN', Ref, process, Pid, {mango_defer_exit, Value}} ->
            exit(Value)
    end.

do_defer(Mod, Fun, Args) ->
    try apply(Mod, Fun, Args) of
        Resp ->
            exit({mango_defer_ok, Resp})
    catch
        throw:Error:Stack ->
            couch_log:error("Defered error: ~w~n    ~p", [{throw, Error}, Stack]),
            exit({mango_defer_throw, Error});
        error:Error:Stack ->
            couch_log:error("Defered error: ~w~n    ~p", [{error, Error}, Stack]),
            exit({mango_defer_error, Error});
        exit:Error:Stack ->
            couch_log:error("Defered error: ~w~n    ~p", [{exit, Error}, Stack]),
            exit({mango_defer_exit, Error})
    end.

assert_ejson({Props}) ->
    assert_ejson_obj(Props);
assert_ejson(Vals) when is_list(Vals) ->
    assert_ejson_arr(Vals);
assert_ejson(null) ->
    true;
assert_ejson(true) ->
    true;
assert_ejson(false) ->
    true;
assert_ejson(String) when is_binary(String) ->
    true;
assert_ejson(Number) when is_number(Number) ->
    true;
assert_ejson(_Else) ->
    false.

assert_ejson_obj([]) ->
    true;
assert_ejson_obj([{Key, Val} | Rest]) when is_binary(Key) ->
    case assert_ejson(Val) of
        true ->
            assert_ejson_obj(Rest);
        false ->
            false
    end;
assert_ejson_obj(_Else) ->
    false.

assert_ejson_arr([]) ->
    true;
assert_ejson_arr([Val | Rest]) ->
    case assert_ejson(Val) of
        true ->
            assert_ejson_arr(Rest);
        false ->
            false
    end.

check_lang(#doc{id = Id, deleted = true}) ->
    Body =
        {[
            {<<"language">>, <<"query">>}
        ]},
    #doc{id = Id, body = Body};
check_lang(#doc{body = {Props}} = Doc) ->
    case lists:keyfind(<<"language">>, 1, Props) of
        {<<"language">>, <<"query">>} ->
            Doc;
        Else ->
            ?MANGO_ERROR({invalid_ddoc_lang, Else})
    end.

to_lower(Key) when is_binary(Key) ->
    KStr = binary_to_list(Key),
    KLower = string:to_lower(KStr),
    list_to_binary(KLower).

enc_dbname(<<>>) ->
    <<>>;
enc_dbname(<<A:8/integer, Rest/binary>>) ->
    Bytes = enc_db_byte(A),
    Tail = enc_dbname(Rest),
    <<Bytes/binary, Tail/binary>>.

enc_db_byte(N) when N >= $a, N =< $z -> <<N>>;
enc_db_byte(N) when N >= $0, N =< $9 -> <<N>>;
enc_db_byte(N) when N == $/; N == $_; N == $- -> <<N>>;
enc_db_byte(N) ->
    H = enc_hex_byte(N div 16),
    L = enc_hex_byte(N rem 16),
    <<$$, H:8/integer, L:8/integer>>.

dec_dbname(<<>>) ->
    <<>>;
dec_dbname(<<$$, _:8/integer>>) ->
    throw(invalid_dbname_encoding);
dec_dbname(<<$$, H:8/integer, L:8/integer, Rest/binary>>) ->
    Byte = (dec_hex_byte(H) bsl 4) bor dec_hex_byte(L),
    Tail = dec_dbname(Rest),
    <<Byte:8/integer, Tail/binary>>;
dec_dbname(<<N:8/integer, Rest/binary>>) ->
    Tail = dec_dbname(Rest),
    <<N:8/integer, Tail/binary>>.

enc_hex(<<>>) ->
    <<>>;
enc_hex(<<V:8/integer, Rest/binary>>) ->
    H = enc_hex_byte(V div 16),
    L = enc_hex_byte(V rem 16),
    Tail = enc_hex(Rest),
    <<H:8/integer, L:8/integer, Tail/binary>>.

enc_hex_byte(N) when N >= 0, N < 10 -> $0 + N;
enc_hex_byte(N) when N >= 10, N < 16 -> $a + (N - 10);
enc_hex_byte(N) -> throw({invalid_hex_value, N}).

dec_hex(<<>>) ->
    <<>>;
dec_hex(<<_:8/integer>>) ->
    throw(invalid_hex_string);
dec_hex(<<H:8/integer, L:8/integer, Rest/binary>>) ->
    Byte = (dec_hex_byte(H) bsl 4) bor dec_hex_byte(L),
    Tail = dec_hex(Rest),
    <<Byte:8/integer, Tail/binary>>.

dec_hex_byte(N) when N >= $0, N =< $9 -> (N - $0);
dec_hex_byte(N) when N >= $a, N =< $f -> (N - $a) + 10;
dec_hex_byte(N) when N >= $A, N =< $F -> (N - $A) + 10;
dec_hex_byte(N) -> throw({invalid_hex_character, N}).

lucene_escape_field(Bin) when is_binary(Bin) ->
    Str = binary_to_list(Bin),
    Enc = lucene_escape_field(Str),
    iolist_to_binary(Enc);
lucene_escape_field([H | T]) when is_number(H), H >= 0, H =< 255 ->
    if
        H >= $a, $z >= H ->
            [H | lucene_escape_field(T)];
        H >= $A, $Z >= H ->
            [H | lucene_escape_field(T)];
        H >= $0, $9 >= H ->
            [H | lucene_escape_field(T)];
        true ->
            Hi = enc_hex_byte(H div 16),
            Lo = enc_hex_byte(H rem 16),
            [$_, Hi, Lo | lucene_escape_field(T)]
    end;
lucene_escape_field([]) ->
    [].

lucene_escape_query_value(IoList) when is_list(IoList) ->
    lucene_escape_query_value(iolist_to_binary(IoList));
lucene_escape_query_value(Bin) when is_binary(Bin) ->
    IoList = lucene_escape_qv(Bin),
    iolist_to_binary(IoList).

% This escapes the special Lucene query characters
% listed below as well as any whitespace.
%
%   + - && || ! ( ) { } [ ] ^ ~ * ? : \ " /
%

lucene_escape_qv(<<>>) ->
    [];
lucene_escape_qv(<<"&&", Rest/binary>>) ->
    ["\\&&" | lucene_escape_qv(Rest)];
lucene_escape_qv(<<"||", Rest/binary>>) ->
    ["\\||" | lucene_escape_qv(Rest)];
lucene_escape_qv(<<C, Rest/binary>>) ->
    NeedsEscape = "+-(){}[]!^~*?:/\\\" \t\r\n",
    Out =
        case lists:member(C, NeedsEscape) of
            true -> ["\\", C];
            false -> [C]
        end,
    Out ++ lucene_escape_qv(Rest).

lucene_escape_user(Field) ->
    {ok, Path} = parse_field(Field),
    Escaped = [mango_util:lucene_escape_field(P) || P <- Path],
    iolist_to_binary(join(".", Escaped)).

has_suffix(Bin, Suffix) when is_binary(Bin), is_binary(Suffix) ->
    SBin = size(Bin),
    SSuffix = size(Suffix),
    if
        SBin < SSuffix ->
            false;
        true ->
            PSize = SBin - SSuffix,
            case Bin of
                <<_:PSize/binary, Suffix/binary>> ->
                    true;
                _ ->
                    false
            end
    end.

join(_Sep, []) ->
    [];
join(_Sep, [Item]) ->
    [Item];
join(Sep, [Item | Rest]) ->
    [Item, Sep | join(Sep, Rest)].

is_number_string(Value) when is_binary(Value) ->
    is_number_string(binary_to_list(Value));
is_number_string(Value) when is_list(Value) ->
    MP = cached_re(mango_numstring_re, ?NUMSTRING),
    case re:run(Value, MP) of
        nomatch ->
            false;
        _ ->
            true
    end.

cached_re(Name, RE) ->
    case persistent_term:get({?MODULE, Name}, undefined) of
        undefined ->
            {ok, MP} = re:compile(RE),
            ok = persistent_term:put({?MODULE, Name}, MP),
            MP;
        MP ->
            MP
    end.

parse_field(Field) ->
    case binary:match(Field, <<"\\">>, []) of
        nomatch ->
            % Fast path, no regex required
            {ok, check_non_empty(Field, binary:split(Field, <<".">>, [global]))};
        _ ->
            parse_field_slow(Field)
    end.

parse_field_slow(Field) ->
    Path = lists:map(
        fun
            (P) when P =:= <<>> ->
                ?MANGO_ERROR({invalid_field_name, Field});
            (P) ->
                re:replace(P, <<"\\\\">>, <<>>, [global, {return, binary}])
        end,
        re:split(Field, <<"(?<!\\\\)\\.">>)
    ),
    {ok, Path}.

check_non_empty(Field, Parts) ->
    case lists:member(<<>>, Parts) of
        true ->
            ?MANGO_ERROR({invalid_field_name, Field});
        false ->
            Parts
    end.

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

parse_field_test() ->
    ?assertEqual({ok, [<<"ab">>]}, parse_field(<<"ab">>)),
    ?assertEqual({ok, [<<"a">>, <<"b">>]}, parse_field(<<"a.b">>)),
    ?assertEqual({ok, [<<"a.b">>]}, parse_field(<<"a\\.b">>)),
    ?assertEqual({ok, [<<"a">>, <<"b">>, <<"c">>]}, parse_field(<<"a.b.c">>)),
    ?assertEqual({ok, [<<"a">>, <<"b.c">>]}, parse_field(<<"a.b\\.c">>)),
    Exception = {mango_error, ?MODULE, {invalid_field_name, <<"a..b">>}},
    ?assertThrow(Exception, parse_field(<<"a..b">>)).

is_number_string_test() ->
    ?assert(is_number_string("0")),
    ?assert(is_number_string("1")),
    ?assert(is_number_string("1.0")),
    ?assert(is_number_string("1.0E10")),
    ?assert(is_number_string("0d")),
    ?assert(is_number_string("-1")),
    ?assert(is_number_string("-1.0")),
    ?assertNot(is_number_string("hello")),
    ?assertNot(is_number_string("")),
    ?assertMatch({match, _}, re:run("1.0", persistent_term:get({?MODULE, mango_numstring_re}))).

-endif.
