package mg.dot.feufaro.solfa


object HarmonicAnalyzer {
    private val solfaToSemitones = mapOf(
        "d" to 0,  "D" to 1,
        "r" to 2,  "R" to 3,
        "m" to 4,
        "f" to 5,  "F" to 6,
        "s" to 7,  "S" to 8,
        "l" to 9,  "T" to 10,
        "t" to 11
    )

    private val semitoneToDegree = mapOf(
        0  to "I",    1  to "bII",
        2  to "II",   3  to "bIII",
        4  to "III",
        5  to "IV",   6  to "#IV",
        7  to "V",    8  to "bVI",
        9  to "VI",   10 to "bVII",
        11 to "VII"
    )

    private val noteNames = listOf(
        "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"
    )

    private val keyOffsets = mapOf(
        "C"  to 0, "C#" to 1, "Db" to 1, "D"  to 2, "Eb" to 3,
        "E"  to 4, "F"  to 5, "F#" to 6, "Gb" to 6, "G"  to 7,
        "Ab" to 8, "A"  to 9, "Bb" to 10,"B"  to 11
    )

    private val chordDefinitions = listOf(
        ChordDef("maj⁷",     setOf(0, 4, 7, 11), setOf(4, 11),    12),
        ChordDef("⁷",        setOf(0, 4, 7, 10), setOf(4, 10),    12),
        ChordDef("ₘ⁷",       setOf(0, 3, 7, 10), setOf(3, 10),    12),
        ChordDef("dim⁷",     setOf(0, 3, 6, 9),  setOf(3, 6, 9),  12),
        ChordDef("ₘ⁷b⁵",     setOf(0, 3, 6, 10), setOf(3, 6, 10), 12),
        ChordDef("⁷sus⁴",    setOf(0, 5, 7, 10), setOf(5, 10),    12),
        ChordDef("maj⁷sus²", setOf(0, 2, 7, 11), setOf(2, 11),    12),
        ChordDef("maj⁷#¹¹",  setOf(0, 4, 6, 7),  setOf(4, 6),     12),
        ChordDef("add⁹",     setOf(0, 2, 4, 7),  setOf(2, 4),     11),
        ChordDef("ₘadd⁹",    setOf(0, 2, 3, 7),  setOf(2, 3),     11),
        ChordDef("add¹¹",    setOf(0, 4, 5, 7),  setOf(4, 5),     11),

        ChordDef("",     setOf(0, 4, 7),  setOf(4),    8),
        ChordDef("ₘ",    setOf(0, 3, 7),  setOf(3),    8),
        ChordDef("dim",  setOf(0, 3, 6),  setOf(3, 6), 8),
        ChordDef("aug",  setOf(0, 4, 8),  setOf(4, 8), 8),
        ChordDef("sus²", setOf(0, 2, 7),  setOf(2),    8),
        ChordDef("sus⁴", setOf(0, 5, 7),  setOf(5),    8),

        ChordDef("⁵",    setOf(0, 7),     setOf(7),    5),
        ChordDef("",     setOf(0, 4),     setOf(4),    5),
        ChordDef("ₘ",    setOf(0, 3),     setOf(3),    5),

        ChordDef("",     setOf(0),        setOf(),     3)
    )

    private val MULTI_NOTE_SEPARATORS = Regex("""[•/|+ ,]+""")


    fun splitSlices(notes: Map<Int, String>): List<Map<Int, String>> {
        val splitRaw: Map<Int, List<String>> = notes.mapValues { (_, raw) ->
            val parts = raw.split(MULTI_NOTE_SEPARATORS).map { it.trim() }.filter { it.isNotEmpty() }
            if (parts.isEmpty()) listOf("") else parts
        }

        val sliceCount = splitRaw.values.maxOfOrNull { it.size } ?: 1

        return (0 until sliceCount).map { idx ->
            splitRaw.mapValues { (_, parts) ->
                parts.getOrElse(idx) { parts.last() }
            }
        }
    }

    fun analyzeDegree(notes: Map<Int, String>): String {
        val slices = splitSlices(notes)
        val isMulti = slices.size > 1

        val degrees = slices.map { slice ->
            val cleaned = cleanNotes(slice)
            if (!isMulti) printTable(cleaned)
            val match = findChordMatch(cleaned) ?: return@map ""
            val rootDegree = semitoneToDegree[match.root] ?: ""
            buildSlashNotation("$rootDegree${match.quality}", match, ::semitoneToDegreeStr)
        }

        if (isMulti) {
            printTableMulti(notes, slices)
        }

        val degreeLine = degrees.filter { it.isNotEmpty() }.joinToString("  ")

        /*println("├──────────────────┤")
        println("│ DEGREE: $degreeLine")
        println("└──────────────────┘")*/

        return degreeLine
    }

    fun analyzeChordName(notes: Map<Int, String>, songKey: String = "C"): String {
        val keyOffset = keyOffsets[songKey] ?: 0
        val slices = splitSlices(notes)

        val names = slices.map { slice ->
            val cleaned = cleanNotes(slice)
            val match = findChordMatch(cleaned) ?: return@map ""
            val absoluteRoot = (keyOffset + match.root) % 12
            val chordName = noteNames[absoluteRoot] + match.quality
            if (match.bass != -1 && match.bass != match.root) {
                val absoluteBass = (keyOffset + match.bass) % 12
                "$chordName/${noteNames[absoluteBass]}"
            } else {
                chordName
            }
        }

        return names.filter { it.isNotEmpty() }.joinToString("  ")
    }

