%% -------------------------------------------------------------------
%%
%% cuttlefish_validator: models a cuttlefish validator
%%
%% Copyright (c) 2013 Basho Technologies, Inc.  All Rights Reserved.
%%
%% This file is provided to you 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(cuttlefish_validator).

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

-record(validator, {
    name::string(),
    description::string(),
    func::fun()
    }).
-type validator() :: #validator{}.
-type validator_fun() :: fun((any()) -> boolean()).
-type raw_validator() :: {validator, string(), string(), validator_fun()}.
-export_type([validator/0]).

-export([
    parse/1,
    parse_and_merge/2,
    is_validator/1,
    name/1,
    description/1,
    func/1,
    replace/2]).

-spec parse(raw_validator()) -> validator() | cuttlefish_error:error().
parse({validator, Name, Description, Fun}) ->
    #validator{
        name = Name,
        description = Description,
        func = Fun
    };
parse(X) ->
    {error, {validator_parse, X}}.

%% This assumes it's run as part of a foldl over new schema elements
%% in which case, there's only ever one instance of a key in the list
%% so keyreplace works fine.
-spec parse_and_merge(
    raw_validator(), [validator()]) -> [validator()|cuttlefish_error:error()].
parse_and_merge({validator, ValidatorName, _, _} = ValidatorSource, Validators) ->
    NewValidator = parse(ValidatorSource),
    case lists:keyfind(ValidatorName, #validator.name, Validators) of
        false ->
            [ NewValidator | Validators];
        _OldMapping ->
            lists:keyreplace(ValidatorName, #validator.name, Validators, NewValidator)
    end.

-spec is_validator(any()) -> boolean().
is_validator(V) -> is_tuple(V) andalso element(1, V) =:= validator.

-spec name(validator()) -> string().
name(V) -> V#validator.name.

-spec description(validator()) -> string().
description(V) -> V#validator.description.

-spec func(validator()) -> fun().
func(V) -> V#validator.func.

-spec replace(validator(), [validator()]) -> [validator()].
replace(Validator, ListOfValidators) ->
    Exists = lists:keymember(name(Validator), #validator.name, ListOfValidators),
    case Exists of
        true ->
            lists:keyreplace(name(Validator), #validator.name, ListOfValidators, Validator);
        _ ->
            [Validator | ListOfValidators]
    end.

-ifdef(TEST).

-define(XLATE(X), lists:flatten(cuttlefish_error:xlate(X))).

parse_test() ->
    ValidatorDataStruct = {
        validator,
        "name",
        "description",
        fun(X) -> X*2 end
    },

    Validator = parse(ValidatorDataStruct),

    ?assertEqual("name", Validator#validator.name),
    ?assertEqual("description", Validator#validator.description),
    F = Validator#validator.func,
    ?assertEqual(4, F(2)),
    ok.


getter_test() ->
    Validator = #validator{
        name = "name",
        description = "description",
        func = fun(X) -> X*2 end
    },

    ?assertEqual("name", name(Validator)),
    ?assertEqual("description", description(Validator)),

    Fun = func(Validator),
    ?assertEqual(4, Fun(2)),
    ok.


replace_test() ->
    Element1 = #validator{
        name = "name18",
        description = "description18",
        func = fun(X) -> X*2 end
    },
    ?assertEqual(4, (Element1#validator.func)(2)),

    Element2 = #validator{
        name = "name1",
        description = "description1",
        func = fun(X) -> X*4 end
    },
    ?assertEqual(8, (Element2#validator.func)(2)),

    SampleValidators = [Element1, Element2],

    Override = #validator{
        name = "name1",
        description = "description1",
        func = fun(X) -> X*5 end
    },
    ?assertEqual(25, (Override#validator.func)(5)),

    NewValidators = replace(Override, SampleValidators),
    ?assertEqual([Element1, Override], NewValidators),
    ok.

remove_duplicates_test() ->
    Sample1 = #validator{
        name = "name1",
        description = "description1",
        func = fun(X) -> X*3 end
    },
    ?assertEqual(6, (Sample1#validator.func)(2)),

    Sample2 = #validator{
        name = "name1",
        description = "description1",
        func = fun(X) -> X*4 end
    },
    ?assertEqual(8, (Sample2#validator.func)(2)),

    SampleValidators = [Sample1, Sample2],

    [NewValidator|_] = parse_and_merge(
        {validator, "name1", "description2", fun(X) -> X*10 end},
        SampleValidators),
    F = func(NewValidator),
    ?assertEqual(50, F(5)),
    ?assertEqual("description2", description(NewValidator)),
    ?assertEqual("name1", name(NewValidator)),
    ok.

parse_error_test() ->
    {ErrorAtom, ErrorTerm} = parse(not_a_raw_validator),
    ?assertEqual(error, ErrorAtom),
    ?assertEqual(
        "Poorly formatted input to cuttlefish_validator:parse/1 : not_a_raw_validator",
        ?XLATE(ErrorTerm)),
    ok.

is_validator_test() ->
    ?assert(not(is_validator(not_a_validator))),

    V = #validator{
        name = "name1",
        description = "description1",
        func = fun(X) -> X*4 end
    },
    ?assertEqual(8, (V#validator.func)(2)),
    ?assert(is_validator(V)),
    ok.

-endif.
