Pith - wedge
package com.vgmlr.wedge

import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.BringIntoViewSpec
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.input.PlatformImeOptions
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.first

private val EDITOR_PAD = 16.dp
private val EDITOR_TOP_GAP = 12.dp
private val GUIDE_W = 1.dp
private val SEARCH_HL = Color(0xFFB8860B)
private val CURSOR_LIFT = 40.dp
private val SEARCH_HIT_GAP = 50.dp

@OptIn(ExperimentalFoundationApi::class)
private val NoAutoScroll = object : BringIntoViewSpec {
    override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float) = 0f
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun NoteEditor(
    vm: MainViewModel,
    prefs: PreferenceManager,
    onSettings: () -> Unit
) {
    val context = LocalContext.current
    val density = LocalDensity.current
    val focusManager = LocalFocusManager.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val ime = WindowInsets.ime
    val keyboardController = LocalSoftwareKeyboardController.current
    val nextTimeBgColor by prefs.nextTimeBg.collectAsState("#486860")
    val bgColorHex by prefs.bgColor.collectAsState(initial = "#131313")
    val incognitoActive by prefs.incognitoMode.collectAsState(initial = false)
    val btnPadRight by prefs.btnPadRight.collectAsState(WedgeConfig.BTN_PAD_RIGHT_DEFAULT)
    val btnPadBottom by prefs.btnPadBottom.collectAsState(WedgeConfig.BTN_PAD_BOTTOM_DEFAULT)
    val editorPadLeft by prefs.editorPadLeft.collectAsState(WedgeConfig.EDITOR_PAD_LEFT_DEFAULT)
    val pruneOn by prefs.autoPrune.collectAsState(WedgeConfig.PRUNE_DAYS_DEFAULT)
    val editorStyle by vm.editorStyle.collectAsState()
    val syncFlash by vm.syncFlash.collectAsState()
    val olderPrompt by vm.olderPrompt.collectAsState()
    val scope = rememberCoroutineScope()

    var textState by remember { mutableStateOf(TextFieldValue("")) }
    var lastSavedText by remember { mutableStateOf("") }
    var isLoaded by remember { mutableStateOf(false) }
    var lastDel by remember { mutableLongStateOf(0L) }
    val scrollState = rememberScrollState()
    var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
    var containerHeight by remember { mutableIntStateOf(0) }
    var fieldsHeight by remember { mutableIntStateOf(0) }

    val contentTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
    val editorTop = contentTop + TOP_BAR_H + EDITOR_TOP_GAP

    var btnGroup by remember { mutableStateOf(StackGroup.NONE) }
    var showSync by remember { mutableStateOf(false) }
    var showVersionDialog by remember { mutableStateOf(false) }
    var undoHistory by remember { mutableStateOf(emptyList<TextFieldValue>()) }
    var undoLineIndex by remember { mutableIntStateOf(-1) }

    var showCalc by remember { mutableStateOf(false) }
    var calcValue by remember { mutableStateOf("") }
    val calcFocusRequester = remember { FocusRequester() }
    
    var searchQuery by remember { mutableStateOf("") }
    var searchHit by remember { mutableIntStateOf(-1) }
    val searchFocusRequester = remember { FocusRequester() }
    var editorFocused by remember { mutableStateOf(false) }
    val editorFocusRequester = remember { FocusRequester() }
    var linkPrompt by remember { mutableStateOf<Pair<String, Int>?>(null) }
    var linkCursor by remember { mutableStateOf<Int?>(null) }
    
    val headerFormat by prefs.headerFormat.collectAsState(initial = WedgeConfig.HEADER_FORMAT_DEFAULT)
    val nextTimeOffset = remember(textState.text, headerFormat) {
        WedgeEditorEngine.findNextTimeHighlight(textState.text, headerFormat)
    }

    var showDatePicker by remember { mutableStateOf(false) }
    var showCalendarOverlay by remember { mutableStateOf(false) }
    var selectedDate by remember { mutableLongStateOf(0L) }
    var calendarTimeStr by remember { mutableStateOf("00:00") }
    var calendarTextStr by remember { mutableStateOf("Event") }
    val calendarFocusRequester = remember { FocusRequester() }

    val boldColorHex by prefs.boldColor.collectAsState(initial = WedgeConfig.BOLD_COLOR_DEFAULT)
    val focusColor = parseColor(boldColorHex)
    val linkColorHex by prefs.linkColor.collectAsState(initial = WedgeConfig.LINK_COLOR_DEFAULT)
    val urlColor = parseColor(linkColorHex)
    val linksActive by prefs.linkEnabled.collectAsState(initial = false)
    
    LaunchedEffect(showCalc) {
        if (showCalc) {
            delay(100)
            runCatching { calcFocusRequester.requestFocus() }
                .onSuccess { keyboardController?.show() }
        }
    }

    LaunchedEffect(showCalendarOverlay) {
        if (showCalendarOverlay) {
            delay(100)
            runCatching { calendarFocusRequester.requestFocus() }
                .onSuccess { keyboardController?.show() }
        }
    }

    var showEncrypt by remember { mutableStateOf(false) }
    var passPhrase by remember { mutableStateOf("") }
    val encryptFocusRequester = remember { FocusRequester() }
    var isDecryptMode by remember { mutableStateOf(false) }
    val isEncrypted = WedgeSecurity.isEncrypted(textState.text)

    val phraseSpec by prefs.phraseSpec.collectAsState(PhraseSpec())
    var quickPhrase by remember { mutableStateOf<String?>(null) }

    val guideOn by prefs.guideEnabled.collectAsState(WedgeConfig.GUIDE_ENABLED_DEFAULT)
    val guideChars by prefs.guideChars.collectAsState(WedgeConfig.GUIDE_CHARS_DEFAULT)
    val guideOpacity by prefs.guideOpacity.collectAsState(WedgeConfig.GUIDE_OPACITY_DEFAULT)
    
    LaunchedEffect(showEncrypt) {
        if (showEncrypt) {
            delay(100)
            runCatching { encryptFocusRequester.requestFocus() }
                .onSuccess { keyboardController?.show() }
        }
    }
    
    LaunchedEffect(linkCursor) {
        val off = linkCursor ?: return@LaunchedEffect
        delay(100)
        runCatching { editorFocusRequester.requestFocus() }
            .onSuccess { keyboardController?.show() }
        textState = textState.copy(selection = TextRange(off.coerceIn(0, textState.text.length)))
        linkCursor = null
    }
    
    val uriHandler = LocalUriHandler.current
    val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager

    val exportLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.CreateDocument("text/plain")
    ) { uri ->
        uri?.let {
            context.contentResolver.openOutputStream(it)?.use { stream ->
                stream.write(textState.text.toByteArray())
            }
        }
    }

    fun refreshAlarms(content: String): String {
        val now = java.util.Calendar.getInstance()
        val currentMinutes = now.get(java.util.Calendar.HOUR_OF_DAY) * 60 + now.get(java.util.Calendar.MINUTE)
        val todayOtcDay = WedgeDate.getOtcParts(now.timeInMillis).first

        val lines = content.split("\n")
        var currentHeaderOtcDay: Long? = null
        val generalHeaderRegex = WedgeHeader.matchRegex(headerFormat)

        val updatedLines = lines.map { line ->
            val headerMatch = generalHeaderRegex.find(line.trim())
            if (headerMatch != null) {
                currentHeaderOtcDay = WedgeHeader.otcDayFromMatch(headerMatch)?.toLong()
                line
            } else {
                val match = Regex("""^(\d{2}):(\d{2})\s*(.*)(?<!\s)(?:\((-\d+(\.\d+)?|0)?\)|\[(-\d+(\.\d+)?|0)?])$""").find(line.trim())
                if (match != null) {
                    val h = match.groupValues[1].toInt()
                    val m = match.groupValues[2].toInt()
                    val desc = match.groupValues[3]
                    val isAlarm = match.value.endsWith(")")

                    val alarmMinutes = h * 60 + m
                    val headerDay = currentHeaderOtcDay ?: todayOtcDay

                    val valueInside = if (headerDay < todayOtcDay) {
                        "0"
                    } else if (headerDay == todayOtcDay) {
                        val diffMinutes = alarmMinutes - currentMinutes
                        if (diffMinutes <= 0) {
                            "0"
                        } else {
                            val hours = (diffMinutes / 60.0).coerceAtLeast(0.1)
                            "-${String.format(java.util.Locale.US, "%.1f", hours)}"
                        }
                    } else {
                        val diffDays = headerDay - todayOtcDay
                        val diffMinutes = (diffDays * 24 * 60) + alarmMinutes - currentMinutes
                        val hours = (diffMinutes / 60.0).coerceAtLeast(0.1)
                        "-${String.format(java.util.Locale.US, "%.1f", hours)}"
                    }

                    val leadingSpaces = line.takeWhile { it.isWhitespace() }
                    val open = if (isAlarm) "(" else "["
                    val close = if (isAlarm) ")" else "]"
                    "${leadingSpaces}${match.groupValues[1]}:${match.groupValues[2]}  ${desc.trim()}$open$valueInside$close"
                } else {
                    line
                }
            }
        }
        return updatedLines.joinToString("\n")
    }

    fun refreshDateMath(content: String): String {
        val dateMathRefreshed = WedgeRegex.DATE_MATH_REFRESH.replace(content) { match ->
            val d = match.groupValues[1].toInt()
            val m = match.groupValues[2]
            val y = match.groupValues[3]
            WedgeDate.getDateMath(d, m, y.toIntOrNull())?.let { "$d$m$y$it" } ?: match.value
        }
        return refreshAlarms(dateMathRefreshed)
    }

    // past days
    fun prune(content: String, on: Boolean = pruneOn, fmt: String = headerFormat): String =
        if (on) WedgeEditorEngine.removePastDays(content, fmt) else content

    suspend fun saveNote(
        content: String,
        updateWidget: Boolean = false,
        bumpTimestamp: Boolean = true
    ) {
        withContext(Dispatchers.IO) {
            val isDifferent = content != lastSavedText
            if (!isDifferent && !updateWidget) return@withContext

            if (isDifferent) {
                if (bumpTimestamp) {
                    vm.dao.save(NoteEntity(id = 1, content = content))
                    withContext(Dispatchers.Main) { vm.noteActivity() }
                } else {
                    vm.dao.updateContentPreservingTimestamp(content)
                }
                withContext(Dispatchers.Main) { lastSavedText = content }
            }
            if (updateWidget) {
                NoteWidgetProvider.triggerUpdate(context)
            }
        }
    }

    // save, archive
    fun commitEncrypted(content: String) {
        scope.launch {
            withContext(NonCancellable) {
                saveNote(content, updateWidget = true)
                withContext(Dispatchers.IO) {
                    runCatching { WedgeBackup.run(context.applicationContext) }
                }
            }
        }
    }

    LaunchedEffect(Unit) {
        val initialNote = vm.dao.getNoteSync()
        if (initialNote != null) {
            val refreshed = refreshDateMath(
                prune(initialNote.content, prefs.autoPrune.first(), prefs.headerFormat.first())
            )
            textState = TextFieldValue(refreshed)
            if (refreshed != initialNote.content) {
                saveNote(refreshed, updateWidget = true, bumpTimestamp = false)
            } else {
                lastSavedText = refreshed
            }
        }
        isLoaded = true
    }

    LaunchedEffect(Unit) {
        snapshotFlow { textState.text }.collectLatest { text ->
            if (!isLoaded || text == lastSavedText) return@collectLatest
            delay(500)
            saveNote(text, updateWidget = true)
        }
    }

    LaunchedEffect(Unit) {
        vm.syncEvent.collect { stamp ->
            if (stamp == 0L || !isLoaded) return@collect
            val content = withContext(Dispatchers.IO) { vm.dao.getNoteSync()?.content ?: "" }
            if (content != textState.text) {
                val refreshed = refreshDateMath(prune(content))
                textState = TextFieldValue(refreshed, TextRange(0))
                lastSavedText = content
                if (refreshed != content) {
                    saveNote(refreshed, updateWidget = true, bumpTimestamp = false)
                }
            }
        }
    }

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_PAUSE) {
                focusManager.clearFocus()
                btnGroup = StackGroup.NONE
                val currentText = textState.text
                scope.launch {
                    withContext(NonCancellable) {
                        if (isLoaded) saveNote(currentText, updateWidget = true)
                    }
                }
            } else if (event == Lifecycle.Event.ON_RESUME) {
                if (isLoaded) {
                    val refreshed = refreshDateMath(prune(textState.text))
                    if (refreshed != textState.text) {
                        textState = textState.copy(text = refreshed)
                        scope.launch {
                            saveNote(refreshed, updateWidget = true, bumpTimestamp = false)
                        }
                    }
                }
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }

    BackHandler {
        if (showSync) showSync = false
        else if (btnGroup != StackGroup.NONE) btnGroup = StackGroup.NONE
        else (context as? ComponentActivity)?.finish()
    }

    LaunchedEffect(Unit) { focusManager.clearFocus() }

    var raiseTick by remember { mutableIntStateOf(0) }

    LaunchedEffect(raiseTick) {
        if (raiseTick == 0) return@LaunchedEffect
        delay(40)
        val kHeight = ime.getBottom(density)
        if (kHeight <= 0 || containerHeight <= 0) return@LaunchedEffect
        val layout = textLayoutResult ?: return@LaunchedEffect
        runCatching {
            val topPad = with(density) { editorTop.toPx() }
            val lift = with(density) { CURSOR_LIFT.toPx() } + fieldsHeight
            val cursorBottom =
                topPad + layout.getCursorRect(textState.selection.start).bottom + lift
            val visibleHeight = containerHeight - kHeight
            if (cursorBottom > scrollState.value + visibleHeight) {
                scrollState.scrollTo((cursorBottom - visibleHeight).toInt().coerceAtLeast(0))
            }
        }
    }

    val isKeyboardOpen = WindowInsets.ime.getBottom(density) > 0
    LaunchedEffect(isKeyboardOpen) {
        if (!isKeyboardOpen) {
            focusManager.clearFocus()
        }
    }

    LaunchedEffect(searchQuery) { searchHit = -1 }

    val searchHasMatch = remember(searchQuery, textState.text) {
        searchQuery.isNotEmpty() && textState.text.contains(searchQuery, ignoreCase = true)
    }

    val nextMatch: () -> Unit = next@{
        val q = searchQuery
        if (q.isEmpty()) return@next
        val txt = textState.text
        val from = (searchHit + 1).coerceAtMost(txt.length)
        val hit = txt.indexOf(q, from, ignoreCase = true)
            .let { if (it == -1) txt.indexOf(q, 0, ignoreCase = true) else it }
        if (hit == -1) return@next
        searchHit = hit
        val layout = textLayoutResult ?: return@next
        runCatching {
            if (containerHeight <= 0) return@runCatching
            val topPad = with(density) { editorTop.toPx() }
            val gap = with(density) { SEARCH_HIT_GAP.toPx() }
            val visibleHeight = containerHeight - ime.getBottom(density)
            val restY = visibleHeight - fieldsHeight - gap
            val line = layout.getLineForOffset(hit.coerceIn(0, layout.layoutInput.text.text.length))
            val target = topPad + layout.getLineBottom(line) - restY
            scope.launch { scrollState.animateScrollTo(target.toInt().coerceAtLeast(0)) }
        }
    }

    // keyed transform
    val hlQuery = searchQuery
    val hlLinks = linksActive
    val dimColor = editorStyle.color.copy(alpha = 0.6f)
    val editorTransform = remember(hlQuery, hlLinks, focusColor, urlColor, dimColor) {
        VisualTransformation { styleText(it, hlQuery, hlLinks, focusColor, urlColor, dimColor) }
    }

    Scaffold(
        contentWindowInsets = WindowInsets(0, 0, 0, 0)
    ) { p ->
        val measurer = rememberTextMeasurer()
        val guideX = remember(guideOn, guideChars, editorStyle) {
            if (!guideOn || guideChars < 1) null
            else measurer.measure(
                AnnotatedString("0".repeat(guideChars)),
                editorStyle
            ).size.width.toFloat()
        }
        val guideColor = editorStyle.color.copy(alpha = guideOpacity / 100f)
        val padLeft = editorPadLeft.dp

        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(bottom = p.calculateBottomPadding())
                .background(parseColor(bgColorHex))
        ) {
            CompositionLocalProvider(LocalBringIntoViewSpec provides NoAutoScroll) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .drawBehind {
                            val x = (guideX ?: return@drawBehind) + padLeft.toPx()
                            if (x > size.width) return@drawBehind
                            drawLine(
                                guideColor,
                                Offset(x, 0f),
                                Offset(x, size.height),
                                GUIDE_W.toPx()
                            )
                        }
                        .onGloballyPositioned { containerHeight = it.size.height }
                        .verticalScroll(scrollState)
                        .navigationBarsPadding()
                        .imePadding()
                ) {
                    BasicTextField(
                        value = textState,
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(start = padLeft, end = EDITOR_PAD, bottom = EDITOR_PAD)
                            .padding(top = editorTop)
                            .fillMaxSize()
                            .onFocusChanged { editorFocused = it.isFocused }
                            .focusRequester(editorFocusRequester)
                            .pointerInput(linksActive) {
                                awaitEachGesture {
                                    val down = awaitFirstDown(false, PointerEventPass.Initial)
                                    if (btnGroup == StackGroup.MENU || btnGroup == StackGroup.ARTICLE) {
                                        btnGroup = StackGroup.NONE
                                    }
                                    if (!linksActive) return@awaitEachGesture
                                    val layout = textLayoutResult ?: return@awaitEachGesture
                                    val ln = layout.getLineForVerticalPosition(down.position.y)
                                    if (down.position.x > layout.getLineRight(ln)) return@awaitEachGesture
                                    val off = layout.getOffsetForPosition(down.position)
                                    val url = urlAt(textState.text, off) ?: return@awaitEachGesture
                                    down.consume()
                                    while (true) {
                                        val c = awaitPointerEvent(PointerEventPass.Initial)
                                            .changes.firstOrNull { it.id == down.id }
                                            ?: return@awaitEachGesture
                                        c.consume()
                                        if (!c.pressed) {
                                            linkPrompt = url to off
                                            return@awaitEachGesture
                                        }
                                        if ((c.position - down.position).getDistance() >
                                            viewConfiguration.touchSlop
                                        ) return@awaitEachGesture
                                    }
                                }
                            }
                            .drawBehind {
                                val layout = textLayoutResult ?: return@drawBehind
                                val offset = nextTimeOffset ?: return@drawBehind
                                if (offset < layout.layoutInput.text.text.length) {
                                    try {
                                        val line = layout.getLineForOffset(offset)
                                        val top = layout.getLineTop(line)
                                        val bottom = layout.getLineBottom(line)
                                        val left = layout.getLineLeft(line)
                                        val right = layout.getLineRight(line)
                                        drawRect(
                                            color = parseColor(nextTimeBgColor),
                                            topLeft = Offset(left, top),
                                            size = Size(right - left, bottom - top)
                                        )
                                    } catch (_: Exception) {
                                    }
                                }
                            },
                        onValueChange = { newValue ->
                            val before = textState.text
                            val result = WedgeEditorEngine.applyTextChange(
                                newValue,
                                textState,
                                undoHistory,
                                undoLineIndex,
                                lastDel
                            )
                            textState = result.text
                            undoHistory = result.undoHistory
                            undoLineIndex = result.undoLineIndex
                            lastDel = result.lastDel
                            if (result.text.text != before) raiseTick++
                        },
                        onTextLayout = { textLayoutResult = it },
                        textStyle = editorStyle,
                        cursorBrush = SolidColor(editorStyle.color),
                        keyboardOptions = KeyboardOptions(
                            platformImeOptions = if (incognitoActive) PlatformImeOptions("privateImeOptions=true,com.google.android.inputmethod.latin.noPersonalizedLearning=true") else null
                        ),
                        visualTransformation = editorTransform
                    )
                }
            }

            EditorTopBar(
                modifier = Modifier.align(Alignment.TopCenter),
                group = btnGroup,
                onGroupChange = { btnGroup = it },
                field = {
                    val slot = Modifier.weight(1f)
                    when {
                        showCalendarOverlay -> CalendarOverlay(
                            timeValue = calendarTimeStr,
                            onTimeValueChange = { calendarTimeStr = it },
                            textValue = calendarTextStr,
                            onTextValueChange = { calendarTextStr = it },
                            onDismiss = { showCalendarOverlay = false },
                            onEnterPressed = {
                                val updatedText = WedgeCalendar.insertCalendarEntry(
                                    currentText = textState.text,
                                    timestamp = selectedDate,
                                    timeStr = calendarTimeStr.ifEmpty { "00:00" },
                                    entryText = calendarTextStr.ifEmpty { "Event" },
                                    headerFormat = headerFormat
                                )
                                val rawTime = calendarTimeStr.ifEmpty { "00:00" }
                                val finalTime = if (!rawTime.contains(":") && rawTime.length == 4) {
                                    "${rawTime.substring(0, 2)}:${rawTime.substring(2)}"
                                } else if (!rawTime.contains(":") && rawTime.length == 3) {
                                    "0${rawTime.substring(0, 1)}:${rawTime.substring(1)}"
                                } else {
                                    rawTime
                                }
                                val finalTxt = calendarTextStr.ifEmpty { "Event" }
                                val standardPattern = "$finalTime  $finalTxt"
                                val omittedPattern = "       $finalTxt"
                                val standardIndex = updatedText.indexOf(standardPattern)
                                val newEntryIndex =
                                    if (standardIndex != -1) standardIndex
                                    else updatedText.indexOf(omittedPattern)
                                val newSelection = if (newEntryIndex != -1) {
                                    val matchLength =
                                        if (standardIndex != -1) standardPattern.length
                                        else omittedPattern.length
                                    TextRange(newEntryIndex + matchLength)
                                } else {
                                    textState.selection
                                }
                                textState = textState.copy(
                                    text = updatedText,
                                    selection = newSelection
                                )
                                showCalendarOverlay = false
                            },
                            focusRequester = calendarFocusRequester,
                            modifier = slot
                        )

                        showEncrypt -> EncryptionOverlay(
                            passPhrase = passPhrase,
                            onPassPhraseChange = { passPhrase = it },
                            onDismiss = { showEncrypt = false; passPhrase = "" },
                            focusRequester = encryptFocusRequester,
                            isDecrypt = isDecryptMode,
                            onAction = {
                                if (passPhrase.isNotEmpty()) {
                                    if (isEncrypted) {
                                        WedgeSecurity.decrypt(textState.text, passPhrase)?.let {
                                            textState = textState.copy(
                                                text = it,
                                                selection = TextRange(0, it.length)
                                            )
                                            showEncrypt = false
                                            passPhrase = ""
                                        } ?: run { passPhrase = "" }
                                    } else {
                                        val encrypted =
                                            WedgeSecurity.encrypt(textState.text, passPhrase)
                                        textState = textState.copy(
                                            text = encrypted,
                                            selection = TextRange(0, encrypted.length)
                                        )
                                        commitEncrypted(encrypted)
                                        showEncrypt = false
                                        passPhrase = ""
                                    }
                                }
                            },
                            modifier = slot
                        )

                        showCalc -> CalculatorOverlay(
                            calcValue = calcValue,
                            onCalcValueChange = { calcValue = it },
                            onDismiss = { showCalc = false },
                            focusRequester = calcFocusRequester,
                            modifier = slot
                        )

                        else -> SearchOverlay(
                            query = searchQuery,
                            onQueryChange = { searchQuery = it },
                            onDismiss = { focusManager.clearFocus() },
                            onNext = nextMatch,
                            hasMatch = searchHasMatch,
                            focusRequester = searchFocusRequester,
                            modifier = slot
                        )
                    }
                },
                isEncrypted = isEncrypted,
                hasSelection = !textState.selection.collapsed,
                onSync = {
                    btnGroup = StackGroup.NONE
                    focusManager.clearFocus()
                    showSync = !showSync
                },
                onSecurityAction = {
                    btnGroup = StackGroup.NONE
                    showCalc = false
                    isDecryptMode = isEncrypted
                    showEncrypt = true
                },
                onExport = {
                    btnGroup = StackGroup.NONE
                    val otcTimestamp = WedgeDate.getOtcDate()
                    exportLauncher.launch("wedge_$otcTimestamp.txt")
                },
                onSettings = {
                    btnGroup = StackGroup.NONE
                    focusManager.clearFocus()
                    onSettings()
                },
                onShowVersion = {
                    btnGroup = StackGroup.NONE
                    showVersionDialog = true
                },
                onQuickSend = {
                    btnGroup = StackGroup.NONE
                    scope.launch {
                        saveNote(textState.text)
                        vm.quickSend()
                    }
                },
                onRemoveDay = {
                    val newText = WedgeEditorEngine.removeTopDay(textState.text, headerFormat)
                    if (newText != textState.text) {
                        undoHistory = (listOf(textState) + undoHistory).take(2)
                        textState = textState.copy(
                            text = newText,
                            selection = TextRange(0)
                        )
                    }
                },
                onSort = {
                    val sel = textState.selection
                    if (!sel.collapsed) {
                        val txt = textState.text
                        val start = txt.lastIndexOf('\n', sel.min - 1)
                            .let { if (it == -1) 0 else it + 1 }
                        val end =
                            txt.indexOf('\n', sel.max).let { if (it == -1) txt.length else it }
                        val block = txt.substring(start, end)
                        val lines = block.split('\n')
                        val baseIndent = lines.firstOrNull { it.isNotBlank() }
                            ?.takeWhile { it.isWhitespace() }?.length ?: 0
                        val groups = mutableListOf<MutableList<String>>()
                        for (line in lines) {
                            if (groups.isEmpty() || (line.isNotBlank() && line.takeWhile { it.isWhitespace() }.length <= baseIndent)) {
                                groups.add(mutableListOf(line))
                            } else {
                                groups.last().add(line)
                            }
                        }
                        val sorted = groups.sortedWith { g1, g2 ->
                            String.CASE_INSENSITIVE_ORDER.compare(g1[0], g2[0])
                        }.flatten().joinToString("\n")
                        textState = textState.copy(
                            text = txt.replaceRange(start, end, sorted),
                            selection = TextRange(start, start + sorted.length)
                        )
                    }
                },
                onToggleCalc = {
                    showCalendarOverlay = false
                    showEncrypt = false
                    showCalc = !showCalc
                },
                onCalendar = { showDatePicker = true }
            )
            
            Column(
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .fillMaxWidth()
                    .imePadding()
                    .navigationBarsPadding()
                    .onGloballyPositioned {
                        if (btnGroup == StackGroup.NONE) fieldsHeight = it.size.height
                        if (btnGroup == StackGroup.NONE && !syncFlash) fieldsHeight = it.size.height
                    },
                horizontalAlignment = Alignment.End
            ) {
                EditorButtonStack(
                    group = btnGroup,
                    onGroupChange = { btnGroup = it },
                    padRight = btnPadRight,
                    padBottom = btnPadBottom,
                    editorFocused = editorFocused,
                    hasSelection = !textState.selection.collapsed,
                    syncFlash = syncFlash,
                    isEncrypted = isEncrypted,
                    onStrike = {
                        if (editorFocused) {
                            textState = appendStrike(textState)
                            raiseTick++
                        }
                    },
                    onDeleteLine = {
                        val next = deleteLine(textState)
                        if (next.text != textState.text) {
                            undoHistory = (listOf(textState) + undoHistory).take(2)
                            textState = next
                            undoLineIndex = next.text
                                .substring(0, next.selection.start)
                                .count { it == '\n' }
                            raiseTick++
                        }
                    },
                    onMoveLineUp = {
                        if (editorFocused) {
                            textState = moveLine(textState, true)
                            raiseTick++
                        }
                    },
                    onMoveLineDown = {
                        if (editorFocused) {
                            textState = moveLine(textState, false)
                            raiseTick++
                        }
                    },
                    onCopy = {
                        scope.launch {
                            saveNote(textState.text)
                            val sel = textState.selection
                            val start = sel.min.coerceIn(0, textState.text.length)
                            val end = sel.max.coerceIn(0, textState.text.length)
                            if (start != end) {
                                val selectedText = textState.text.substring(start, end)
                                clipboardManager.setPrimaryClip(
                                    ClipData.newPlainText(
                                        "wedge_note",
                                        selectedText
                                    )
                                )
                            }
                        }
                    },
                    onPaste = {
                        val clipboardText = clipboardManager.primaryClip?.getItemAt(0)?.text ?: ""
                        if (clipboardText.isNotEmpty()) {
                            val sel = textState.selection
                            val start = sel.min.coerceIn(0, textState.text.length)
                            val end = sel.max.coerceIn(0, textState.text.length)
                            val newText = textState.text.replaceRange(start, end, clipboardText)
                            val newCursorPos = start + clipboardText.length
                            textState =
                                textState.copy(text = newText, selection = TextRange(newCursorPos))
                        }
                    },
                    onQuickCrypt = {
                        btnGroup = StackGroup.NONE
                        if (isEncrypted) {
                            showCalc = false
                            showCalendarOverlay = false
                            isDecryptMode = true
                            showEncrypt = true
                        } else {
                            focusManager.clearFocus()
                            scope.launch {
                                quickPhrase = withContext(Dispatchers.IO) {
                                    runCatching {
                                        WedgeSecurity.generatePhrase(context.applicationContext, phraseSpec)
                                    }.getOrNull()
                                }
                            }
                        }
                    },
                    undoEnabled = undoHistory.isNotEmpty(),
                    onUndo = {
                        if (undoHistory.isNotEmpty()) {
                            textState = undoHistory.first()
                            undoHistory = undoHistory.drop(1)
                        }
                    }
                )
            }

            if (showDatePicker) {
                WedgeCalendarPicker(
                    onDateSelected = { ts ->
                        selectedDate = ts
                        calendarTimeStr = ""
                        calendarTextStr = ""
                        showCalc = false
                        showEncrypt = false
                        showCalendarOverlay = true
                        showDatePicker = false
                    },
                    onDismiss = { showDatePicker = false }
                )
            }
            if (showSync) {
                Box(
                    Modifier
                        .fillMaxSize()
                        .background(Color.Black.copy(alpha = 0.45f))
                        .clickable(
                            interactionSource = remember { MutableInteractionSource() },
                            indication = null
                        ) { showSync = false }
                )
            }
            if (showSync) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(12.dp)
                        .imePadding()
                        .clickable(
                            interactionSource = remember { MutableInteractionSource() },
                            indication = null
                        ) { showSync = false },
                    contentAlignment = Alignment.Center
                ) {
                    Column(
                        modifier = Modifier
                            .verticalScroll(rememberScrollState())
                            .clickable(
                                interactionSource = remember { MutableInteractionSource() },
                                indication = null
                            ) { }
                    ) {
                        SyncViewContent(vm, prefs)
                    }
                }
            }
        }
    }
    VersionDialog(showVersionDialog, { showVersionDialog = false }, uriHandler)
    linkPrompt?.let { (url, off) ->
        ConfirmDialog(
            text = "Open URL?",
            onConfirm = {
                linkPrompt = null
                runCatching { uriHandler.openUri(url) }
            },
            onDismiss = {
                linkPrompt = null
                linkCursor = off
            },
            confirmText = "Go",
            dismissText = "No"
        )
    }
    quickPhrase?.let { phrase ->
        PhraseDialog(
            phrase = phrase,
            onEncrypt = {
                quickPhrase = null
                val encrypted = WedgeSecurity.encrypt(textState.text, phrase)
                textState = textState.copy(text = encrypted, selection = TextRange(0))
                commitEncrypted(encrypted)
            },
            onDismiss = { quickPhrase = null }
        )
    }
    if (olderPrompt) {
        OlderDataDialog(
            onForce = { vm.confirmSendNoteText() },
            onDismiss = { vm.dismissOlderPrompt() }
        )
    }
}

