#! /usr/bin/env python3
#

import json5
import argparse
from pathlib import Path
from jinja2 import Environment, FileSystemLoader

def load_idl(path):
    with open(path) as f:
        return json5.load(f)

def format_method_qualifiers(method):
    """ Build qualifier string: const noexcept
    """
    quals = []
    if method.get('const', False):
        quals.append(' const')
    if method.get('noexcept', False):
        quals.append(' noexcept')

    return ' '.join(quals)

def format_method_staticqual(method):
    """ Build qualifier string for a static method: noexcept
    """
    quals = []
    if method.get('noexcept', False):
        quals.append(' noexcept')

    return ' '.join(quals)


def format_args(args, include_names=True):
    """ Format argument list for a method
    """
    if not args:
        return ''
    if include_names:
        return ', '.join(f"{p['type']} {p['name']}" for p in args)
    else:
        return ', '.join(p['type'] for p in args)

def format_args_nonames(args):
    return format_args(args, False)

def format_arg_names(args):
    """ Format argument names, for forwarding
    """
    names = [p['name'] for p in args]
    return ', '.join([f"_dcast({names[0]})"] + names[1:])

def format_arg_names_nodata(args):
    """ Format argument names for forwarding, omit data ('self') arg
    """
    names = [p['name'] for p in args[1:]]
    return ', '.join(names)

def format_args_nodata(args):
    """ Format arguments, but exclude data arg
    """
    return format_args(args[1:])

def format_args_routing(args):
    """ Format argument names, for routing
    """
    names = [p['name'] for p in args]
    return ', '.join([f"O::data()"] + names[1:])

def format_args_impl_const(args, drepr):
    """ Format argument names, for implementation (IFoo_DRepr)
    """
    names = [f"{p['type']} {p['name']}" for p in args]
    return ', '.join([f"const {drepr} & self"] + names[1:])

def format_args_impl_nonconst(args, drepr):
    """ Format argument names, for implementation (IFoo_DRepr)
    """
    names = [f"{p['type']} {p['name']}" for p in args]
    return ', '.join([f"{drepr} & self"] + names[1:])


