/*
 * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
 *
 * 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.
 *
 */

/*
 * Minimal, optional AST-like structures captured during parsing.
 *
 * These classes are lightweight and focused on editor features:
 * - highlighting (precise ranges for ids/types)
 * - documentation (raw doc text + summary)
 * - basic autocomplete (visible names per scope)
 *
 * They are populated only when an optional MiniAstSink is provided to the compiler.
 */

package net.sergeych.lyng.miniast

import net.sergeych.lyng.Pos

// Ranges reuse existing Pos to stay consistent with compiler diagnostics
data class MiniRange(val start: Pos, val end: Pos)

// Simple documentation payload: raw text and a derived summary (first non-empty line)
data class MiniDoc(
    val range: MiniRange,
    val raw: String,
    val summary: String?,
    val tags: Map<String, List<String>> = emptyMap()
) {
    companion object {
        fun parse(range: MiniRange, lines: Iterable<String>, extraTags: Map<String, List<String>> = emptyMap()): MiniDoc {
            val parsedTags = mutableMapOf<String, MutableList<String>>()
            var currentTag: String? = null
            val currentContent = mutableListOf<String>()

            fun flush() {
                currentTag?.let { tag ->
                    val trimmedContent = if (currentContent.size > 1) {
                        // First line is common content, we want to trim indent from subsequent lines
                        // based on THEIR common indent.
                        val firstLine = currentContent[0]
                        val remaining = currentContent.drop(1).joinToString("\n").trimIndent()
                        if (remaining.isEmpty()) firstLine else firstLine + "\n" + remaining
                    } else {
                        currentContent.joinToString("\n")
                    }
                    parsedTags.getOrPut(tag) { mutableListOf() }.add(trimmedContent.trim())
                }
                currentContent.clear()
            }

            val descriptionLines = mutableListOf<String>()
            var inTags = false

            for (rawLine in lines) {
                for (line in rawLine.lines()) {
                    // Strip leading '*' and space if present (standard doc comment style)
                    val cleanedLine = line.trim().let {
                        if (it.startsWith("*")) it.substring(1).removePrefix(" ") else line
                    }
                    val trimmed = cleanedLine.trim()
                    if (trimmed.startsWith("@")) {
                        inTags = true
                        flush()
                        val parts = trimmed.substring(1).split(Regex("\\s+"), 2)
                        currentTag = parts[0]
                        val tagFirstLine = parts.getOrNull(1) ?: ""
                        if (tagFirstLine.isNotEmpty()) {
                            // Find where the content starts in the cleaned line to preserve relative indentation
                            val contentIndex = cleanedLine.indexOf(tagFirstLine)
                            if (contentIndex >= 0) {
                                currentContent.add(cleanedLine.substring(contentIndex))
                            } else {
                                currentContent.add(tagFirstLine)
                            }
                        }
                    } else {
                        if (inTags) {
                            currentContent.add(cleanedLine)
                        } else {
                            descriptionLines.add(cleanedLine)
                        }
                    }
                }
            }
            flush()

            val raw = descriptionLines.joinToString("\n").trimIndent().trim()
            val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim()

            val finalTags = parsedTags.toMutableMap()
            extraTags.forEach { (k, v) ->
                finalTags.getOrPut(k) { mutableListOf() }.addAll(v)
            }

            return MiniDoc(range, raw, summary, finalTags)
        }
    }
}

sealed interface MiniNode { val range: MiniRange }

// Identifier roles we can confidently assign at parse time without binding
enum class IdRole {
    DeclFun, DeclClass, DeclVal, DeclVar, Param, Label, TypeName, Ref
}

// Type references (syntax-level, position rich)
sealed interface MiniTypeRef : MiniNode

data class MiniTypeName(
    override val range: MiniRange,
    val segments: List<Segment>,
    val nullable: Boolean
) : MiniTypeRef {
    data class Segment(val name: String, val range: MiniRange)
}

data class MiniGenericType(
    override val range: MiniRange,
    val base: MiniTypeRef,
    val args: List<MiniTypeRef>,
    val nullable: Boolean
) : MiniTypeRef

data class MiniFunctionType(
    override val range: MiniRange,
    val receiver: MiniTypeRef?,
    val contextReceivers: List<MiniTypeRef>,
    val params: List<MiniTypeRef>,
    val returnType: MiniTypeRef,
    val nullable: Boolean
) : MiniTypeRef

data class MiniTypeVar(
    override val range: MiniRange,
    val name: String,
    val nullable: Boolean
) : MiniTypeRef

data class MiniTypeUnion(
    override val range: MiniRange,
    val options: List<MiniTypeRef>,
    val nullable: Boolean
) : MiniTypeRef

