% This file is part of Jiffy released under the MIT license.
% See the LICENSE file for more information.

-module(jiffy).
-export([decode/1, decode/2, encode/1, encode/2]).
-define(NOT_LOADED, not_loaded(?LINE)).

-on_load(init/0).


-type json_value() :: null
                    | true
                    | false
                    | json_string()
                    | json_number()
                    | json_object()
                    | json_array().

-type json_array()  :: [json_value()].
-type json_string() :: atom() | binary().
-type json_number() :: integer() | float().

-type json_object() :: {[{json_string(),json_value()}]}
                        | #{json_string() => json_value()}.

-type json_preencoded() :: {json, iodata()}.

-type jiffy_decode_result() :: json_value()
                        | {has_trailer, json_value(), binary()}.

-type decode_option() :: return_maps
                        | use_nil
                        | return_trailer
                        | dedupe_keys
                        | copy_strings
                        | {null_term, any()}
                        | {bytes_per_iter, non_neg_integer()}
                        | {bytes_per_red, non_neg_integer()}.

-type encode_option() :: uescape
                        | pretty
                        | force_utf8
                        | use_nil
                        | escape_forward_slashes
                        | {bytes_per_iter, non_neg_integer()}
                        | {bytes_per_red, non_neg_integer()}.

-type decode_options() :: [decode_option()].
-type encode_options() :: [encode_option()].

-export_type([decode_options/0, encode_options/0]).
-export_type([json_value/0, jiffy_decode_result/0]).

-spec decode(iolist() | binary()) -> jiffy_decode_result().
decode(Data) ->
    decode(Data, []).


-spec decode(iolist() | binary(), decode_options()) -> jiffy_decode_result().
decode(Data, Opts) when is_binary(Data), is_list(Opts) ->
    case nif_decode_init(Data, Opts) of
        {error, Error} ->
            error(Error);
        {partial, EJson} ->
            finish_decode(EJson);
        EJson ->
            EJson
    end;
decode(Data, Opts) when is_list(Data) ->
    decode(iolist_to_binary(Data), Opts).


-spec encode(json_value() | json_preencoded()) -> iodata().
encode(Data) ->
    encode(Data, []).


-spec encode(json_value() | json_preencoded(), encode_options()) -> iodata().
encode(Data, Options) ->
    ForceUTF8 = lists:member(force_utf8, Options),
    case nif_encode_init(Data, Options) of
        {error, {invalid_string, _}} when ForceUTF8 == true ->
            FixedData = jiffy_utf8:fix(Data),
            encode(FixedData, Options -- [force_utf8]);
        {error, {invalid_object_member_key, _}} when ForceUTF8 == true ->
            FixedData = jiffy_utf8:fix(Data),
            encode(FixedData, Options -- [force_utf8]);
        {error, Error} ->
            error(Error);
        {partial, IOData} ->
            finish_encode(IOData, []);
        [Bin] when is_binary(Bin) ->
            Bin;
        RevIOData when is_list(RevIOData) ->
            lists:reverse(RevIOData)
    end.


finish_decode({bignum, Value}) ->
    binary_to_integer(Value);
finish_decode({bigdbl, Value}) ->
    % Got something like a 1e400, 1.5e500, or 999...999.5. Split on e/E and if
    % we do have an explicit exponent use math:pow/2 to create the float since
    % Erlang/OTP's binary_to_float can't handle something like 1e10, it needs
    % the first number (the mantissa) to have '.' in it (it has to be 1.0e10).
    case binary:split(Value, [<<$e>>, <<$E>>]) of
        [IStr, EStr] ->
            EVal = binary_to_integer(EStr),
            IVal = case binary:match(IStr, <<$.>>) of
                nomatch -> binary_to_integer(IStr);
                _ -> binary_to_float(IStr)
            end,
            try
                IVal * math:pow(10, EVal)
            catch
                error:badarith ->
                    error({range, Value})
            end;
        [_] ->
            try
                binary_to_float(Value)
            catch
                error:badarg ->
                    error({range, Value})
            end
    end;
finish_decode({Pairs}) when is_list(Pairs) ->
    finish_decode_obj(Pairs, []);
finish_decode(Vals) when is_list(Vals) ->
    finish_decode_arr(Vals, []);
finish_decode({has_trailer, Value, Rest}) ->
    {has_trailer, finish_decode(Value), Rest};
finish_decode(Val) ->
    maybe_map(Val).

maybe_map(Obj) when is_map(Obj) ->
    maps:map(fun finish_decode_map/2, Obj);
maybe_map(Val) ->
    Val.

finish_decode_map(_, V) ->
    finish_decode(V).

finish_decode_obj([], Acc) ->
    {lists:reverse(Acc)};
finish_decode_obj([{K, V} | Pairs], Acc) ->
    finish_decode_obj(Pairs, [{K, finish_decode(V)} | Acc]).

finish_decode_arr([], Acc) ->
    lists:reverse(Acc);
finish_decode_arr([V | Vals], Acc) ->
    finish_decode_arr(Vals, [finish_decode(V) | Acc]).


finish_encode([], Acc) ->
    %% No reverse! The NIF returned us
    %% the pieces in reverse order.
    Acc;
finish_encode([<<_/binary>>=B | Rest], Acc) ->
    finish_encode(Rest, [B | Acc]);
finish_encode([Val | Rest], Acc) when is_integer(Val) ->
    finish_encode(Rest, [integer_to_binary(Val) | Acc]);
finish_encode([{json, Json} | Rest], Acc) ->
    %% Pre-encoded JSON spliced into the output as-is. This came from
    %% enc_unknown.
    finish_encode(Rest, [Json | Acc]);
finish_encode([InvalidEjson | _], _) ->
    error({invalid_ejson, InvalidEjson});
finish_encode(_, _) ->
    error(invalid_ejson).


init() ->
    PrivDir = case code:priv_dir(?MODULE) of
        {error, _} ->
            EbinDir = filename:dirname(code:which(?MODULE)),
            AppPath = filename:dirname(EbinDir),
            filename:join(AppPath, "priv");
        Path ->
            Path
    end,
    erlang:load_nif(filename:join(PrivDir, "jiffy"), 0).


not_loaded(Line) ->
    erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}).

nif_decode_init(_Data, _Opts) ->
    ?NOT_LOADED.

nif_encode_init(_Data, _Options) ->
    ?NOT_LOADED.