def gen_facet(env,
              idl_fname,
              idl,
              output_hpp_dir,
              #output_impl_hpp_subdir,
              output_cpp_dir):


    # true to insert doxygen markup in generated .hpp/.cpp files
    using_dox = idl['using_doxygen']

    # extra include files (or perhaps other definitions)
    facet_includes = idl['includes']
    # extra (post) includes for user .hpp e.g. Sequence.hpp
    user_hpp_includes = idl['user_hpp_includes']
    # arbitrary text after includes, before opening namespaces
    facet_pretext = idl['pretext']
    # detail
    facet_detail_subdir = idl['detail_subdir']
    facet_ns1 = idl['namespace1']
    facet_ns2 = idl['namespace2']
    facet_name = idl['facet']          # e.g. Sequence
    facet_name_lc = facet_name.lower()
    facet_brief = idl['brief']
    facet_doc = '\n'.join(idl['doc'])

    output_impl_hpp_dir = output_hpp_dir / facet_detail_subdir

    types = idl['types']
    for ty in types:
        ty['doc'] = '\n'.join(ty['doc'])

    const_methods = idl['const_methods']
    for md in const_methods:
        md['args'] = [{'type': "Copaque", 'name': "data"}] + md['args']
        md['doc'] = '\n'.join(md['doc'])

    nonconst_methods = idl['nonconst_methods']
    for md in nonconst_methods:
        md['args'] = [{'type': "Opaque", 'name': "data"}] + md['args']
        md['doc'] = '\n'.join(md['doc'])

    router_facet_explicit_content = idl.get('router_facet_explicit_content', [])

    # Foo.hpp
    facet_hpp_fname = f'{facet_name}.hpp'
    # AFoo
    abstract_facet = f'A{facet_name}'
    # AFoo.hpp
    abstract_facet_fname = f'{abstract_facet}.hpp'
    # IFoo
    iface_facet = f'I{facet_name}'
    # IFoo_ImplType
    iface_facet_impltype = f'{iface_facet}_ImplType'
    #
    # IFoo_Any
    iface_facet_any = f'{iface_facet}_Any'
    # IFoo_Any.hpp
    iface_facet_any_hpp_fname = f'{iface_facet_any}.hpp'
    # IFoo_Any.cpp
    iface_facet_any_cpp_fname = f'{iface_facet_any}.cpp'
    #
    # IFoo_Xfer
    iface_facet_xfer = f'{iface_facet}_Xfer'
    # IFoo_Xfer.hpp
    iface_facet_xfer_hpp_fname = f'{iface_facet_xfer}.hpp'
    # IFoo_Xfer.cpp
    iface_facet_xfer_cpp_fname = f'{iface_facet_xfer}.cpp'
    #
    # RFoo
    router_facet = f'R{facet_name}'
    # RFoo.hpp
    router_facet_hpp_fname = f'{router_facet}.hpp'

    # REMINDER: this context for FACET definition, e.g. AGCObject
    context = {
        'genfacet': 'xo-facet/codegen/genfacet',
        'genfacet_input': idl_fname,
        'using_dox': using_dox,
        'impl_hpp_subdir': facet_detail_subdir,    # legacy name
        'facet_detail_subdir': facet_detail_subdir,
        #
        'facet_hpp_j2': 'facet.hpp.j2',
        'facet_includes': facet_includes,
        'facet_pretext': facet_pretext,
        'facet_ns1': facet_ns1,
        'facet_ns2': facet_ns2,
        'facet_name_lc': facet_name_lc,
        'facet_hpp_fname': facet_hpp_fname,
        #
        'user_hpp_includes': user_hpp_includes,
        #'name': facet_name,
        'idl_fname': idl_fname,
        #
        'abstract_facet_hpp_j2': 'abstract_facet.hpp.j2',
        'abstract_facet': abstract_facet,
        'abstract_facet_fname': abstract_facet_fname,
        'abstract_facet_doc': facet_doc,
        #
        'iface_facet': iface_facet,
        'iface_facet_impltype': iface_facet_impltype,
        #
        'iface_facet_any': iface_facet_any,
        'iface_facet_any_hpp_j2': 'iface_facet_any.hpp.j2',
        'iface_facet_any_cpp_j2': 'iface_facet_any.cpp.j2',
        'iface_facet_any_hpp_fname': iface_facet_any_hpp_fname,
        'iface_facet_any_cpp_fname': iface_facet_any_cpp_fname,
        #
        'iface_facet_xfer': iface_facet_xfer,
        'iface_facet_xfer_hpp_j2': 'iface_facet_xfer.hpp.j2',
        'iface_facet_xfer_cpp_j2': 'iface_facet_xfer.cpp.j2',
        'iface_facet_xfer_hpp_fname': iface_facet_xfer_hpp_fname,
        'iface_facet_xfer_cpp_fname': iface_facet_xfer_cpp_fname,
        #
        'router_facet': router_facet,
        'router_facet_hpp_j2': 'router_facet.hpp.j2',
        'router_facet_hpp_fname': router_facet_hpp_fname,
        'router_facet_explicit_content': router_facet_explicit_content,
        #
        'types': types,
        #
        'const_methods': const_methods,
        #
        'nonconst_methods': nonconst_methods,
    }

    # generate .hpp files

    templates = {}
    templates[facet_hpp_fname] = [output_hpp_dir,
                                  context['facet_hpp_j2']]
    templates[abstract_facet_fname] = [output_impl_hpp_dir,
                                       context['abstract_facet_hpp_j2']]
    templates[iface_facet_any_hpp_fname] = [output_impl_hpp_dir,
                                            context['iface_facet_any_hpp_j2']]
    templates[iface_facet_any_cpp_fname] = [output_cpp_dir,
                                            context['iface_facet_any_cpp_j2']]
    templates[iface_facet_xfer_hpp_fname] = [output_impl_hpp_dir,
                                             context['iface_facet_xfer_hpp_j2']]
    templates[router_facet_hpp_fname] = [output_impl_hpp_dir,
                                         context['router_facet_hpp_j2']]

    for out_file, record in templates.items():
        out_dir = record[0]
        template_name = record[1]

        print(f'out_dir: [{out_dir}]')
        print(f'out_file: [{out_file}]')
        print(f'template_name: [{template_name}]')

        template = env.get_template(template_name)
        content = template.render(**context)

        (out_dir / out_file).write_text(content)
        print(f"Generated {out_dir}/{out_file}")



