Pith - wedge
package com.vgmlr.wedge

import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale
import java.util.Calendar

data class EditorResult(
    val text: TextFieldValue,
    val undoHistory: List<TextFieldValue>,
    val undoLineIndex: Int,
    val lastDel: Long
)

object WedgeEditorEngine {
    fun removeTopDay(text: String, format: String = WedgeConfig.HEADER_FORMAT_DEFAULT): String {
        val headerRegex = WedgeHeader.matchRegex(format)
        val match = headerRegex.find(text) ?: return text

        val headerLineStart = text.lastIndexOf('\n', match.range.first)
            .let { if (it == -1) 0 else it + 1 }

        val headerLineEnd = text.indexOf('\n', match.range.first)
        if (headerLineEnd == -1) {
            return text.substring(0, headerLineStart)
        }

        var scan = headerLineEnd + 1
        var deleteEnd = text.length
        while (scan <= text.length) {
            val nextNl = text.indexOf('\n', scan)
            val lineEnd = if (nextNl == -1) text.length else nextNl
            if (text.substring(scan, lineEnd).isBlank()) {
                deleteEnd = if (nextNl == -1) text.length else nextNl + 1
                break
            }
            if (nextNl == -1) break
            scan = nextNl + 1
        }

        return text.removeRange(headerLineStart, deleteEnd)
    }

    // auto prune
    fun removePastDays(text: String, format: String = WedgeConfig.HEADER_FORMAT_DEFAULT): String {
        val head = WedgeHeader.matchRegex(format)
        val now = Calendar.getInstance()
        fun day(d: Int) = WedgeHeader.dateRegex(
            format,
            (now.clone() as Calendar).apply { add(Calendar.DAY_OF_MONTH, d) }.timeInMillis
        )
        val past = (1..365).map { day(-it) }
        val keep = (0..365).map { day(it) }
        var out = text
        var from = 0
        while (true) {
            val m = head.find(out, from) ?: return out
            val stale = past.any { it.containsMatchIn(m.value) } &&
                    keep.none { it.containsMatchIn(m.value) }
            if (stale) out = dropBlock(out, m.range.first, head)
            else from = m.range.last + 1
        }
    }

    private fun dropBlock(text: String, start: Int, head: Regex): String {
        val first = text.indexOf('\n', start)
        if (first == -1) return text.substring(0, start)
        var scan = first + 1
        while (scan < text.length) {
            val nl = text.indexOf('\n', scan)
            val end = if (nl == -1) text.length else nl
            val line = text.substring(scan, end)
            if (line.isBlank()) return text.removeRange(start, if (nl == -1) end else nl + 1)
            if (head.containsMatchIn(line)) return text.removeRange(start, scan)
            if (nl == -1) break
            scan = nl + 1
        }
        return text.substring(0, start)
    }
    
