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
data class EditorResult(
val text: TextFieldValue,
val undoHistory: List<TextFieldValue>,
val undoLineIndex: Int,
val lastDel: Long
)
object WedgeEditorEngine {
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)
if (dateMatch != null) {
val d = dateMatch.groupValues[1].toInt()
val m = dateMatch.groupValues[2]
val dateResult = WedgeCalendar.getDateMath(d, m)
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 symbols = DecimalFormatSymbols(Locale.US)
val df = DecimalFormat("0.##", symbols)
val formattedResult = df.format(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 + 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 (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)
}
private fun evaluateMathSequential(expression: String): Double? {
try {
val ops = expression.filter { it in "+-/*" }.map { it.toString() }
val terms = expression.split(WedgeRegex.MATH_SPLIT).filter { it.isNotEmpty() }
if (terms.size != ops.size + 1) return null
var res = terms[0].toDouble()
for (i in ops.indices) {
val op = ops[i]
val term = terms[i + 1]
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[0]) {
'+' -> res + next
'-' -> res - next
'*' -> res * next
'/' -> if (next == 0.0) return null else res / next
else -> res
}
}
return res
} catch (_: Exception) { return null }
}
}