%% The MIT License

%% Copyright (c) 2010-2013 alisdair sullivan <alisdairsullivan@yahoo.ca>

%% Permission is hereby granted, free of charge, to any person obtaining a copy
%% of this software and associated documentation files (the "Software"), to deal
%% in the Software without restriction, including without limitation the rights
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
%% copies of the Software, and to permit persons to whom the Software is
%% furnished to do so, subject to the following conditions:

%% The above copyright notice and this permission notice shall be included in
%% all copies or substantial portions of the Software.

%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
%% THE SOFTWARE.


-module(cloudi_x_jsx).

-export([encode/1, encode/2, decode/1, decode/2]).
-export([is_json/1, is_json/2, is_term/1, is_term/2]).
-export([format/1, format/2, minify/1, prettify/1]).
-export([consult/1, consult/2]).
-export([encoder/3, decoder/3, parser/3]).
-export([resume/3]).

-export_type([json_term/0, json_text/0, token/0]).
-export_type([encoder/0, decoder/0, parser/0, internal_state/0]).
-export_type([config/0]).


-ifdef(TEST).
%% data and helper functions for tests
-export([test_cases/0, special_test_cases/0]).
-export([init/1, handle_event/2]).
-endif.

-type json_term() :: [{binary() | atom(), json_term()}] | [{},...]
    | [json_term()] | []
    | {with_tail, json_term(), binary()}
    | #{ binary() | atom() => json_term() }
    | true | false | null
    | integer() | float()
    | binary() | atom()
    | calendar:datetime().

-type json_text() :: binary().

-type config() :: cloudi_x_jsx_config:config().

-spec encode(Source::json_term()) -> json_text() | {incomplete, encoder()}.

encode(Source) -> encode(Source, []).

-spec encode(Source::json_term(), Config::cloudi_x_jsx_config:options()) -> json_text() | {incomplete, encoder()}.

encode(Source, Config) -> cloudi_x_jsx_to_json:to_json(Source, Config).


-spec decode(Source::json_text()) -> json_term() | {incomplete, decoder()}.

decode(Source) -> decode(Source, []).

-spec decode(Source::json_text(), Config::cloudi_x_jsx_config:options()) -> json_term() | {incomplete, decoder()}.

decode(Source, Config) -> cloudi_x_jsx_to_term:to_term(Source, Config).


-spec format(Source::json_text()) -> json_text().

format(Source) -> format(Source, []).

-spec format(Source::json_text(), Config::cloudi_x_jsx_config:options()) -> json_text().

format(Source, Config) -> cloudi_x_jsx_to_json:format(Source, Config).


-spec minify(Source::json_text()) -> json_text().

minify(Source) -> format(Source, []).


-spec prettify(Source::json_text()) -> json_text().

prettify(Source) -> format(Source, [space, {indent, 2}]).


-spec is_json(Source::binary()) -> boolean() | {incomplete, decoder()}.

is_json(Source) -> is_json(Source, []).

-spec is_json(Source::binary(), Config::cloudi_x_jsx_config:options()) -> boolean() | {incomplete, decoder()}.

is_json(Source, Config) -> cloudi_x_jsx_verify:is_json(Source, Config).


-spec is_term(Source::json_term() | end_stream | end_json) -> boolean() | {incomplete, encoder()}.

is_term(Source) -> is_term(Source, []).

-spec is_term(Source::json_term() | end_stream | end_json,
              Config::cloudi_x_jsx_config:options()) -> boolean() | {incomplete, encoder()}.

is_term(Source, Config) -> cloudi_x_jsx_verify:is_term(Source, Config).


-spec consult(File::file:name_all()) -> list(cloudi_x_jsx_consult:json_value()).

consult(File) -> consult(File, []).

-spec consult(File::file:name_all(), Config::cloudi_x_jsx_consult:config()) -> list(cloudi_x_jsx_consult:json_value()).

