# Copyright (c) 2020 The Linux Foundation
#
# SPDX-License-Identifier: Apache-2.0

import json
import os

from west import log

import zspdx.cmakefileapi

def parseReply(replyIndexPath):
    replyDir, _ = os.path.split(replyIndexPath)

    # first we need to find the codemodel reply file
    try:
        with open(replyIndexPath, 'r') as indexFile:
            js = json.load(indexFile)

            # get reply object
            reply_dict = js.get("reply", {})
            if reply_dict == {}:
                log.err(f"no \"reply\" field found in index file")
                return None
            # get codemodel object
            cm_dict = reply_dict.get("codemodel-v2", {})
            if cm_dict == {}:
                log.err(f"no \"codemodel-v2\" field found in \"reply\" object in index file")
                return None
            # and get codemodel filename
            jsonFile = cm_dict.get("jsonFile", "")
            if jsonFile == "":
                log.err(f"no \"jsonFile\" field found in \"codemodel-v2\" object in index file")
                return None

            return parseCodemodel(replyDir, jsonFile)

    except OSError as e:
        log.err(f"Error loading {replyIndexPath}: {str(e)}")
        return None
    except json.decoder.JSONDecodeError as e:
        log.err(f"Error parsing JSON in {replyIndexPath}: {str(e)}")
        return None

def parseCodemodel(replyDir, codemodelFile):
    codemodelPath = os.path.join(replyDir, codemodelFile)

    try:
        with open(codemodelPath, 'r') as cmFile:
            js = json.load(cmFile)

            cm = zspdx.cmakefileapi.Codemodel()

            # for correctness, check kind and version
            kind = js.get("kind", "")
            if kind != "codemodel":
                log.err(f"Error loading CMake API reply: expected \"kind\":\"codemodel\" in {codemodelPath}, got {kind}")
                return None
            version = js.get("version", {})
            versionMajor = version.get("major", -1)
            if versionMajor != 2:
                if versionMajor == -1:
                    log.err(f"Error loading CMake API reply: expected major version 2 in {codemodelPath}, no version found")
                    return None
                log.err(f"Error loading CMake API reply: expected major version 2 in {codemodelPath}, got {versionMajor}")
                return None

            # get paths
            paths_dict = js.get("paths", {})
            cm.paths_source = paths_dict.get("source", "")
            cm.paths_build = paths_dict.get("build", "")

            # get configurations
            configs_arr = js.get("configurations", [])
            for cfg_dict in configs_arr:
                cfg = parseConfig(cfg_dict, replyDir)
                if cfg:
                    cm.configurations.append(cfg)

            # and after parsing is done, link all the indices
            linkCodemodel(cm)

            return cm

    except OSError as e:
        log.err(f"Error loading {codemodelPath}: {str(e)}")
        return None
    except json.decoder.JSONDecodeError as e:
        log.err(f"Error parsing JSON in {codemodelPath}: {str(e)}")
        return None

def parseConfig(cfg_dict, replyDir):
    cfg = zspdx.cmakefileapi.Config()
    cfg.name = cfg_dict.get("name", "")

    # parse and add each directory
    dirs_arr = cfg_dict.get("directories", [])
    for dir_dict in dirs_arr:
        if dir_dict != {}:
            cfgdir = zspdx.cmakefileapi.ConfigDir()
            cfgdir.source = dir_dict.get("source", "")
            cfgdir.build = dir_dict.get("build", "")
            cfgdir.parentIndex = dir_dict.get("parentIndex", -1)
            cfgdir.childIndexes = dir_dict.get("childIndexes", [])
            cfgdir.projectIndex = dir_dict.get("projecttIndex", -1)
            cfgdir.targetIndexes = dir_dict.get("targetIndexes", [])
            minCMakeVer_dict = dir_dict.get("minimumCMakeVersion", {})
            cfgdir.minimumCMakeVersion = minCMakeVer_dict.get("string", "")
            cfgdir.hasInstallRule = dir_dict.get("hasInstallRule", False)
            cfg.directories.append(cfgdir)

    # parse and add each project
    projects_arr = cfg_dict.get("projects", [])
    for prj_dict in projects_arr:
        if prj_dict != {}:
            prj = zspdx.cmakefileapi.ConfigProject()
            prj.name = prj_dict.get("name", "")
            prj.parentIndex = prj_dict.get("parentIndex", -1)
            prj.childIndexes = prj_dict.get("childIndexes", [])
            prj.directoryIndexes = prj_dict.get("directoryIndexes", [])
            prj.targetIndexes = prj_dict.get("targetIndexes", [])
            cfg.projects.append(prj)

    # parse and add each target
    cfgTargets_arr = cfg_dict.get("targets", [])
    for cfgTarget_dict in cfgTargets_arr:
        if cfgTarget_dict != {}:
            cfgTarget = zspdx.cmakefileapi.ConfigTarget()
            cfgTarget.name = cfgTarget_dict.get("name", "")
            cfgTarget.id = cfgTarget_dict.get("id", "")
            cfgTarget.directoryIndex = cfgTarget_dict.get("directoryIndex", -1)
            cfgTarget.projectIndex = cfgTarget_dict.get("projectIndex", -1)
            cfgTarget.jsonFile = cfgTarget_dict.get("jsonFile", "")

            if cfgTarget.jsonFile != "":
                cfgTarget.target = parseTarget(os.path.join(replyDir, cfgTarget.jsonFile))
            else:
                cfgTarget.target = None

            cfg.configTargets.append(cfgTarget)

    return cfg