def gen_facet_impl(env,
                   idl_fname,
                   idl,
                   facet_idl,
                   output_hpp_dir,
                   output_impl_hpp_subdir,
                   output_cpp_dir):


    # true to insert doxygen markup in generated .hpp/.cpp files
    using_dox = idl['using_doxygen']

    # extra include files (or perhaps other definitions)

    # facet_includes: include section for AFoo.hpp:
    # <xo/gc/GCObject.hpp>
    #facet_includes = facet_idl['includes']
    facet_includes = idl['includes']

    # <xo/printable2/Printable.hpp>
    # sequence
    facet_detail_subdir = facet_idl['detail_subdir']
    # xo - facet_ns1: outer namespace for facet [e.g. xo]
    facet_ns1 = facet_idl['namespace1']
    # facet_ns2: nested namespace for facet [print]
    facet_ns2 = facet_idl['namespace2']

    # Sequence - facet_name: facet name
    facet_name = facet_idl['facet']
    # sequence - facet_name_lc: lower case [e.g. sequence]
    facet_name_lc = facet_name.lower()
    # brief doc for facet
    facet_brief = idl['brief']
    # doc section for facet
    facet_doc = '\n'.join(idl['doc'])

    facet_types = facet_idl['types']
    for ty in facet_types:
        ty['doc'] = '\n'.join(ty['doc'])

    const_methods = facet_idl['const_methods']
    for md in const_methods:
        md['args'] = [{'type': "Copaque",
                       'name': "data"}] + md['args']
        md['doc'] = '\n'.join(md['doc'])

    nonconst_methods = facet_idl['nonconst_methods']
    for md in nonconst_methods:
        md['args'] = [{'type': "Opaque",
                       'name': "data"}] + md['args']
        md['doc'] = '\n'.join(md['doc'])

    # Foo.hpp
    facet_hpp_fname = f'{facet_name}.hpp'
    # AFoo
    abstract_facet = f'A{facet_name}'
    # AFoo.hpp
    abstract_facet_fname = f'{abstract_facet}.hpp'
    # IFoo
    iface_facet = f'I{facet_name}'
    # IFoo_ImplType
    iface_facet_impltype = f'{iface_facet}_ImplType'
    #
    # IFoo_Any
    iface_facet_any = f'{iface_facet}_Any'
    # IFoo_Any.hpp
    iface_facet_any_hpp_fname = f'{iface_facet_any}.hpp'
    # IFoo_Any.cpp
    iface_facet_any_cpp_fname = f'{iface_facet_any}.cpp'
    #
    # IFoo_Xfer
    iface_facet_xfer = f'{iface_facet}_Xfer'
    # IFoo_Xfer.hpp
    iface_facet_xfer_hpp_fname = f'{iface_facet_xfer}.hpp'
    # IFoo_Xfer.cpp
    iface_facet_xfer_cpp_fname = f'{iface_facet_xfer}.cpp'
    #
    # RFoo
    router_facet = f'R{facet_name}'
    # RFoo.hpp
    router_facet_hpp_fname = f'{router_facet}.hpp'
    # user defined content -- whatever you want
    router_facet_explicit_content = facet_idl['router_facet_explicit_content']

    # ================================================================
    # vars for IFacet_DRepr
    # ----------------------------------------------------------------

    # DList
    data_repr = idl['repr']
    # dlist
    data_repr_lc = data_repr.lower()
    # DList.hpp
    data_repr_hpp_fname = f'{data_repr}.hpp'

    # repr_ns1: outer namespace for repr [e.g. xo].
    #           (need not match facet)
    repr_ns1 = idl['namespace1']
    # repr_ns2: nested namespace for repr [e.g. scm].
    repr_ns2 = idl['namespace2']

    # local_types: addition type defs (e.g. repr_ns2 != facet_ns2)
    local_types = idl['local_types']

    # iface_facet_repr: IFoo_DRepr
    iface_facet_repr = f'{iface_facet}_{data_repr}'
    # iface_facet_repr_hpp_fname: IFoo_DRepr.hpp
    iface_facet_repr_hpp_fname = f'{iface_facet_repr}.hpp'
    # iface_facet_repr_cpp_fname: IFoo_DRepr.cpp
    iface_facet_repr_cpp_fname = f'{iface_facet_repr}.cpp'

    # ================================================================

    context = {
        'genfacet': 'xo-facet/codegen/genfacet',
        'genfacet_input': idl_fname,
        'using_dox': using_dox,
        'impl_hpp_subdir': output_impl_hpp_subdir,
        #
        'facet_hpp_j2': 'facet.hpp.j2',
        'facet_includes': facet_includes,
        'facet_detail_subdir': facet_detail_subdir,
        'facet_ns1': facet_ns1,
        'facet_ns2': facet_ns2,
        'facet_name': facet_name,
        'facet_name_lc': facet_name_lc,
        'facet_hpp_fname': facet_hpp_fname,
        #'name': facet_name,
        'idl_fname': idl_fname,
        #
        'abstract_facet_hpp_j2': 'abstract_facet.hpp.j2',
        'abstract_facet': abstract_facet,
        'abstract_facet_fname': abstract_facet_fname,
        'abstract_facet_doc': facet_doc,
        #
        'iface_facet': iface_facet,
        'iface_facet_impltype': iface_facet_impltype,
        #
        'iface_facet_any': iface_facet_any,
        'iface_facet_any_hpp_j2': 'iface_facet_any.hpp.j2',
        'iface_facet_any_cpp_j2': 'iface_facet_any.cpp.j2',
        'iface_facet_any_hpp_fname': iface_facet_any_hpp_fname,
        'iface_facet_any_cpp_fname': iface_facet_any_cpp_fname,
        #
        'iface_facet_xfer': iface_facet_xfer,
        'iface_facet_xfer_hpp_j2': 'iface_facet_xfer.hpp.j2',
        'iface_facet_xfer_cpp_j2': 'iface_facet_xfer.cpp.j2',
        'iface_facet_xfer_hpp_fname': iface_facet_xfer_hpp_fname,
        'iface_facet_xfer_cpp_fname': iface_facet_xfer_cpp_fname,
        #
        'router_facet': router_facet,
        'router_facet_hpp_j2': 'router_facet.hpp.j2',
        'router_facet_hpp_fname': router_facet_hpp_fname,
        'router_facet_explicit_content': router_facet_explicit_content,
        #
        'types': facet_types,
        'local_types': local_types,
        #
        'const_methods': const_methods,
        #
        'nonconst_methods': nonconst_methods,

        # ----------------------------------------------------------------
        # vars for IFacet_DRepr
        # ----------------------------------------------------------------

        'repr_ns1': repr_ns1,
        'repr_ns2': repr_ns2,
        #
        'data_repr': data_repr,
        'data_repr_lc': data_repr_lc,
        'data_repr_hpp_fname': data_repr_hpp_fname,
        #
        'iface_facet_repr': iface_facet_repr,
        'iface_facet_repr_hpp_j2': 'iface_facet_repr.hpp.j2',
        'iface_facet_repr_hpp_fname': iface_facet_repr_hpp_fname,
        'iface_facet_repr_cpp_j2': 'iface_facet_repr.cpp.j2',
        'iface_facet_repr_cpp_fname': iface_facet_repr_cpp_fname,
        #

    }

    # generate .hpp files

    templates = {}
    templates[iface_facet_repr_hpp_fname] = [output_hpp_dir,
                                             context['iface_facet_repr_hpp_j2']]
    templates[iface_facet_repr_cpp_fname] = [output_cpp_dir,
                                             context['iface_facet_repr_cpp_j2']]

