#!/usr/bin/env swipl
% -*- mode: prolog -*-

:- initialization(main, main).

:- use_module(library(protobufs)).
:- use_module(library(apply), [maplist/2]).

% TODO: update this documentation:
% The following was generated by running parse_descriptor_proto_dump.pl
% and extracting the contents. See also descriptor_proto.pl

:- use_module(protoc_gen_prolog_pb/plugin_pb).
:- use_module(protoc_gen_prolog_pb/descriptor_pb).

main(Argv) :-
    with_output_to(string(Result), main2(Argv, FileName)),
    Error = [], % or: Error = ['This is the error message']
    % TODO: use protobufs:protobuf_serialize_to_codes/3
    %       MessageType='.google.protobuf.compiler.CodeGeneratorResponse'
    Response = protobuf([ % message CodeGeneratorResponse
                          repeated( 1, string(Error)), % optional string error = 1
                          embedded(15, File)           % repeated File = 15
                        ]
                        ),
    File = protobuf([ % message File
                      string( 1, Name),   % optional string name = 1
                      string(15, Content) % optional string content = 15
                    ]),
    atom_concat(FilePart, ".proto", FileName),  % TODO: file_name_extension/3
    atom_concat(FilePart, "_pb.pl", Name),
    Content = Result,
    protobuf_message(Response, ResponseWireStream),
    set_stream(user_output, encoding(octet)),
    set_stream(user_output, type(binary)),
    format(user_output, '~s', [ResponseWireStream]),
    halt.

main2(Argv, FileName) :-
    set_stream(user_input, encoding(octet)),
    set_stream(user_input, type(binary)),
    read_stream_to_codes(user_input, RequestWireStream),
    % Because of the way the code is structured, bugs can cause
    % backtracking into a clause that gives an uninformative
    % instantiation error. To debug this, use the following code:
    %   :- use_module(library(prolog_stack)).  % For catch_with_backtrace
    %   catch_with_backtrace(
    %       protobuf_parse_from_codes(...),
    %       Error,
    %       ( print_message(error, Error),  halt(1) )),
    protobuf_parse_from_codes(RequestWireStream,
                              '.google.protobuf.compiler.CodeGeneratorRequest',
                              Request),
    Request.file_to_generate = [FileName|_],
    file_base_name(FileName, FileBaseName),
    file_name_extension(ModuleName0, Extension, FileBaseName),
    atomic_concat(ModuleName0, '_pb', ModuleName),
    assertion(Extension == 'proto'),
    format('% ~w~n', ['-*- mode: prolog coding:utf-8 -*-']),
    format('~n% ~w~n', ['This file was generated by protoc-gen-swipl']),
    format('% ~w~n~n', ['as a plugin from protoc (the Protobuf compiler)']),
    format('~q.~n', [(:- module(ModuleName, []))]),
    format('~q.~n', [(:- encoding(utf8))]),
    (   current_prolog_flag(version_git, Version)
    ->  format('swi_prolog_version(~q).~n', [Version])
    ;   current_prolog_flag(version_data, swi(Major, Minor, Path, Extra)),
        (   Extra == []
        ->  format('swi_prolog_version(\'~w.~w.~w\').~n',    [Major, Minor, Path])
        ;   format('swi_prolog_version(\'~w.~w.~w.~w\').~n', [Major, Minor, Path, Extra])
        )
    ),
    ProtocVersion = Request.compiler_version,
    (   ProtocVersion.suffix == ''
    ->  format('protoc_version(\'~w.~w.~w\').~n',
               [ProtocVersion.major, ProtocVersion.minor, ProtocVersion.patch])
    ;   format('protoc_version(\'~w.~w.~w.~w\').~n',
               [ProtocVersion.major, ProtocVersion.minor, ProtocVersion.patch, ProtocVersion.suffix])
    ),
    ReqVersion = req_version{major:3, minor:6, patch:1}, % from Ubuntu PPA
    assertion(ProtocVersion.major > ReqVersion.major
            ;    (   ProtocVersion.major == ReqVersion.major,
                     ProtocVersion.minor > ReqVersion.minor)
             ;   (   ProtocVersion.major == ReqVersion.major,
                     ProtocVersion.minor == ReqVersion.minor,
                     ProtocVersion.patch >= ReqVersion.patch)),
    format('prototoc_gen_swipl_args(~q).~n', [Argv]),
    get_time(Time),
    stamp_date_time(Time, DateUtc, 'UTC'),
    stamp_date_time(Time, DateLocal, local),
    format_time(atom(TS_utc), '%FT%T%z', DateUtc, posix),
    format_time(atom(TS_local), '%FT%T%z', DateLocal, posix),
    format('protoc_run_time(~q, ~q).~n', [TS_utc, TS_local]),
    format('file_to_generate(~q).~n~n', [Request.file_to_generate]),
    generated_preds(Preds),
    atomic_list_concat(Preds, ',\n    ', PredsStr),
    format(':- multifile~n    ~w~n', [PredsStr]),
    format(':- discontiguous~n    ~w~n~n~n', [PredsStr]), % Not needed: multifile implies this
    (   false  % change to "true" for debugging
               % these 2 facts add a lot to load time (0.33 sec vs 0.02 sec)
    ->  format('~n% for debugging:~n', []),
        % remove the source code stuff for debugging output - we don't use it:
        maplist(nb_set_dict_value(source_code_info, ' <deleted> '), Request.proto_file),
        % (   select_dict(_{source_code_info:_}, Request, RequestWithoutSourceCodeInfo)
        % ->  true
        % ;   RequestWithoutSourceCodeInfo = Request
        % ),
        % print_term(RequestWithoutSourceCodeInfo, [indent_arguments(4),output(current_output)]),
        % TODO: use print_term to print the request -- there's a bug for dict{x: -5} which outputs as "dict{x:-5}", which can't be read
        %   format('request(~n', []),
        %   print_term(Request, [indent_arguments(4),output(current_output)]),
        %   format(').~n', []),
        format('~q.~n', [request(Request)]),
        format('request_wire_stream(~q).~n', [RequestWireStream]),
        format('% (end of debbuging facts.~n~n', [])
    ;   true
    ),

    expand_request(Request),

    format('~nend_of_file.~n', []).

