package net.sergeych.toread.fb2

internal object Fb2XmlMapper {
    fun fromXml(xml: String): Fb2Book {
        val root = SimpleXml.parse(xml)
        if (root.localName != "FictionBook") {
            throw Fb2ParseException("Expected FictionBook root element, got ${root.name}")
        }

        val description = root.first("description")
            ?: throw Fb2ParseException("FB2 document is missing description")
        val titleInfo = description.first("title-info")
            ?: throw Fb2ParseException("FB2 document is missing description/title-info")
        val documentInfo = description.first("document-info")

        val bodies = root.children("body")
        val body = bodies.firstOrNull { it.attributes["name"] != "notes" }
            ?: root.first("body")
        val notes = bodies
            .filter { it.attributes["name"] == "notes" }
            .flatMap { it.children("section") }
            .map(::sectionFrom)
            .mapNotNull { section -> section.id?.let { it to section } }
            .toMap()

        return Fb2Book(
            title = titleInfo.firstText("book-title")
                ?: throw Fb2ParseException("FB2 document is missing book-title"),
            authors = titleInfo.children("author").map(::authorFrom),
            language = titleInfo.firstText("lang"),
            sourceLanguage = titleInfo.firstText("src-lang"),
            genres = titleInfo.children("genre").mapNotNull { it.text().ifBlank { null } },
            annotation = titleInfo.first("annotation")?.text()?.ifBlank { null },
            keywords = titleInfo.firstText("keywords"),
            date = titleInfo.first("date")?.text()?.ifBlank { null },
            translators = titleInfo.children("translator").map(::authorFrom),
            sequences = titleInfo.children("sequence").mapNotNull(::sequenceFrom),
            coverImages = titleInfo.first("coverpage")?.children("image")?.mapNotNull(::imageRefFrom).orEmpty(),
            documentInfo = documentInfoFrom(documentInfo),
            bodyTitle = body?.first("title")?.children("p")?.mapNotNull { it.text().ifBlank { null } }.orEmpty(),
            bodyImages = body?.children("image")?.mapNotNull(::imageRefFrom).orEmpty(),
            bodyEpigraphs = body?.children("epigraph")?.map(::epigraphFrom).orEmpty(),
            sections = body?.children("section")?.map(::sectionFrom).orEmpty(),
            notes = notes,
            binaries = root.children("binary").mapNotNull(::binaryFrom),
        )
    }

