package net.sergeych.toread.text

interface HyphenationPlugin {
    val languageTags: Set<String>

    fun hyphenateWord(word: String): String
}

class HyphenationRegistry(
    plugins: List<HyphenationPlugin> = listOf(
        EnglishHyphenationPlugin,
        RussianHyphenationPlugin,
    ),
) {
    private val byLanguage = plugins
        .flatMap { plugin -> plugin.languageTags.map { it.lowercase() to plugin } }
        .toMap()

    fun pluginFor(languageTag: String?): HyphenationPlugin? {
        val normalized = languageTag?.lowercase()?.takeIf { it.isNotBlank() } ?: return null
        return byLanguage[normalized] ?: byLanguage[normalized.substringBefore('-')]
    }

    fun hyphenate(text: String, languageTag: String?): String {
        val plugin = pluginFor(languageTag) ?: return text
        return hyphenate(text, plugin)
    }

    private fun hyphenate(text: String, plugin: HyphenationPlugin): String = buildString(text.length + text.length / 12) {
        var wordStart = -1

        fun flushToken(end: Int) {
            if (wordStart >= 0) {
                val token = text.substring(wordStart, end)
                append(if (SoftHyphen !in token && token.all(Char::isLetter)) plugin.hyphenateWord(token) else token)
                wordStart = -1
            }
        }

        text.forEachIndexed { index, char ->
            if (char.isLetterOrDigit() || char == SoftHyphen) {
                if (wordStart < 0) wordStart = index
            } else {
                flushToken(index)
                append(char)
            }
        }
        flushToken(text.length)
    }
}

object EnglishHyphenationPlugin : HyphenationPlugin {
    override val languageTags: Set<String> = setOf("en", "eng")

    override fun hyphenateWord(word: String): String {
        if (word.length < 7 || SoftHyphen in word) return word
        val breaks = mutableListOf<Int>()
        for (index in 2 until word.lastIndex - 1) {
            val prev = word[index - 1]
            val current = word[index]
            val next = word[index + 1]
            if (prev.isVowel() && current.isConsonant() && next.isVowel()) breaks += index
            if (current.isConsonant() && next.isConsonant() && word.getOrNull(index + 2)?.isVowel() == true) {
                breaks += index + 1
            }
        }
        return insertBreaks(word, breaks, minPrefix = 3, minSuffix = 3)
    }
}

object RussianHyphenationPlugin : HyphenationPlugin {
    override val languageTags: Set<String> = setOf("ru", "rus")

    override fun hyphenateWord(word: String): String {
        if (SoftHyphen in word || !word.isRussianWord() || word.isLikelyAbbreviation()) return word

        val lower = word.lowercase()
        val candidates = mutableSetOf<Int>()
        addRussianPrefixBreak(lower, candidates)
        addRussianSyllableBreaks(lower, candidates)

        return insertBreaks(word, candidates.filter { lower.isLegalRussianBreak(it) }, minPrefix = 2, minSuffix = 2)
    }

    private fun addRussianPrefixBreak(word: String, candidates: MutableSet<Int>) {
        RussianPrefixes.firstOrNull { prefix -> word.startsWith(prefix) }
            ?.length
            ?.let(candidates::add)
    }

    private fun addRussianSyllableBreaks(word: String, candidates: MutableSet<Int>) {
        val vowelIndexes = word.indices.filter { word[it].isRussianVowel() }
        vowelIndexes.zipWithNext().forEach { (leftVowel, rightVowel) ->
            val clusterStart = leftVowel + 1
            val clusterEnd = rightVowel
            val clusterLength = clusterEnd - clusterStart
            when {
                clusterLength == 0 -> candidates += rightVowel
                clusterLength == 1 -> {
                    val consonant = word[clusterStart]
                    candidates += if (consonant.isRussianJoiner()) clusterStart + 1 else clusterStart
                }
                clusterLength == 2 -> {
                    val joiner = (clusterStart until clusterEnd).firstOrNull { word[it].isRussianJoiner() }
                    candidates += if (joiner != null) joiner + 1 else clusterStart + 1
                }
                else -> addRussianClusterBreak(word, clusterStart, clusterEnd, candidates)
            }
        }
    }

    private fun addRussianClusterBreak(
        word: String,
        clusterStart: Int,
        clusterEnd: Int,
        candidates: MutableSet<Int>,
    ) {
        val cluster = word.substring(clusterStart, clusterEnd)
        val joiner = (clusterStart until clusterEnd).firstOrNull { word[it].isRussianJoiner() }
        if (joiner != null) {
            if (joiner < clusterEnd - 1) candidates += joiner + 1
            return
        }

        if (cluster == "ств") {
            candidates += clusterStart + 2
            return
        }

        if (cluster.endsWith("ств")) {
            candidates += clusterEnd - 3
            return
        }

        val onsetBreak = (clusterStart until clusterEnd)
            .firstOrNull { index -> RussianRightOnsets.any { onset -> word.startsWith(onset, index) } }
        candidates += onsetBreak ?: (clusterEnd - 1)
    }
}

private val RussianPrefixes = listOf(
    "сверх",
    "подъ",
    "пере",
    "пред",
    "без",
    "бес",
    "воз",
    "вос",
    "низ",
    "нис",
    "раз",
    "рас",
    "роз",
    "рос",
    "под",
    "при",
    "про",
    "над",
    "меж",
    "вз",
    "вс",
    "из",
    "ис",
    "от",
    "об",
)

private val RussianRightOnsets = listOf(
    "ст",
    "ск",
    "сп",
    "сн",
    "сл",
    "см",
    "св",
    "бр",
    "бл",
    "вр",
    "вл",
    "гр",
    "гл",
    "др",
    "тр",
    "кр",
    "кл",
    "пр",
    "пл",
    "фр",
    "фл",
)

const val SoftHyphen: Char = '\u00AD'

private fun insertBreaks(word: String, breaks: List<Int>, minPrefix: Int, minSuffix: Int): String {
    val legalBreaks = breaks.distinct()
        .filter { it >= minPrefix && word.length - it >= minSuffix }
        .toSet()
    if (legalBreaks.isEmpty()) return word

    return buildString(word.length + legalBreaks.size) {
        word.forEachIndexed { index, char ->
            if (index in legalBreaks) append(SoftHyphen)
            append(char)
        }
    }
}

private fun Char.isVowel(): Boolean = lowercaseChar() in "aeiouy"

private fun Char.isConsonant(): Boolean = isLetter() && !isVowel()

private fun Char.isRussianVowel(): Boolean = lowercaseChar() in "аеёиоуыэюя"

private fun Char.isRussianConsonant(): Boolean =
    lowercaseChar() in "бвгджзклмнпрстфхцчшщ"

private fun Char.isRussianJoiner(): Boolean = lowercaseChar() in "йьъ"

private fun Char.isRussianLetter(): Boolean = lowercaseChar() in "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"

private fun String.isRussianWord(): Boolean = all(Char::isRussianLetter)

private fun String.isLikelyAbbreviation(): Boolean = length > 1 && all { it.isUpperCase() }

private fun String.isLegalRussianBreak(index: Int): Boolean {
    if (index < 2 || length - index < 2) return false
    if (take(index).none(Char::isRussianVowel) || drop(index).none(Char::isRussianVowel)) return false

    val left = this[index - 1]
    val right = this[index]
    if (right.lowercaseChar() in "ьъйы") return false
    if (right.isRussianJoiner()) return false
    if (left.isRussianConsonant() && right.isRussianVowel()) return false
    return true
}
