:- module(scasp_json,
          [ scasp_results_json/2        % +Results, -Dict
          ]).
:- use_module(output).
:- use_module(options).
:- use_module(clp/disequality).
:- use_module(clp/clpq).
:- use_module(source_ref).

/** <module> s(CASP) JSON I/O

*/

%!  scasp_results_json(+Results, -Dict) is det.
%
%   Generate a JSON document from  the   results  of the s(CASP) solver.
%   Results is a dict holding:
%
%     - query
%       The query represented as a list or as a conjunction.
%     - answers
%       A list of answers.  Each answer is a dict holding the keys
%       below. This dict is normally created using scasp_solve/4.
%       - query
%       - answer
%         Nth answer (1, 2, ...)
%       - bindings
%       - model
%       - tree
%         The justification tree.  Optional.
%       - time
%     - inputs
%       Description of the input.  Typically a list of file names.
%	Optional.
%     - cpu
%       COU time used to get all answers (optional).

:- det(scasp_results_json/2).
scasp_results_json(Result, Dict) :-
    _{ query: Query,
       answers: Answers
     } :< Result,
    Dict0 = scasp_result{ solver: Version,
                          query: JQuery,
                          answers: JAnswers
                       },
    scasp_version(Version),
    query_json(Query, JQuery),
    maplist(answer_json, Answers, JAnswers),
    copy_dict_slots([cpu-time, inputs], Dict0, Dict).

copy_dict_slots(Slots, Dict0, Dict) :-
    foldl(copy_dict_slot, Slots, Dict0, Dict).

copy_dict_slot(Name, Dict0, Dict), atom(Name) =>
    (   Value = Dict0.get(Name)
    ->  Dict = Dict0.put(Name, Value)
    ;   Dict0 = Dict
    ).
copy_dict_slot(Name-To, Dict0, Dict) =>
    (   Value = Dict0.get(Name)
    ->  Dict = Dict0.put(To, Value)
    ;   Dict0 = Dict
    ).


:- meta_predicate query_json(:, -).
:- det(query_json/2).
query_json(_:Query, JQuery), is_list(Query) =>
    delete(Query, o_nmr_check, Query1),
    delete(Query1, true, Query2),
    plain_term_json(Query2, JQuery).
query_json(_:Query, JQuery) =>
    comma_list(Query, List),
    plain_term_json(List, JQuery).

%!  answer_json(+Answer, -Dict) is det.

