#!/usr/bin/env python3

"""
Create sets of C++ classes based on json schemas
"""

import platform
import argparse
import json
import os
import sys
import re
import Util


# consts
IsPython3 = ( sys.version_info[0] >= 3 )


# Templates

file_header_template = """
/*******************************************************************************
This file is part of the pdi stack.

This is free software. you can redistribute it and/or modify it but without any
warranty.

File            : %CLASSNAME%.%FILEEXT%
Author          : Suraj I.
created Date    : 1st Jan 2024

Note            : THIS FILE IS AUTOGENERATED - DO NOT EDIT MANUALLY
*******************************************************************************/
"""

class_header_template = """
%FILEHEADER%

#ifndef _%UPPERNAME%_H_
#define _%UPPERNAME%_H_

%HEADERINCLUDE%

%PRECLASSDECLARATION%

%CLASSDECLARATION%

%POSTCLASSDECLARATION%

#endif  // _%UPPERNAME%_H_
"""

class_header_macrodefine_template = """
#define %MACRODEFINE%
"""

class_cpp_template = """
%FILEHEADER%

%CPPINCLUDES%

%PRECLASSDEFINITION%

%CLASSDEFINITION%

%POSTCLASSDEFINITION%
"""

class_includes_template = """
#include %CLASSINCLUDES%
"""

template_class_template = """
template <class T>
"""

class_declaration_template = """
/**
 * @name    %CLASSNAME%
 *
 * @purpose %CLASSDESC%
 *
 */
class %CLASSNAME% : %PARENTCLASSDECLARATION% {

  public:

%CLASSCONSTRUCTORPUBLIC%
    
%CLASSDESTRUCTORPUBLIC%

%CLASSMETHODSPUBLIC%

%CLASSVARSPUBLIC%

  private:

%CLASSCONSTRUCTORPRIVATE%

%CLASSDESTRUCTORPRIVATE%

%CLASSMETHODSPRIVATE%

%CLASSVARSPRIVATE%

  protected:

%CLASSCONSTRUCTORPROTECTED%

%CLASSDESTRUCTORPROTECTED%

%CLASSMETHODSPROTECTED%

%CLASSVARSPROTECTED%
};
"""

class_definition_template = """
%CLASSCONSTRUCTORPUBLIC%
%CLASSCONSTRUCTORPRIVATE%
%CLASSCONSTRUCTORPROTECTED%
    
%CLASSDESTRUCTORPUBLIC%
%CLASSDESTRUCTORPRIVATE%
%CLASSDESTRUCTORPROTECTED%

%CLASSMETHODSPUBLIC%
%CLASSMETHODSPRIVATE%
%CLASSMETHODSPROTECTED%
"""

class_method_declr_template = """
    /** %METHODDESC% */
    %PREMETHODDECLR%%METHODRETURNTYPE% %METHODNAME%(%METHODARGS%)%POSTMETHODDECLR%;
"""

class_method_def_template = """
    /**
     * @purpose %METHODDESC%
     */
    %PREMETHODDECLR%%METHODRETURNTYPE% %SCOPE%%METHODNAME%(%METHODARGS%)%POSTMETHODDECLR%{
      %METHODBODY%
    }
"""

class_variable_template = """
    %PREVARDECLR%%CLASSVARTYPE% %CLASSVARNAME%%POSTVARDECLR%;
"""

def build_file_header_template(definition, fileext):

    template = Util.expand_token(file_header_template, '%CLASSNAME%', Util.build_classname(definition["name"]))
    template = Util.expand_token(template, '%FILEEXT%', fileext)
    return template

def build_class_includes_template(definition):

    template = ""

    if "includes" in definition:
        for include in definition["includes"]:
            template = Util.expand_token(class_includes_template, '%CLASSINCLUDES%', include, True)
    
    return template

def build_premethod_declaration_template(definition, method):

    template = ""  
    return template

def build_postmethod_declaration_template(definition, method):

    template = ""  
    return template

def build_parentclass_declaration_template(definition):

    template = ""

    if "parent" in definition:
        if "access" in definition["parent"] and "name" in definition["parent"]:
            template = definition["parent"]["access"] + " " + definition["parent"]["name"]
            if "template" in definition["parent"] and "args" in definition["parent"]["template"]:
                template_args = ""
                for arg in definition["parent"]["template"]["args"]:
                    template_args += arg + ","
                template += "<" + template_args.strip(",") + ">"

    return template

def build_class_var_template(definition, member):

    template = class_variable_template + os.linesep
    template = Util.expand_token(template, '%CLASSVARTYPE%', member["type"])
    template = Util.expand_token(template, '%CLASSVARNAME%', member["name"])
    template = Util.expand_token(template, '%PREVARDECLR%', member["preprop"] if "preprop" in member else "")
    template = Util.expand_token(template, '%POSTVARDECLR%', member["postprop"] if "postprop" in member else "")
    return template

