/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ImportsCollector } from "@idlizer/core";
import { D, Hs, IdentityTransformer, lw, LWKind, std, T, utils } from "@idlizer/ost";
import { mapFileName, mergeStructs } from "./utils.js";

export function postprocess(decls: lw.LWDeclaration[]): lw.LWDeclaration[] {
    decls = mergeNamespaces(decls)
    decls = mergeStructs(decls)
    return decls
}

function mergeNamespaces(decls: lw.LWDeclaration[]): lw.LWDeclaration[] {
    const index = new Map<string, lw.NamespaceDeclaration[]>()
    const others: lw.LWDeclaration[] = []
    decls.forEach(decl => {
        if (decl.kind !== lw.LWKind.NamespaceDeclaration) {
            others.push(decl)
            return
        }
        if (!index.has(decl.name)) {
            index.set(decl.name, [])
        }
        index.get(decl.name)?.push(decl)
    })

    const result: lw.LWDeclaration[] = others
    index.forEach((records, name) => {
        if (records.length === 0) {
            return
        }
        if (records.length === 1) {
            result.push(records[0])
            return
        }
        result.push(D.ns(name, mergeNamespaces(records.map(r => r.members).flat())))
    })
    return result
}

/////////////////////////////////////////////////////

interface ResultFile {
    moduleLikeImports: ImportsCollector
    body: lw.LWDeclaration[]
}

export type InitImports = () => ImportsCollector

/**
 * Hook that is invoked when an unknown name is encountered.
 * Returns:
 *   `result` is what should replace `name` in generated code.
 *   `name`, `source`, and `alias` form an import statement: `import { name as alias } from 'source'`
 */
export type OnUnknownImport = (name: string) => { result: string, name: string, source: string, alias?: string } | undefined

class RefSearcher extends IdentityTransformer {
    private seenNames: Map<string, string[]>
    private localNames: Set<string>[] = []
    constructor(
        private decls: lw.LWDeclaration[],
        private fileName: string,
        private registry: Map<string, string>,
        private imports: ImportsCollector,
        private knownImports?: Map<string, string>,
        private onUnknownImport?: OnUnknownImport,
    ) {
        super()
        this.seenNames = new Map(decls.map(it => [it.name, ['.']]))
    }

    private nsStack: string[] = []
    private trimNs(name:string): string {
        const prefix = this.nsStack.join('.') + '.'
        if (name.startsWith(prefix)) {
            return name.substring(prefix.length)
        }
        return name
    }
    goNamespaceDeclaration(decl: lw.NamespaceDeclaration): lw.NamespaceDeclaration {
        this.nsStack.push(decl.name)
        const r = super.goNamespaceDeclaration(decl)
        this.nsStack.pop()
        return r
    }

    private getBase(name:string) {
        return name.split('.').at(0)!
    }
    private mapToPackage(name:string): string {
        if (!this.knownImports) {
            return name
        }
        if (this.knownImports.has(name)) {
            return this.knownImports.get(name)!
        }
        return name
    }

    private isLocalName(name:string) {
        return !!this.localNames.find(s => s.has(name))
    }

