import { EOL } from "node:os";
import * as idl from "../idl/index.js"
import { ReferenceResolver } from "../peer-generation/ReferenceResolver.js";

export enum IDLValidationDiagnosticsCode {
    INVALID_EXTENDED_ATTRIBUTE = 1000,
    ENUM_IS_NOT_CONSISTENT     = 1001,
    REFERENCE_IS_NOT_RESOLVED  = 1002,
}

type IDLValidationErrorDescriptionBox = Record<IDLValidationDiagnosticsCode, string>

const ENG_ErrorDescription: IDLValidationErrorDescriptionBox = {
    [IDLValidationDiagnosticsCode.INVALID_EXTENDED_ATTRIBUTE]: "Invalid extended attribute",
    [IDLValidationDiagnosticsCode.ENUM_IS_NOT_CONSISTENT]: "Enum includes both string and number values",
    [IDLValidationDiagnosticsCode.REFERENCE_IS_NOT_RESOLVED]: "Can not resolve reference",
}

export const IDLValidationErrorDescription = {
    en_EN: ENG_ErrorDescription
}

export interface IDLLinterOptions {
    validEntryAttributes: Map<idl.IDLKind, string[]>,
    checkEnumsConsistency: boolean,
    checkReferencesResolved: boolean,
}

export const DefaultIDLLinterOptions: IDLLinterOptions = {
    validEntryAttributes: new Map([
        [idl.IDLKind.Import, ["Deprecated", "Documentation"]],
        [idl.IDLKind.Namespace, ["DefaultExport", "Deprecated", "Documentation", "VerbatimDts"]],
        [idl.IDLKind.Const, ["DefaultExport", "Deprecated", "Documentation"]],
        [idl.IDLKind.Property, ["DefaultExport", "Optional", "Accessor", "Deprecated", "CommonMethod", "Protected", "DtsName", "Documentation"]],
        [idl.IDLKind.Interface, ["DefaultExport", "Predefined", "TSType", "CPPType", "Entity", "Interfaces", "ParentTypeArguments", "Component", "Synthetic", "Deprecated", "HandWrittenImplementation", "NativeOnly", "Documentation", "TypeParameters", "ComponentInterface"]],
        [idl.IDLKind.Callback, ["DefaultExport", "Deprecated", "Async", "Synthetic", "Documentation", "TypeParameters"]],
        [idl.IDLKind.Method, ["DefaultExport", "Optional", "DtsTag", "DtsName", "Throws", "Deprecated", "IndexSignature", "Protected", "Documentation", "CallSignature", "TypeParameters"]],
        [idl.IDLKind.Callable, ["DefaultExport", "CallSignature", "Deprecated", "Documentation", "CallSignature"]],
        [idl.IDLKind.Typedef, ["DefaultExport", "Deprecated", "Import", "Documentation", "TypeParameters"]],
        [idl.IDLKind.Enum, ["DefaultExport", "Deprecated", "Documentation"]],
        [idl.IDLKind.EnumMember, ["OriginalEnumMemberName", "Deprecated", "Documentation"]],
        [idl.IDLKind.Constructor, ["Deprecated", "Documentation"]]
    ]),
    checkEnumsConsistency: true,
    checkReferencesResolved: false,
}

interface IDLLinterContextRecord {
    typeParameters: Set<string>
}
class IDLLinterContext {

    private stack: IDLLinterContextRecord[] = [{ typeParameters: new Set() }]
    constructor() {}

    enter(rec:IDLLinterContextRecord) {
        this.stack.push(rec)
    }
    leave() {
        this.stack.pop()
    }
    get current() {
        return this.stack.at(-1)!
    }

    hasTypeParameter(name:string) {
        for (let i = this.stack.length - 1; i >= 0; --i) {
            if (this.stack[i].typeParameters.has(name)) {
                return true
            }
        }
        return false
    }
}

function parseTypeParameter(param:string): string {
    const extendsIdx = param.indexOf('extends')
    if (extendsIdx !== -1) {
        return param.substring(0, extendsIdx).trim()
    }
    const eqIdx = param.indexOf('=')
    if (eqIdx !== -1) {
        return param.substring(0, eqIdx).trim()
    }
    return param
}

export class IDLLinter {

    protected context = new IDLLinterContext()
    public diagnostics: IDLLinterDiagnosticsSummary[] = []

    constructor(
        protected file: idl.IDLFile,
        protected resolver: ReferenceResolver,
        protected options: IDLLinterOptions
    ) {}

    ///


    public visit(): IDLLinterDiagnosticsSummary[] {
        idl.forEachChild(this.file, node => {
            const leave = this.enter(node)
            this.check(node)
            return leave
        })
        return this.diagnostics
    }

    protected enter(node:idl.IDLNode): (() => void) | undefined {
        if (idl.isInterface(node) || idl.isTypedef(node) || idl.isMethod(node) || idl.isCallable(node) || idl.isCallback(node)) {
            this.context.enter({ typeParameters: new Set(node.typeParameters?.map(parseTypeParameter) ?? []) })
            return () => this.context.leave()
        }
    }

    protected check(node: idl.IDLNode) {
        if (idl.isEntry(node)) {
            this.checkValidAttributes(node, this.options.validEntryAttributes.get(node.kind) ?? [])
        }
        if (this.options.checkEnumsConsistency && idl.isEnum(node)) {
            this.checkEnumConsistency(node)
        }
        if (this.options.checkReferencesResolved && idl.isReferenceType(node)) {
            this.checkReferenceResolved(node)
        }
    }

