Pith - kerf
package com.vgmlr.kerf

import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.*
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material.icons.outlined.DataArray
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONObject

private val ACTION_ICON = 32.dp
private val ACTION_GLYPH_URL = 20.dp
private val ACTION_GLYPH_DEL = 22.dp
private val ACTIONS_LEAD = 4.dp
private val COMMENT_INNER = 12.dp
private const val COMMENT_DIM_ALPHA = 0.5f
private const val NUM_ALPHA = 0.6f
private val NUM_GAP = 6.dp
private val COMMENT_ICON = 14.dp
private val COMMENT_ICON_TOP = 13.dp
private val DEL_ACTIVE_BG = Color(0xFF880000)
private val REPLY_ACTIVE_BG = Color(0xFF6E8A80)
private val DEL_BTN_ICON = BTN_ICON + 2.dp
private val DEL_BTN_PAD = (BTN_SIZE - DEL_BTN_ICON) / 2
private val SAVE_ICON = BTN_ICON + 2.dp
private val SAVE_PAD = (BTN_SIZE - SAVE_ICON) / 2
private val SWEEP_ICON = BTN_ICON + 4.dp
private val SWEEP_PAD = (BTN_SIZE - SWEEP_ICON) / 2
private const val DELETE_TIMEOUT_MS = 3_000L
private val LIST_TOP = 20.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditScreen(id: Int, vm: KerfViewModel, onBack: () -> Unit) {
    var title by remember { mutableStateOf("") }
    var content by remember { mutableStateOf("") }
    var lastSavedText by remember { mutableStateOf("") }
    var focusIndex by remember { mutableIntStateOf(-1) }

    val dbNote by vm.observeNote(id).collectAsState(null)
    val context = LocalContext.current
    val scope = rememberCoroutineScope()

    val scroll = rememberScrollState()

    var showMenu by remember { mutableStateOf(false) }
    var showDataDialog by remember { mutableStateOf(false) }
    var deleteMode by remember { mutableStateOf(false) }
    var deleteTick by remember { mutableIntStateOf(0) }

    val isReplyMode = dbNote?.replies != null
    val blocks = remember(content) { KerfUtils.parseBlocks(content) }
    
    @Suppress("UNUSED_VARIABLE")
    val count = remember(content) { KerfUtils.countNumberedItems(content) }

    fun applyBlocks(next: List<KerfUtils.Block>) {
        val updated = KerfUtils.buildContent(next)
        content = updated
        lastSavedText = updated
        vm.updateNote(id, title, updated)
    }

    val repliesJson = remember(dbNote?.replies) {
        if (dbNote?.replies != null) JSONObject(dbNote!!.replies!!) else null
    }

    val exportLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.CreateDocument("text/plain")
    ) { uri ->
        uri?.let {
            try {
                context.contentResolver.openOutputStream(it)?.use { os ->
                    os.write(content.toByteArray())
                }
            } catch (_: Exception) {}
        }
    }

    BackHandler {
        vm.updateNoteImmediate(id, title, content)
        onBack()
    }

    LaunchedEffect(dbNote) {
        dbNote?.let { note ->
            if (lastSavedText.isEmpty() && content.isEmpty()) {
                title = note.title
                content = note.content
                lastSavedText = note.content
            } else if (note.content != lastSavedText) {
                content = note.content
                lastSavedText = note.content
            }
        }
    }

    LaunchedEffect(deleteMode, deleteTick) {
        if (!deleteMode) return@LaunchedEffect
        delay(DELETE_TIMEOUT_MS)
        deleteMode = false
    }

    Scaffold(
        containerColor = MaterialTheme.colorScheme.background,
        topBar = {
            Column {
                TopAppBar(
                    title = { },
                    colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
                    actions = {
                        Spacer(Modifier.width(BAR_EDGE))
                        KerfButton(Icons.AutoMirrored.Filled.ArrowBack, "Back") {
                            vm.updateNoteImmediate(id, title, content)
                            onBack()
                        }
                        Spacer(Modifier.width(BAR_GAP))
                        KerfBlankButton(Modifier.weight(1f)) {
                            BasicTextField(
                                value = title,
                                onValueChange = {
                                    if (it.length <= 50) {
                                        title = it
                                        vm.updateNote(id, it, content)
                                    }
                                },
                                modifier = Modifier.fillMaxWidth(),
                                singleLine = true,
                                textStyle = LocalTextStyle.current.copy(
                                    color = MaterialTheme.colorScheme.onPrimary,
                                    fontSize = KerfDimens.TextSizeTitle,
                                    textAlign = TextAlign.End
                                ),
                                cursorBrush = SolidColor(MaterialTheme.colorScheme.onPrimary),
                                decorationBox = { inner ->
                                    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
                                        if (title.isEmpty()) {
                                            Text(
                                                text = "Untitled",
                                                color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.4f),
                                                fontSize = KerfDimens.TextSizeTitle,
                                                maxLines = 1
                                            )
                                        }
                                        inner()
                                    }
                                }
                            )
                        }
                        Spacer(Modifier.width(BAR_GAP))
                        KerfButton(
                            Icons.Default.Share,
                            if (isReplyMode) "Share Replies" else "Share",
                            iconModifier = if (isReplyMode) {
                                Modifier.offset(x = 1.dp).graphicsLayer { scaleX = -1f }
                            } else Modifier
                        ) {
                            if (isReplyMode) {
                                KerfUtils.shareReplies(context, title, dbNote?.replies)
                            } else {
                                val header = "Kerf: ${title.ifBlank { "Untitled" }}\n\n"
                                shareNote(context, header + content)
                                vm.updateLastShared(id)
                            }
                        }
                        Spacer(Modifier.width(BAR_GAP))
                        KerfButton(
                            if (deleteMode) Icons.Filled.Clear else Icons.Outlined.Clear,
                            if (deleteMode) "Done" else "Delete Items",
                            iconSize = DEL_BTN_ICON,
                            iconPad = DEL_BTN_PAD,
                            bg = if (deleteMode) DEL_ACTIVE_BG else null
                        ) { deleteMode = !deleteMode }
                        Spacer(Modifier.width(BAR_GAP))
                        Box {
                            KerfMenuTrigger { showMenu = true }
                            KerfDeploy(
                                expanded = showMenu,
                                onDismiss = { showMenu = false },
                                items = listOf(
                                    KerfDeployItem(
                                        Icons.AutoMirrored.Filled.ReplyAll,
                                        if (isReplyMode) "Collapse Replies" else "Reply",
                                        bg = if (isReplyMode) REPLY_ACTIVE_BG else null
                                    ) {
                                        showMenu = false
                                        vm.toggleReplyMode(id, title, content)
                                    },
                                    KerfDeployItem(
                                        if (dbNote?.isPinned == true) Icons.Filled.PushPin else Icons.Outlined.PushPin,
                                        if (dbNote?.isPinned == true) "Unpin" else "Pin"
                                    ) { vm.togglePin(id); showMenu = false },
                                    KerfDeployItem(
                                        Icons.Outlined.Save,
                                        "Export",
                                        iconSize = SAVE_ICON,
                                        iconPad = SAVE_PAD
                                    ) {
                                        val fileName = "${title.ifBlank { "Untitled" }.replace(" ", "_")}.kerf"
                                        exportLauncher.launch(fileName)
                                        showMenu = false
                                    },
                                    KerfDeployItem(Icons.Outlined.DataArray, "Data") {
                                        showDataDialog = true; showMenu = false
                                    },
                                    KerfDeployItem(
                                        Icons.Outlined.DeleteSweep,
                                        "Clear",
                                        iconSize = SWEEP_ICON,
                                        iconPad = SWEEP_PAD
                                    ) {
                                        val clearedContent = KerfUtils.DEFAULT_PREFIX
                                        content = clearedContent
                                        lastSavedText = clearedContent
                                        vm.clearNoteAndSharedDate(id)
                                        showMenu = false
                                    }
                                )
                            )
                        }
                        Spacer(Modifier.width(BAR_EDGE_ACTIONS))
                    }
                )
            }
        }
    ) { p ->
        Box(Modifier
            .fillMaxSize()
            .navigationBarsPadding()
            .imePadding()) {
            Column(Modifier
                .fillMaxSize()
                .verticalScroll(scroll)
                .padding(top = p.calculateTopPadding() + LIST_TOP - (TOP_BAR_H - BTN_SIZE) / 2, bottom = 100.dp)) {
                blocks.forEachIndexed { index, block ->
                    val num = index + 1

                    ItemRow(
                        num = num,
                        text = block.header,
                        hasUrl = KerfUtils.UrlRegex.containsMatchIn(block.header),
                        deleteMode = deleteMode,
                        readOnly = isReplyMode,
                        autoFocus = index == focusIndex,
                        onFocused = { focusIndex = -1 },
                        onChange = { applyBlocks(blocks.replaced(index, block.copy(header = it))) },
                        onOpenUrl = {
                            KerfUtils.UrlRegex.find(block.header)?.value?.let { KerfUtils.openAddress(context, it) }
                        },
                        onDelete = {
                            val original = content
                            val updated = KerfUtils.deleteAndReindex(original, block.lineIndex)
                            content = updated
                            lastSavedText = updated
                            vm.deleteLineWithReplySync(id, title, original, block.lineIndex)
                            deleteTick++
                        }
                    )

                    Spacer(Modifier.height(4.dp))

                    if (!isReplyMode || block.comment.isNotBlank()) {
                        Spacer(Modifier.height(2.dp))
                        CommentRow(
                            num = num,
                            text = block.comment,
                            filled = !isReplyMode,
                            readOnly = isReplyMode,
                            onChange = { applyBlocks(blocks.replaced(index, block.copy(comment = it))) }
                        )
                        Spacer(Modifier.height(6.dp))
                    }

                    if (isReplyMode) {
                        Spacer(Modifier.height(6.dp))
                        ReplyField(
                            itemNum = num,
                            initialText = repliesJson?.optString(num.toString(), "") ?: "",
                            onUpdate = { vm.updateReply(id, num, it) }
                        )
                    }

                    Spacer(Modifier.height(16.dp))
                }
                Row(
                    Modifier
                        .fillMaxWidth()
                        .padding(horizontal = KerfDimens.PaddingLarge),
                    horizontalArrangement = Arrangement.End
                ) {
                    IconButton(
                        onClick = {
                            val next = blocks + KerfUtils.Block("", "", 0)
                            applyBlocks(next)
                            focusIndex = next.lastIndex
                            scope.launch {
                                delay(100)
                                scroll.animateScrollTo(scroll.maxValue)
                            }
                        },
                        modifier = Modifier.size(ACTION_ICON)
                    ) {
                        Icon(
                            Icons.Default.Add,
                            contentDescription = "Add Item",
                            modifier = Modifier.size(ACTION_GLYPH_DEL + 1.dp),
                            tint = MaterialTheme.colorScheme.onPrimary.copy(0.5f)
                        )
                    }
                }
            }
        }
    }
    if (showDataDialog) {
        DataDialog(
            createdAt = dbNote?.createdAt,
            updatedAt = dbNote?.updatedAt,
            lastShared = dbNote?.lastShared,
            onDismiss = { showDataDialog = false }
        )
    }
}