private fun appendStrike(state: TextFieldValue): TextFieldValue {
    val txt = state.text
    val cursor = state.selection.start.coerceIn(0, txt.length)
    val lineStart = txt.lastIndexOf('\n', cursor - 1) + 1
    val lineEnd = txt.indexOf('\n', cursor).let { if (it == -1) txt.length else it }
    val line = txt.substring(lineStart, lineEnd)

    if (line.isBlank() || line.trimEnd().takeLastWhile { it in "*!~?" }.contains('~')) return state

    val insertAt = lineStart + line.trimEnd().length
    return state.copy(
        text = txt.substring(0, insertAt) + '~' + txt.substring(insertAt),
        selection = TextRange(if (cursor >= insertAt) cursor + 1 else cursor)
    )
}

private fun deleteLine(state: TextFieldValue): TextFieldValue {
    val txt = state.text
    val sel = state.selection
    val lineStart = txt.lastIndexOf('\n', sel.min - 1).let { if (it == -1) 0 else it + 1 }
    val lineEnd = txt.indexOf('\n', sel.max)
    val (newText, newCursor) = if (lineEnd == -1) {
        val removeStart = if (lineStart > 0) lineStart - 1 else 0
        txt.removeRange(removeStart, txt.length) to removeStart
    } else {
        txt.removeRange(lineStart, lineEnd + 1) to lineStart
    }
    return state.copy(
        text = newText,
        selection = TextRange(newCursor.coerceIn(0, newText.length))
    )
}