data class MiniTypeIntersection(
    override val range: MiniRange,
    val options: List<MiniTypeRef>,
    val nullable: Boolean
) : MiniTypeRef

// Script and declarations (lean subset; can be extended later)
sealed interface MiniNamedDecl : MiniNode {
    val name: String
    val doc: MiniDoc?
    // Start position of the declaration name identifier in source; end can be derived as start + name.length
    val nameStart: Pos
    val isExtern: Boolean
    val isStatic: Boolean
}

sealed interface MiniDecl : MiniNamedDecl

data class MiniScript(
    override val range: MiniRange,
    val declarations: MutableList<MiniDecl> = mutableListOf(),
    val imports: MutableList<MiniImport> = mutableListOf(),
    val statements: MutableList<MiniStmt> = mutableListOf()
) : MiniNode

data class MiniParam(
    val name: String,
    val type: MiniTypeRef?,
    // Start position of parameter name in source
    val nameStart: Pos
)

data class MiniFunDecl(
    override val range: MiniRange,
    override val name: String,
    val params: List<MiniParam>,
    val returnType: MiniTypeRef?,
    val body: MiniBlock?,
    override val doc: MiniDoc?,
    override val nameStart: Pos,
    val receiver: MiniTypeRef? = null,
    override val isExtern: Boolean = false,
    override val isStatic: Boolean = false,
) : MiniDecl

data class MiniValDecl(
    override val range: MiniRange,
    override val name: String,
    val mutable: Boolean,
    val type: MiniTypeRef?,
    val initRange: MiniRange?,
    override val doc: MiniDoc?,
    override val nameStart: Pos,
    val receiver: MiniTypeRef? = null,
    override val isExtern: Boolean = false,
    override val isStatic: Boolean = false,
) : MiniDecl

data class MiniClassDecl(
    override val range: MiniRange,
    override val name: String,
    val bases: List<String>,
    val bodyRange: MiniRange?,
    val ctorFields: List<MiniCtorField> = emptyList(),
    val classFields: List<MiniCtorField> = emptyList(),
    override val doc: MiniDoc?,
    override val nameStart: Pos,
    // Built-in extension: list of member declarations (functions and fields)
    val members: List<MiniMemberDecl> = emptyList(),
    override val isExtern: Boolean = false,
    override val isStatic: Boolean = false,
    val isObject: Boolean = false
) : MiniDecl

data class MiniEnumDecl(
    override val range: MiniRange,
    override val name: String,
    val entries: List<String>,
    override val doc: MiniDoc?,
    override val nameStart: Pos,
    override val isExtern: Boolean = false,
    override val isStatic: Boolean = false,
    val entryPositions: List<Pos> = emptyList()
) : MiniDecl

data class MiniTypeAliasDecl(
    override val range: MiniRange,
    override val name: String,
    val typeParams: List<String>,
    val target: MiniTypeRef?,
    override val doc: MiniDoc?,
    override val nameStart: Pos,
    override val isExtern: Boolean = false,
    override val isStatic: Boolean = false,
) : MiniDecl

data class MiniCtorField(
    val name: String,
    val mutable: Boolean,
    val type: MiniTypeRef?,
    val nameStart: Pos
)

sealed interface MiniStmt : MiniNode

data class MiniBlock(
    override val range: MiniRange,
    val locals: MutableList<String> = mutableListOf()
) : MiniStmt

data class MiniIdentifier(
    override val range: MiniRange,
    val name: String,
    val role: IdRole
) : MiniNode

// --- Class member declarations (for built-in/registry docs) ---
sealed interface MiniMemberDecl : MiniNamedDecl {
    override val isStatic: Boolean
}

data class MiniMemberFunDecl(
    override val range: MiniRange,
    override val name: String,
    val params: List<MiniParam>,
    val returnType: MiniTypeRef?,
    override val doc: MiniDoc?,
    override val nameStart: Pos,
    override val isStatic: Boolean = false,
    override val isExtern: Boolean = false,
    val body: MiniBlock? = null
) : MiniMemberDecl

data class MiniMemberValDecl(
    override val range: MiniRange,
    override val name: String,
    val mutable: Boolean,
    val type: MiniTypeRef?,
    val initRange: MiniRange?,
    override val doc: MiniDoc?,
    override val nameStart: Pos,
    override val isStatic: Boolean = false,
    override val isExtern: Boolean = false,
) : MiniMemberDecl

data class MiniMemberTypeAliasDecl(
    override val range: MiniRange,
    override val name: String,
    val typeParams: List<String>,
    val target: MiniTypeRef?,
    override val doc: MiniDoc?,
    override val nameStart: Pos,
    override val isStatic: Boolean = false,
    override val isExtern: Boolean = false,
) : MiniMemberDecl