def build_class_method_template(definition, member, isCppFile):

    class_mthd_template = Util.clean_template(class_method_def_template if (definition["onlyHeader"] or isCppFile) else class_method_declr_template)
    
    method_name = definition["name"]
    method_args = ""
    METHODRETURNTYPETOKEN = '%METHODRETURNTYPE%'

    if "args" in member:
        for arg in member["args"]:
            method_args += arg["type"] + " " + arg["name"] + ","
        method_args = method_args.strip(",")

    if "type" in member and member["type"] == "constructor":
        METHODRETURNTYPETOKEN = METHODRETURNTYPETOKEN+" "
    elif "type" in member and member["type"] == "destructor":
        method_name = "~"+definition["name"]
        METHODRETURNTYPETOKEN = METHODRETURNTYPETOKEN+" "
    elif "type" in member and member["type"] == "method":
        method_name = member["name"]
    else:
        return ""

    template = class_mthd_template + os.linesep

    template = Util.expand_token(template, '%METHODNAME%', method_name)
    template = Util.expand_token(template, '%METHODDESC%', member["description"] if "description" in member else member["name"])
    template = Util.expand_token(template, '%METHODARGS%', method_args)
    template = Util.expand_token(template, METHODRETURNTYPETOKEN, member["returnType"] if "returnType" in member else "")
    template = Util.expand_token(template, '%PREMETHODDECLR%', build_premethod_declaration_template(definition, member))
    template = Util.expand_token(template, '%POSTMETHODDECLR%', build_postmethod_declaration_template(definition, member))

    if definition["onlyHeader"] or isCppFile:
        template = Util.expand_token(template, '%METHODBODY%', member["body"] if "body" in member else "")
        template = Util.expand_token(template, '%SCOPE%', definition["name"]+"::" if isCppFile else "")


    return template

def build_preclass_definition_template(definition):

    template = ""  
    return template

def build_preclass_declaration_template(definition):

    template = ""

    if "macrodefines" in definition:
        for macrodef in definition["macrodefines"]:
            if "macroDefine" in macrodef:
                template += Util.expand_token(class_header_macrodefine_template, '%MACRODEFINE%', macrodef["macroDefine"])

    return template

def build_postclass_declaration_template(definition):

    template = ""  

    if "postclassdeclaration" in definition:
        for item in definition["postclassdeclaration"]:
            template += build_class_var_template(definition, item)

    return template

def build_postclass_definition_template(definition):
    
    template = ""  
    return template

def build_class_members_template(template, definition, isCppFile):

    access_based_ctor_template = {'public': '', 'private': '', 'protected': ''}
    access_based_dtor_template = {'public': '', 'private': '', 'protected': ''}
    access_based_mthd_template = {'public': '', 'private': '', 'protected': ''}
    access_based_vars_template = {'public': '', 'private': '', 'protected': ''}

    for member in definition["members"]:
        mthd_access = member["access"].lower()

        if "type" in member and member["type"] == "constructor":
            access_based_ctor_template[mthd_access] += build_class_method_template(definition, member, isCppFile)

        if "type" in member and member["type"] == "destructor":
            access_based_dtor_template[mthd_access] += build_class_method_template(definition, member, isCppFile)

        if "type" in member and member["type"] == "method":
            access_based_mthd_template[mthd_access] += build_class_method_template(definition, member, isCppFile)

        if "type" in member and ( member["type"] != "constructor" and member["type"] != "destructor" and member["type"] != "method" ):
            access_based_vars_template[mthd_access] += build_class_var_template(definition, member)

    template = Util.expand_token(template, '%CLASSCONSTRUCTORPUBLIC%', access_based_ctor_template["public"])
    template = Util.expand_token(template, '%CLASSCONSTRUCTORPRIVATE%', access_based_ctor_template["private"])
    template = Util.expand_token(template, '%CLASSCONSTRUCTORPROTECTED%', access_based_ctor_template["protected"])

    template = Util.expand_token(template, '%CLASSDESTRUCTORPUBLIC%', access_based_dtor_template["public"])
    template = Util.expand_token(template, '%CLASSDESTRUCTORPRIVATE%', access_based_dtor_template["private"])
    template = Util.expand_token(template, '%CLASSDESTRUCTORPROTECTED%', access_based_dtor_template["protected"])

    template = Util.expand_token(template, '%CLASSMETHODSPUBLIC%', access_based_mthd_template["public"])
    template = Util.expand_token(template, '%CLASSMETHODSPRIVATE%', access_based_mthd_template["private"])
    template = Util.expand_token(template, '%CLASSMETHODSPROTECTED%', access_based_mthd_template["protected"])

    template = Util.expand_token(template, '%CLASSVARSPUBLIC%', access_based_vars_template["public"])
    template = Util.expand_token(template, '%CLASSVARSPRIVATE%', access_based_vars_template["private"])
    template = Util.expand_token(template, '%CLASSVARSPROTECTED%', access_based_vars_template["protected"])

    return template

