// Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
// This source file is part of the Cangjie project, licensed under Apache-2.0
// with Runtime Library Exception.
//
// See https://cangjie-lang.cn/pages/LICENSE for license information.

package cjpm.config

import std.collection.*
import std.fs.*
import std.process.*
import stdx.serialization.serialization.*
import stdx.encoding.json.*

const PACKAGE_REQUIRES = "package-requires"
const NATIVE_COMPILE_OPTION = "native-compile-option"
const NATIVE_LINK_OPTION = "native-link-option"
const C_LIBS = "c_libs"

// '.cjpm-history' model
public class ModuleResolve <: Serializable<ModuleResolve> {
    public var resolves: ArrayList<ResolveItem> = ArrayList<ResolveItem>()
    public var binDeps: HashMap<String, BinDeps> = HashMap<String, BinDeps>()
    public var crossBinDeps: HashMap<String, BinDeps> = HashMap<String, BinDeps>()

    public func serialize(): DataModel {
        return DataModelStruct()
            .add(field<ArrayList<ResolveItem>>("resolves", this.resolves))
            .add(field<HashMap<String, BinDeps>>("binDeps", this.binDeps))
            .add(field<HashMap<String, BinDeps>>("crossBinDeps", this.crossBinDeps))
    }

    public static func deserialize(dm: DataModel): ModuleResolve {
        var dms = match (dm) {
            case data: DataModelStruct => data
            case _ => throw Exception("this data is not DataModelStruct")
        }
        var result = ModuleResolve()
        result.resolves = ArrayList<ResolveItem>.deserialize(dms.get("resolves"))
        result.binDeps = HashMap<String, BinDeps>.deserialize(dms.get("binDeps"))
        result.crossBinDeps = HashMap<String, BinDeps>.deserialize(dms.get("crossBinDeps"))
        return result
    }

    public func forEachPkg(fn: (ResolveItem) -> Bool): Bool {
        for (v in this.resolves) {
            if (!fn(v)) {
                return false
            }
        }
        return true
    }
}

// the 'incremental-cache.json' model for each module.
public class IncrementalCache <: Serializable<IncrementalCache> {
    public var packageInformation: HashMap<String, CacheItem> = HashMap<String, CacheItem>()
    public var jsonHash: String = ""
    public var rootPkgName: String = ""

    public func serialize(): DataModel {
        return DataModelStruct()
            .add(field<HashMap<String, CacheItem>>("packageInformation", this.packageInformation))
            .add(field<String>("jsonHash", this.jsonHash))
            .add(field<String>("rootPackageName", this.rootPkgName))
    }

    public static func deserialize(dm: DataModel): IncrementalCache {
        var dms = match (dm) {
            case data: DataModelStruct => data
            case _ => throw Exception("this data is not DataModelStruct")
        }
        var result = IncrementalCache()
        result.packageInformation = HashMap<String, CacheItem>.deserialize(
            (dms.get("packageInformation") as DataModelStruct) ?? DataModelStruct())
        result.jsonHash = String.deserialize((dms.get("jsonHash") as DataModelString) ?? DataModelString(""))
        result.rootPkgName = String.deserialize((dms.get("rootPackageName") as DataModelString) ?? DataModelString(""))
        return result
    }
}

public class CacheItem <: Serializable<CacheItem> {
    public var rootPkgName: String = ""
    public var fileNameHash: String = ""
    public var timeStamp: Int64 = Int64.Min
    public var isBuilt: Bool = false
    public var exeName: String = ""
    public var hasSubPkgs: Bool = false

    public init() {}

    public init(rootPkgName: String, fileNameHash: String, timeStamp: Int64) {
        this.rootPkgName = rootPkgName
        this.fileNameHash = fileNameHash
        this.timeStamp = timeStamp
    }

    public init(rootPkgName: String, fileNameHash: String, timeStamp: Int64, hasSubPkgs: Bool) {
        this.rootPkgName = rootPkgName
        this.fileNameHash = fileNameHash
        this.timeStamp = timeStamp
        this.hasSubPkgs = hasSubPkgs
    }

    public func serialize(): DataModel {
        return DataModelStruct()
            .add(field<String>("rootPackageName", this.rootPkgName))
            .add(field<String>("fileNameHash", this.fileNameHash))
            .add(field<Int64>("timeStamp", this.timeStamp))
            .add(field<Bool>("isBuilt", this.isBuilt))
            .add(field<String>("exeName", this.exeName))
            .add(field<Bool>("hasSubPkgs", this.hasSubPkgs))
    }

