/*
 * 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.
 */
protected package std.deriving.impl

import std.deriving.api.*
import std.deriving.resolve.*
import std.ast.*
import std.collection.*

public func diagnose(
    inputOptions: InputOptions,
    target: DerivingTarget,
    logger: Logger
): Array<ExtendDecl> {
    let results = ArrayList<ExtendDecl>()
    let resolvedList: Array<(VarDecl, ?TypeNode)> = match (target.declaration) {
        case c: ClassDecl => resolveFields(c)
        case str: StructDecl => resolveFields(str)
        case _ => []
    }

    let ctorParameterTypes = target.namedFields |> mapNotNone<TargetNamedAttribute, (Decl, ?TypeNode)> { f =>
        match (f.typeNode) {
            case Some(t) => (f.decl, t)
            case None => None
        }
    }

    if (target.isSealedClass) {
        logger.error(findSealedKeyword(target.declaration), "Sealed classes are not supported")
    }
    if (target.isNonFinal) {
        logger.error(findAbstractKeyword(target.declaration),
            "Non-final classes are not supported (open, abstract or sealed)")
    }

    let fieldTypes = resolvedList |> map<(VarDecl, ?TypeNode),  (Decl, ?TypeNode)> { it => it } |>
        concat<(Decl, ?TypeNode)>(ctorParameterTypes) |>
        collectHashMap

    for (interfaceName in inputOptions.allInterfaceNames) {
        let deriving = findDeriving(interfaceName) ?? continue
        diagnoseInterface(deriving, interfaceName, target, fieldTypes, results, logger)
    }

    if (!inputOptions.ordering.isEmpty()) {
        diagnoseOrdering(target, inputOptions, results, logger)
    }

    diagnoseConstraints(inputOptions, logger)

    return results.toArray()
}

private func findAbstractKeyword(decl: Decl): Token {
    decl.modifiers.iterator().filter { it =>
        it.keyword.kind == TokenKind.OPEN ||
        it.keyword.kind == TokenKind.ABSTRACT ||
        it.keyword.kind == TokenKind.SEALED
    }.first()?.keyword ?? decl.keyword
}

private func findSealedKeyword(decl: Decl): Token {
    decl.modifiers.iterator().filter { it =>
        it.keyword.kind == TokenKind.SEALED
    }.first()?.keyword ?? decl.keyword
}

private func diagnoseInterface(
    deriving: Deriving,
    interfaceName: QualifiedName,
    target: DerivingTarget,
    fieldTypes: HashMap<Identifier, ?TypeNode>,
    results: ArrayList<ExtendDecl>,
    logger: Logger
): Unit {
    let typeInfo = deriving.queryInterfaceInfo(interfaceName)
    let ifaceSimple = interfaceName.toSimpleNameQN()

    let errors = ArrayList<Tokens>()
    for (f in target.namedFields) {
        if (let Some(fieldType) <- fieldTypes[f.identifier]) {
            let expectedType = typeInfo.genericsInjector.injectGenerics(ifaceSimple, fieldType, target)
            let id = f.identifier.token
            errors.add(quote(
                let _: $expectedType = $id
            ))
        } else {
            logger.error(f.identifier.token,
                "Type for field '${f.identifier}' is unknown. Please specify its type explicitly.")
        }
    }

    diagnoseEnumCaseTypes(target, interfaceName, typeInfo, errors)

    if (!errors.isEmpty()) {
        let ext = target.createExtendSkeleton()
        let constraints = target.genericConstraints(typeInfo, target.findIntefaceSettings(interfaceName))
        ext.appendConstraints(constraints)
        appendErrors(ext.body, errors)
        results.add(ext)
    }
}

private func diagnoseEnumCaseTypes(
    target: DerivingTarget,
    interfaceName: QualifiedName,
    typeInfo: DerivingInterfaceInfo,
    errors: ArrayList<Tokens>
): Unit {
    var counter = 0
    for (c in target.enumCases) {
        for (t in c.constructor.typeArguments) {
            let checkFunctionName = Token(TokenKind.IDENTIFIER, "check_${c.name}_${counter}")
            let genericName = Token(TokenKind.IDENTIFIER, "T_${c.name}_${counter}")
            let genericDecl = GenericDecl(genericName)

            let simpleName = interfaceName.toSimpleNameQN()

            let upperBound = typeInfo.genericsInjector.injectGenerics(simpleName, t, target)
            let constraints = typeInfo.genericsInjector.constraintsFor(simpleName, genericDecl, target) |>
                map { g: GenericConstraint => g.toTokens() } |>
                joinTokens(delimiter: quote(&))

            counter++
            errors.add(quote(
                func $checkFunctionName<$genericName>(_: $t): $upperBound where $constraints { throw Exception() }
                let _ = $checkFunctionName<$t>
            ))
        }
    }
}