private fun moveLine(state: TextFieldValue, up: Boolean): TextFieldValue {
    val txt = state.text
    val cur = state.selection.start.coerceIn(0, txt.length)
    val start = txt.lastIndexOf('\n', cur - 1) + 1
    val end = txt.indexOf('\n', cur).let { if (it == -1) txt.length else it }
    val line = txt.substring(start, end)
    val col = cur - start

    val (moved, at) = if (up) {
        if (start == 0) return state
        val prevStart = txt.lastIndexOf('\n', start - 2) + 1
        val prev = txt.substring(prevStart, start - 1)
        (txt.substring(0, prevStart) + line + "\n" + prev + txt.substring(end)) to prevStart
    } else {
        if (end == txt.length) return state
        val nextEnd = txt.indexOf('\n', end + 1).let { if (it == -1) txt.length else it }
        val next = txt.substring(end + 1, nextEnd)
        (txt.substring(0, start) + next + "\n" + line + txt.substring(nextEnd)) to (start + next.length + 1)
    }
    return relist(state, moved, at, col)
}

// re-number list
private fun relist(state: TextFieldValue, txt: String, at: Int, col: Int): TextFieldValue {
    val plain = state.copy(text = txt, selection = TextRange(at + col))
    val row = txt.substring(0, at).count { it == '\n' }
    val lines = txt.split('\n').toMutableList()
    val old = lines[row]
    val head = WedgeRegex.LIST_TRIGGER.find(old) ?: return plain
    val ws = head.groupValues[1]
    fun item(i: Int) = WedgeRegex.LIST_TRIGGER.find(lines[i])?.takeIf { it.groupValues[1] == ws }
    var a = row
    while (a > 0 && item(a - 1) != null) a--
    var z = row
    while (z < lines.lastIndex && item(z + 1) != null) z++
    val base = (a..z).minOf { item(it)!!.groupValues[2].toIntOrNull() ?: return plain }
    for (i in a..z) {
        val m = item(i)!!
        lines[i] = "$ws${base + i - a}. " + lines[i].substring(m.range.last + 1)
    }
    val now = lines[row]
    val c = if (col >= head.range.last + 1) col + now.length - old.length
    else col.coerceAtMost(now.length)
    val rowStart = lines.take(row).sumOf { it.length + 1 }
    return state.copy(text = lines.joinToString("\n"), selection = TextRange(rowStart + c))
}