    public static func deserialize(dm: DataModel): CacheItem {
        var dms = match (dm) {
            case data: DataModelStruct => data
            case _ => throw Exception("this data is not DataModelStruct")
        }
        var result = CacheItem()
        result.rootPkgName = String.deserialize((dms.get("rootPackageName") as DataModelString) ?? DataModelString(""))
        result.fileNameHash = String.deserialize((dms.get("fileNameHash") as DataModelString) ?? DataModelString(""))
        result.isBuilt = Bool.deserialize((dms.get("isBuilt") as DataModelBool) ?? DataModelBool(false))
        result.timeStamp = Int64.deserialize((dms.get("timeStamp") as DataModelInt) ?? DataModelInt(Int64.Min))
        result.exeName = String.deserialize((dms.get("exeName") as DataModelString) ?? DataModelString(""))
        result.hasSubPkgs = Bool.deserialize((dms.get("hasSubPkgs") as DataModelBool) ?? DataModelBool(false))
        return result
    }
}

public class BinDeps <: Serializable<BinDeps> {
    public var packageRequires: HashMap<String, RequiresPackagesInfo> = HashMap<String, RequiresPackagesInfo>()
    public var packageRequiresSort: ArrayList<String> = ArrayList<String>()

    public func serialize(): DataModel {
        let result = DataModelStruct()
        result.add(field<HashMap<String, RequiresPackagesInfo>>(PACKAGE_REQUIRES, this.packageRequires))
        result.add(field<ArrayList<String>>("packageRequiresSort", this.packageRequiresSort))
        return result
    }

    public static func deserialize(dm: DataModel): BinDeps {
        var dms = match (dm) {
            case data: DataModelStruct => data
            case _ => throw Exception("this data is not DataModelStruct")
        }
        var result = BinDeps()
        result.packageRequires = HashMap<String, RequiresPackagesInfo>.deserialize(
            (dms.get(PACKAGE_REQUIRES) as DataModelStruct) ?? DataModelStruct())
        result.packageRequiresSort = ArrayList<String>.deserialize(
            (dms.get("packageRequiresSort") as DataModelSeq) ?? DataModelSeq())
        return result
    }

    public func isEmpty(): Bool {
        return this.packageRequiresSort.isEmpty()
    }
}

// generate '.cjpm-history' data item
public class ResolveItem <: Serializable<ResolveItem> {
    public var rootPkgName: String = ""
    public var packagePath: String = ""
    public var modulePath: String = ""
    public var fullName: String = ""
    public var outputType: String = ""
    public var compileOption: String = ""
    public var version: String = ""
    public var customizedOption: HashMap<String, String> = HashMap<String, String>()
    public var linkOption: String = ""
    public var nativeCompileOption: String = ""
    public var nativeLinkOption: String = ""
    public var isMacroPackage: Bool = false
    public var requires: HashSet<String> = HashSet<String>()
    public var stdRequires: HashSet<String> = HashSet<String>()
    public var requireDoublePkgs: ArrayList<(String, String)> = ArrayList<(String, String)>()
    public var cLibs: HashMap<String, String> = HashMap<String, String>()
    public var srcDir: String = ""
    public var buildFlag: Bool = false
    public var nativePlatform: Bool = false // When cross-compiling, don't need to compile native platform by default
    public var targetPlatform: Bool = true // The target platform needs to be compiled by default
    public var accessLevel: String = "public"
    public var hasTestFiles: Bool = false
    public var hasProdFiles: Bool = false
    public var hasTestsInProdFiles: Bool = false // It's detected by cjc --scan-dependency mode

    public init() {}

    public init(toml: TomlInfo) {
        this.rootPkgName = toml.pkg.name
        this.outputType = toml.pkg.outputType
        this.compileOption = toml.pkg.compileOption
        this.nativeCompileOption = toml.pkg.nativeCompileOption
        this.customizedOption = HashMap<String, String>(toml.profile.customizedOption)
        this.linkOption = toml.pkg.linkOption
        this.nativeLinkOption = toml.pkg.nativeLinkOption
        this.cLibs = toml.ffi.cLibs
        this.srcDir = toml.pkg.srcDir
        this.modulePath = toml.pkg.path
        this.version = toml.pkg.version
    }

    public func isPureTestPkg(): Bool {
        return hasTestFiles && !hasProdFiles
    }

    public func serialize(): DataModel {
        return serializeResolve(this)
    }

    public static func deserialize(dm: DataModel): ResolveItem {
        return deserializeResolve(dm)
    }

    public func forEachDependency(fn: (String) -> Unit): Unit {
        for (k in this.requires) {
            fn(k)
        }
        return
    }
}