data class MiniInitDecl(
    override val range: MiniRange,
    override val nameStart: Pos,
) : MiniMemberDecl {
    override val name: String get() = "init"
    override val doc: MiniDoc? get() = null
    override val isStatic: Boolean get() = false
    override val isExtern: Boolean get() = false
}

// Streaming sink to collect mini-AST during parsing. Implementations may assemble a tree or process events.
interface MiniAstSink {
    fun onScriptStart(start: Pos) {}
    fun onScriptEnd(end: Pos, script: MiniScript) {}

    fun onDocCandidate(doc: MiniDoc) {}

    fun onEnterClass(node: MiniClassDecl) {}
    fun onExitClass(end: Pos) {}

    fun onEnterFunction(node: MiniFunDecl?) {}
    fun onExitFunction(end: Pos) {}

    fun onImport(node: MiniImport) {}
    fun onFunDecl(node: MiniFunDecl) {}
    fun onValDecl(node: MiniValDecl) {}
    fun onInitDecl(node: MiniInitDecl) {}
    fun onClassDecl(node: MiniClassDecl) {}
    fun onEnumDecl(node: MiniEnumDecl) {}
    fun onTypeAliasDecl(node: MiniTypeAliasDecl) {}

    fun onBlock(node: MiniBlock) {}
    fun onIdentifier(node: MiniIdentifier) {}
}

// Tracer interface used by the compiler's type parser to simultaneously build
// a MiniTypeRef (syntax-level) while the semantic TypeDecl is produced as usual.
// Keep it extremely small for now: the current parser supports only simple
// identifiers with optional nullable suffix. We can extend callbacks later for
// generics and function types without breaking callers.
interface MiniTypeTrace {
    /**
     * Report a simple (possibly qualified in the future) type name.
     * @param segments ordered list of name parts with precise ranges
     * @param nullable whether the trailing `?` was present
     */
    fun onSimpleName(segments: List<MiniTypeName.Segment>, nullable: Boolean)
}

// A simple builder that assembles a MiniScript tree from sink callbacks.
class MiniAstBuilder : MiniAstSink {
    private var currentScript: MiniScript? = null
    private val blocks = ArrayDeque<MiniBlock>()
    private val classStack = ArrayDeque<MiniClassDecl>()
    private var lastDoc: MiniDoc? = null
    private var scriptDepth: Int = 0
    private var functionDepth: Int = 0

    fun build(): MiniScript? = currentScript

    override fun onScriptStart(start: Pos) {
        if (scriptDepth == 0) {
            currentScript = MiniScript(MiniRange(start, start))
        }
        scriptDepth++
    }

    override fun onScriptEnd(end: Pos, script: MiniScript) {
        scriptDepth = (scriptDepth - 1).coerceAtLeast(0)
        if (scriptDepth == 0) {
            // finalize root range only when closing the outermost script
            currentScript = currentScript?.copy(range = MiniRange(currentScript!!.range.start, end))
        }
    }

    override fun onDocCandidate(doc: MiniDoc) {
        lastDoc = doc
    }

    override fun onEnterClass(node: MiniClassDecl) {
        val attach = node.copy(doc = node.doc ?: lastDoc)
        classStack.addLast(attach)
        lastDoc = null
    }

    override fun onExitClass(end: Pos) {
        val finished = classStack.removeLastOrNull()
        if (finished != null) {
            val updated = finished.copy(range = MiniRange(finished.range.start, end))
            // Always add to top-level for now to ensure visibility in light engine
            currentScript?.declarations?.add(updated)
        }
    }

    override fun onEnterFunction(node: MiniFunDecl?) {
        functionDepth++
    }

    override fun onExitFunction(end: Pos) {
        functionDepth--
    }

    override fun onImport(node: MiniImport) {
        currentScript?.imports?.add(node)
    }