private func diagnoseConstraints(
    inputOptions: InputOptions,
    logger: Logger
): Unit {
    let generics = inputOptions.decl.generics() |> associateBy { it: GenericDecl => it.identifier }
    for (t in inputOptions.origin) {
        for (c in t.constraints) {
            if (!generics.contains(c.genericParameter)) {
                logger.error(c.genericParameter.token, "Generic parameter ${c.genericParameter} is missing")
            }
        }
    }
}

private func diagnoseOrdering(
    target: DerivingTarget,
    inputOptions: InputOptions,
    results: ArrayList<ExtendDecl>,
    logger: Logger
): Unit {
    let fieldsSet = target.namedFields |> map { f: TargetNamedAttribute => f.identifier } |> collectHashSet
    let excludedFields = inputOptions.excluded |> collectHashSet
    let errors = ArrayList<Tokens>()

    for (o in inputOptions.ordering) {
        if (excludedFields.contains(o)) {
            logger.error(o.token, "Field ${o.value} is excluded via @DeriveExclude")
            let t = o.token
            errors.add(quote(let _ = (()).$t))
        } else if (!fieldsSet.contains(o)) {
            match (findDecl(target, o)) {
                case Some(p: PropDecl) where p.isStatic() =>
                    // it would appear in the fieldsSet otherwise
                    logger.error(o.token, "Property ${o.value} is static, can't include it")
                case Some(_: PropDecl) =>
                    // it would appear in the fieldsSet otherwise
                    logger.error(o.token, "Property ${o.value} is not included, apply @DeriveInclude on it")
                case Some(f: VarDecl) where f.isStatic() =>
                    logger.error(o.token, "Field ${o.value} is static, can't include it")
                case Some(other) =>
                    logger.error(o.token, "'${o.value}' should be a field or property, got ${describeUnknown(other)}")
                case _ =>
                    let t = o.token
                    errors.add(quote(let _ = this.$t))
            }
        }
    }

    let missing = fieldsSet |> filter { f: Identifier => !inputOptions.ordering.contains(f) } |> collectArray
    if (!missing.isEmpty()) {
        let at = inputOptions.ordering |> map { t: Identifier => t.token } |> first
        let message = "Not all attributes included: ${missing}"
        match (at) {
            case Some(at) => logger.error(at, message)
            case None => throw MacroContextException(message) // we have no tokens to complain at
        }
    }

    for (duplicated in findDuplicates(inputOptions.ordering)) {
        logger.error(duplicated.token, "'${duplicated}' is duplicated in @DeriveOrder")
    }

    if (!errors.isEmpty()) {
        let ext = target.createExtendSkeleton()
        appendErrors(ext.body, errors)
        results.add(ext)
    }
}

private func findDecl(target: DerivingTarget, name: Identifier): ?Decl {
    target.declaration.allNamedDecls().filter { it => it.identifier.value == name.value }.first()
}

private func describeUnknown(decl: Decl): String {
    let keyword = decl.keyword
    if (keyword.kind != TokenKind.ILLEGAL && !keyword.value.isEmpty()) {
        return keyword.value
    }

    return decl.astKind
}

private func appendErrors(body: Body, errors: Collection<Tokens>) {
    if (!errors.isEmpty()) {
        let errorsBody = errors |> joinTokens(delimiter: Token(TokenKind.NL, "\n"))

        let functionName = Token(TokenKind.IDENTIFIER, "derivingChecks_")
        let functionName2 = Token(TokenKind.IDENTIFIER, "derivingChecks2_")
        let errorFunctionDecl = FuncDecl(quote(
            private func $functionName(): Unit {
                $errorsBody
                $functionName2()
            }
        ))
        let unusedHack = FuncDecl(quote(
            private func $functionName2(): Unit {
                $functionName()
            }
        ))

        body.decls.add(errorFunctionDecl)
        body.decls.add(unusedHack)
    }
}

private func findDuplicates(names: Iterable<Identifier>): Array<Identifier> {
    let visited = HashSet<Identifier>()
    let duplicates = ArrayList<Identifier>()

    for (name in names) {
        if (!visited.add(name)) {
            duplicates.add(name)
        }
    }

    return duplicates.toArray()
}