consult(File, Config) -> cloudi_x_jsx_consult:consult(File, Config).


-type decoder() :: fun((json_text() | end_stream | end_json) -> any()).

-spec decoder(Handler::module(), State::any(), Config::cloudi_x_jsx_config:options()) -> decoder().

decoder(Handler, State, Config) -> cloudi_x_jsx_decoder:decoder(Handler, State, Config).


-type encoder() :: fun((json_term() | end_stream | end_json) -> any()).

-spec encoder(Handler::module(), State::any(), Config::cloudi_x_jsx_config:options()) -> encoder().

encoder(Handler, State, Config) -> cloudi_x_jsx_encoder:encoder(Handler, State, Config).


-type token() :: [token()]
    | start_object
    | end_object
    | start_array
    | end_array
    | {key, binary()}
    | {string, binary()}
    | binary()
    | {number, integer() | float()}
    | {integer, integer()}
    | {float, float()}
    | integer()
    | float()
    | {literal, true}
    | {literal, false}
    | {literal, null}
    | true
    | false
    | null
    | end_json.


-type parser() :: fun((token() | end_stream) -> any()).

-spec parser(Handler::module(), State::any(), Config::cloudi_x_jsx_config:options()) -> parser().

parser(Handler, State, Config) -> cloudi_x_jsx_parser:parser(Handler, State, Config).

-opaque internal_state() :: tuple().

-spec resume(Term::json_text() | token(), InternalState::internal_state(),
             Config::cloudi_x_jsx_config:options()) -> cloudi_x_jsx:decoder() | {incomplete, cloudi_x_jsx:decoder()}.

resume(Term, {decoder, State, Handler, Acc, Stack}, Config) ->
    cloudi_x_jsx_decoder:resume(Term, State, Handler, Acc, Stack, cloudi_x_jsx_config:parse_config(Config));
resume(Term, {parser, State, Handler, Stack}, Config) ->
    cloudi_x_jsx_parser:resume(Term, State, Handler, Stack, cloudi_x_jsx_config:parse_config(Config)).

-ifdef(TEST).

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


%% test handler
init([]) -> [].

handle_event(end_json, State) -> lists:reverse([end_json] ++ State);
handle_event(Event, State) -> [Event] ++ State.


test_cases() ->
    empty_array()
    ++ nested_array()
    ++ empty_object()
    ++ nested_object()
    ++ strings()
    ++ literals()
    ++ integers()
    ++ floats()
    ++ compound_object().

%% segregate these so we can skip them in `cloudi_x_jsx_to_term`
special_test_cases() -> special_objects() ++ special_array().


empty_array() -> [{"[]", <<"[]">>, [], [start_array, end_array]}].


nested_array() ->
    [{
        "[[[]]]",
        <<"[[[]]]">>,
        [[[]]],
        [start_array, start_array, start_array, end_array, end_array, end_array]
    }].


empty_object() -> [{"{}", <<"{}">>, [{}], [start_object, end_object]}].


nested_object() ->
    [{
        "{\"key\":{\"key\":{}}}",
        <<"{\"key\":{\"key\":{}}}">>,
        [{<<"key">>, [{<<"key">>, [{}]}]}],
        [
            start_object,
                {key, <<"key">>},
                start_object,
                    {key, <<"key">>},
                    start_object,
                    end_object,
                end_object,
            end_object
        ]
    }].


naked_strings() ->
    Raw = [
        "",
        "hello world"
    ],
    [
        {
            String,
            <<"\"", (list_to_binary(String))/binary, "\"">>,
            list_to_binary(String),
            [{string, list_to_binary(String)}]
        }
        || String <- Raw
    ].


strings() ->
    naked_strings()
    ++ [ wrap_with_array(Test) || Test <- naked_strings() ]
    ++ [ wrap_with_object(Test) || Test <- naked_strings() ].