def parseTarget(targetPath):
    try:
        with open(targetPath, 'r') as targetFile:
            js = json.load(targetFile)

            target = zspdx.cmakefileapi.Target()

            target.name = js.get("name", "")
            target.id = js.get("id", "")
            target.type = parseTargetType(js.get("type", "UNKNOWN"))
            target.backtrace = js.get("backtrace", -1)
            target.folder = js.get("folder", "")

            # get paths
            paths_dict = js.get("paths", {})
            target.paths_source = paths_dict.get("source", "")
            target.paths_build = paths_dict.get("build", "")

            target.nameOnDisk = js.get("nameOnDisk", "")

            # parse artifacts if present
            artifacts_arr = js.get("artifacts", [])
            target.artifacts = []
            for artifact_dict in artifacts_arr:
                artifact_path = artifact_dict.get("path", "")
                if artifact_path != "":
                    target.artifacts.append(artifact_path)

            target.isGeneratorProvided = js.get("isGeneratorProvided", False)

            # call separate functions to parse subsections
            parseTargetInstall(target, js)
            parseTargetLink(target, js)
            parseTargetArchive(target, js)
            parseTargetDependencies(target, js)
            parseTargetSources(target, js)
            parseTargetSourceGroups(target, js)
            parseTargetCompileGroups(target, js)
            parseTargetBacktraceGraph(target, js)

            return target

    except OSError as e:
        log.err(f"Error loading {targetPath}: {str(e)}")
        return None
    except json.decoder.JSONDecodeError as e:
        log.err(f"Error parsing JSON in {targetPath}: {str(e)}")
        return None

def parseTargetType(targetType):
    if targetType == "EXECUTABLE":
        return zspdx.cmakefileapi.TargetType.EXECUTABLE
    elif targetType == "STATIC_LIBRARY":
        return zspdx.cmakefileapi.TargetType.STATIC_LIBRARY
    elif targetType == "SHARED_LIBRARY":
        return zspdx.cmakefileapi.TargetType.SHARED_LIBRARY
    elif targetType == "MODULE_LIBRARY":
        return zspdx.cmakefileapi.TargetType.MODULE_LIBRARY
    elif targetType == "OBJECT_LIBRARY":
        return zspdx.cmakefileapi.TargetType.OBJECT_LIBRARY
    elif targetType == "UTILITY":
        return zspdx.cmakefileapi.TargetType.UTILITY
    else:
        return zspdx.cmakefileapi.TargetType.UNKNOWN

def parseTargetInstall(target, js):
    install_dict = js.get("install", {})
    if install_dict == {}:
        return
    prefix_dict = install_dict.get("prefix", {})
    target.install_prefix = prefix_dict.get("path", "")

    destinations_arr = install_dict.get("destinations", [])
    for destination_dict in destinations_arr:
        dest = zspdx.cmakefileapi.TargetInstallDestination()
        dest.path = destination_dict.get("path", "")
        dest.backtrace = destination_dict.get("backtrace", -1)
        target.install_destinations.append(dest)

def parseTargetLink(target, js):
    link_dict = js.get("link", {})
    if link_dict == {}:
        return
    target.link_language = link_dict.get("language", {})
    target.link_lto = link_dict.get("lto", False)
    sysroot_dict = link_dict.get("sysroot", {})
    target.link_sysroot = sysroot_dict.get("path", "")

    fragments_arr = link_dict.get("commandFragments", [])
    for fragment_dict in fragments_arr:
        fragment = zspdx.cmakefileapi.TargetCommandFragment()
        fragment.fragment = fragment_dict.get("fragment", "")
        fragment.role = fragment_dict.get("role", "")
        target.link_commandFragments.append(fragment)

