package net.sergeych.toread.fb2

internal data class XmlElement(
    val name: String,
    val attributes: Map<String, String>,
    val nodes: List<XmlNode>,
) {
    val localName: String = name.substringAfter(':')

    fun children(localName: String): List<XmlElement> =
        nodes.filterIsInstance<XmlNode.ElementNode>().map { it.element }.filter { it.localName == localName }

    fun first(localName: String): XmlElement? = children(localName).firstOrNull()

    fun firstText(localName: String): String? = first(localName)?.text()?.ifBlank { null }

    fun text(): String = buildString {
        nodes.forEach { node ->
            when (node) {
                is XmlNode.ElementNode -> append(node.element.text())
                is XmlNode.TextNode -> append(node.text)
            }
        }
    }.trim()
}

internal sealed interface XmlNode {
    data class ElementNode(val element: XmlElement) : XmlNode
    data class TextNode(val text: String) : XmlNode
}

internal object SimpleXml {
    fun parse(xml: String): XmlElement = Parser(xml).parse()

    private class Parser(private val xml: String) {
        private var index = 0

        fun parse(): XmlElement {
            skipBom()
            skipMisc()
            val root = readElement()
            skipMisc()
            if (index < xml.length) {
                throw error("Unexpected content after root element")
            }
            return root
        }

        private fun readElement(): XmlElement {
            expect('<')
            if (peek() == '/') throw error("Unexpected closing tag")
            if (startsWith("!--")) return skipCommentAndReadNext()
            if (startsWith("?")) {
                skipProcessingInstruction()
                skipMisc()
                return readElement()
            }

            val name = readName()
            val attributes = linkedMapOf<String, String>()
            while (true) {
                skipWhitespace()
                when {
                    startsWith("/>") -> {
                        index += 2
                        return XmlElement(name, attributes, emptyList())
                    }
                    startsWith(">") -> {
                        index++
                        break
                    }
                    else -> {
                        val attributeName = readName()
                        skipWhitespace()
                        expect('=')
                        skipWhitespace()
                        attributes[attributeName] = readAttributeValue()
                    }
                }
            }

            val nodes = mutableListOf<XmlNode>()
            while (index < xml.length) {
                when {
                    startsWith("</") -> {
                        index += 2
                        val closingName = readName()
                        if (closingName != name) throw error("Expected closing tag for $name, got $closingName")
                        skipWhitespace()
                        expect('>')
                        return XmlElement(name, attributes, nodes)
                    }
                    startsWith("<!--") -> skipComment()
                    startsWith("<![CDATA[") -> nodes += XmlNode.TextNode(readCData())
                    startsWith("<?") -> skipProcessingInstruction()
                    peek() == '<' -> nodes += XmlNode.ElementNode(readElement())
                    else -> nodes += XmlNode.TextNode(decodeEntities(readText()))
                }
            }
            throw error("Unclosed tag $name")
        }

        private fun skipCommentAndReadNext(): XmlElement {
            index--
            skipComment()
            skipMisc()
            return readElement()
        }

        private fun readName(): String {
            val start = index
            while (index < xml.length) {
                val char = xml[index]
                if (char.isWhitespace() || char == '/' || char == '>' || char == '=') break
                index++
            }
            if (start == index) throw error("Expected XML name")
            return xml.substring(start, index)
        }

        private fun readAttributeValue(): String {
            val quote = peek()
            if (quote != '"' && quote != '\'') throw error("Expected quoted attribute value")
            index++
            val start = index
            while (index < xml.length && xml[index] != quote) index++
            if (index >= xml.length) throw error("Unclosed attribute value")
            val value = xml.substring(start, index)
            index++
            return decodeEntities(value)
        }

        private fun readText(): String {
            val start = index
            while (index < xml.length && xml[index] != '<') index++
            return xml.substring(start, index)
        }

        private fun readCData(): String {
            index += "<![CDATA[".length
            val end = xml.indexOf("]]>", index)
            if (end < 0) throw error("Unclosed CDATA section")
            val value = xml.substring(index, end)
            index = end + 3
            return value
        }

        private fun skipMisc() {
            while (true) {
                skipWhitespace()
                when {
                    startsWith("<?") -> skipProcessingInstruction()
                    startsWith("<!--") -> skipComment()
                    else -> return
                }
            }
        }

        private fun skipProcessingInstruction() {
            val end = xml.indexOf("?>", index)
            if (end < 0) throw error("Unclosed processing instruction")
            index = end + 2
        }

        private fun skipComment() {
            val end = xml.indexOf("-->", index)
            if (end < 0) throw error("Unclosed XML comment")
            index = end + 3
        }

        private fun skipWhitespace() {
            while (index < xml.length && xml[index].isWhitespace()) index++
        }

        private fun skipBom() {
            if (xml.startsWith("\uFEFF")) index = 1
        }

        private fun startsWith(value: String): Boolean = xml.startsWith(value, index)

        private fun peek(): Char {
            if (index >= xml.length) throw error("Unexpected end of XML")
            return xml[index]
        }

        private fun expect(char: Char) {
            if (peek() != char) throw error("Expected '$char'")
            index++
        }

        private fun error(message: String): Fb2ParseException =
            Fb2ParseException("$message at offset $index")
    }
}

private fun decodeEntities(value: String): String {
    if ('&' !in value) return value
    return buildString(value.length) {
        var index = 0
        while (index < value.length) {
            val char = value[index]
            if (char != '&') {
                append(char)
                index++
                continue
            }
            val end = value.indexOf(';', index + 1)
            if (end < 0) {
                append(char)
                index++
                continue
            }
            val entity = value.substring(index + 1, end)
            append(
                when {
                    entity == "lt" -> '<'
                    entity == "gt" -> '>'
                    entity == "amp" -> '&'
                    entity == "quot" -> '"'
                    entity == "apos" -> '\''
                    entity.startsWith("#x") -> entity.drop(2).toIntOrNull(16)?.toChar() ?: '&'
                    entity.startsWith("#") -> entity.drop(1).toIntOrNull()?.toChar() ?: '&'
                    else -> '&'
                }
            )
            if (entity.startsWith("#").not() && entity !in setOf("lt", "gt", "amp", "quot", "apos")) {
                append(entity)
                append(';')
            }
            index = end + 1
        }
    }
}