    fun applyTextChange(
        newValue: TextFieldValue,
        textState: TextFieldValue,
        undoHistory: List<TextFieldValue>,
        undoLineIndex: Int,
        lastDel: Long
    ): EditorResult {
        var currentUndoHistory = undoHistory
        var currentUndoLineIndex = undoLineIndex
        var currentLastDel = lastDel

        if (newValue.text.length < textState.text.length) {
            val currentLine = textState.text.substring(
                0,
                textState.selection.start.coerceIn(0, textState.text.length)
            ).count { it == '\n' }
            if (currentUndoHistory.isEmpty() || currentLine != currentUndoLineIndex) {
                currentUndoHistory = (listOf(textState) + currentUndoHistory).take(2)
                currentUndoLineIndex = currentLine
            }
        } else if (newValue.text != textState.text && currentUndoHistory.isNotEmpty()) {
            val currentLine = newValue.text.substring(
                0,
                newValue.selection.start.coerceIn(0, newValue.text.length)
            ).count { it == '\n' }
            if (currentLine != currentUndoLineIndex) currentUndoHistory = emptyList()
        }

        if (newValue.text.length < textState.text.length) {
            val now = System.currentTimeMillis()
            if (now - currentLastDel < 45) return EditorResult(textState, currentUndoHistory, currentUndoLineIndex, currentLastDel)
            currentLastDel = now

            val oldText = textState.text
            val oldSel = textState.selection

            if ((oldText.length - newValue.text.length) > 1 && oldSel.collapsed) {
                val start = (oldSel.start - 1).coerceAtLeast(0)
                val suppressedText = oldText.removeRange(start, oldSel.start)
                return EditorResult(
                    newValue.copy(text = suppressedText, selection = TextRange(start)),
                    currentUndoHistory, currentUndoLineIndex, currentLastDel
                )
            }

            val expectedSuffix = oldText.substring(oldSel.max)
            val actualSuffix =
                newValue.text.substring(newValue.selection.max.coerceAtMost(newValue.text.length))

            if (expectedSuffix != actualSuffix) {
                val safePrefix = newValue.text.substring(
                    0,
                    newValue.selection.max.coerceAtMost(newValue.text.length)
                )
                return EditorResult(
                    newValue.copy(text = safePrefix + expectedSuffix),
                    currentUndoHistory, currentUndoLineIndex, currentLastDel
                )
            } else {
                return EditorResult(newValue, currentUndoHistory, currentUndoLineIndex, currentLastDel)
            }
        }

        var updatedValue = newValue
        val lengthDelta = newValue.text.length - textState.text.length
        val newCursor = newValue.selection.start
        val txt = newValue.text

        if (lengthDelta == 1 && newCursor > 0 && newCursor <= txt.length && (txt[newCursor - 1] == '\n' || txt[newCursor - 1] == ' ')) {
            try {
                val textBeforeCursor =
                    txt.substring(0, (newCursor - 1).coerceAtLeast(0))
                val lastNewlineIndex = textBeforeCursor.lastIndexOf('\n')
                val currentLine =
                    if (lastNewlineIndex == -1) textBeforeCursor else textBeforeCursor.substring(
                        lastNewlineIndex + 1
                    )
                val dateMatch = WedgeRegex.DATE_MATH_TRIGGER.find(currentLine)
                val mathMatch = WedgeRegex.MATH_TRIGGER.find(currentLine)
                val hrMatch = WedgeRegex.HR_TRIGGER.find(currentLine)
                val dashMatch = WedgeRegex.DASH_TRIGGER.find(currentLine)
                val listMatch = WedgeRegex.LIST_TRIGGER.find(currentLine)
                val bulletMatch = WedgeRegex.BULLET_TRIGGER.find(currentLine)
                if (dateMatch != null) {
                    val d = dateMatch.groupValues[1].toInt()
                    val m = dateMatch.groupValues[2]
                    val y = dateMatch.groupValues[3].toIntOrNull()
                    val dateResult = WedgeDate.getDateMath(d, m, y)
                    if (dateResult != null) {
                        val textBeforeLine =
                            txt.substring(0, (newCursor - 1).coerceAtLeast(0) - 3)
                        val textAfterLine =
                            txt.substring((if (txt[newCursor - 1] == ' ') newCursor else newCursor - 1).coerceAtMost(txt.length))
                        val finalNewText =
                            textBeforeLine + dateResult + textAfterLine
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(newCursor + dateResult.length - 3 - (if (txt[newCursor - 1] == ' ') 1 else 0))
                        )
                    }
                } else if (mathMatch != null) {
                    val result = evaluateMathSequential(mathMatch.value)
                    if (result != null) {
                        val formattedResult = fmt(result)
                        val textBeforeLine =
                            txt.substring(0, (newCursor - 1).coerceAtLeast(0))
                        val textAfterLine =
                            txt.substring((if (txt[newCursor - 1] == ' ') newCursor else newCursor - 1).coerceAtMost(txt.length))
                        val finalNewText =
                            textBeforeLine + formattedResult + cascade(formattedResult, textAfterLine)
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(newCursor + formattedResult.length - (if (txt[newCursor - 1] == ' ') 1 else 0))
                        )
                    }
                } else if (hrMatch != null) {
                    val count = hrMatch.groupValues[1].toIntOrNull() ?: 0
                    if (count > 0) {
                        val hrline = "—".repeat(count.coerceAtMost(100))
                        val matchStart = (newCursor - 1) - hrMatch.value.length
                        val finalNewText = txt.substring(0, matchStart) + hrline + txt.substring(newCursor)
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(matchStart + hrline.length)
                        )
                    }
                } else if (dashMatch != null) {
                    val counts = dashMatch.groupValues[1].toIntOrNull() ?: 0
                    if (counts > 0) {
                        val dashes = "— ".repeat(counts.coerceAtMost(100))
                        val dashStart = (newCursor - 1) - dashMatch.value.length
                        val finalNewText = txt.substring(0, dashStart) + dashes + txt.substring(newCursor)
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(dashStart + dashes.length)
                        )
                    }
                } else if (listMatch != null && txt[newCursor - 1] == '\n') {
                    val ws = listMatch.groupValues[1]
                    val currentNum = listMatch.groupValues[2].toInt()
                    val prefix = "$ws${currentNum + 1}. "
                    val textBeforeLine = txt.substring(0, newCursor)
                    val textAfterLine = txt.substring(newCursor)
                    val lines = textAfterLine.split("\n").toMutableList()
                    var nextExpected = currentNum + 1
                    for (i in lines.indices) {
                        val m = WedgeRegex.LIST_TRIGGER.find(lines[i])
                        if (m != null) {
                            val lws = m.groupValues[1]
                            val foundNum = m.groupValues[2].toInt()
                            if (lws == ws && foundNum == nextExpected) {
                                lines[i] =
                                    "$lws${foundNum + 1}. " + lines[i].substring(m.range.last + 1)
                                nextExpected++
                            } else break
                        } else if (i > 0) break
                    }
                    val finalNewText =
                        textBeforeLine + prefix + lines.joinToString("\n")
                    updatedValue = newValue.copy(
                        text = finalNewText,
                        selection = TextRange(newCursor + prefix.length)
                    )
                } else if (bulletMatch != null && txt[newCursor - 1] == '\n') {
                    val prefix = "${bulletMatch.groupValues[1]}${bulletMatch.groupValues[2]} "
                    val finalNewText = txt.substring(0, newCursor) + prefix + txt.substring(newCursor)
                    updatedValue = newValue.copy(
                        text = finalNewText,
                        selection = TextRange(newCursor + prefix.length)
                    )
                } else if (txt[newCursor - 1] == '\n') {
                    val indent = currentLine.takeWhile { it == ' ' || it == '\t' }
                    if (indent.isNotEmpty()) {
                        val finalNewText = txt.substring(0, newCursor) + indent + txt.substring(newCursor)
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(newCursor + indent.length)
                        )
                    }
                }
            } catch (_: Exception) {
            }
        }
        return EditorResult(updatedValue, currentUndoHistory, currentUndoLineIndex, currentLastDel)
    }

    fun findNextTimeHighlight(text: String, format: String = WedgeConfig.HEADER_FORMAT_DEFAULT): Int? {
        val now = Calendar.getInstance()
        val currentMinutes = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE)

        val headerRegex = WedgeHeader.dateRegex(format, now.timeInMillis)
        val match = headerRegex.find(text) ?: return null

        val headerIdx = match.range.first

        val sub = text.substring(headerIdx)
        val lines = sub.split("\n")
        var charOffset = headerIdx

        for (i in 1 until lines.size) {
            val line = lines[i]
            charOffset += lines[i - 1].length + 1
            if (line.trim().isEmpty()) return null

            val timeMatch = WedgeRegex.TIME_ENTRY.find(line)
            val countdownMatch = WedgeRegex.COUNTDOWN_TIME.find(line)
            val targetMatch = countdownMatch ?: timeMatch

            if (targetMatch != null) {
                val h = targetMatch.groupValues[1].toInt()
                val m = targetMatch.groupValues[2].toInt()
                if (h * 60 + m > currentMinutes) return charOffset
            }
        }
        return null
    }

    private fun fmt(d: Double): String =
        DecimalFormat("0.##", DecimalFormatSymbols(Locale.US)).format(d)

    // chain recalc
    private fun cascade(seed: String, rest: String): String {
        var acc = seed.toDoubleOrNull() ?: return rest
        var src = rest
        val out = StringBuilder()
        while (true) {
            val m = WedgeRegex.MATH_CHAIN.find(src) ?: break
            val v = applyOps(acc, m.groupValues[1]) ?: break
            val shown = fmt(v)
            acc = shown.toDoubleOrNull() ?: break
            out.append(m.groupValues[1]).append('=').append(shown)
            src = src.substring(m.range.last + 1)
        }
        return out.toString() + src
    }

    private fun evaluateMathSequential(expression: String): Double? {
        val head = expression.takeWhile { it !in "+-/*=" }
        return applyOps(head.toDoubleOrNull() ?: return null, expression.substring(head.length))
    }

    private fun applyOps(seed: Double, tail: String): Double? {
        try {
            val ops = tail.filter { it in "+-/*" }
            val terms = tail.split(WedgeRegex.MATH_SPLIT).filter { it.isNotEmpty() }
            if (terms.size != ops.length) return null
            var res = seed
            for (i in ops.indices) {
                val op = ops[i]
                val term = terms[i]
                val isPct = term.endsWith("%")
                val num = if (isPct) term.dropLast(1).toDouble() else term.toDouble()
                val next = if (isPct) {
                    if (op == '+' || op == '-') res * (num / 100.0) else num / 100.0
                } else num
                res = when (op) {
                    '+' -> res + next
                    '-' -> res - next
                    '*' -> res * next
                    '/' -> if (next == 0.0) return null else res / next
                    else -> res
                }
            }
            return res
        } catch (_: Exception) { return null }
    }
}