#!/usr/bin/env python3

# This script is a slight adaptation of the traditional ModuleToXsd.py
# (in particular, with all regexes and core logic copied from there)
# but here with generation of the same info in JSON format.
# All of this primarily for subsequent, direct ingestion of the JSON by TethysL.
# (I.e., not having to go through an unnecessary "xsd" representation).

# NOTE: In a tentative PR of a while ago (2021), which I then declined myself, I had
# refactored things to reuse the same regexes/parse logic for both .xsd and .json
# generation, but I'm now avoiding that approach given that the `.xsd`s are actually
# not well-used anyway (and they may even be removed from being generated at some point.)

import re
import os

moduleRootPattern = re.compile( "(([\w_]+)Module)[\.][cC][pP]*" )
sourceFolderPattern = re.compile( "(.*[%/])[\w_]+[\.][cC][pP]*" )

regComponentPattern = re.compile('[\s]+registerComponent[\(][\s]*new[\s]+([\w_]+)')
regMissionCompPattern = re.compile( '[\s]*registerBehaviorCreator[\(][\s]*([\w_]+)IF::' )
importInterfacePattern = re.compile( '#include[\s]+[\"]([\w/_]+IF.h)"' )
namespacePattern = re.compile( '[\s]*namespace[\s]+([\w_]+)' )
staticConstStrPattern = re.compile( '[\s]*static[\s]+const[\s]+Str[\s]+([\w_]+)[\(][\s]*[\"]([\w_]+)[\"]' )
staticConstConfigURIPattern = re.compile( '[\s]*static[\s]+const[\s]+ConfigURI[\s]+([\w_]+)[\(][\s]*[\w_]+[\s]*,[\s]*[\"]([\w_]+)[\"](?:[\s]*,[\s]*[\"]([^\"]+)[\"][\s]*)?([,\)])?' )
staticConstRestartConfigURIPattern = re.compile( '[\s]*static[\s]+const[\s]+RestartConfigURI[\s]+([\w_]+)[\(][\s]*[\w_]+[\s]*,[\s]*[\"]([\w_]+)[\"](?:[\s]*,[\s]*[\"]([^\"]+)[\"][\s]*)?([,\)])?' )
staticConstBlobURIPattern = re.compile( '[\s]*static[\s]+const[\s]+BlobURI[\s]+([\w_]+)[\(][\s]*[\w_]+[\s]*,[\s]*[\"]([\w_]+)[\"](?:[\s]*,[\s]*[\"]([^\"]+)[\"][\s]*)?([,\)])?' )
staticConstDataURIPattern = re.compile( '[\s]*static[\s]+const[\s]+DataURI[\s]+([\w_]+)[\(][\s]*[\w_]+[\s]*,[\s]*[\"]([\w_]+)[\"](?:[\s]*,[\s]*[\"]([^\"]+)[\"][\s]*)?([,\)])?' )
staticConstNameURIPattern = re.compile( '[\s]*static[\s]+const[\s]+NameURI[\s]+([\w_]+)[\(][\s]*[\"]([\w_]+)[\"](?:[\s]*,[\s]*[\"]([^\"]+)[\"][\s]*)?([,\)])?' )
staticConstOutputURIPattern = re.compile( '[\s]*static[\s]+const[\s]+OutputURI[\s]+([\w_]+)[\(][\s]*[\"]([\w_]+)[\"](?:[\s]*,[\s]*[\"]([^\"]+)[\"][\s]*)?([,\)])?' )
staticConstSettingURIPattern = re.compile( '[\s]*static[\s]+const[\s]+SettingURI[\s]+([\w_]+)[\(][\s]*[\"]([\w_]+)[\"](?:[\s]*,[\s]*[\"]([^\"]+)[\"][\s]*)?([,\)])?' )
staticConstStringSettingURIPattern = re.compile( '[\s]*static[\s]+const[\s]+StringSettingURI[\s]+([\w_]+)[\(][\s]*[\"]([\w_]+)[\"](?:[\s]*,[\s]*[\"]([^\"]+)[\"][\s]*)?([,\)])?' )
moreDescriptionPattern = re.compile( '[\s]*[\"]([^\"]+)[\"][\s]*([,\)])?')
missonComponentPattern = re.compile( '[\s]*:[\s]*Behavior[\s]*[\(][\s]*[^,]+,[^,]+,[\s]*([^,\s]+),[\s]*([^\)\s]+)' )
altMissonComponentPattern = re.compile( '[\s]*:[\s]*([\w]+)' )
otherComponentPattern = re.compile( '[\s]*:[\s]*[\w]+[\s]*[\(]')
newSettingReaderPattern = re.compile( '[\s]*[\w_]+[\s]*=[\s]*newSettingReader[\(][\s]*([\w:_]+)' )
newSettingVariableReaderPattern = re.compile( '[\s]*[\w_]+[\s]*=[\s]*newSettingReader[\(][\s]*([\w:_]+)[\s]*,[\s]*new[\s]+StrValue' )
newInputReaderPattern = re.compile( '[\s]*[\w_]+[\s]*=[\s]*Slate::NewInputReader[\(][\s]*([\w:_]+)' )
newOutputWriterPattern = re.compile( '[\s]*[\w_]+[\s]*=[\s]*Slate::NewOutputWriter[\(][\s]*([\w:_]+)' )
findArgWriterPattern = re.compile( '[\s]*[\w_]+[\s]*=[\s]*findArgWriter[\(][\s]*([\w:_]+)' )
newUniversalWriterPattern = re.compile( '[\s]*[\w_]+[\s]*=[\s]*(?:Slate::New|new)UniversalWriter[\(][\s]*([\w:_]+)' )
universalURIPattern = re.compile( 'UniversalURI::([\w_]+)' )
enumStartPattern = re.compile( '[\s]*[\w_]+?[\s]*enum' )
enumBehaviorPattern = re.compile( '[\s]*([\w_]+)' )
enumEndPattern = re.compile( '[\s]*[\}]' )
staticConstDoublePattern = re.compile( '[\s]*static[\s]+const[\s]+double[\s]+([\w_]+)[\s]*[\(][\s]*([^\s\)]+)[\s]*[\)]' )

