% 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_doc).

-export([
    from_bson/1,

    apply_update/2,
    update_as_insert/1,
    has_operators/1,

    get_field/2,
    get_field/3,
    rem_field/2,
    set_field/3
]).

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

from_bson({Props}) ->
    DocProps =
        case lists:keytake(<<"_id">>, 1, Props) of
            {value, {<<"_id">>, DocId0}, RestProps} ->
                DocId =
                    case DocId0 of
                        {[{<<"$id">>, Id}]} ->
                            Id;
                        Else ->
                            Else
                    end,
                [{<<"_id">>, DocId} | RestProps];
            false ->
                Props
        end,
    Doc = couch_doc:from_json_obj({DocProps}),
    case Doc#doc.id of
        <<"">> ->
            Doc#doc{id = couch_uuids:new(), revs = {0, []}};
        _ ->
            Doc
    end.

apply_update(#doc{body = {Props}} = Doc, Update) ->
    NewProps = apply_update(Props, Update),
    Doc#doc{body = {NewProps}};
apply_update({Props}, {Update}) ->
    Result = do_update({Props}, Update),
    case has_operators(Result) of
        true ->
            ?MANGO_ERROR(update_leaves_operators);
        false ->
            ok
    end,
    Result.

update_as_insert({Update}) ->
    NewProps = do_update_to_insert(Update, {[]}),
    apply_update(NewProps, {Update}).

has_operators(#doc{body = Body}) ->
    has_operators(Body);
has_operators({Props}) when is_list(Props) ->
    has_operators_obj(Props);
has_operators(Arr) when is_list(Arr) ->
    has_operators_arr(Arr);
has_operators(Val) when is_atom(Val) ->
    false;
has_operators(Val) when is_number(Val) ->
    false;
has_operators(Val) when is_binary(Val) ->
    false.

has_operators_obj([]) ->
    false;
has_operators_obj([{K, V} | Rest]) ->
    case K of
        <<"$", _/binary>> ->
            true;
        _ ->
            case has_operators(V) of
                true ->
                    true;
                false ->
                    has_operators_obj(Rest)
            end
    end.

has_operators_arr([]) ->
    false;
has_operators_arr([V | Rest]) ->
    case has_operators(V) of
        true ->
            true;
        false ->
            has_operators_arr(Rest)
    end.

do_update(Props, []) ->
    Props;
do_update(Props, [{Op, Value} | Rest]) ->
    UpdateFun = update_operator_fun(Op),
    NewProps =
        case UpdateFun of
            undefined ->
                lists:keystore(Op, 1, Props, {Op, Value});
            Fun when is_function(Fun, 2) ->
                case Value of
                    {ValueProps} ->
                        Fun(Props, ValueProps);
                    _ ->
                        ?MANGO_ERROR({invalid_operand, Op, Value})
                end
        end,
    do_update(NewProps, Rest).

update_operator_fun(<<"$", _/binary>> = Op) ->
    OperatorFuns = [
        % Object operators
        {<<"$inc">>, fun do_update_inc/2},
        {<<"$rename">>, fun do_update_rename/2},
        {<<"$setOnInsert">>, fun do_update_set_on_insert/2},
        {<<"$set">>, fun do_update_set/2},
        {<<"$unset">>, fun do_update_unset/2},

        % Array opereators
        {<<"$addToSet">>, fun do_update_add_to_set/2},
        {<<"$pop">>, fun do_update_pop/2},
        {<<"$pullAll">>, fun do_update_pull_all/2},
        {<<"$pull">>, fun do_update_pull/2},
        {<<"$pushAll">>, fun do_update_push_all/2},
        {<<"$push">>, fun do_update_push/2},

        % Bitwise Operators
        {<<"$bit">>, fun do_update_bitwise/2}
    ],
    case lists:keyfind(Op, 1, OperatorFuns) of
        {Op, Fun} ->
            Fun;
        false ->
            ?MANGO_ERROR({update_operator_not_supported, Op})
    end;
update_operator_fun(_) ->
    undefined.

do_update_inc(Props, []) ->
    Props;
do_update_inc(Props, [{Field, Incr} | Rest]) ->
    if
        is_number(Incr) -> ok;
        true -> ?MANGO_ERROR({invalid_increment, Incr})
    end,
    NewProps =
        case get_field(Props, Field, fun is_number/1) of
            Value when is_number(Value) ->
                set_field(Props, Field, Value + Incr);
            not_found ->
                set_field(Props, Field, Incr);
            _ ->
                Props
        end,
    do_update_inc(NewProps, Rest).

do_update_rename(Props, []) ->
    Props;
do_update_rename(Props, [{OldField, NewField} | Rest]) ->
    NewProps =
        case rem_field(Props, OldField) of
            {RemProps, OldValue} ->
                set_field(RemProps, NewField, OldValue);
            _ ->
                Props
        end,
    do_update_rename(NewProps, Rest).

do_update_set_on_insert(Props, _) ->
    % This is only called during calls to apply_update/2
    % which means this isn't an insert, so drop it on
    % the floor.
    Props.

do_update_set(Props, []) ->
    Props;
do_update_set(Props, [{Field, Value} | Rest]) ->
    NewProps = set_field(Props, Field, Value),
    do_update_set(NewProps, Rest).

do_update_unset(Props, []) ->
    Props;
do_update_unset(Props, [{Field, _} | Rest]) ->
    NewProps =
        case rem_field(Props, Field) of
            {RemProps, _} ->
                RemProps;
            _ ->
                Props
        end,
    do_update_unset(NewProps, Rest).

do_update_add_to_set(Props, []) ->
    Props;
do_update_add_to_set(Props, [{Field, NewValue} | Rest]) ->
    ToAdd =
        case NewValue of
            {[{<<"$each">>, NewValues}]} when is_list(NewValues) ->
                NewValues;
            {[{<<"$each">>, NewValue}]} ->
                [NewValue];
            Else ->
                [Else]
        end,
    NewProps =
        case get_field(Props, Field) of
            OldValues when is_list(OldValues) ->
                FinalValues = lists:foldl(
                    fun(V, Acc) ->
                        lists:append(Acc, [V])
                    end,
                    OldValues,
                    ToAdd
                ),
                set_field(Props, Field, FinalValues);
            _ ->
                Props
        end,
    do_update_add_to_set(NewProps, Rest).

do_update_pop(Props, []) ->
    Props;
do_update_pop(Props, [{Field, Pos} | Rest]) ->
    NewProps =
        case get_field(Props, Field) of
            OldValues when is_list(OldValues) ->
                NewValues =
                    case Pos > 0 of
                        true ->
                            lists:sublist(OldValues, 1, length(OldValues) - 1);
                        false ->
                            lists:sublist(OldValues, 2, length(OldValues) - 1)
                    end,
                set_field(Props, Field, NewValues);
            _ ->
                Props
        end,
    do_update_pop(NewProps, Rest).

do_update_pull_all(Props, []) ->
    Props;
do_update_pull_all(Props, [{Field, Values} | Rest]) ->
    ToRem =
        case is_list(Values) of
            true -> Values;
            false -> [Values]
        end,
    NewProps =
        case get_field(Props, Field) of
            OldValues when is_list(OldValues) ->
                NewValues = lists:foldl(
                    fun(ValToRem, Acc) ->
                        % The logic in these filter functions is a bit
                        % subtle. The way to think of this is that we
                        % return true for all elements we want to keep.
                        FilterFun =
                            case has_operators(ValToRem) of
                                true ->
                                    fun(A) ->
                                        Sel = mango_selector:normalize(ValToRem),
                                        not mango_selector:match(A, Sel)
                                    end;
                                false ->
                                    fun(A) -> A /= ValToRem end
                            end,
                        lists:filter(FilterFun, Acc)
                    end,
                    OldValues,
                    ToRem
                ),
                set_field(Props, Field, NewValues);
            _ ->
                Props
        end,
    do_update_add_to_set(NewProps, Rest).

do_update_pull(Props, []) ->
    Props;
do_update_pull(Props, [{Field, Value} | Rest]) ->
    ToRem =
        case Value of
            {[{<<"$each">>, Values}]} when is_list(Values) ->
                Values;
            {[{<<"$each">>, Value}]} ->
                [Value];
            Else ->
                [Else]
        end,
    NewProps = do_update_pull_all(Props, [{Field, ToRem}]),
    do_update_pull(NewProps, Rest).

do_update_push_all(_, []) ->
    [];
do_update_push_all(Props, [{Field, Values} | Rest]) ->
    ToAdd =
        case is_list(Values) of
            true -> Values;
            false -> [Values]
        end,
    NewProps =
        case get_field(Props, Field) of
            OldValues when is_list(OldValues) ->
                NewValues = OldValues ++ ToAdd,
                set_field(Props, Field, NewValues);
            _ ->
                Props
        end,
    do_update_push_all(NewProps, Rest).

do_update_push(Props, []) ->
    Props;
do_update_push(Props, [{Field, Value} | Rest]) ->
    ToAdd =
        case Value of
            {[{<<"$each">>, Values}]} when is_list(Values) ->
                Values;
            {[{<<"$each">>, Value}]} ->
                [Value];
            Else ->
                [Else]
        end,
    NewProps = do_update_push_all(Props, [{Field, ToAdd}]),
    do_update_push(NewProps, Rest).

do_update_bitwise(Props, []) ->
    Props;
do_update_bitwise(Props, [{Field, Value} | Rest]) ->
    DoOp =
        case Value of
            {[{<<"and">>, Val}]} when is_integer(Val) ->
                fun(V) -> V band Val end;
            {[{<<"or">>, Val}]} when is_integer(Val) ->
                fun(V) -> V bor Val end;
            _ ->
                fun(V) -> V end
        end,
    NewProps =
        case get_field(Props, Field, fun is_number/1) of
            Value when is_number(Value) ->
                NewValue = DoOp(Value),
                set_field(Props, Field, NewValue);
            _ ->
                Props
        end,
    do_update_bitwise(NewProps, Rest).

do_update_to_insert([], Doc) ->
    Doc;
do_update_to_insert([{<<"$setOnInsert">>, {FieldProps}}], Doc) ->
    lists:foldl(
        fun({Field, Value}, DocAcc) ->
            set_field(DocAcc, Field, Value)
        end,
        Doc,
        FieldProps
    );
do_update_to_insert([{_, _} | Rest], Doc) ->
    do_update_to_insert(Rest, Doc).

get_field(Props, Field) ->
    get_field(Props, Field, no_validation).

get_field(Props, Field, Validator) when is_binary(Field) ->
    {ok, Path} = mango_util:parse_field(Field),
    get_field(Props, Path, Validator);
get_field(Props, [], no_validation) ->
    Props;
get_field(Props, [], Validator) ->
    case (catch Validator(Props)) of
        true ->
            Props;
        _ ->
            invalid_value
    end;
get_field({Props}, [Name | Rest], Validator) ->
    case lists:keyfind(Name, 1, Props) of
        {Name, Value} ->
            get_field(Value, Rest, Validator);
        false ->
            not_found
    end;
get_field(Values, [Name | Rest], Validator) when is_list(Values) ->
    % Name might be an integer index into an array
    try
        Pos = binary_to_integer(Name),
        case Pos >= 0 andalso Pos < length(Values) of
            true ->
                % +1 because Erlang uses 1 based list indices
                Value = lists:nth(Pos + 1, Values),
                get_field(Value, Rest, Validator);
            false ->
                bad_path
        end
    catch
        error:badarg ->
            bad_path
    end;
get_field(_, [_ | _], _) ->
    bad_path.

rem_field(Props, Field) when is_binary(Field) ->
    {ok, Path} = mango_util:parse_field(Field),
    rem_field(Props, Path);
rem_field({Props}, [Name]) ->
    case lists:keytake(Name, 1, Props) of
        {value, Value, NewProps} ->
            {NewProps, Value};
        false ->
            not_found
    end;
rem_field({Props}, [Name | Rest]) ->
    case lists:keyfind(Name, 1, Props) of
        {Name, Value} ->
            case rem_field(Value, Rest) of
                {NewValue, Ret} ->
                    NewObj = {lists:keystore(Name, 1, Props, {Name, NewValue})},
                    {NewObj, Ret};
                Else ->
                    Else
            end;
        false ->
            not_found
    end;
rem_field(Values, [Name]) when is_list(Values) ->
    % Name might be an integer index into an array
    try
        Pos = binary_to_integer(Name),
        case Pos >= 0 andalso Pos < length(Values) of
            true ->
                % +1 because Erlang uses 1 based list indices
                rem_elem(Pos + 1, Values);
            false ->
                bad_path
        end
    catch
        error:badarg ->
            bad_path
    end;
rem_field(Values, [Name | Rest]) when is_list(Values) ->
    % Name might be an integer index into an array
    try
        Pos = binary_to_integer(Name),
        case Pos >= 0 andalso Pos < length(Values) of
            true ->
                % +1 because Erlang uses 1 based list indices
                Value = lists:nth(Pos + 1, Values),
                case rem_field(Value, Rest) of
                    {NewValue, Ret} ->
                        {set_elem(Pos + 1, Values, NewValue), Ret};
                    Else ->
                        Else
                end;
            false ->
                bad_path
        end
    catch
        error:badarg ->
            bad_path
    end;
rem_field(_, [_ | _]) ->
    bad_path.

set_field(Props, Field, Value) when is_binary(Field) ->
    {ok, Path} = mango_util:parse_field(Field),
    set_field(Props, Path, Value);
set_field({Props}, [Name], Value) ->
    {lists:keystore(Name, 1, Props, {Name, Value})};
set_field({Props}, [Name | Rest], Value) ->
    case lists:keyfind(Name, 1, Props) of
        {Name, Elem} ->
            Result = set_field(Elem, Rest, Value),
            {lists:keystore(Name, 1, Props, {Name, Result})};
        false ->
            Nested = make_nested(Rest, Value),
            {lists:keystore(Name, 1, Props, {Name, Nested})}
    end;
set_field(Values, [Name], Value) when is_list(Values) ->
    % Name might be an integer index into an array
    try
        Pos = binary_to_integer(Name),
        case Pos >= 0 andalso Pos < length(Values) of
            true ->
                % +1 because Erlang uses 1 based list indices
                set_elem(Pos, Values, Value);
            false ->
                Values
        end
    catch
        error:badarg ->
            Values
    end;
set_field(Values, [Name | Rest], Value) when is_list(Values) ->
    % Name might be an integer index into an array
    try
        Pos = binary_to_integer(Name),
        case Pos >= 0 andalso Pos < length(Values) of
            true ->
                % +1 because Erlang uses 1 based list indices
                Elem = lists:nth(Pos + 1, Values),
                Result = set_field(Elem, Rest, Value),
                set_elem(Pos, Values, Result);
            false ->
                Values
        end
    catch
        error:badarg ->
            Values
    end;
set_field(Value, [_ | _], _) ->
    Value.

make_nested([], Value) ->
    Value;
make_nested([Name | Rest], Value) ->
    {[{Name, make_nested(Rest, Value)}]}.

rem_elem(1, [Value | Rest]) ->
    {Rest, Value};
rem_elem(I, [Item | Rest]) when I > 1 ->
    {Tail, Value} = rem_elem(I + 1, Rest),
    {[Item | Tail], Value}.

set_elem(1, [_ | Rest], Value) ->
    [Value | Rest];
set_elem(I, [Item | Rest], Value) when I > 1 ->
    [Item | set_elem(I - 1, Rest, Value)].
