package com.vgmlr.wedge
import java.text.SimpleDateFormat
import java.util.*
object WedgeCalendar {
private data class ChildLine(val start: Int, val end: Int, val content: String)
private data class HeaderInfo(val start: Int, val otcDay: Int)
private fun parseTimeToMinutes(timeStr: String): Int? {
val sanitized = if (!timeStr.contains(":") && timeStr.length == 4) {
"${timeStr.substring(0, 2)}:${timeStr.substring(2)}"
} else if (!timeStr.contains(":") && timeStr.length == 3) {
"0${timeStr.substring(0, 1)}:${timeStr.substring(1)}"
} else {
timeStr
}
val parts = sanitized.split(":")
if (parts.size != 2) return null
val h = parts[0].toIntOrNull() ?: return null
val m = parts[1].toIntOrNull() ?: return null
return h * 60 + m
}
fun insertCalendarEntry(
currentText: String,
timestamp: Long,
timeStr: String,
entryText: String
): String {
val otcDay = WedgeDate.getOtcParts(timestamp).first
val dayOfWeek = SimpleDateFormat("EEE", Locale.US).format(Date(timestamp))
val calendar = Calendar.getInstance().apply { timeInMillis = timestamp }
val dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
val month = SimpleDateFormat("MMM", Locale.US).format(Date(timestamp))
val targetHeader = "$otcDay ${dayOfWeek.lowercase(Locale.US)} $dayOfMonth ${month.lowercase(Locale.US)}*"
val formattedTime = if (!timeStr.contains(":") && timeStr.length == 4) {
"${timeStr.substring(0, 2)}:${timeStr.substring(2)}"
} else if (!timeStr.contains(":") && timeStr.length == 3) {
"0${timeStr.substring(0, 1)}:${timeStr.substring(1)}"
} else {
timeStr
}
val newTimeEntry = "$formattedTime $entryText"
val headerRegex = Regex("""(?i)$otcDay\s+$dayOfWeek\s+0?$dayOfMonth\s+$month""")
val headerMatch = headerRegex.find(currentText)
if (headerMatch != null) {
val headerEnd = headerMatch.range.last + 1
var lineEnd = headerEnd
while (lineEnd < currentText.length && currentText[lineEnd] != '\n') {
lineEnd++
}
val headerLineEndIndex = lineEnd
val childLines = mutableListOf<ChildLine>()
var scanIndex = headerLineEndIndex
if (scanIndex < currentText.length && currentText[scanIndex] == '\n') {
scanIndex++
}
val generalHeaderRegex = Regex("""^(\d+)\s+([a-zA-Z]{3})\s+(\d{1,2})\s+([a-zA-Z]{3})""", RegexOption.IGNORE_CASE)
while (scanIndex < currentText.length) {
var nextLineEnd = scanIndex
while (nextLineEnd < currentText.length && currentText[nextLineEnd] != '\n') {
nextLineEnd++
}
val lineContent = currentText.substring(scanIndex, nextLineEnd)
if (lineContent.trim().isEmpty() || generalHeaderRegex.containsMatchIn(lineContent)) {
break
}
childLines.add(ChildLine(start = scanIndex, end = nextLineEnd, content = lineContent))
scanIndex = nextLineEnd
if (scanIndex < currentText.length && currentText[scanIndex] == '\n') {
scanIndex++
}
}
val newTimeVal = parseTimeToMinutes(timeStr) ?: 0
var matchingLine: ChildLine? = null
var insertBeforeLine: ChildLine? = null
for (line in childLines) {
val match = WedgeRegex.TIME_ENTRY.find(line.content)
if (match != null) {
val h = match.groupValues[1].toInt()
val m = match.groupValues[2].toInt()
val lineTimeVal = h * 60 + m
if (lineTimeVal == newTimeVal) {
matchingLine = line
break
} else if (lineTimeVal > newTimeVal && insertBeforeLine == null) {
insertBeforeLine = line
}
}
}
if (matchingLine != null) {
return currentText.substring(0, matchingLine.end) + "\n $entryText" + currentText.substring(matchingLine.end)
}
return if (insertBeforeLine != null) {
currentText.substring(0, insertBeforeLine.start) + "$newTimeEntry\n" + currentText.substring(insertBeforeLine.start)
} else if (childLines.isNotEmpty()) {
val lastLine = childLines.last()
currentText.substring(0, lastLine.end) + "\n$newTimeEntry" + currentText.substring(lastLine.end)
} else {
currentText.substring(0, headerLineEndIndex) + "\n$newTimeEntry" + currentText.substring(headerLineEndIndex)
}
}
val generalHeaderRegex = Regex("""(?m)^(\d+)\s+([a-zA-Z]{3})\s+(\d{1,2})\s+([a-zA-Z]{3})""")
val matches = generalHeaderRegex.findAll(currentText).toList()
if (matches.isEmpty()) {
val headerBlock = "$targetHeader\n$newTimeEntry\n\n"
return headerBlock + currentText
}
val headers = matches.map { m ->
val startIdx = m.range.first
val otcDayVal = m.groupValues[1].toIntOrNull() ?: 0
HeaderInfo(start = startIdx, otcDay = otcDayVal)
}
var isDescending = false
if (headers.size >= 2) {
isDescending = headers.first().otcDay >= headers.last().otcDay
}
val targetOtcDay = otcDay.toInt()
if (isDescending) {
val insertBeforeHeader = headers.find { it.otcDay < targetOtcDay }
if (insertBeforeHeader != null) {
return currentText.substring(0, insertBeforeHeader.start) + "$targetHeader\n$newTimeEntry\n\n" + currentText.substring(insertBeforeHeader.start)
}
} else {
val insertBeforeHeader = headers.find { it.otcDay > targetOtcDay }
if (insertBeforeHeader != null) {
return currentText.substring(0, insertBeforeHeader.start) + "$targetHeader\n$newTimeEntry\n\n" + currentText.substring(insertBeforeHeader.start)
}
}
val lastHeader = headers.last()
var scanIdx = lastHeader.start
while (scanIdx < currentText.length && currentText[scanIdx] != '\n') scanIdx++
if (scanIdx < currentText.length && currentText[scanIdx] == '\n') scanIdx++
while (scanIdx < currentText.length) {
var lineEnd = scanIdx
while (lineEnd < currentText.length && currentText[lineEnd] != '\n') lineEnd++
val lineContent = currentText.substring(scanIdx, lineEnd)
if (lineContent.trim().isEmpty() || generalHeaderRegex.containsMatchIn(lineContent)) break
scanIdx = lineEnd
if (scanIdx < currentText.length && currentText[scanIdx] == '\n') scanIdx++
}
val prefix = currentText.substring(0, scanIdx)
val suffix = currentText.substring(scanIdx)
val spacing = if (prefix.endsWith("\n\n")) "" else if (prefix.endsWith("\n")) "\n" else "\n\n"
return prefix + spacing + "$targetHeader\n$newTimeEntry\n\n" + suffix.trimStart()
}
}