:- det(answer_json/2).
answer_json(Answer, Dict),
    _{ answer:Counter,
       bindings:Bindings,
       model:Model,
       tree:_:Tree,
       time:Time
     } :< Answer =>
    Dict = scasp_answer{answer: Counter,
                        time: Time.cpu,
                        bindings: JBindings,
                        model: JModel,
                        tree: JTree,
                        constraints: Constraints},
    maplist(binding_json, Bindings, Pairs),
    dict_create(JBindings, #, Pairs),
    maplist(model_term_json, Model, JModel),
    tree_json(Tree, JTree),
    constraints_json(t(Bindings,Model,JTree), Constraints).
answer_json(Answer, Dict),
    _{ answer:Counter,
       bindings:Bindings,
       model:Model,
       time:Time
     } :< Answer =>
    Dict = scasp_answer{answer: Counter,
                        time: Time.cpu,
                        bindings: JBindings,
                        model: JModel,
                        constraints: Constraints},
    maplist(binding_json, Bindings, Pairs),
    dict_create(JBindings, #, Pairs),
    maplist(model_term_json, Model, JModel),
    constraints_json(t(Bindings,Model), Constraints).

binding_json(Name=Value, Name-JValue) :-
    model_term_json(Value, JValue).

tree_json(Root-Children, Dict) =>
    Dict = #{ node: JRoot,
              children: JChildren },
    node_json(Root, JRoot),
    maplist(tree_json, Children, JChildren).

node_json(chs(Node), Dict) =>
    model_term_json(Node, Dict0),
    Dict = Dict0.put(chs, true).
node_json(assume(Node), Dict) =>
    model_term_json(Node, Dict0),
    Dict = Dict0.put(assume, true).
node_json(proved(Node), Dict) =>
    model_term_json(Node, Dict0),
    Dict = Dict0.put(proved, true).
node_json(abduced(Node), Dict) =>
    model_term_json(Node, Dict0),
    Dict = Dict0.put(abduced, true).
node_json(goal_origin(Node, Origin), Dict) =>
    scasp_source_reference_file_line(Origin, File, Line),
    node_json(Node, Dict0),
    Dict1 = Dict0.put(source_file, File),
    Dict  = Dict1.put(source_line, Line).
node_json(Node, Dict) =>
    model_term_json(Node, Dict).


%!  model_term_json(+ModelTerm, -Dict) is det.

:- det(model_term_json/2).
model_term_json(not(-Term), Dict) =>
    Dict = scasp_model_term{truth: likely,   value: TermJSON},
    module_term_json(Term, TermJSON).
model_term_json(-Term, Dict) =>
    Dict = scasp_model_term{truth: false,    value: TermJSON},
    module_term_json(Term, TermJSON).
model_term_json(not(Term), Dict) =>
    Dict = scasp_model_term{truth: unlikely, value: TermJSON},
    module_term_json(Term, TermJSON).
model_term_json(Term, Dict) =>
    Dict = scasp_model_term{truth: true,     value: TermJSON},
    module_term_json(Term, TermJSON).

module_term_json(M:Term, Dict) =>
    plain_term_json(Term, Dict0),
    Dict = Dict0.put(module, M).
module_term_json(Term, Dict) =>
    plain_term_json(Term, Dict).

%!  plain_term_json(+Term, -Dict) is det.

:- det(plain_term_json/2).
plain_term_json(Var, Dict), var(Var) =>
    (   ovar_var_name(Var, Name)
    ->  Dict = prolog{type: var,
                      name: Name}
    ;   ovar_is_singleton(Var)
    ->  Dict = prolog{type: var}
    ).
plain_term_json(Var, Dict), var_number(Var, Num) =>
    (   Num == '_'
    ->  Dict = prolog{type: var}
    ;   format(string(S), '~p', [Var]),
        Dict = prolog{type: var,
                      name: S}
    ).
plain_term_json(Atom, Dict), atom(Atom) =>
    Dict = atom{type:atom, value:Atom}.
plain_term_json(Num, Dict), rational(Num, N, D) =>
    (   current_prolog_flag(scasp_real, Decimals),
        (   integer(Decimals)
        ->  truncate(Num, Decimals, Value)
        ;   Decimals == float
        ->  Value is float(Num)
        )
    ->  Dict = prolog{type:number, value:Value}
    ;   Dict = prolog{type:rational, numerator:N, denominator:D}
    ).
plain_term_json(Num, Dict), number(Num) =>
    Dict = prolog{type:number, value:Num}.
plain_term_json(List, JList), is_list(List) =>
    maplist(plain_term_json, List, JList).
plain_term_json(Compound, Dict), compound(Compound) =>
    compound_name_arguments(Compound, Name, Arguments),
    Dict = prolog{type:compound, functor:Name, args:JArgs},
    maplist(plain_term_json, Arguments, JArgs).

truncate(Rat, Decimals, Value) :-
    Z is Rat * 10**Decimals,
    ZA is truncate(Z),
    Value is float(ZA / 10**Decimals).

%!  constraints_json(+Term, -Dict) is det.

:- det(constraints_json/2).
constraints_json(Term, Dict) :-
    term_attvars(Term, Attvars),
    include(has_constraints, Attvars, CVars),
    copy_term(CVars, Copy),
    inline_constraints(Copy, []),
    maplist(constraint_json, Copy, Constraints),
    dict_create(Dict, #, Constraints).

has_constraints(Var) :-
    get_neg_var(Var, _List),
    !.
has_constraints(Var) :-
    is_clpq_var(Var).

constraint_json('| '(Var, {'\u2209'(Var, List)}), Pair) =>
    Pair = Name-constraint{ type: not_in, set: JList},
    var_name(Var, Name),
    maplist(plain_term_json, List, JList).
constraint_json('| '(Var, {Term}), Pair) =>
    Pair = Name-constraint{ type: clpq, constraints: Constraints},
    var_name(Var, Name),
    comma_list(Term, Constraints0),
    maplist(clpq_json(Var), Constraints0, Constraints).

var_name('$VAR'(Name0), Name) =>
    Name = Name0.
var_name(Var, Name), ovar_var_name(Var, Name0) =>
    Name = Name0.

clpq_json(Var, Term, Dict), Term =.. [Op, Var, Arg] =>
    Dict = constraint{type:  Op, value: JArg},
    plain_term_json(Arg, JArg).


:- multifile
    json:json_dict_pairs/2.

json:json_dict_pairs(Dict, Pairs) :-
    is_dict(Dict, Tag),
    order(Tag, Order),
    dict_keys(Dict, All),
    sort(Order, Ordered),
    ord_subtract(All, Ordered, Unordered),
    phrase(json_pairs(Order, Dict), Pairs, Pairs1),
    phrase(json_pairs(Unordered, Dict), Pairs1).

json_pairs([], _) -->
    [].
json_pairs([H|T], Dict) -->
    (   {get_dict(H, Dict, Value)}
    ->  [H-Value]
    ;   []
    ),
    json_pairs(T, Dict).

order(scasp_result,     [solver,inputs,query,time,answers]).
order(scasp_answer,     [answer,time,bindings,model,tree]).
order(scasp_model_term, [truth, value, chs, assume, proved]).
order(prolog,           [type,functor,numerator,denominator,name,value]).
order(constraint,       [type,set,constraints,value]).