%% -------------------------------------------------------------------
%%
%% cuttlefish_util: various cuttlefish utility functions
%%
%% 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_util).

-include_lib("kernel/include/logger.hrl").

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

-export([
    read_and_parse_file/2,
    replace_proplist_value/3,
    numerify/1,
    ceiling/1,
    levenshtein/2]).

%% Legacy API
-export([
     conf_get_value/2,
     conf_get_value/3,
     filter_by_variable_starts_with/2,
     matches_for_variable_def/2,
     fuzzy_variable_match/2
]).

-spec read_and_parse_file(Filename :: file:name_all(),
                          ParseFun :: fun((binary()) -> any())) -> any().
read_and_parse_file(Filename, ParseFun) ->
    AbsFilename = filename:absname(Filename),
    case erl_prim_loader:get_file(AbsFilename) of
        {ok, Bin, _} ->
            ParseFun(strip_bom(Bin));
        error ->
            {error, undefined}
    end.

%% @deprecated
conf_get_value(Key, Conf) ->
    _ = ?LOG_WARNING("cuttlefish_util:conf_get_value/2 has been deprecated. use cuttlefish:conf_get/2"),
    cuttlefish:conf_get(Key, Conf).

%% @deprecated
conf_get_value(Key, Conf, Default) ->
    _ = ?LOG_WARNING("cuttlefish_util:conf_get_value/3 has been deprecated. use cuttlefish:conf_get/3"),
    cuttlefish:conf_get(Key, Conf, Default).

%% @deprecated
filter_by_variable_starts_with(Prefix, Conf) ->
    _ = ?LOG_WARNING("cuttlefish_util:filter_by_variable_starts_with/2 has been deprecated. use cuttlefish_variable:filter_by_prefix/2"),
    cuttlefish_variable:filter_by_prefix(Prefix, Conf).

%% @deprecated
matches_for_variable_def(VarDef, Conf) ->
    _ = ?LOG_WARNING("cuttlefish_util:matches_for_variable_def/2 has been deprecated. use cuttlefish_variable:fuzzy_matches/2"),
    cuttlefish_variable:fuzzy_matches(VarDef, Conf).

%% @deprecated
fuzzy_variable_match(Var, VarDef) ->
    _ = ?LOG_WARNING("cuttlefish_util:fuzzy_variable_match/2 has been deprecated. use cuttlefish_variable:is_fuzzy_match/2"),
    cuttlefish_variable:is_fuzzy_match(Var, VarDef).

%% @doc replace the element in a proplist
-spec replace_proplist_value(atom() | string(), any(), [{string(), any()}]) -> [{string(), any()}].
replace_proplist_value(Key, Value, Proplist) ->
    lists:keystore(Key, 1, Proplist, {Key, Value}).

%% @doc Turn a string into a number if `list_to_float' or
%% `list_to_integer' accept it as valid
-spec numerify(string()) -> integer()|float()|cuttlefish_error:error().
numerify([$.|_]=Num) -> numerify([$0|Num]);
numerify(String) ->
    try list_to_float(String) of
        Float -> Float
    catch
        _:_ ->
            try list_to_integer(String) of
                Int -> Int
            catch
                _:_ ->
                    {error, {number_parse, String}}
            end
    end.

%% @doc remember when you learned about decimal places. about a minute
%% later, you learned about rounding up and down. This is rounding up.
-spec ceiling(float()) -> integer().
ceiling(X) ->
    T = erlang:trunc(X),
    case (X - T) of
        Neg when Neg < 0 -> T;
        Pos when Pos > 0 -> T + 1;
        _ -> T
    end.

%% Levenshtein code by Adam Lindberg, Fredrik Svensson via
%% http://www.trapexit.org/String_similar_to_(Levenshtein)

%% @doc Calculates the Levenshtein distance between two strings
-spec levenshtein(string(), string()) -> non_neg_integer().
levenshtein(Samestring, Samestring) -> 0;
levenshtein(String, []) -> length(String);
levenshtein([], String) -> length(String);
levenshtein(Source, Target) ->
    levenshtein_rec(Source, Target, lists:seq(0, length(Target)), 1).