def build_class_declaration_template(definition):

    template = Util.clean_template(class_declaration_template)  
    template = Util.expand_token(template, '%CLASSNAME%', definition["name"])
    template = Util.expand_token(template, '%CLASSDESC%', definition["description"])
    template = Util.expand_token(template, '%PARENTCLASSDECLARATION%', build_parentclass_declaration_template(definition))
    template = build_class_members_template(template, definition, False)
    return template

def build_class_definition_template(definition):

    template = Util.clean_template(class_definition_template)  
    template = build_class_members_template(template, definition, True)
    return template

def generate_header(schema, id, desc, definition, outpath):

    print("Generating %s.h file" % definition["name"])

    template = Util.clean_template(class_header_template)
    template = Util.expand_token(template, '%UPPERNAME%', definition["name"].upper())
    template = Util.expand_token(template, '%FILEHEADER%', build_file_header_template(definition, "h"))
    template = Util.expand_token(template, '%HEADERINCLUDE%', build_class_includes_template(definition))
    template = Util.expand_token(template, '%PRECLASSDECLARATION%', build_preclass_declaration_template(definition))
    template = Util.expand_token(template, '%CLASSDECLARATION%', build_class_declaration_template(definition))
    template = Util.expand_token(template, '%POSTCLASSDECLARATION%', build_postclass_declaration_template(definition))
    template = Util.clean_template(template, True)

    outfile = outpath + Util.build_classname(definition["name"]) + ".h"
    with open(outfile, mode="w", encoding="ascii", newline=os.linesep) as file:
        file.write(template) 
    
    # Format the generated file with clang-formatter
    Util.format_file("clang", outfile)

def generate_source(schema, id, desc, definition, outpath):

    print("Generating %s.cpp file" % definition["name"])

    template = Util.clean_template(class_cpp_template)
    cppincld = Util.expand_token(class_includes_template, '%CLASSINCLUDES%', "\""+Util.build_classname(definition["name"])+".h\"", True)
    template = Util.expand_token(template, '%FILEHEADER%', build_file_header_template(definition, "cpp"))
    template = Util.expand_token(template, '%CPPINCLUDES%', cppincld)
    template = Util.expand_token(template, '%PRECLASSDEFINITION%', build_preclass_definition_template(definition))
    template = Util.expand_token(template, '%CLASSDEFINITION%', build_class_definition_template(definition))
    template = Util.expand_token(template, '%POSTCLASSDEFINITION%', build_postclass_definition_template(definition))
    template = Util.clean_template(template, True)

    outfile = outpath + Util.build_classname(definition["name"]) + ".cpp"
    with open(outfile, mode="w", encoding="ascii", newline=os.linesep) as file:
        file.write(template) 
    
    # Format the generated file with clang-formatter
    Util.format_file("clang", outfile)

def generate_definitionItem(schema, id, desc, definition):

    definitionItems = None
    if "$defItems" in definition:
        definitionItems = json.loads("[]")
        for defItem in definition["$defItems"]:
            baseDefStr = json.dumps(definition)
            for key, value in defItem.items(): 
                baseDefStr = baseDefStr.replace("$$"+key+"$$", value)
            definitionItems.append(json.loads(baseDefStr))

    return definitionItems

def generate_classes(args, schema, outpath):
    
    if "id" in schema and "definitions" in schema:
        id = schema["id"]
        desc = schema["id"]

        if "description" in schema:
            desc = schema["description"]
        
        for definition in schema["definitions"]:

            definitionItems = generate_definitionItem(schema, id, desc, definition)

            if definitionItems:

                for defItem in definitionItems:
                    # print(defItem)
                    generate_header(schema, id, desc, defItem, outpath)
                    if not defItem["onlyHeader"]:
                        generate_source(schema, id, desc, defItem, outpath)
            else:

                generate_header(schema, id, desc, definition, outpath)
                if not definition["onlyHeader"]:
                    generate_source(schema, id, desc, definition, outpath)


def get_outpath(args):

    path = os.path.dirname(os.path.realpath(__file__))

    if args.outpath != "":
        path = args.outpath
    
    if not path.endswith(os.sep):
        path += os.sep

    return path


def get_schema(args):

    with open(args.schema) as file:
        schema = json.load(file)

    return schema


if __name__ == "__main__":

    usage = """
        JsonToCpp [--schema|-s schema] [--outpath|-o outpath]
    """

    parser = argparse.ArgumentParser(usage=usage)
    parser.add_argument('--schema', '-s', help='schema file to process', required=True)
    parser.add_argument('--outpath', '-o', help='path to keep generated files wrt jsonToCpp script', required=True)

    args = parser.parse_args()

    try:
        schema = get_schema(args)
        outpath = get_outpath(args)

        generate_classes(args, schema, outpath)

    except Exception as ex:
        print("Exception : %s" %ex)

    except KeyboardInterrupt:
        print("Aborted by keyboard interrupt !")