naked_integers() ->
    Raw = [
        1, 2, 3,
        127, 128, 129,
        255, 256, 257,
        65534, 65535, 65536,
        18446744073709551616,
        18446744073709551617
    ],
    [
        {
            integer_to_list(X),
            list_to_binary(integer_to_list(X)),
            X,
            [{integer, X}]
        }
        || X <- Raw ++ [ -1 * Y || Y <- Raw ] ++ [0]
    ].


integers() ->
    naked_integers()
    ++ [ wrap_with_array(Test) || Test <- naked_integers() ]
    ++ [ wrap_with_object(Test) || Test <- naked_integers() ].


naked_floats() ->
    Raw = [
        0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9,
        1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9,
        1234567890.0987654321,
        0.0e0,
        1234567890.0987654321e16,
        0.1e0, 0.1e1, 0.1e2, 0.1e4, 0.1e8, 0.1e16, 0.1e308,
        1.0e0, 1.0e1, 1.0e2, 1.0e4, 1.0e8, 1.0e16, 1.0e308,
        2.2250738585072014e-308,    %% min normalized float
        1.7976931348623157e308,     %% max normalized float
        5.0e-324,                   %% min denormalized float
        2.225073858507201e-308      %% max denormalized float
    ],
    [
        {
            sane_float_to_list(X),
            list_to_binary(sane_float_to_list(X)),
            X,
            [{float, X}]
        }
        || X <- Raw ++ [ -1 * Y || Y <- Raw ]
    ].


floats() ->
    naked_floats()
    ++ [ wrap_with_array(Test) || Test <- naked_floats() ]
    ++ [ wrap_with_object(Test) || Test <- naked_floats() ].


naked_literals() ->
    [
        {
            atom_to_list(Literal),
            atom_to_binary(Literal, unicode),
            Literal,
            [{literal, Literal}]
        }
        || Literal <- [true, false, null]
    ].


literals() ->
    naked_literals()
    ++ [ wrap_with_array(Test) || Test <- naked_literals() ]
    ++ [ wrap_with_object(Test) || Test <- naked_literals() ].


compound_object() ->
    [{
        "[{\"alpha\":[1,2,3],\"beta\":{\"alpha\":[1.0,2.0,3.0],\"beta\":[true,false]}},[{}]]",
        <<"[{\"alpha\":[1,2,3],\"beta\":{\"alpha\":[1.0,2.0,3.0],\"beta\":[true,false]}},[{}]]">>,
        [[{<<"alpha">>, [1, 2, 3]}, {<<"beta">>, [{<<"alpha">>, [1.0, 2.0, 3.0]}, {<<"beta">>, [true, false]}]}], [[{}]]],
        [
            start_array,
                start_object,
                    {key, <<"alpha">>},
                    start_array,
                        {integer, 1},
                        {integer, 2},
                        {integer, 3},
                    end_array,
                    {key, <<"beta">>},
                    start_object,
                        {key, <<"alpha">>},
                        start_array,
                            {float, 1.0},
                            {float, 2.0},
                            {float, 3.0},
                        end_array,
                        {key, <<"beta">>},
                        start_array,
                            {literal, true},
                            {literal, false},
                        end_array,
                    end_object,
                end_object,
                start_array,
                    start_object,
                    end_object,
                end_array,
            end_array
        ]
    }].


special_objects() ->
    [
        {
            "[{key, atom}]",
            <<"{\"key\":\"atom\"}">>,
            [{key, atom}],
            [start_object, {key, <<"key">>}, {string, <<"atom">>}, end_object]
        },
        {
            "[{1, true}]",
            <<"{\"1\":true}">>,
            [{1, true}],
            [start_object, {key, <<"1">>}, {literal, true}, end_object]
        }
    ].


special_array() ->
    [    
        {
            "[foo, bar]",
            <<"[\"foo\",\"bar\"]">>,
            [foo, bar],
            [start_array, {string, <<"foo">>}, {string, <<"bar">>}, end_array]
        }
    ].