private fun urlAt(text: String, offset: Int): String? =
    WedgeRegex.URL.findAll(text).firstOrNull { offset in it.range }?.value

// span styling
private fun styleText(
    src: AnnotatedString,
    query: String,
    links: Boolean,
    focus: Color,
    url: Color,
    dim: Color
): TransformedText {
    val t = src.text
    val bold = SpanStyle(fontWeight = FontWeight.Bold, color = focus)
    val italic = SpanStyle(fontStyle = FontStyle.Italic)
    val under = SpanStyle(textDecoration = TextDecoration.Underline)
    val strike = SpanStyle(textDecoration = TextDecoration.LineThrough)
    val out = buildAnnotatedString {
        append(t)
        fun mark(r: Regex, s: SpanStyle, g: Int = 0) = r.findAll(t).forEach { m ->
            m.groups[g]?.let { addStyle(s, it.range.first, it.range.last + 1) }
        }
        mark(WedgeRegex.BOLD_LINE, bold)
        mark(WedgeRegex.BOLD_SCOPE, bold, 1)
        mark(WedgeRegex.ITALIC_LINE, italic)
        mark(WedgeRegex.ITALIC_SCOPE, italic, 1)
        mark(WedgeRegex.UNDERLINE_LINE, under, 1)
        mark(WedgeRegex.UNDERLINE_SCOPE, under, 1)
        mark(WedgeRegex.STRIKE_LINE, strike, 1)
        mark(WedgeRegex.STRIKE_SCOPE, strike, 1)
        mark(WedgeRegex.COMMENT_LINE, SpanStyle(color = dim))
        if (links) mark(
            WedgeRegex.URL,
            SpanStyle(color = url, textDecoration = TextDecoration.Underline)
        )
        if (query.isNotEmpty()) {
            var i = t.indexOf(query, 0, true)
            while (i >= 0) {
                addStyle(SpanStyle(background = SEARCH_HL), i, i + query.length)
                i = t.indexOf(query, i + query.length, true)
            }
        }
    }
    return TransformedText(out, OffsetMapping.Identity)
}