    override fun onFunDecl(node: MiniFunDecl) {
        val attach = node.copy(doc = node.doc ?: lastDoc)
        val currentClass = classStack.lastOrNull()
        if (currentClass != null && functionDepth == 0) {
            // Convert MiniFunDecl to MiniMemberFunDecl for inclusion in members
            val member = MiniMemberFunDecl(
                range = attach.range,
                name = attach.name,
                params = attach.params,
                returnType = attach.returnType,
                doc = attach.doc,
                nameStart = attach.nameStart,
                isStatic = attach.isStatic,
                isExtern = attach.isExtern,
                body = attach.body
            )
            // Need to update the class in the stack since it's immutable-ish (data class)
            // Check if we already have this member (from a previous onFunDecl call for the same function)
            val existing = currentClass.members.filterIsInstance<MiniMemberFunDecl>().find { it.name == attach.name && it.nameStart == attach.nameStart }
            if (existing != null) {
                val members = currentClass.members.map { if (it === existing) member else it }
                classStack.removeLast()
                classStack.addLast(currentClass.copy(members = members))
            } else {
                classStack.removeLast()
                classStack.addLast(currentClass.copy(members = currentClass.members + member))
            }
        } else {
            // Check if already in declarations to avoid duplication
            val existing = currentScript?.declarations?.find { it.name == attach.name && it.nameStart == attach.nameStart }
            if (existing != null) {
                val idx = currentScript?.declarations?.indexOf(existing) ?: -1
                if (idx >= 0) currentScript?.declarations?.set(idx, attach)
            } else {
                currentScript?.declarations?.add(attach)
            }
        }
        lastDoc = null
    }

    override fun onValDecl(node: MiniValDecl) {
        val attach = node.copy(doc = node.doc ?: lastDoc)
        val currentClass = classStack.lastOrNull()
        if (currentClass != null && functionDepth == 0) {
            val member = MiniMemberValDecl(
                range = attach.range,
                name = attach.name,
                mutable = attach.mutable,
                type = attach.type,
                initRange = attach.initRange,
                doc = attach.doc,
                nameStart = attach.nameStart,
                isStatic = attach.isStatic,
                isExtern = attach.isExtern
            )
            // Duplicates for vals are rare but possible if Compiler calls it twice
            val existing = currentClass.members.filterIsInstance<MiniMemberValDecl>().find { it.name == attach.name && it.nameStart == attach.nameStart }
            if (existing != null) {
                val members = currentClass.members.map { if (it === existing) member else it }
                classStack.removeLast()
                classStack.addLast(currentClass.copy(members = members))
            } else {
                classStack.removeLast()
                classStack.addLast(currentClass.copy(members = currentClass.members + member))
            }
        } else {
            val existing = currentScript?.declarations?.find { it.name == attach.name && it.nameStart == attach.nameStart }
            if (existing != null) {
                val idx = currentScript?.declarations?.indexOf(existing) ?: -1
                if (idx >= 0) currentScript?.declarations?.set(idx, attach)
            } else {
                currentScript?.declarations?.add(attach)
            }
        }
        lastDoc = null
    }

    override fun onClassDecl(node: MiniClassDecl) {
        // This is the old way, we might want to deprecate it or make it call onEnterClass
        // For now, if we are NOT using enter/exit, keep behavior.
        // But Compiler.kt will be updated to use enter/exit.
        if (classStack.isEmpty()) {
            val attach = node.copy(doc = node.doc ?: lastDoc)
            currentScript?.declarations?.add(attach)
            lastDoc = null
        }
    }

    override fun onEnumDecl(node: MiniEnumDecl) {
        val attach = node.copy(doc = node.doc ?: lastDoc)
        currentScript?.declarations?.add(attach)
        lastDoc = null
    }

    override fun onTypeAliasDecl(node: MiniTypeAliasDecl) {
        val attach = node.copy(doc = node.doc ?: lastDoc)
        val currentClass = classStack.lastOrNull()
        if (currentClass != null && functionDepth == 0) {
            val member = MiniMemberTypeAliasDecl(
                range = attach.range,
                name = attach.name,
                typeParams = attach.typeParams,
                target = attach.target,
                doc = attach.doc,
                nameStart = attach.nameStart,
                isStatic = attach.isStatic,
                isExtern = attach.isExtern
            )
            val existing = currentClass.members.filterIsInstance<MiniMemberTypeAliasDecl>()
                .find { it.name == attach.name && it.nameStart == attach.nameStart }
            val updatedMembers = if (existing != null) {
                currentClass.members.map { if (it === existing) member else it }
            } else {
                currentClass.members + member
            }
            classStack.removeLast()
            classStack.addLast(currentClass.copy(members = updatedMembers))
        } else {
            val existing = currentScript?.declarations?.find { it.name == attach.name && it.nameStart == attach.nameStart }
            if (existing != null) {
                val idx = currentScript?.declarations?.indexOf(existing) ?: -1
                if (idx >= 0) currentScript?.declarations?.set(idx, attach)
            } else {
                currentScript?.declarations?.add(attach)
            }
        }
        lastDoc = null
    }

    override fun onBlock(node: MiniBlock) {
        blocks.addLast(node)
    }
}

// Import statement representation for highlighting and docs
data class MiniImport(
    override val range: MiniRange,
    val segments: List<Segment>
) : MiniNode {
    data class Segment(val name: String, val range: MiniRange)
}