    private goName(name: string): string {
        if (name.startsWith('@'))
            return name
        const record = this.registry.get(name)
        if (record) {
            let val = name
            if (val.startsWith(record)) {
                val = val.substring(record.length)
                while (val.startsWith('.')) {
                    val = val.substring(1)
                }
            }
            if (record === this.fileName)
                return this.trimNs(val)
            const baseName = this.getBase(val);
            const source = this.mapToPackage(mapFileName(record))
            const conflictingNames = this.seenNames.get(baseName)
            if (conflictingNames) {
                const alias = source.replace('.', '_') + '_' + baseName
                if (!conflictingNames.includes(source)) {
                    conflictingNames.push(source)
                    this.imports.addFeature(baseName, source, alias)
                }
                return this.trimNs(conflictingNames[0] === source ? val : alias)
            } else {
                this.seenNames.set(baseName, [source])
                this.imports.addFeature(baseName, source)
                return this.trimNs(val)
            }
        } else {
            const mapped = this.onUnknownImport?.(name)
            if (mapped) {
                this.imports.addFeature(mapped.name, mapped.source)
                return this.trimNs(mapped.result)
            }
        }
        return this.trimNs(name)
    }
    override goValueType(type: lw.ValueType): lw.LWType {
        return type.name.startsWith('@')
            ? super.goValueType(type)
            : T.c(this.goName(type.name), ...type.args.map(t => this.goType(t)))
    }
    override goConstructorExpression(expr: lw.ConstructorExpression): lw.ConstructorExpression {
        expr = super.goConstructorExpression(expr)
        if ('name' in expr.data) {
            expr.data.name = this.goName(expr.data.name)
        }
        return expr
    }
    override goVariableExpression(expr: lw.VariableExpression): lw.VariableExpression {
        expr = super.goVariableExpression(expr) as lw.VariableExpression
        if (utils.hasHint(expr, std.names.hints.isType))
            expr.name = this.goName(expr.name)
        if (!this.isLocalName(expr.name))
            expr.name = this.goName(expr.name)
        return expr
    }
    override goFunctionDeclaration(decl: lw.FunctionDeclaration): lw.FunctionDeclaration {
        const store = new Set<string>()
        decl.parameters.forEach(param => {
            store.add(param.name)
        })
        store.add(decl.name.split('.').at(-1)!)
        this.localNames.push(store)
        const r = super.goFunctionDeclaration(decl)
        this.localNames.pop()
        return r
    }
    override goLambdaExpression(expr: lw.LambdaExpression): lw.LambdaExpression {
        const store = new Set<string>()
        expr.parameters.forEach(param => {
            store.add(param.name)
        })
        this.localNames.push(store)
        const r = super.goLambdaExpression(expr)
        this.localNames.pop()
        return r
    }
    override goCompoundStatement(stmt: lw.CompoundStatement): lw.CompoundStatement {
        const store = new Set<string>()
        this.localNames.push(store)
        const r = super.goCompoundStatement(stmt)
        this.localNames.pop()
        return r
    }
    override goDeclarationStatement(stmt: lw.DeclarationStatement): lw.DeclarationStatement {
        this.localNames.at(-1)?.add(stmt.varName)
        return super.goDeclarationStatement(stmt)
    }
    go(): lw.LWDeclaration[] {
        return this.decls.map(it => this.goDeclaration(it))
    }
}

function putToNs(declarations:lw.LWDeclaration[], defaultNamespace?: string): lw.LWDeclaration[] {
    const index = new Map<string, lw.LWDeclaration[]>()
    const result: lw.LWDeclaration[] = []
    declarations.forEach(decl => {
        const clause = decl.name.split('.')
        if (clause.length === 1) {
            result.push(decl)
            return
        }
        const [base, ...rest] = clause
        if (!index.has(base)) {
            index.set(base, [])
        }
        // Improve: clone!!!
        decl.name = rest.join('.')
        index.get(base)?.push(decl)
    })

    index.forEach((decls, name) => {
        const nsHints = name == defaultNamespace ? [Hs.asDefault()] : []
        result.push(D.ns(name, putToNs(decls), nsHints))
    })
    return result
}

interface FormFilesOptions {
    knownReference:Map<string, string>
    defaultNamespaces: Map<string, string>
    knownImports: Map<string, string>
    defaultImports?: InitImports
    onUnknownImport?: OnUnknownImport
}

export function formFiles(knownPackages: Set<string>, declarations: lw.LWDeclaration[], options?:FormFilesOptions): Map<string, ResultFile> {

    // form files
    const files = new Map<string, lw.LWDeclaration[]>()
    const refIndex = new Map<string, string>()
    if (options?.knownReference) {
        options.knownReference.forEach((val, key) => {
            refIndex.set(key, val)
        })
    }
    declarations.forEach(decl => {
        const chunks = decl.name.split('.')
        const clause: string[] = []
        while (chunks.length) {
            clause.push(chunks.shift()!)
            const prefix = clause.join('.')
            if (knownPackages.has(prefix)) {
                if (!files.has(prefix)) {
                    files.set(prefix, [])
                }
                // Improve: clone!!!!
                refIndex.set(decl.name, prefix)
                decl.name = chunks.join('.')
                files.get(prefix)?.push(decl)
                return
            }
        }

        if (!files.has('other')) {
            files.set('other', [])
        }
        files.get('other')?.push(decl)
        refIndex.set(decl.name, 'other')
    })

    // do namespace stuff
    const nsFiles = new Map<string, ResultFile>()
    files.forEach((decls, fileName) => {
        const imports = options?.defaultImports?.() ?? new ImportsCollector()
        const defaultNamespace = options?.defaultNamespaces.get(fileName)
        const nsDecls = new RefSearcher(putToNs(decls, defaultNamespace), fileName, refIndex, imports, options?.knownImports, options?.onUnknownImport).go()
        nsFiles.set(fileName, {
            moduleLikeImports: imports,
            body: nsDecls
        })
    })

    return nsFiles
}