def appendDescription(description, descDone, ifLines, object):
    if description:
        while not descDone:
            ifLine = ifLines.pop(0)
            moreDescriptionMatches = moreDescriptionPattern.match(ifLine)
            if moreDescriptionMatches:
                description = description + moreDescriptionMatches.group(1)
                descDone = moreDescriptionMatches.group(2)
            else:
                descDone = True
        object["description"] = description


def parse_to_list(lrauv_dir, moduleSourceFilename):
    moduleRootMatches = moduleRootPattern.search( moduleSourceFilename )
    moduleRoot = moduleRootMatches.group(1)
    moduleRootBase = moduleRootMatches.group(2)
    # print('Parsing %s' % moduleSourceFilename)

    sourceFolderMatches = sourceFolderPattern.match( moduleSourceFilename )
    sourceFolder = sourceFolderMatches.group(1)


    moduleFile = open( os.path.normpath(moduleSourceFilename) )

    result = []

    for moduleLine in moduleFile.readlines():

        object = {}

        componentClass = None
        interfaceName = None
        parentClass = None
        isBehaviorClass = False
        regComponentMatches = regComponentPattern.match( moduleLine )
        if regComponentMatches:
            componentClass = regComponentMatches.group( 1 )
            object["name"] = componentClass
            object["substitutionGroup"] = "Tethys:Sensor"
            object["type"] = "Tethys:SensorType"
            result.append(object)
            object = {}

        regMissionCompMatches = regMissionCompPattern.match( moduleLine )
        if regMissionCompMatches and not parentClass:
            componentClass = regMissionCompMatches.group( 1 )
            isBehaviorClass = True
        if not componentClass:
             continue
        else:
             interfaceName = componentClass

        behaviorType = None
        dict = {}
        classSourceFile = open( os.path.normpath(sourceFolder + componentClass + ".cpp") )
        sourceLines = classSourceFile.readlines()
        # Use while loop here because sourceLines may get prepended
        encounteredAnImportInterface = False
        while len(sourceLines) > 0:
            sourceLine = sourceLines.pop(0)
            importInterfaceMatches = importInterfacePattern.match( sourceLine )
            if importInterfaceMatches:
                encounteredAnImportInterface = True
                interfacePath = importInterfaceMatches.group(1)
                if re.search( '/', interfacePath ) or interfacePath == "VehicleIF.h":
                    interfacePath = lrauv_dir + "/Source/" + interfacePath
                else:
                    interfacePath = sourceFolder + interfacePath

                namespace = ""
                interfaceFile = open( os.path.normpath(interfacePath) )
                ifLines = interfaceFile.readlines()
                # Use while loop here because ifLines may get prepended
                while len(ifLines) > 0:
                    ifLine = ifLines.pop(0)

                    importInterfaceMatches = importInterfacePattern.match( ifLine )
                    if importInterfaceMatches:
                        interfacePath = importInterfaceMatches.group(1)
                        if re.search( '/', interfacePath ):
                            interfacePath = lrauv_dir + "/Source/" + interfacePath
                        else:
                            interfacePath = sourceFolder + interfacePath
                        if os.path.exists( interfacePath ):
                            interfaceFile = open( os.path.normpath(interfacePath) )
                            childIfLines = interfaceFile.readlines()
                            childIfLines.extend(ifLines)
                            ifLines = childIfLines

                    namespaceMatches = namespacePattern.match( ifLine )
                    if namespaceMatches:
                        namespace = namespaceMatches.group(1)
                        #print "namespace="+namespace

                    staticConstStrMatches = staticConstStrPattern.match( ifLine )
                    if staticConstStrMatches:
                        id = staticConstStrMatches.group(1)
                        name = staticConstStrMatches.group(2)
                        dict[namespace + "::" + id ] = name
                        #print namespace + "::" + id + " = " + name
                        if id == "NAME" and namespace.startswith(componentClass):
                            interfaceName = name

                    staticConstNameURIMatches = staticConstNameURIPattern.match( ifLine )
                    if staticConstNameURIMatches and ( namespace.startswith(componentClass) or (parentClass and namespace.startswith(parentClass))):
                        id = staticConstNameURIMatches.group(1)
                        name = staticConstNameURIMatches.group(2)
                        description = staticConstNameURIMatches.group(3)
                        descDone = staticConstNameURIMatches.group(4)
                        while not descDone:
                            ifLine = ifLines.pop(0)
                            moreDescriptionMatches = moreDescriptionPattern.match(ifLine)
                            if moreDescriptionMatches:
                                description = description + moreDescriptionMatches.group(1)
                                descDone = moreDescriptionMatches.group(2)
                            else:
                                descDone = True
                        dict[namespace + "::" + id ] = [name,description]
                        if id == "NAME" and namespace.startswith(componentClass):
                            interfaceName = name

                    staticConstConfigURIMatches = staticConstConfigURIPattern.match( ifLine )
                    if staticConstConfigURIMatches and ( namespace.startswith(componentClass) or (parentClass and namespace.startswith(parentClass))):
                        id = staticConstConfigURIMatches.group(1)
                        name = staticConstConfigURIMatches.group(2)
                        object["name"] = "%s.%s" % (interfaceName, name)
                        object["substitutionGroup"] = "Tethys:Uri"
                        object["type"] = "Tethys:ConfigUriType"
                        description = staticConstConfigURIMatches.group(3)
                        descDone = staticConstConfigURIMatches.group(4)
                        appendDescription(description, descDone, ifLines, object )
                        result.append(object)
                        object = {}

                    staticConstRestartConfigURIMatches = staticConstRestartConfigURIPattern.match( ifLine )
                    if staticConstRestartConfigURIMatches and ( namespace.startswith(componentClass) or (parentClass and namespace.startswith(parentClass))):
                        id = staticConstRestartConfigURIMatches.group(1)
                        name = staticConstRestartConfigURIMatches.group(2)
                        object["name"] = "%s.%s" % (interfaceName, name)
                        object["substitutionGroup"] = "Tethys:Uri"
                        object["type"] = "Tethys:RestartConfigUriType"
                        description = staticConstRestartConfigURIMatches.group(3)
                        descDone = staticConstRestartConfigURIMatches.group(4)
                        appendDescription(description, descDone, ifLines, object )
                        result.append(object)
                        object = {}

                    staticConstBlobURIMatches = staticConstBlobURIPattern.match( ifLine )
                    if staticConstBlobURIMatches and ( namespace.startswith(componentClass) or (parentClass and namespace.startswith(parentClass))):
                        id = staticConstBlobURIMatches.group(1)
                        name = staticConstBlobURIMatches.group(2)
                        object["name"] = "%s.%s" % (interfaceName, name)
                        object["substitutionGroup"] = "Tethys:Uri"
                        object["type"] = "Tethys:BlobUriType"
                        description = staticConstBlobURIMatches.group(3)
                        descDone = staticConstBlobURIMatches.group(4)
                        appendDescription(description, descDone, ifLines,object )
                        result.append(object)
                        object = {}

                    staticConstDataURIMatches = staticConstDataURIPattern.match( ifLine )
                    if staticConstDataURIMatches and ( namespace.startswith(componentClass) or (parentClass and namespace.startswith(parentClass))):
                        id = staticConstDataURIMatches.group(1)
                        name = staticConstDataURIMatches.group(2)
                        object["name"] = "%s.%s" % (interfaceName, name)
                        object["substitutionGroup"] = "Tethys:Uri"
                        object["type"] = "Tethys:DataUriType"
                        description = staticConstDataURIMatches.group(3)
                        descDone = staticConstDataURIMatches.group(4)
                        appendDescription(description, descDone, ifLines, object )
                        result.append(object)
                        object = {}

                    staticConstOutputURIMatches = staticConstOutputURIPattern.match( ifLine )
                    if staticConstOutputURIMatches and ( namespace.startswith(componentClass) or (parentClass and namespace.startswith(parentClass))):
                        id = staticConstOutputURIMatches.group(1)
                        name = staticConstOutputURIMatches.group(2)
                        object["name"] = "%s.%s" % (interfaceName, name)
                        object["substitutionGroup"] = "Tethys:OutputUri"
                        object["type"] = "Tethys:OutputUriType"
                        description = staticConstOutputURIMatches.group(3)
                        descDone = staticConstOutputURIMatches.group(4)
                        appendDescription(description, descDone, ifLines, object )
                        result.append(object)
                        object = {}

                    staticConstSettingURIMatches = staticConstSettingURIPattern.match( ifLine )
                    if staticConstSettingURIMatches and ( namespace.startswith(componentClass) or (parentClass and namespace.startswith(parentClass))):
                        id = staticConstSettingURIMatches.group(1)
                        name = staticConstSettingURIMatches.group(2)
                        object["name"] = "%s.%s" % (interfaceName, name)
                        object["substitutionGroup"] = "Tethys:Uri"
                        object["type"] = "Tethys:SettingUriType"
                        description = staticConstSettingURIMatches.group(3)
                        descDone = staticConstSettingURIMatches.group(4)
                        appendDescription(description, descDone, ifLines, object )
                        result.append(object)
                        object = {}

                    staticConstStringSettingURIMatches = staticConstStringSettingURIPattern.match( ifLine )
                    if staticConstStringSettingURIMatches and ( namespace.startswith(componentClass) or (parentClass and namespace.startswith(parentClass))):
                        id = staticConstStringSettingURIMatches.group(1)
                        name = staticConstStringSettingURIMatches.group(2)
                        object["name"] = "%s.%s" % (interfaceName, name)
                        object["substitutionGroup"] = "Tethys:UriStringSetting"
                        object["type"] = "Tethys:UriStringSettingType"
                        description = staticConstStringSettingURIMatches.group(3)
                        descDone = staticConstStringSettingURIMatches.group(4)
                        appendDescription(description, descDone, ifLines, object )
                        result.append(object)
                        object = {}

                interfaceFile.close()

            key = None
            variableSetting = False

            if isBehaviorClass and not behaviorType:
                missionComponentMatches = missonComponentPattern.match( sourceLine )
                if missionComponentMatches:
                    if missionComponentMatches.group(1) == "true":
                        if missionComponentMatches.group(2) == "true":
                            behaviorType = "SequenceType"
                        else:
                            behaviorType = "ParallelType"
                else:
                    altMissionComponentMatches = altMissonComponentPattern.match( sourceLine )
                    if altMissionComponentMatches:
                        parentClass = altMissionComponentMatches.group(1)
                        parentClassSourceFile = open( os.path.normpath(sourceFolder + parentClass + ".cpp") )
                        parentSourceLines = parentClassSourceFile.readlines()
                        parentSourceLines.extend( sourceLines )
                        sourceLines = parentSourceLines

                if behaviorType:
                    object["name"] = componentClass
                    object["type"] = "Tethys:BehaviorType"
                    object["substitutionGroup"] = "Tethys:Behavior"
                    desc = dict.get(componentClass + "IF::NAME", None)
                    if desc and isinstance(desc,list) and desc[1]:
                        object["description"] = desc[1]
                    result.append(object)
                    object = {}

            newInputReaderMatches = newInputReaderPattern.match( sourceLine )
            if newInputReaderMatches:
                key = newInputReaderMatches.group(1)

            newSettingReaderMatches = newSettingReaderPattern.match( sourceLine )
            if newSettingReaderMatches:
                key = newSettingReaderMatches.group(1)
                variableSetting = None != newSettingVariableReaderPattern.match( sourceLine )

            newOutputWriterMatches = newOutputWriterPattern.match( sourceLine )
            if newOutputWriterMatches:
                key = newOutputWriterMatches.group(1)

            findArgWriterMatches = findArgWriterPattern.match( sourceLine )
            if findArgWriterMatches:
                key = findArgWriterMatches.group(1)

            newUniversalWriterMatches = newUniversalWriterPattern.match( sourceLine )
            if newUniversalWriterMatches:
                key = newUniversalWriterMatches.group(1)

            if not key:
                continue

            value = dict.get(key, None)

            if not value:
                universalURIMatches = universalURIPattern.match( key )
                if universalURIMatches:
                    value = universalURIMatches.group(1).lower()

            if value:
                if variableSetting:
                    object["name"] = "%s.%s" % (componentClass, value)
                    object["substitutionGroup"] = "Tethys:UriStringSetting"
                    object["type"] = "Tethys:UriStringSettingType"
                else:
                    object["name"] = "%s.%s" % (componentClass, value)
                    object["substitutionGroup"] = "Tethys:Uri"
                    object["type"] = "Tethys:UriType"
                result.append(object)
                object = {}

        classSourceFile.close()

        if not encounteredAnImportInterface:
            print("ERROR: No interface file encountered in file " + sourceFolder + componentClass + ".cpp")

        enumMode = False
        enumValue = 0
        classInterfaceFile = open( os.path.normpath(sourceFolder + componentClass + "IF.h") )
        for ifLine in classInterfaceFile.readlines():
            if enumMode:
                enumBehaviorMatch = enumBehaviorPattern.match( ifLine )
                if enumBehaviorMatch:
                    enumBehavior = enumBehaviorMatch.group(1)
                    result.append({
                        "COMMENT": componentClass + '.' + enumBehavior + '=' + str(enumValue)
                    })
                    enumValue = enumValue + 1
                else:
                    if enumEndPattern.match( ifLine ):
                        enumMode = True
                        enumValue = 0
            elif enumStartPattern.match( ifLine ):
                    enumMode = True
            else:
                staticConstDoubleMatches = staticConstDoublePattern.match( ifLine )
                if staticConstDoubleMatches:
                    varName = staticConstDoubleMatches.group(1)
                    value = staticConstDoubleMatches.group(2)
                    result.append({
                        "COMMENT": componentClass + '.' + varName + '= ' + value
                    })

        classInterfaceFile.close()

    moduleFile.close()

    # return elements sorted by `name` (if having such member):
    return moduleRootBase, sorted(result, key=lambda m: m.get('name', ''))
