/*
 * 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 stdx.syntax

import std.collection.ArrayList
import std.fs.*
import std.sort.*

class Parser {
    private let impl: ParserImpl
    private let filePath: String

    private init(filePath: String) {
        impl = ParserImpl(filePath)
        this.filePath = filePath
    }

    static func parseFile(filePath: String): (?SourceFile, Array<Diagnostic>) {
        let fileInfo = try {
            let path = canonicalize(filePath)
            FileInfo(path)
        } catch (e: Exception) {
            throw Exception("IOException: ${e.message}")
        }
        if (fileInfo.path.extensionName != "cj") {
            throw Exception("IOException: The file extension of the source file must be 'cj'.")
        } else if (!fileInfo.canRead()) {
            throw Exception("IOException: The file is not readable.")
        } else if (!fileInfo.isRegular()) {
            throw Exception("IOException: The path ${fileInfo.path.toString()} is not a file!")
        }
        let (node, diags) = Parser(fileInfo.path.toString()).parse()
        let file: ?SourceFile = if (let Some(root) <- node) {
            root as SourceFile
        } else {
            None
        }
        (file, diags)
    }

    static func parseText(text: String): (?SyntaxTreeNode, Array<Diagnostic>) {
        let (node, diags) = Parser("").parseFromText(text)
        let res: ?SyntaxTreeNode = if (let Some(root) <- node) {
            root as SyntaxTreeNode
        } else {
            None
        }
        (res, diags)
    }

    static func parseTokens(tokens: Tokens, refreshPos: Bool): (?SyntaxTreeNode, Array<Diagnostic>) {
        let (node, diags) = Parser("").parseFromTokens(tokens, refreshPos)
        let res: ?SyntaxTreeNode = if (let Some(root) <- node) {
            root as SyntaxTreeNode
        } else {
            None
        }
        (res, diags)
    }

    static func parsePackage(dirPath: String): (?Package, Array<Diagnostic>) {
        let fileList = ArrayList<SourceFile>()
        let fileImpls = ArrayList<(SyntaxNodeImpl, String)>()
        let diagsList = ArrayList<Diagnostic>()
        var parseFailed = false
        var pkg: ?Package = None
        let fileInfos = try {
            let path: Path = Path(dirPath)
            var fileInfos = Directory.readFrom(dirPath)
            let comparator = {l: FileInfo, r: FileInfo => l.name.compare(r.name)}
            sort(fileInfos, by: comparator)
            fileInfos
        } catch (e: Exception) {
            throw Exception("IOException: ${e.message}")
        }
        for (fileInfo in fileInfos) {
            if (fileInfo.path.extensionName != "cj") {
                continue
            }
            var (file, diags) = parseFile(fileInfo.path.toString())
            diagsList.add(all: diags)
            if (parseFailed) {
                continue
            }
            if (let Some(f) <- file) {
                fileList.add(f)
                fileImpls.add((f.nodeImpl, fileInfo.path.toString()))
            } else {
                parseFailed = true
            }
        }

        if (!parseFailed) {
            let (isMacro, name) = checkPackage(fileList)
            pkg = Package(isMacro, name, fileImpls.toArray())
        }
        (pkg, diagsList.toArray())
    }

    private func parse(): (?SyntaxTreeNode, Array<Diagnostic>) {
        let (root, diags) = impl.parseFile()
        (buildSyntaxTree(root), diags)
    }

    private func parseFromText(text: String): (?SyntaxTreeNode, Array<Diagnostic>) {
        let (root, diags) = impl.parseText(text)
        (buildSyntaxTree(root), diags)
    }

    private func parseFromTokens(tokens: Tokens, refreshPos: Bool): (?SyntaxTreeNode, Array<Diagnostic>) {
        let (root, diags) = impl.parseTokens(tokens, refreshPos)
        if (!refreshPos) {
            var startIndex = 0
            while (startIndex < tokens.size && tokens.get(startIndex).kind == TokenKind.NL) {
                startIndex++
            }
            let startPos = CodePosition(tokens.get(startIndex).pos.line, tokens.get(startIndex).pos.column,
                FileInfos("", ""))
            return (buildSyntaxTree(root, startPos: startPos), diags)
        }
        (buildSyntaxTree(root), diags)
    }

    // build syntax tree by syntax impl tree
    private func buildSyntaxTree(node: ?SyntaxNodeImpl, startPos!: ?CodePosition = None): ?SyntaxTreeNode {
        if (let Some(root) <- node) {
            if (filePath.size > 0) { // called from parseFile
                SyntaxNodeImplTranslator.translateFile(root, filePath)
            } else if (let Some(stPos) <- startPos) { // called from parseTokens with refreshPos is false
                SyntaxNodeImplTranslator.translate(root, stPos, Option<SyntaxTreeNode>.None)
            } else { // called from parseText or parseTokens
                SyntaxNodeImplTranslator.translate(root, CodePosition(1, 1, FileInfos("", "")),
                    Option<SyntaxTreeNode>.None)
            }
        } else {
            None
        }
    }
}

func getPackageName(file: SourceFile) {
    if (let Some(pkgHeader) <- file.pkgHeader) {
        return String.join(pkgHeader.packageNameIdentifiers, delimiter: ".")
    }
    return "default"
}

func checkPackage(sourceFiles: ArrayList<SourceFile>): (Bool, String) {
    if (sourceFiles.size == 0) {
        return (false, "")
    }
    var packageName = getPackageName(sourceFiles[0])
    var isMacro: Bool = sourceFiles[0].isMacroPackage
    for (f in sourceFiles) {
        if (packageName != getPackageName(f)) {
            throw Exception("ParseException: found more than one package declaration for the package.")
        }
        if (isMacro != f.isMacroPackage) {
            throw Exception("ParseException: found different macro package declaration.")
        }
    }
    return (isMacro, packageName)
}