#    templates[facet_hpp_fname] = [output_hpp_dir,
#                                  context['facet_hpp_j2']]
#    templates[abstract_facet_fname] = [output_impl_hpp_dir,
#                                       context['abstract_facet_hpp_j2']]
#    templates[iface_facet_any_hpp_fname] = [output_impl_hpp_dir,
#                                            context['iface_facet_any_hpp_j2']]
#    templates[iface_facet_any_cpp_fname] = [output_cpp_dir,
#                                            context['iface_facet_any_cpp_j2']]
#    templates[iface_facet_xfer_hpp_fname] = [output_impl_hpp_dir,
#                                             context['iface_facet_xfer_hpp_j2']]
#    templates[router_facet_hpp_fname] = [output_impl_hpp_dir,
#                                         context['router_facet_hpp_j2']]

    for out_file, record in templates.items():
        out_dir = record[0]
        template_name = record[1]

        print(f'out_dir: [{out_dir}]')
        print(f'out_file: [{out_file}]')
        print(f'template_name: [{template_name}]')

        template = env.get_template(template_name)
        content = template.render(**context)

        (out_dir / out_file).write_text(content)
        print(f"Generated {out_dir}/{out_file}")

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--input', required=True, help='input IDL JSON5 file')
    # --facet-dir: only with mode=implementation
    parser.add_argument('--facet-dir', required=False, help='base dir for facet json')

    args = parser.parse_args()

    idl_fname = args.input
    idl = load_idl(idl_fname)

    output_hpp_dir = Path(idl['output_hpp_dir'])
    output_hpp_dir.mkdir(parents=False, exist_ok=True)

    # output_impl_hpp_subdir: prefer IDL, fall back to CLI
    output_impl_hpp_subdir = Path(idl['output_impl_subdir'])
    output_impl_hpp_dir = output_hpp_dir / output_impl_hpp_subdir
    output_impl_hpp_dir.mkdir(parents=False, exist_ok=True)

    output_cpp_dir = Path(idl['output_cpp_dir'])
    output_cpp_dir.mkdir(parents=False, exist_ok=True)

    # setup jinja2
    #template_dir = Path(args.templates)
    template_dir = Path(__file__).parent
    #template_dir = Path(__file__).parent / 'codegen'

    print(f'template_dir: [{template_dir}]')

    env = Environment(loader = FileSystemLoader(template_dir),
                      trim_blocks = True,
                      lstrip_blocks = True,
                      keep_trailing_newline = True)

    # custom filters.
    # A filter 'foo' provides ability to write '{{var | foo}}' to expand
    # j2 variable 'var' to 'filters[foo](var)'
    #
    env.filters['qualifiers'] = format_method_qualifiers
    env.filters['staticqual'] = format_method_staticqual
    env.filters['args'] = format_args
    env.filters['argtypes'] = format_args_nonames
    env.filters['argnames'] = format_arg_names
    env.filters['argnamesnodata'] = format_arg_names_nodata
    env.filters['argsnodata'] = format_args_nodata
    env.filters['argrouting'] = format_args_routing
    env.filters['argimplconst'] = format_args_impl_const
    env.filters['argimplnonconst'] = format_args_impl_nonconst

    if idl['mode'] == 'facet':
        gen_facet(env=env,
                  idl_fname=idl_fname,
                  idl=idl,
                  output_hpp_dir=output_hpp_dir,
                  output_cpp_dir=output_cpp_dir)
    elif idl['mode'] == 'implementation':
        # idl:   json5 for (iface, data) combination
        # facet: json5 for abstract iface

        facet_idl_fname = args.facet_dir + '/' + idl['facet_idl']
        facet_idl = load_idl(facet_idl_fname)

        gen_facet_impl(env=env,
                       idl_fname=idl_fname,
                       idl=idl,
                       facet_idl=facet_idl,
                       output_hpp_dir=output_impl_hpp_dir,
                       output_impl_hpp_subdir=output_impl_hpp_subdir,
                       output_cpp_dir=output_cpp_dir)

if __name__ == '__main__':
    main()