nb_set_dict_value(Key, Value, Dict) :-
    nb_set_dict(Key, Dict, Value).

generated_preds(Preds) :-
        Preds = [
     'protobufs:proto_meta_normalize/2,           %   protobufs:proto_meta_normalize(Unnormalized, Normalized)',
     'protobufs:proto_meta_package/3,             %   protobufs:proto_meta_package(Package, FileName, Options)',
     'protobufs:proto_meta_message_type/3,        %   protobufs:proto_meta_message_type(       Fqn,     Package, Name)',
     'protobufs:proto_meta_field_name/4,          %   protobufs:proto_meta_field_name(         Fqn,     FieldNumber, FieldName, FqnName)',
     'protobufs:proto_meta_field_json_name/2,     %   protobufs:proto_meta_field_json_name(    FqnName, JsonName)',
     'protobufs:proto_meta_field_label/2,         %   protobufs:proto_meta_field_label(        FqnName, LabelRepeatOptional) % LABEL_OPTIONAL, LABEL_REQUIRED, LABEL_REPEATED',
     'protobufs:proto_meta_field_type/2,          %   protobufs:proto_meta_field_type(         FqnName, Type) % TYPE_INT32, TYPE_MESSAGE, etc',
     'protobufs:proto_meta_field_type_name/2,     %   protobufs:proto_meta_field_type_name(    FqnName, TypeName)',
     'protobufs:proto_meta_field_default_value/2, %   protobufs:proto_meta_field_default_value(FqnName, DefaultValue)',
     'protobufs:proto_meta_field_option_packed/1, %   protobufs:proto_meta_field_option_packed(FqnName)',
     'protobufs:proto_meta_enum_type/3,           %   protobufs:proto_meta_enum_type(          FqnName, Fqn, Name)',
     'protobufs:proto_meta_enum_value/3.          %   protobufs:proto_meta_enum_value(         FqnName, Name, Number)'
            ].

:- det(expand_request/1).
expand_request(Request) :-
    format('~n% Generating proto_meta_... facts:~n', []),
    format('  % compiler_version: ~q~n', [Request.compiler_version]),
    format('  % file_to_generate: ~q~n', [Request.file_to_generate]), % list
    (   get_dict(parameter, Request, Request_parameter)
    ->  format('  % parameter: ~q~n', [Request_parameter])
    ;   format('  % parameter: (none)~n', [])
    ),
    % Request.parameter comes from protoc=--swipl_out=..., which allows
    % specifying a "parameter:dir".
    % TODO: https://github.com/SWI-Prolog/contrib-protobufs/issues/7
    %       - optionally process all (recursive) imports
    %       - generate use_module directives for imports
    maplist(expand_file(Request.file_to_generate), Request.proto_file),
    format('~n% End of generated proto_meta_... facts.~n', []).