@Composable
fun LineActions(modifier: Modifier, hasUrl: Boolean, deleteMode: Boolean, onOpenUrl: () -> Unit, onDelete: () -> Unit) {
    val active = deleteMode || hasUrl
    Box(
        modifier
            .size(ACTION_ICON)
            .clip(RoundedCornerShape(ROUND_END))
            .then(if (active) Modifier.clickable(onClick = if (deleteMode) onDelete else onOpenUrl) else Modifier),
        contentAlignment = Alignment.Center
    ) {
        if (deleteMode) {
            Icon(Icons.Default.Clear, null, modifier = Modifier.size(ACTION_GLYPH_DEL), tint = MaterialTheme.colorScheme.onPrimary.copy(0.5f))
        } else if (hasUrl) {
            Icon(Icons.AutoMirrored.Filled.OpenInNew, null, modifier = Modifier.size(ACTION_GLYPH_URL), tint = MaterialTheme.colorScheme.onPrimary.copy(0.5f))
        }
    }
}

private fun <T> List<T>.replaced(index: Int, value: T) = toMutableList().also { it[index] = value }

@Composable
private fun Modifier.backspaceGuard(): Modifier {
    var last by remember { mutableLongStateOf(0L) }
    return onPreviewKeyEvent { event ->
        if (event.key == Key.Backspace && event.type == KeyEventType.KeyDown) {
            val now = System.currentTimeMillis()
            if (now - last < 70) true else { last = now; false }
        } else false
    }
}