func serializeResolve(r: ResolveItem): DataModel {
    let result = DataModelStruct()
    result
        .add(field<String>("rootPackageName", r.rootPkgName))
        .add(field<String>("modulePath", r.modulePath))
        .add(field<String>("packagePath", r.packagePath))
        .add(field<String>("fullName", r.fullName))
        .add(field<Bool>("isMacroPackage", r.isMacroPackage))
        .add(field<String>(OUTPUT_TYPE, r.outputType))
        .add(field<String>(COMPILE_OPTION, r.compileOption))
        .add(field<String>(NATIVE_COMPILE_OPTION, r.nativeCompileOption))
        .add(field<HashMap<String, String>>(CUSTOMIZED_OPTION, r.customizedOption))
        .add(field<String>(LINK_OPTION, r.linkOption))
        .add(field<String>(NATIVE_LINK_OPTION, r.nativeLinkOption))
        .add(field<HashSet<String>>("requires", r.requires))
        .add(field<HashSet<String>>("std-requires", r.stdRequires))
        .add(field<HashMap<String, String>>(C_LIBS, r.cLibs))
        .add(field<Bool>("hasTestFiles", r.hasTestFiles))
        .add(field<Bool>("hasProdFiles", r.hasProdFiles))
        .add(field<Bool>("hasTestsInProdFiles", r.hasTestsInProdFiles))
        .add(field<String>("accessLevel", r.accessLevel))
    return result
}

func deserializeResolve(dm: DataModel): ResolveItem {
    var dms = match (dm) {
        case data: DataModelStruct => data
        case _ => throw Exception("this data is not DataModelStruct")
    }
    var result = ResolveItem()
    result.packagePath = String.deserialize(dms.get("packagePath"))
    result.modulePath = String.deserialize(dms.get("modulePath"))
    result.fullName = String.deserialize(dms.get("fullName"))
    result.rootPkgName = String.deserialize(dms.get("rootPackageName"))
    result.isMacroPackage = Bool.deserialize((dms.get("isMacroPackage") as DataModelBool) ?? DataModelBool(false))
    result.outputType = String.deserialize((dms.get(OUTPUT_TYPE) as DataModelString) ?? DataModelString(""))
    result.compileOption = String.deserialize((dms.get(COMPILE_OPTION) as DataModelString) ?? DataModelString(""))
    result.nativeCompileOption = String.deserialize(
        (dms.get(NATIVE_COMPILE_OPTION) as DataModelString) ?? DataModelString(""))
    result.customizedOption = HashMap<String, String>.deserialize(
        (dms.get(CUSTOMIZED_OPTION) as DataModelStruct) ?? DataModelStruct())
    result.linkOption = String.deserialize((dms.get(LINK_OPTION) as DataModelString) ?? DataModelString(""))
    result.nativeLinkOption = String.deserialize(
        (dms.get(NATIVE_LINK_OPTION) as DataModelString) ?? DataModelString(""))
    result.requires = HashSet<String>.deserialize(dms.get("requires"))
    result.stdRequires = HashSet<String>.deserialize(dms.get("std-requires"))
    result.cLibs = HashMap<String, String>.deserialize(dms.get(C_LIBS))
    result.hasProdFiles = Bool.deserialize((dms.get("hasProdFiles") as DataModelBool) ?? DataModelBool(false))
    result.hasTestFiles = Bool.deserialize((dms.get("hasTestFiles") as DataModelBool) ?? DataModelBool(false))
    result.hasTestsInProdFiles = Bool.deserialize(
        (dms.get("hasTestsInProdFiles") as DataModelBool) ?? DataModelBool(false))
    result.accessLevel = String.deserialize((dms.get("accessLevel") as DataModelString) ?? DataModelString("public"))
    return result
}

// load '.cjpm-history' file from filePath
public func loadHistoryFile(filePath: String): Option<ModuleResolve> {
    var model: ModuleResolve = ModuleResolve()
    var (buf, flag) = loadStringFromFile(filePath)
    if (!flag) {
        return Option<ModuleResolve>.None
    }
    try {
        model = ModuleResolve.deserialize(DataModel.fromJson(JsonValue.fromStr(buf)))
    } catch (e: Exception) {
        eprintln("Error: load ${filePath} failed\n${e.toString()}")
        return Option<ModuleResolve>.None
    }
    return model
}