    fun toXml(book: Fb2Book): String = buildString {
        appendLine("""<?xml version="1.0" encoding="UTF-8"?>""")
        append("""<FictionBook xmlns="${Fb2Format.Namespace}" xmlns:xlink="${Fb2Format.XLinkNamespace}">""")
        append("<description><title-info>")
        appendRepeated("genre", book.genres)
        val titleAuthors = book.authors.ifEmpty { listOf(Fb2Author(nickname = "Unknown")) }
        titleAuthors.forEach { appendAuthor(it) }
        appendElement("book-title", book.title)
        book.annotation?.takeIf { it.isNotBlank() }?.let { appendElement("annotation", it) }
        book.keywords?.takeIf { it.isNotBlank() }?.let { appendElement("keywords", it) }
        book.date?.takeIf { it.isNotBlank() }?.let { appendElement("date", it) }
        if (book.coverImages.isNotEmpty()) {
            append("<coverpage>")
            book.coverImages.forEach { appendImage(it) }
            append("</coverpage>")
        }
        book.language?.takeIf { it.isNotBlank() }?.let { appendElement("lang", it) }
        book.sourceLanguage?.takeIf { it.isNotBlank() }?.let { appendElement("src-lang", it) }
        book.translators.forEach { translator ->
            append("<translator>")
            appendAuthorFields(translator)
            append("</translator>")
        }
        book.sequences.forEach { sequence ->
            append("""<sequence name="${escapeAttribute(sequence.name)}"""")
            sequence.number?.takeIf { it.isNotBlank() }?.let { append(""" number="${escapeAttribute(it)}"""") }
            append("/>")
        }
        append("</title-info><document-info>")
        val documentAuthors = book.documentInfo.authors.ifEmpty { titleAuthors }
        documentAuthors.forEach { appendAuthor(it) }
        appendElement("date", book.documentInfo.date?.takeIf { it.isNotBlank() } ?: "1970-01-01")
        appendElement("id", book.documentInfo.id?.takeIf { it.isNotBlank() } ?: "toread-generated")
        appendElement("version", book.documentInfo.version?.takeIf { it.isNotBlank() } ?: "1.0")
        append("</document-info></description><body>")
        book.bodyImages.forEach { appendImage(it) }
        if (book.bodyTitle.isNotEmpty()) {
            append("<title>")
            book.bodyTitle.forEach { appendElement("p", it) }
            append("</title>")
        }
        book.sections.forEach { appendSection(it) }
        append("</body>")
        book.binaries.forEach { binary ->
            append("""<binary id="${escapeAttribute(binary.id)}" content-type="${escapeAttribute(binary.contentType)}">""")
            append(binary.base64)
            append("</binary>")
        }
        append("</FictionBook>")
    }

    private fun documentInfoFrom(element: XmlElement?): Fb2DocumentInfo = Fb2DocumentInfo(
        authors = element?.children("author")?.map(::authorFrom).orEmpty(),
        date = element?.first("date")?.text()?.ifBlank { null },
        id = element?.firstText("id"),
        version = element?.firstText("version"),
    )

    private fun authorFrom(element: XmlElement): Fb2Author = Fb2Author(
        firstName = element.firstText("first-name"),
        middleName = element.firstText("middle-name"),
        lastName = element.firstText("last-name"),
        nickname = element.firstText("nickname"),
    )

    private fun sequenceFrom(element: XmlElement): Fb2Sequence? {
        val name = element.attributes["name"]?.takeIf { it.isNotBlank() } ?: return null
        return Fb2Sequence(name = name, number = element.attributes["number"]?.takeIf { it.isNotBlank() })
    }

    private fun binaryFrom(element: XmlElement): Fb2Binary? {
        val id = element.attributes["id"]?.takeIf { it.isNotBlank() } ?: return null
        val contentType = element.attributes["content-type"]?.takeIf { it.isNotBlank() } ?: return null
        return Fb2Binary(id = id, contentType = contentType, base64 = element.text().filterNot(Char::isWhitespace))
    }

    private fun imageRefFrom(element: XmlElement): Fb2ImageRef? {
        val href = element.attributeLocal("href")?.takeIf { it.isNotBlank() } ?: return null
        return Fb2ImageRef(href = href, alt = element.attributes["alt"]?.takeIf { it.isNotBlank() })
    }

    private fun sectionFrom(element: XmlElement): Fb2Section {
        val blocks = element.nodes.mapNotNull { node ->
            val child = (node as? XmlNode.ElementNode)?.element ?: return@mapNotNull null
            when (child.localName) {
                "p" -> Fb2Block.Paragraph(textFrom(child))
                "subtitle" -> Fb2Block.Subtitle(textFrom(child))
                "epigraph" -> Fb2Block.Epigraph(epigraphFrom(child))
                "cite" -> Fb2Block.Cite(citeFrom(child))
                "poem" -> Fb2Block.Poem(poemFrom(child))
                "image" -> imageRefFrom(child)?.let(Fb2Block::Image)
                "empty-line" -> Fb2Block.EmptyLine
                else -> null
            }
        }

        return Fb2Section(
            id = element.attributes["id"]?.takeIf { it.isNotBlank() },
            title = element.first("title")?.text()?.ifBlank { null },
            paragraphs = blocks.filterIsInstance<Fb2Block.Paragraph>()
                .mapNotNull { it.content.plainText.ifBlank { null } },
            images = blocks.filterIsInstance<Fb2Block.Image>().map { it.image },
            sections = element.children("section").map(::sectionFrom),
            blocks = blocks,
        )
    }

    private fun poemFrom(element: XmlElement): Fb2Poem =
        Fb2Poem(
            title = titleFrom(element.first("title")),
            epigraphs = element.children("epigraph").map(::epigraphFrom),
            blocks = element.nodes.mapNotNull { node ->
                val child = (node as? XmlNode.ElementNode)?.element ?: return@mapNotNull null
                when (child.localName) {
                    "subtitle" -> Fb2PoemBlock.Subtitle(textFrom(child))
                    "stanza" -> Fb2PoemBlock.Stanza(stanzaFrom(child))
                    else -> null
                }
            },
            textAuthors = element.children("text-author").map(::textFrom),
            date = element.first("date")?.text()?.ifBlank { null },
        )

    private fun epigraphFrom(element: XmlElement): Fb2Epigraph =
        Fb2Epigraph(
            blocks = element.nodes.mapNotNull { node ->
                val child = (node as? XmlNode.ElementNode)?.element ?: return@mapNotNull null
                when (child.localName) {
                    "p" -> Fb2EpigraphBlock.Paragraph(textFrom(child))
                    "poem" -> Fb2EpigraphBlock.Poem(poemFrom(child))
                    "cite" -> Fb2EpigraphBlock.Cite(citeFrom(child))
                    "empty-line" -> Fb2EpigraphBlock.EmptyLine
                    else -> null
                }
            },
            textAuthors = element.children("text-author").map(::textFrom),
        )

    private fun citeFrom(element: XmlElement): Fb2Cite =
        Fb2Cite(
            blocks = element.nodes.mapNotNull { node ->
                val child = (node as? XmlNode.ElementNode)?.element ?: return@mapNotNull null
                when (child.localName) {
                    "p" -> Fb2EpigraphBlock.Paragraph(textFrom(child))
                    "subtitle" -> Fb2EpigraphBlock.Subtitle(textFrom(child))
                    "poem" -> Fb2EpigraphBlock.Poem(poemFrom(child))
                    "empty-line" -> Fb2EpigraphBlock.EmptyLine
                    else -> null
                }
            },
            textAuthors = element.children("text-author").map(::textFrom),
        )

    private fun stanzaFrom(element: XmlElement): Fb2Stanza =
        Fb2Stanza(
            title = titleFrom(element.first("title")),
            subtitle = element.first("subtitle")?.let(::textFrom),
            verses = element.children("v").map(::textFrom),
        )

    private fun titleFrom(element: XmlElement?): List<Fb2Text> =
        element?.children("p")?.map(::textFrom).orEmpty()

    private fun textFrom(element: XmlElement): Fb2Text =
        Fb2Text(spansFrom(element.nodes).mergeAdjacent().trimBoundaryWhitespace())

    private fun spansFrom(
        nodes: List<XmlNode>,
        styles: Set<Fb2TextStyle> = emptySet(),
        href: String? = null,
        linkType: String? = null,
    ): List<Fb2TextSpan> =
        nodes.flatMap { node ->
            when (node) {
                is XmlNode.TextNode -> listOfNotNull(
                    node.text.takeIf { it.isNotEmpty() }?.let {
                        Fb2TextSpan(
                            text = it,
                            styles = styles,
                            href = href,
                            linkType = linkType,
                        )
                    }
                )
                is XmlNode.ElementNode -> {
                    val element = node.element
                    val nestedStyles = when (element.localName) {
                        "emphasis" -> styles + Fb2TextStyle.Emphasis
                        "strong" -> styles + Fb2TextStyle.Strong
                        "code" -> styles + Fb2TextStyle.Code
                        "strikethrough" -> styles + Fb2TextStyle.Strikethrough
                        "sup" -> styles + Fb2TextStyle.Superscript
                        "sub" -> styles + Fb2TextStyle.Subscript
                        else -> styles
                    }
                    val nestedHref = element.attributeLocal("href")
                        ?.takeIf { element.localName == "a" && it.isNotBlank() }
                        ?: href
                    val nestedLinkType = element.attributes["type"]
                        ?.takeIf { element.localName == "a" && it.isNotBlank() }
                        ?: linkType
                    spansFrom(element.nodes, nestedStyles, nestedHref, nestedLinkType)
                }
            }
        }

    private fun List<Fb2TextSpan>.mergeAdjacent(): List<Fb2TextSpan> {
        if (isEmpty()) return emptyList()
        val merged = mutableListOf<Fb2TextSpan>()
        forEach { span ->
            val previous = merged.lastOrNull()
            if (previous != null &&
                previous.styles == span.styles &&
                previous.href == span.href &&
                previous.linkType == span.linkType
            ) {
                merged[merged.lastIndex] = previous.copy(text = previous.text + span.text)
            } else {
                merged += span
            }
        }
        return merged
    }

    private fun List<Fb2TextSpan>.trimBoundaryWhitespace(): List<Fb2TextSpan> {
        if (isEmpty()) return emptyList()
        val trimmed = toMutableList()
        trimmed[0] = trimmed[0].copy(text = trimmed[0].text.trimStart())
        trimmed[trimmed.lastIndex] = trimmed.last().copy(text = trimmed.last().text.trimEnd())
        return trimmed.filter { it.text.isNotEmpty() }
    }

    private fun StringBuilder.appendSection(section: Fb2Section) {
        append("<section>")
        section.title?.takeIf { it.isNotBlank() }?.let {
            append("<title>")
            appendElement("p", it)
            append("</title>")
        }
        section.images.forEach { appendImage(it) }
        section.paragraphs.forEach { appendElement("p", it) }
        section.sections.forEach { appendSection(it) }
        append("</section>")
    }

    private fun StringBuilder.appendAuthor(author: Fb2Author) {
        append("<author>")
        appendAuthorFields(author)
        append("</author>")
    }

    private fun StringBuilder.appendAuthorFields(author: Fb2Author) {
        author.firstName?.takeIf { it.isNotBlank() }?.let { appendElement("first-name", it) }
        author.middleName?.takeIf { it.isNotBlank() }?.let { appendElement("middle-name", it) }
        author.lastName?.takeIf { it.isNotBlank() }?.let { appendElement("last-name", it) }
        author.nickname?.takeIf { it.isNotBlank() }?.let { appendElement("nickname", it) }
        if (author.displayName.isBlank()) appendElement("nickname", "Unknown")
    }

    private fun StringBuilder.appendImage(image: Fb2ImageRef) {
        append("""<image xlink:href="${escapeAttribute(image.href)}"""")
        image.alt?.takeIf { it.isNotBlank() }?.let { append(""" alt="${escapeAttribute(it)}"""") }
        append("/>")
    }

    private fun StringBuilder.appendRepeated(tag: String, values: List<String>) {
        values.forEach { value ->
            if (value.isNotBlank()) appendElement(tag, value)
        }
    }

    private fun StringBuilder.appendElement(tag: String, text: String) {
        append('<')
        append(tag)
        append('>')
        append(escapeText(text))
        append("</")
        append(tag)
        append('>')
    }

    private fun escapeText(value: String): String = buildString(value.length) {
        value.forEach { char ->
            when (char) {
                '&' -> append("&amp;")
                '<' -> append("&lt;")
                '>' -> append("&gt;")
                else -> append(char)
            }
        }
    }

    private fun escapeAttribute(value: String): String = buildString(value.length) {
        value.forEach { char ->
            when (char) {
                '&' -> append("&amp;")
                '<' -> append("&lt;")
                '"' -> append("&quot;")
                '\'' -> append("&apos;")
                else -> append(char)
            }
        }
    }
}

private fun XmlElement.attributeLocal(localName: String): String? =
    attributes.entries.firstOrNull { it.key.substringAfter(':') == localName }?.value