def parseTargetArchive(target, js):
    archive_dict = js.get("archive", {})
    if archive_dict == {}:
        return
    target.archive_lto = archive_dict.get("lto", False)

    fragments_arr = archive_dict.get("commandFragments", [])
    for fragment_dict in fragments_arr:
        fragment = zspdx.cmakefileapi.TargetCommandFragment()
        fragment.fragment = fragment_dict.get("fragment", "")
        fragment.role = fragment_dict.get("role", "")
        target.archive_commandFragments.append(fragment)

def parseTargetDependencies(target, js):
    dependencies_arr = js.get("dependencies", [])
    for dependency_dict in dependencies_arr:
        dep = zspdx.cmakefileapi.TargetDependency()
        dep.id = dependency_dict.get("id", "")
        dep.backtrace = dependency_dict.get("backtrace", -1)
        target.dependencies.append(dep)

def parseTargetSources(target, js):
    sources_arr = js.get("sources", [])
    for source_dict in sources_arr:
        src = zspdx.cmakefileapi.TargetSource()
        src.path = source_dict.get("path", "")
        src.compileGroupIndex = source_dict.get("compileGroupIndex", -1)
        src.sourceGroupIndex = source_dict.get("sourceGroupIndex", -1)
        src.isGenerated = source_dict.get("isGenerated", False)
        src.backtrace = source_dict.get("backtrace", -1)
        target.sources.append(src)

def parseTargetSourceGroups(target, js):
    sourceGroups_arr = js.get("sourceGroups", [])
    for sourceGroup_dict in sourceGroups_arr:
        srcgrp = zspdx.cmakefileapi.TargetSourceGroup()
        srcgrp.name = sourceGroup_dict.get("name", "")
        srcgrp.sourceIndexes = sourceGroup_dict.get("sourceIndexes", [])
        target.sourceGroups.append(srcgrp)

def parseTargetCompileGroups(target, js):
    compileGroups_arr = js.get("compileGroups", [])
    for compileGroup_dict in compileGroups_arr:
        cmpgrp = zspdx.cmakefileapi.TargetCompileGroup()
        cmpgrp.sourceIndexes = compileGroup_dict.get("sourceIndexes", [])
        cmpgrp.language = compileGroup_dict.get("language", "")
        cmpgrp.sysroot = compileGroup_dict.get("sysroot", "")

        commandFragments_arr = compileGroup_dict.get("compileCommandFragments", [])
        for commandFragment_dict in commandFragments_arr:
            fragment = commandFragment_dict.get("fragment", "")
            if fragment != "":
                cmpgrp.compileCommandFragments.append(fragment)

        includes_arr = compileGroup_dict.get("includes", [])
        for include_dict in includes_arr:
            grpInclude = zspdx.cmakefileapi.TargetCompileGroupInclude()
            grpInclude.path = include_dict.get("path", "")
            grpInclude.isSystem = include_dict.get("isSystem", False)
            grpInclude.backtrace = include_dict.get("backtrace", -1)
            cmpgrp.includes.append(grpInclude)

        precompileHeaders_arr = compileGroup_dict.get("precompileHeaders", [])
        for precompileHeader_dict in precompileHeaders_arr:
            grpHeader = zspdx.cmakefileapi.TargetCompileGroupPrecompileHeader()
            grpHeader.header = precompileHeader_dict.get("header", "")
            grpHeader.backtrace = precompileHeader_dict.get("backtrace", -1)
            cmpgrp.precompileHeaders.append(grpHeader)

        defines_arr = compileGroup_dict.get("defines", [])
        for define_dict in defines_arr:
            grpDefine = zspdx.cmakefileapi.TargetCompileGroupDefine()
            grpDefine.define = define_dict.get("define", "")
            grpDefine.backtrace = define_dict.get("backtrace", -1)
            cmpgrp.defines.append(grpDefine)

        target.compileGroups.append(cmpgrp)