wrap_with_array({Title, JSON, Term, Events}) ->
    {
        "[" ++ Title ++ "]",
        <<"[", JSON/binary, "]">>,
        [Term],
        [start_array] ++ Events ++ [end_array]
    }.


wrap_with_object({Title, JSON, Term, Events}) ->
    {
        "{\"key\":" ++ Title ++ "}",
        <<"{\"key\":", JSON/binary, "}">>,
        [{<<"key">>, Term}],
        [start_object, {key, <<"key">>}] ++ Events ++ [end_object]
    }.


sane_float_to_list(X) ->
    [Output] = io_lib:format("~p", [X]),
    Output.


incremental_decode(JSON) ->
    Final = lists:foldl(
        fun(Byte, Decoder) -> {incomplete, F} = Decoder(Byte), F end,
        decoder(cloudi_x_jsx, [], [stream]),
        json_to_bytes(JSON)
    ),
    Final(end_stream).


incremental_parse(Events) ->
    Final = lists:foldl(
        fun(Event, Parser) -> {incomplete, F} = Parser(Event), F end,
        parser(?MODULE, [], [stream]),
        lists:map(fun(X) -> [X] end, Events)
    ),
    Final(end_stream).


%% used to convert a json text into a list of codepoints to be incrementally
%% parsed
json_to_bytes(JSON) -> json_to_bytes(JSON, []).

json_to_bytes(<<>>, Acc) -> [<<>>] ++ lists:reverse(Acc);
json_to_bytes(<<X, Rest/binary>>, Acc) -> json_to_bytes(Rest, [<<X>>] ++ Acc).


%% actual tests!
decode_test_() ->
    Data = test_cases(),
    [{Title, ?_assertEqual(Events ++ [end_json], (decoder(?MODULE, [], []))(JSON))}
        || {Title, JSON, _, Events} <- Data
    ] ++
    [{Title ++ " (incremental)", ?_assertEqual(Events ++ [end_json], incremental_decode(JSON))}
        || {Title, JSON, _, Events} <- Data
    ].


parse_test_() ->
    Data = test_cases(),
    [{Title, ?_assertEqual(Events ++ [end_json], (parser(?MODULE, [], []))(Events ++ [end_json]))}
        || {Title, _, _, Events} <- Data
    ] ++
    [{Title ++ " (incremental)", ?_assertEqual(Events ++ [end_json], incremental_parse(Events))}
        || {Title, _, _, Events} <- Data
    ].


encode_test_() ->
    Data = test_cases(),
    [
        {
            Title, ?_assertEqual(
                Events ++ [end_json],
                (cloudi_x_jsx:encoder(cloudi_x_jsx, [], []))(Term)
            )
        } || {Title, _, Term, Events} <- Data
    ].

end_stream_test_() ->
    Tokens = [start_object, end_object, end_json],
    [
        {"encoder end_stream", ?_assertEqual(
            Tokens,
            begin
                {incomplete, F} = (cloudi_x_jsx:parser(cloudi_x_jsx, [], [stream]))([start_object, end_object]),
                F(end_stream)
            end
        )},
        {"encoder end_json", ?_assertEqual(
            Tokens,
            begin
                {incomplete, F} = (cloudi_x_jsx:parser(cloudi_x_jsx, [], [stream]))([start_object, end_object]),
                F(end_json)
            end
        )},
        {"decoder end_stream", ?_assertEqual(
            Tokens,
            begin {incomplete, F} = (cloudi_x_jsx:decoder(cloudi_x_jsx, [], [stream]))(<<"{}">>), F(end_stream) end
        )},
        {"decoder end_json", ?_assertEqual(
            Tokens,
            begin {incomplete, F} = (cloudi_x_jsx:decoder(cloudi_x_jsx, [], [stream]))(<<"{}">>), F(end_json) end
        )}
    ].


-endif.