%% Recurses over every character in the source string and calculates a list of distances
levenshtein_rec([SrcHead|SrcTail], Target, DistList, Step) ->
    levenshtein_rec(SrcTail, Target, levenshtein_distlist(Target, DistList, SrcHead, [Step], Step), Step + 1);
levenshtein_rec([], _, DistList, _) ->
    lists:last(DistList).

%% Generates a distance list with distance values for every character in the target string
levenshtein_distlist([TargetHead|TargetTail], [DLH|DLT], SourceChar, NewDistList, LastDist) when length(DLT) > 0 ->
    Min = lists:min([LastDist + 1, hd(DLT) + 1, DLH + dif(TargetHead, SourceChar)]),
    levenshtein_distlist(TargetTail, DLT, SourceChar, NewDistList ++ [Min], Min);
levenshtein_distlist([], _, _, NewDistList, _) ->
    NewDistList.

% Calculates the difference between two characters or other values
dif(C, C) -> 0;
dif(_, _) -> 1.

%% Strip Unicode BOM (Byte Order Mark) from beginning of binary
strip_bom(<<239, 187, 191, Rest/binary>>) -> Rest;  % UTF-8
strip_bom(<<254, 255, Rest/binary>>) -> Rest;       % UTF-16 BE
strip_bom(<<255, 254, 0, 0, Rest/binary>>) -> Rest; % UTF-32 LE
strip_bom(<<255, 254, Rest/binary>>) -> Rest;       % UTF-16 LE
strip_bom(<<0, 0, 254, 255, Rest/binary>>) -> Rest; % UTF-32 BE
strip_bom(Bin) -> Bin.

-ifdef(TEST).

replace_proplist_value_test() ->
    Proplist = [
        {"test1", 1},
        {"test2", 2},
        {"test3", 3}
    ],

    NewProplist = replace_proplist_value("test2", 8, Proplist),
    ?assertEqual(
        8,
        proplists:get_value("test2", NewProplist)
        ),
    ok.

replace_proplist_value_when_undefined_test() ->
    Proplist = [
        {"test1", 1},
        {"test2", 2}
    ],

    NewProplist = replace_proplist_value("test3", 3, Proplist),
        ?assertEqual(
        3,
        proplists:get_value("test3", NewProplist)
        ),
    ok.

levenshtein_test() ->
    ?assertEqual(0, levenshtein("X", "X")),
    ?assertEqual(1, levenshtein("X", "XX")),
    ?assertEqual(1, levenshtein("penguin", "penguino")),
    ?assertEqual(1, levenshtein("dtrace", "ctrace")),
    ?assertEqual(5, levenshtein("anti_entropy", "anti_entropy.tick")),
    ?assertEqual(1, levenshtein("anti_entropy", "anti-entropy")),
    ?assertEqual(4, levenshtein("", "four")),
    ?assertEqual(4, levenshtein("four", "")),
    ok.

ceiling_test() ->
    ?assertEqual(9, ceiling(8.99999)),
    ?assertEqual(9, ceiling(8.00001)),
    ?assertEqual(9, ceiling(9.0)),
    ?assertEqual(-2, ceiling(-2.0000001)),
    ?assertEqual(-2, ceiling(-2.9999999)),
    ok.

numerify_test() ->
    ?assertEqual(42, numerify("42")),
    ?assertEqual(42.0, numerify("42.0")),
    ?assertEqual(0.5, numerify(".5")),
    ok.

strip_bom_test() ->
    Content = <<"foo = bar">>,
    ?assertEqual(Content, strip_bom(Content)),
    ?assertEqual(Content, strip_bom(<<(unicode:encoding_to_bom(utf8))/binary, Content/binary>>)),
    ?assertEqual(Content, strip_bom(<<(unicode:encoding_to_bom(utf16))/binary, Content/binary>>)),
    ?assertEqual(Content, strip_bom(<<(unicode:encoding_to_bom({utf16, big}))/binary, Content/binary>>)),
    ?assertEqual(Content, strip_bom(<<(unicode:encoding_to_bom({utf16, little}))/binary, Content/binary>>)),
    ?assertEqual(Content, strip_bom(<<(unicode:encoding_to_bom({utf32, big}))/binary, Content/binary>>)),
    ?assertEqual(Content, strip_bom(<<(unicode:encoding_to_bom({utf32, little}))/binary, Content/binary>>)),
    ok.

-endif.