:- det(expand_file/2).
expand_file(FileToGenerate, File) :-
    (   memberchk(File.name, FileToGenerate)
    ->  format('  % Processing file ~q~n', [File.name]),
        expand_file_impl(File)
    ;   format('  % Skipping file ~q~n', [File.name])
    ).

:- det(expand_file_impl/1).
expand_file_impl(File) :-
    lookup_pieces('.google.protobuf.FileDescriptorProto',
                  File,
                  _{
                    name:              ''              -File_name,
                    package:           ''              -File_package,
                    dependency:        []              -File_dependency,
                    public_dependency: []              -_,
                    weak_dependency:   []              -_,
                    message_type:      []              -File_message_type, 
                    enum_type:         []              -File_enum_type,
                    service:           []               -_,
                    extension:         []              -_File_extension,
                    options:           '.google.protobuf.FileOptions'{} -File_options,
                    source_code_info:  _               -_,
                    syntax:            ''              -_
                   }),
    % TODO: is there anything in File_options that we should check?
    % TODO: do anything with File_dependency? (which is a list)
    %       See https://github.com/SWI-Prolog/contrib-protobufs/issues/7
    format('  %  -- package(~q) name(~q) dependency(~q)~n',
           [File_package, File_name, File_dependency]),
    % TODO: handle _Fileextensions - see unittest.proto
    add_to_fqn('', File_package, Package),
    output_fact(protobufs:proto_meta_package(Package, File_name, File_options)),
    maplist(expand_DescriptorProto(Package), File_message_type),
    maplist(expand_EnumDescriptorProto(Package), File_enum_type).

:- det(expand_DescriptorProto/2).
expand_DescriptorProto(Fqn, MessageType) :-
    lookup_pieces('.google.protobuf.DescriptorProto',
                  MessageType,
                  _{
                    name:            ''      -MessageType_name,
                    field:           []       -MessageType_field,
                    extension:       []      -_,
                    nested_type:     []      -MessageType_nested_type,
                    enum_type:       []      -MessageType_enum_type,
                    extension_range: []      -_,
                    oneof_decl:      []      -_,
                    options:         []      -_,
                    reserved_range:  []      -_,
                    reserved_name:   []      -_
                   }),
    add_to_fqn(Fqn, MessageType_name, FqnName),
    fqn_no_dot(FqnName, FqnNameNoDot),
    output_fact(protobufs:proto_meta_normalize(FqnName, FqnName)),
    output_fact(protobufs:proto_meta_normalize(FqnNameNoDot, FqnName)),
    output_fact(protobufs:proto_meta_message_type(FqnName, Fqn, MessageType_name)),
    maplist(expand_FieldDescriptorProto(FqnName), MessageType_field),
    maplist(expand_DescriptorProto(FqnName), MessageType_nested_type),
    maplist(expand_EnumDescriptorProto(FqnName), MessageType_enum_type).

fqn_no_dot(FqnName, FqnNameNoDot) :-
    atom_concat('.', FqnNameNoDot, FqnName).

:- det(expand_FieldDescriptorProto/2).
expand_FieldDescriptorProto(Fqn, Field) :-
    lookup_pieces('.google.protobuf.FieldDescriptorProto', Field,
                  _{
                    name:            ''               -Field_name,
                    number:          0                -Field_number,
                    label:           0                -Field_label, % enum Label
                    type:            0                -Field_type, % enum Type
                    type_name:       ''               -Field_type_name,
                    extendee:        _                -_,
                    default_value:   ''               -Field_default_value,
                    oneof_index:     _                -_,
                    json_name:       ''               -Field_json_name,
                    options:         '.google.protobuf.FieldOptions'{} -Field_options,
                    proto3_optional: _                -_
                   }),
    add_to_fqn(Fqn, Field_name, FqnName),
    output_fact(protobufs:proto_meta_field_name(Fqn, Field_number, Field_name, FqnName)),
    output_fact(protobufs:proto_meta_field_json_name(FqnName, Field_json_name)),
    output_fact(protobufs:proto_meta_field_label(FqnName, Field_label)),
    output_fact(protobufs:proto_meta_field_type(FqnName, Field_type)),
    output_fact(protobufs:proto_meta_field_type_name(FqnName, Field_type_name)),
    output_fact(protobufs:proto_meta_field_default_value(FqnName, Field_default_value)),
    expand_FieldOptions(FqnName, Field_options).