    protected checkValidAttributes(entry: idl.IDLEntry, validAttributes: string[]): void {
        if (!entry.extendedAttributes)
            return
        for (const attr of entry.extendedAttributes) {
            if (!validAttributes.includes(attr.name)) {
                const file = idl.getFileFor(entry)
                this.diagnostics.push({
                    code: IDLValidationDiagnosticsCode.INVALID_EXTENDED_ATTRIBUTE,
                    file: file?.fileName ?? '',
                    message: `Invalid attribute '${attr.name}'`,
                    position: [0, attr.name.length]
                })
            }
        }
    }

    protected checkEnumConsistency(entry: idl.IDLEnum): void {
        const fileName = idl.getFileFor(entry)?.fileName
        let hasNumber = false
        let hasString = false
        for (const element of entry.elements) {
            if (typeof element.initializer === 'string')
                hasString = true
            if (typeof element.initializer === 'number')
                hasNumber = true
        }
        if (hasNumber && hasString) {
            this.diagnostics.push({
                code: IDLValidationDiagnosticsCode.ENUM_IS_NOT_CONSISTENT,
                file: fileName ?? '',
                message: "Enum includes both string and number values",
                position: [0, 0]
            })
        }
    }

    protected checkReferenceResolved(reference: idl.IDLReferenceType): void {
        if (this.context.hasTypeParameter(reference.name)) {
            return
        }
        const resolved = this.resolver.resolveTypeReference(reference)
        if (resolved === undefined) {
            const parentFile = idl.getFileFor(reference)!
            const parentNamespace = idl.fetchNamespaceFrom(reference)
            const scopeName = parentNamespace ? idl.getFQName(parentNamespace) : idl.getPackageName(parentFile)
            this.diagnostics.push({
                code: IDLValidationDiagnosticsCode.REFERENCE_IS_NOT_RESOLVED,
                file: parentFile.fileName ?? '',
                message: `Can not resolve reference '${reference.name}' defined in scope '${scopeName}'`,
                position: [0, 0]
            })
        }
    }
}

interface IDLLinterDiagnosticsSummary {
    code: IDLValidationDiagnosticsCode
    message: string
    file: string
    position: [number, number]
}

export class IDLLinterError extends Error {
    constructor(
        message:string,
        public size: number
    ) {
        super(message)
    }
}

function printErrors(errors:IDLLinterDiagnosticsSummary[], text:string) {
    errors.sort((a, b) => a.position[0] - b.position[0])
    let ptr = 0
    let lines = 1
    let cols = 1
    return errors.map(error => {
        while (ptr < error.position[0] && ptr < text.length) {
            if (text[ptr] === '\n') {
                ++lines
                cols = 0
            }
            ++cols
            ++ptr
        }
        return `E: IDL${error.code} ${IDLValidationErrorDescription.en_EN[error.code]} -- ${error.file}:${lines}:${cols} ${error.message}`
    }).join(EOL)
}

function printErrorWithoutLocation(errors:IDLLinterDiagnosticsSummary[]) {
    errors.sort((a, b) => a.position[0] - b.position[0])
    return errors.map(error => {
        return `E: IDL${error.code} ${IDLValidationErrorDescription.en_EN[error.code]} -- ${error.file} ${error.message}`
    }).join(EOL)
}

function prettyPrintErrors(errors:IDLLinterDiagnosticsSummary[], text:string) {
    errors.sort((a, b) => a.position[0] - b.position[0])

    const windowSize = 2
    let window: string[] = []

    let ptr = 0
    let lines = 1
    let cols = 1

    let lineBuffer = ''
    return errors.map(error => {
        while (ptr < error.position[0] && ptr < text.length) {
            if (text[ptr] === '\n') {
                ++lines
                cols = 0
                while (window.length > windowSize) {
                    window.shift()
                }
                window.push(lineBuffer)
                lineBuffer = ''
            } else {
                if (text[ptr] !== '\r') {
                    lineBuffer += text[ptr]
                }
            }

            ++cols
            ++ptr
        }

        let currentLine = lineBuffer
        let ii = ptr
        while (ii < text.length && text[ii] !== '\n') {
            currentLine += text[ii]
            ++ii
        }

        let errorLine = '       '
        ii = 0
        while (ii < cols) {
            errorLine += ' '
            ++ii
        }
        ii = 0
        errorLine += '\x1b[31m'
        while (ii < error.position[1]) {
            errorLine += '~'
            ++ii
        }
        errorLine += '\x1b[0m'

        const windowLines = [...window, currentLine].map((line, i) => {
            const idx = (lines - (window.length - i))
            const idxText = idx.toString().padStart(5, ' ')
            const idxColored = idx === lines
                ? '\x1b[1m' + idxText + '\x1b[0m'
                : idxText
            return idxColored + ' | ' + line
        })

        let errorText = ''
            + windowLines.join(EOL)
            + EOL + errorLine + EOL
            + `\x1b[31mERROR\x1b[0m: IDL${error.code} ${IDLValidationErrorDescription.en_EN[error.code]}`
            + EOL + `${error.file}:${lines}:${cols} ${error.message}`
            + EOL
        return errorText
    }).join(EOL)
}

export function verifyIDLLinter(file: idl.IDLFile, resolver: ReferenceResolver, options: IDLLinterOptions): true {
    const result = new IDLLinter(file, resolver, options).visit()
    if (result.length) {
        const errorMessage = printErrorWithoutLocation(result)
        throw new IDLLinterError(errorMessage, result.length)
    }
    return true
}