@Composable
private fun ItemRow(
    num: Int,
    text: String,
    hasUrl: Boolean,
    deleteMode: Boolean,
    readOnly: Boolean,
    autoFocus: Boolean,
    onFocused: () -> Unit,
    onChange: (String) -> Unit,
    onOpenUrl: () -> Unit,
    onDelete: () -> Unit
) {
    var value by remember { mutableStateOf(TextFieldValue(text, TextRange(text.length))) }
    val focus = remember { FocusRequester() }

    LaunchedEffect(text) {
        if (text != value.text.trim()) value = TextFieldValue(text, TextRange(text.length))
    }
    LaunchedEffect(autoFocus) {
        if (!autoFocus) return@LaunchedEffect
        withFrameNanos { }
        runCatching { focus.requestFocus() }
        onFocused()
    }

    Row(
        Modifier
            .fillMaxWidth()
            .padding(horizontal = KerfDimens.PaddingLarge),
        verticalAlignment = Alignment.Top
    ) {
        Text(
            text = "$num.",
            color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = NUM_ALPHA),
            fontSize = KerfDimens.TextSizeLarge,
            fontWeight = FontWeight.Medium,
            modifier = Modifier.padding(top = 6.dp, end = NUM_GAP)
        )
        BasicTextField(
            value = value,
            onValueChange = { new ->
                val clean = new.text.replace("\n", "")
                value = if (clean == new.text) new else new.copy(text = clean, selection = TextRange(clean.length))
                onChange(clean)
            },
            readOnly = readOnly,
            modifier = Modifier
                .weight(1f)
                .padding(top = 6.dp)
                .focusRequester(focus)
                .backspaceGuard(),
            textStyle = LocalTextStyle.current.copy(
                color = MaterialTheme.colorScheme.onSurfaceVariant,
                lineBreak = LineBreak.Paragraph,
                fontSize = KerfDimens.TextSizeLarge,
                lineHeight = 24.sp,
                platformStyle = PlatformTextStyle(includeFontPadding = false)
            ),
            cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface)
        )
        LineActions(Modifier.padding(start = ACTIONS_LEAD), hasUrl, deleteMode, onOpenUrl, onDelete)
    }
}