:- det(expand_FieldOptions/2).
expand_FieldOptions(FqnName, Options) :-
    lookup_pieces('.google.protobuf.FieldOptions', Options,
                  _{
                    ctype:                _     -_,
                    packed:               false -Option_packed,
                    jstype:               _     -_,
                    lazy:                 false -_,
                    deprecated:           false -_, % TODO: output warning if a deprecated field is used
                    weak:                 false -_,
                    uninterpreted_option: _     -_
                   }),
    (   Option_packed = true
    ->  output_fact(protobufs:proto_meta_field_option_packed(FqnName))
    ;   true
    ).

:- det(expand_EnumDescriptorProto/2).
expand_EnumDescriptorProto(Fqn, EnumType) :-
    lookup_pieces('.google.protobuf.EnumDescriptorProto', EnumType,
                  _{
                    name:           '' -EnumType_name,
                    value:          [] -EnumType_value,
                    options:        _  -_,
                    reserved_range: _  -_,
                    reserved_name:  _  -_
                   }),
    add_to_fqn(Fqn, EnumType_name, FqnName),
    output_fact(protobufs:proto_meta_enum_type(FqnName, Fqn, EnumType_name)),
    maplist(expand_EnumValueDescriptorProto(FqnName), EnumType_value).

:- det(expand_EnumValueDescriptorProto/2).
expand_EnumValueDescriptorProto(Fqn, Value) :-
    lookup_pieces('.google.protobuf.EnumValueDescriptorProto', Value,
                  _{
                    name:    ''-Value_name,
                    number:  0-Value_number,
                    options: _-_
                   }),
    output_fact(protobufs:proto_meta_enum_value(Fqn, Value_name, Value_number)).

:- det(lookup_pieces/3).
%! lookup_pieces(+Tag, +DataDict, ?LookupDict) is det.
% Given a =DataDict=, look up the items in =LookupDict= If =DataDict=
% contains any keys that aren't in =LookupDict=, this predicate
% fails. This is to catch typos. For example: =|lookup_pieces(d,
% d{a:1,b:2}, _{a:0-A,bb:0-B,c:[]-C})|= will fail but
% =|lookup_pieces(d, d{a:1,b:2}, _{a:0-A,b:0-B,c:[]-C})|= will succeed
% with =|A=1,B=2,C=[]|=. In other words, =LookupDict= must contain all
% the possible keys in =DataDict= (with suitable defaults, of course).
% @param Tag the tag for =DataDict=
% @param DataDict items in =LookupDict= are looked up in here.
%        Its tag must unify with =Tag= (i.e., =|is_dict(DataDict,Tag)|=).
% @param LookupDict a dict where each entry is of the form =Default-Value=.
%        Each key is looked up in =DataDict= - if it's there, the value
%        from =DataDict= is unified with =Value=; if it's not there,
%        =Value= is unified with =Default=.
lookup_pieces(Tag, DataDict, LookupDict) :-
    is_dict(DataDict, Tag0),
    assertion(Tag == Tag0),
    dict_pairs(LookupDict, _, LookupPairs),
    lookup_piece_pairs(LookupPairs, DataDict).

lookup_piece_pairs([], RemainderDict) =>
    RemainderDict = _{}. % For debugging: assertion(RemainderDict = _{})
lookup_piece_pairs([Key-(Default-Value)|KDVs], DataDict0) =>
    dict_create(D0, _, [Key-Value]),
    (   select_dict(D0, DataDict0, DataDict)
    ->  true
    ;   Value = Default,
        DataDict = DataDict0
    ),
    lookup_piece_pairs(KDVs, DataDict).

add_to_fqn(Fqn, Name, FqnName) :-
    atomic_list_concat([Fqn, Name], '.', FqnName).

:- det(output_fact/1).
output_fact(protobufs:Fact) =>
    Fact =.. [Name|Args0],
    maplist(string_atom, Args0, Args1),
    Fact1 =.. [Name|Args1],
    format('    ~q.~n', [protobufs:Fact1]).

string_atom(String, Atom) :-
    (   string(String)
    ->  atom_string(Atom, String)
    ;   String = Atom % TODO: if dict, process the items (from proto_meta_package(Package, File_name, File_options))
    ).

end_of_file.