public func configureModuleResolveData(targetDir: String, nativeDir: String, resolve: ModuleResolve,
    macroDep!: HashSet<String> = HashSet<String>()):
    (HashSet<String>, HashMap<String, ArrayList<String>>, ArrayList<String>) {
    let ldPath: HashSet<String> = getLdPathConfig(targetDir, nativeDir, resolve, macroDep: macroDep)
    let cLibLinkMap = getClibLink(resolve.resolves)
    let cjPathList = ArrayList<String>()
    cjPathList.add(all: getPackageRequires(resolve.binDeps))
    cjPathList.add(all: getPackageRequires(resolve.crossBinDeps))
    return (ldPath, cLibLinkMap, cjPathList)
}

@When[os == "macOS" && arch == "aarch64"]
private const currentArch: String = "aarch64"
@When[os == "macOS" && arch != "aarch64"]
private const currentArch: String = "x86_64"

@When[os == "macOS"]
private func getExtraRuntimePaths(): Array<String> {
    // DYLD_LIBRARY_PATH cannot be propagated by shells (or any system level interpreters) on MacOS if
    // System Integration Protection (SIP) is enabled. Here we add Cangjie runtime directory into
    // DYLD_LIBRARY_PATH to ensure cjc could find runtime libraries when SIP is enabled.
    var maybeCangjieHome = Process.current.getEnv("CANGJIE_HOME")
    // If CANGJIE_HOME is not set, use the directory where cjc is located.
    if (maybeCangjieHome.isNone() && cjcPath.isSome()) {
        let cjcBinDir = Path(cjcPath.getOrThrow()).parent
        if (!cjcBinDir.isEmpty()) {
            let cjcBinFileInfo = FileInfo(cjcBinDir).parentDirectory
            maybeCangjieHome = cjcBinFileInfo?.path.toString()
        }
    }
    if (maybeCangjieHome.isSome() && !maybeCangjieHome.getOrThrow().isEmpty()) {
        return [Path("${maybeCangjieHome.getOrThrow()}/runtime/lib/darwin_${currentArch}_cjnative").toString()]
    }
    return Array<String>()
}

@When[os != "macOS"]
private func getExtraRuntimePaths(): Array<String> {
    Array<String>()
}

// Get LD_LIBRARY_PATH by '.cjpm-history' file.
public func getLdPathConfig(targetDir: String, nativeDir: String, resolve: ModuleResolve,
    macroDep!: HashSet<String> = HashSet<String>()): HashSet<String> {
    var ldPath: HashSet<String> = HashSet<String>()
    for (v in resolve.resolves) {
        // Set LD_LIBRARY_PATH for each dynamiclib
        if (v.outputType == DYNAMIC_TYPE) {
            ldPath.add(Path(targetDir).join(v.rootPkgName).toString())
        }
        for ((_, path) in v.cLibs) {
            ldPath.add(path)
        }
        // Set LD_LIBRARY_PATH for native dynamic deps of macro packages
        if (!crossCompileTarget.isEmpty() && macroDep.contains(v.fullName) && v.outputType == DYNAMIC_TYPE) {
            var path: Path = Path(nativeDir).join(v.rootPkgName)
            ldPath.add(path.toString())
        }
    }
    for ((_, item) in resolve.binDeps) {
        for ((_, require) in item.packageRequires) {
            ldPath.add(require.libPath)
        }
    }
    for (path in getExtraRuntimePaths()) {
        ldPath.add(path)
    }
    return ldPath
}

func getPackageRequires(binDeps: HashMap<String, BinDeps>): ArrayList<String> {
    var cjPathList: ArrayList<String> = ArrayList<String>()
    for ((_, item) in binDeps) {
        for ((_, require) in item.packageRequires) {
            if (!cjPathList.contains(require.cjPath)) {
                cjPathList.add(require.cjPath)
            }
        }
    }
    return cjPathList
}

// Store cLibLink information for each module.
func getClibLink(items: ArrayList<ResolveItem>): HashMap<String, ArrayList<String>> {
    let nameSet = HashSet<String>()
    let cLibLinkMap = HashMap<String, ArrayList<String>>()
    for (v in items) {
        if (nameSet.contains(v.rootPkgName)) {
            continue
        }

        let allOptions = ArrayList<String>()

        for ((name, path) in v.cLibs) {
            allOptions.add(all: getForeignStr(name, path), at: 0)
        }

        nameSet.add(v.rootPkgName)

        if (!allOptions.isEmpty()) {
            cLibLinkMap[v.rootPkgName] = allOptions
        }
    }
    return cLibLinkMap
}

func getForeignStr(name: String, path: String): ArrayList<String> {
    return ArrayList<String>(["-L", path, "-l${name}"])
}