def parseTargetBacktraceGraph(target, js):
    backtraceGraph_dict = js.get("backtraceGraph", {})
    if backtraceGraph_dict == {}:
        return
    target.backtraceGraph_commands = backtraceGraph_dict.get("commands", [])
    target.backtraceGraph_files = backtraceGraph_dict.get("files", [])

    nodes_arr = backtraceGraph_dict.get("nodes", [])
    for node_dict in nodes_arr:
        node = zspdx.cmakefileapi.TargetBacktraceGraphNode()
        node.file = node_dict.get("file", -1)
        node.line = node_dict.get("line", -1)
        node.command = node_dict.get("command", -1)
        node.parent = node_dict.get("parent", -1)
        target.backtraceGraph_nodes.append(node)

# Create direct pointers for all Configs in Codemodel
# takes: Codemodel
def linkCodemodel(cm):
    for cfg in cm.configurations:
        linkConfig(cfg)

# Create direct pointers for all contents of Config
# takes: Config
def linkConfig(cfg):
    for cfgDir in cfg.directories:
        linkConfigDir(cfg, cfgDir)
    for cfgPrj in cfg.projects:
        linkConfigProject(cfg, cfgPrj)
    for cfgTarget in cfg.configTargets:
        linkConfigTarget(cfg, cfgTarget)

# Create direct pointers for ConfigDir indices
# takes: Config and ConfigDir
def linkConfigDir(cfg, cfgDir):
    if cfgDir.parentIndex == -1:
        cfgDir.parent = None
    else:
        cfgDir.parent = cfg.directories[cfgDir.parentIndex]

    if cfgDir.projectIndex == -1:
        cfgDir.project = None
    else:
        cfgDir.project = cfg.projects[cfgDir.projectIndex]

    cfgDir.children = []
    for childIndex in cfgDir.childIndexes:
        cfgDir.children.append(cfg.directories[childIndex])

    cfgDir.targets = []
    for targetIndex in cfgDir.targetIndexes:
        cfgDir.targets.append(cfg.configTargets[targetIndex])

# Create direct pointers for ConfigProject indices
# takes: Config and ConfigProject
def linkConfigProject(cfg, cfgPrj):
    if cfgPrj.parentIndex == -1:
        cfgPrj.parent = None
    else:
        cfgPrj.parent = cfg.projects[cfgPrj.parentIndex]

    cfgPrj.children = []
    for childIndex in cfgPrj.childIndexes:
        cfgPrj.children.append(cfg.projects[childIndex])

    cfgPrj.directories = []
    for dirIndex in cfgPrj.directoryIndexes:
        cfgPrj.directories.append(cfg.directories[dirIndex])

    cfgPrj.targets = []
    for targetIndex in cfgPrj.targetIndexes:
        cfgPrj.targets.append(cfg.configTargets[targetIndex])

# Create direct pointers for ConfigTarget indices
# takes: Config and ConfigTarget
def linkConfigTarget(cfg, cfgTarget):
    if cfgTarget.directoryIndex == -1:
        cfgTarget.directory = None
    else:
        cfgTarget.directory = cfg.directories[cfgTarget.directoryIndex]

    if cfgTarget.projectIndex == -1:
        cfgTarget.project = None
    else:
        cfgTarget.project = cfg.projects[cfgTarget.projectIndex]

    # and link target's sources and source groups
    for ts in cfgTarget.target.sources:
        linkTargetSource(cfgTarget.target, ts)
    for tsg in cfgTarget.target.sourceGroups:
        linkTargetSourceGroup(cfgTarget.target, tsg)
    for tcg in cfgTarget.target.compileGroups:
        linkTargetCompileGroup(cfgTarget.target, tcg)

# Create direct pointers for TargetSource indices
# takes: Target and TargetSource
def linkTargetSource(target, targetSrc):
    if targetSrc.compileGroupIndex == -1:
        targetSrc.compileGroup = None
    else:
        targetSrc.compileGroup = target.compileGroups[targetSrc.compileGroupIndex]

    if targetSrc.sourceGroupIndex == -1:
        targetSrc.sourceGroup = None
    else:
        targetSrc.sourceGroup = target.sourceGroups[targetSrc.sourceGroupIndex]

# Create direct pointers for TargetSourceGroup indices
# takes: Target and TargetSourceGroup
def linkTargetSourceGroup(target, targetSrcGrp):
    targetSrcGrp.sources = []
    for srcIndex in targetSrcGrp.sourceIndexes:
        targetSrcGrp.sources.append(target.sources[srcIndex])

# Create direct pointers for TargetCompileGroup indices
# takes: Target and TargetCompileGroup
def linkTargetCompileGroup(target, targetCmpGrp):
    targetCmpGrp.sources = []
    for srcIndex in targetCmpGrp.sourceIndexes:
        targetCmpGrp.sources.append(target.sources[srcIndex])