@Composable
private fun CommentRow(num: Int, text: String, filled: Boolean, readOnly: Boolean, onChange: (String) -> Unit) {
    var value by remember { mutableStateOf(TextFieldValue(text, TextRange(text.length))) }
    val textAlpha = if (filled) 1f else 0.7f
    val measurer = rememberTextMeasurer()
    val numW = with(LocalDensity.current) {
        measurer.measure("$num.", LocalTextStyle.current.copy(fontSize = KerfDimens.TextSizeLarge, fontWeight = FontWeight.Bold)).size.width.toDp()
    }
    
    LaunchedEffect(text) {
        if (text != value.text.trimEnd('\n')) value = TextFieldValue(text, TextRange(text.length))
    }

    Row(
        Modifier
            .fillMaxWidth()
            .background(MaterialTheme.colorScheme.surface.copy(alpha = if (filled) 1f else COMMENT_DIM_ALPHA)),
        verticalAlignment = Alignment.Top
    ) {
        Box(
            Modifier
                .padding(start = KerfDimens.PaddingLarge, top = COMMENT_ICON_TOP)
                .width(numW + NUM_GAP),
            contentAlignment = Alignment.TopStart
        ) {
            Icon(
                Icons.AutoMirrored.Filled.ArrowForward,
                null,
                modifier = Modifier.size(COMMENT_ICON),
                tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = NUM_ALPHA * textAlpha)
            )
        }
        BasicTextField(
            value = value,
            onValueChange = { value = it; onChange(it.text) },
            readOnly = readOnly,
            modifier = Modifier
                .weight(1f)
                .padding(
                    end = COMMENT_INNER,
                    top = 8.dp,
                    bottom = 8.dp
                )
                .backspaceGuard(),
            textStyle = LocalTextStyle.current.copy(
                color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = textAlpha),
                lineBreak = LineBreak.Paragraph,
                fontSize = KerfDimens.TextSizeLarge,
                lineHeight = 24.sp,
                platformStyle = PlatformTextStyle(includeFontPadding = false)
            ),
            cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface)
        )
    }
}

@Composable
fun ReplyField(itemNum: Int, initialText: String, onUpdate: (String) -> Unit) {
    var textValue by remember(itemNum) {
        mutableStateOf(TextFieldValue(initialText.ifEmpty { "$itemNum. " }))
    }

    LaunchedEffect(initialText) {
        if (textValue.text != initialText && initialText.isNotEmpty()) {
            textValue = textValue.copy(text = initialText)
        }
    }

    Box(Modifier
        .fillMaxWidth()
        .background(MaterialTheme.colorScheme.surface)) {
        BasicTextField(
            value = textValue,
            onValueChange = { newValue ->
                var v = newValue
                val prefix = "$itemNum. "
                if (!v.text.startsWith(prefix)) {
                    v = v.copy(text = prefix, selection = TextRange(prefix.length))
                }
                textValue = v
                onUpdate(v.text)
            },
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 16.dp, vertical = 8.dp)
                .backspaceGuard(),
            textStyle = LocalTextStyle.current.copy(
                color = MaterialTheme.colorScheme.onSurfaceVariant,
                fontSize = KerfDimens.TextSizeLarge,
                lineHeight = 24.sp,
                platformStyle = PlatformTextStyle(includeFontPadding = false)
            ),
            cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface)
        )
    }
}