    private fun cleanOneNote(raw: String): String {
        var clean = raw
            .replace(Regex("[()₁₂₃₄¹²³⁴]"), "")
            .replace("'", "").replace(",", "")
            .replace("z", "").replace("w", "").replace("@", "")
            .replace("―", "")
            .replace("—", "")
            .trim()

        clean = when {
            clean.contains("di") -> "D"
            clean.contains("ri") -> "R"
            clean.contains("fi") -> "F"
            clean.contains("si") -> "S"
            clean.contains("ta") -> "T"
            else -> clean
        }
        return clean
    }

    private fun cleanNotes(notes: Map<Int, String>): Map<Int, String> =
        notes.mapValues { (_, raw) -> cleanOneNote(raw) }

    private fun printTable(cleanedNotes: Map<Int, String>) {
        val voiceNames = mapOf(1 to "S", 2 to "A", 3 to "T", 4 to "B")
//        println("┌──────┬───────────┐")
//        println("│ Voix │ Note      │")
//        println("├──────┼───────────┤")
        (1..4).forEach { i ->
            //println("│ ${voiceNames[i]}    │ ${(cleanedNotes[i] ?: "").padEnd(9)} │")
        }
    }

    private fun printTableMulti(
        raw: Map<Int, String>,
        slices: List<Map<Int, String>>
    ) {
        val voiceNames = mapOf(1 to "S", 2 to "A", 3 to "T", 4 to "B")
        val headers = (1..slices.size).joinToString(" │ ") { "Note${it}  " }
//        println("┌──────┬───────────┐")
//        println("│ Voix │ $headers│")
//        println("├──────┼───────────┤")
        (1..4).forEach { voiceIdx ->
            val cols = slices.mapIndexed { _, slice ->
                (slice[voiceIdx] ?: "").padEnd(5)
            }.joinToString(" • ")
            //println("│ ${voiceNames[voiceIdx]}    │ $cols │")
        }
    }

    private fun findChordMatch(notes: Map<Int, String>): FullMatch? {
        val activeNotes = notes.values
            .filter { it.isNotEmpty() }
            .mapNotNull { solfaToSemitones[it] }
        if (activeNotes.isEmpty()) return null

        val uniqueSemitones = activeNotes.toSet()
        val bassRaw = notes[4]?.trim() ?: ""
        val bassSemitone = solfaToSemitones[bassRaw] ?: -1
        var bestMatch: FullMatch? = null
        var bestScore = -1

        if (bassSemitone != -1) {
            val intervals = uniqueSemitones.map { (it - bassSemitone + 12) % 12 }.toSet()
            val result = detectQuality(intervals)
            if (result != null && result.weight >= 8) {
                bestMatch = FullMatch(bassSemitone, result.quality, bassSemitone)
                bestScore = result.weight
            }
        }

        for (rootCandidate in uniqueSemitones) {
            val intervals = uniqueSemitones.map { (it - rootCandidate + 12) % 12 }.toSet()
            val result = detectQuality(intervals) ?: continue
            val bassBonus = if (rootCandidate == bassSemitone) 2 else 0
            val diatonicBonus = if (rootCandidate in setOf(0,2,4,5,7,9,11)) 1 else 0
            val score = result.weight + bassBonus + diatonicBonus

            if (score > bestScore) {
                bestScore = score
                bestMatch = FullMatch(rootCandidate, result.quality, bassSemitone)
            }
        }

        return bestMatch
    }

    private fun detectQuality(intervals: Set<Int>): QualityResult? {
        if (!intervals.contains(0)) return null

        var bestDef: ChordDef? = null
        var bestScore = -1

        for (def in chordDefinitions) {
            if (!intervals.containsAll(def.mandatory)) continue

            val matchingNotes = intervals.count { it in def.structure }
            val foreignNotes = intervals.count { it !in def.structure }

            val maxForeign = if (def.structure.size <= 2) 0 else 1
            if (foreignNotes > maxForeign) continue

            val score = def.baseWeight + matchingNotes - (foreignNotes * 3)
            if (score > bestScore) {
                bestScore = score
                bestDef = def
            }
        }

        return if (bestDef != null) QualityResult(bestDef.name, bestScore) else null
    }

    private fun semitoneToDegreeStr(semitone: Int): String =
        semitoneToDegree[semitone] ?: ""

    private fun buildSlashNotation(
        chordName: String,
        match: FullMatch,
        toStr: (Int) -> String
    ): String {
        return if (match.bass != -1 && match.bass != match.root) {
            "$chordName/${toStr(match.bass)}"
        } else {
            chordName
        }
    }

    data class ChordDef(
        val name: String,
        val structure: Set<Int>,
        val mandatory: Set<Int>,
        val baseWeight: Int
    )

    private data class FullMatch(val root: Int, val quality: String, val bass: Int)
    private data class QualityResult(val quality: String, val weight: Int)
}