Pith - shim
package com.vgmlr.shim

import android.content.Intent
import android.graphics.Color as AndroidColor
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Lightbulb
import androidx.compose.material.icons.outlined.Park
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.material.icons.automirrored.outlined.Login
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.TextRange
import android.content.ClipData
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalUriHandler

private const val IME_GRACE = 600L
private const val IME_WAIT = 2500L
private const val IME_SETTLE = 250L
private const val SAVE_HOLD = 2000L

class ShimMainActivity : ComponentActivity() {
    private lateinit var db: ShimDatabase
    private lateinit var themeManager: ShimThemeManager
    private var shimListState = mutableStateOf<List<ShimClass>>(emptyList())
    private val tagSetsState = mutableStateOf<List<String>>(emptyList())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        window.setBackgroundDrawableResource(R.color.background_color)
        db = ShimDatabase(this)
        themeManager = ShimThemeManager(this)

        enableEdgeToEdge(
            statusBarStyle = SystemBarStyle.dark(AndroidColor.TRANSPARENT),
            navigationBarStyle = SystemBarStyle.dark(AndroidColor.TRANSPARENT)
        )

        setContent {
            ShimTheme(themeManager) {
                MainScreen()
            }
        }
    }

    override fun onResume() {
        super.onResume()
        themeManager.refresh()
        refreshList()
    }

    private fun refreshList() {
        shimListState.value = db.getAllShims()
        tagSetsState.value = db.getTagSets()
    }

    @OptIn(
        ExperimentalMaterial3Api::class,
        ExperimentalFoundationApi::class,
        ExperimentalLayoutApi::class
    )
    @Composable
    fun MainScreen() {
        val lifecycleOwner = LocalLifecycleOwner.current
        val windowInfo = LocalWindowInfo.current
        var searchQuery by remember { mutableStateOf("") }
        var isSearchExpanded by remember { mutableStateOf(false) }
        val imeState = rememberUpdatedState(WindowInsets.isImeVisible)
        var resumeTick by remember { mutableIntStateOf(0) }
        var showTagsDialog by remember { mutableStateOf(false) }

        var showVersionDialog by remember { mutableStateOf(false) }
        var saveStatus by remember { mutableStateOf<String?>(null) }
        val scope = rememberCoroutineScope()
        val listState = rememberLazyListState()
        var shimText by remember { mutableStateOf("") }

        var hashtagText by remember { mutableStateOf(TextFieldValue("")) }
        var editId by remember { mutableStateOf<Int?>(null) }
        val allTags = remember(tagSetsState.value) {
            tagSetsState.value.flatMap {
                it.split(" ").filter { tag -> tag.isNotBlank() }
            }.distinct().sorted()
        }
        val topTags = remember(tagSetsState.value) {
            tagSetsState.value.flatMap {
                it.split(" ").filter { tag -> tag.isNotBlank() }
            }
                .groupingBy { it }
                .eachCount()
                .entries
                .sortedByDescending { it.value }
                .take(5)
                .map { it.key }
        }
        val tagAssociations = remember(tagSetsState.value) {
            val associations = mutableMapOf<String, MutableSet<String>>()
            tagSetsState.value.forEach { set ->
                val tags = set.split(" ").filter { it.isNotBlank() }
                tags.forEach { tag ->
                    val associated = associations.getOrPut(tag) { mutableSetOf() }
                    associated.addAll(tags.filter { it != tag })
                }
            }
            associations.mapValues { it.value.sorted() }
        }

        var marked by remember { mutableStateOf(emptySet<Int>()) }
        var armDel by remember { mutableStateOf(false) }
        val markedShims = shimListState.value.filter { it.id in marked }
        val rail = remember(shimListState.value) { railFor(db.maxId()) }
        val clipboard = LocalClipboard.current
        val uriHandler = LocalUriHandler.current

        fun reload() {
            shimListState.value =
                if (searchQuery.isEmpty()) db.getAllShims()
                else db.searchShims(searchQuery)
        }

        fun save(subset: List<ShimClass>?, keyword: String? = null) {
            scope.launch {
                saveStatus = if (ShimSave.save(
                        this@ShimMainActivity,
                        db,
                        subset,
                        keyword
                    )
                ) "Saved" else "Failed"
            }
        }

        LaunchedEffect(saveStatus) {
            if (saveStatus == null) return@LaunchedEffect
            delay(SAVE_HOLD)
            saveStatus = null
        }

        LaunchedEffect(shimListState.value) {
            val live = shimListState.value.mapTo(HashSet()) { it.id }
            if (marked.any { it !in live }) marked = marked.filterTo(HashSet()) { it in live }
        }

        LaunchedEffect(marked) { armDel = false }

        LaunchedEffect(armDel) {
            if (!armDel) return@LaunchedEffect
            delay(ARM_HOLD)
            armDel = false
        }

        LaunchedEffect(isSearchExpanded) {
            if (!isSearchExpanded || editId == null) return@LaunchedEffect
            editId = null
            shimText = ""
            hashtagText = TextFieldValue("")
        }

        BackHandler(enabled = marked.isNotEmpty()) { marked = emptySet() }

        DisposableEffect(lifecycleOwner) {
            val observer = LifecycleEventObserver { _, event ->
                if (event == Lifecycle.Event.ON_RESUME) {
                    resumeTick++
                    isSearchExpanded = false
                    searchQuery = ""
                    marked = emptySet()
                }
            }
            lifecycleOwner.lifecycle.addObserver(observer)
            onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
        }

        LaunchedEffect(isSearchExpanded, resumeTick) {
            if (isSearchExpanded) return@LaunchedEffect
            snapshotFlow { windowInfo.isWindowFocused }.first { it }
            delay(IME_GRACE)
            withTimeoutOrNull(IME_WAIT) {
                snapshotFlow { imeState.value }.first { it }
            } ?: return@LaunchedEffect
            snapshotFlow { imeState.value }.first { !it }
            delay(IME_SETTLE)
            if (!imeState.value) isSearchExpanded = true
        }

        val importLauncher =
            rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
                uri?.let {
                    if (db.importFile(this@ShimMainActivity, it)) refreshList()
                    else Toast.makeText(
                        this@ShimMainActivity,
                        "Unsupported file",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }

        val topInset = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()

        Box(modifier = Modifier.fillMaxSize()) {
            Scaffold(
                contentWindowInsets = WindowInsets(0, 0, 0, 0)
            ) { p ->
                val navBottom =
                    WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
                val listTop = topInset + BAR_GAP + SEARCH_FIELD_H
                LazyColumn(
                    state = listState,
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(bottom = p.calculateBottomPadding())
                        .background(themeManager.backgroundColor.value)
                        .drawBehind {
                            val x = rail.x.toPx()
                            drawLine(
                                color = themeManager.textColor.value.copy(alpha = RAIL_ALPHA),
                                start = Offset(x, 0f),
                                end = Offset(x, size.height),
                                strokeWidth = RAIL_W.toPx()
                            )
                        }
                        .clickable(
                            interactionSource = remember { MutableInteractionSource() },
                            indication = null
                        ) { marked = emptySet() },
                    contentPadding = PaddingValues(
                        top = listTop + LIST_PAD_TOP,
                        bottom = navBottom + LIST_PAD_V + BTN_SIZE + DOCK_PAD * 2
                    )
                ) {
                    items(shimListState.value, key = { it.id }) { shim ->
                        ShimItem(
                            shim = shim,
                            themeManager = themeManager,
                            railX = rail.x,
                            railGap = rail.gap,
                            isSelected = shim.id in marked,
                            onHashtagClick = { tag ->
                                searchQuery = tag
                                isSearchExpanded = true
                                shimListState.value = db.searchShims(tag)
                            },
                            onNumClick = { marked = marked.toggle(shim.id) }
                        )
                    }
                }
            }

            Row(
                modifier = Modifier
                    .align(Alignment.TopCenter)
                    .fillMaxWidth()
                    .padding(top = topInset + BAR_GAP, start = BAR_EDGE, end = BAR_EDGE_ACTIONS),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.spacedBy(BAR_GAP)
            ) {
                Box(Modifier.weight(1f)) {
                    SearchField(
                        query = searchQuery,
                        status = saveStatus,
                        onQueryChange = {
                            searchQuery = it
                            shimListState.value =
                                if (it.isEmpty()) db.getAllShims() else db.searchShims(it)
                        },
                        onClose = {
                            isSearchExpanded = false
                            searchQuery = ""
                            refreshList()
                        },
                        onDownload = {
                            save(shimListState.value, searchQuery)
                        }
                    )
                }
                ShimButton(Icons.Default.Tag, "Tags") { showTagsDialog = true }
                Box {
                    var expanded by remember { mutableStateOf(false) }
                    ShimButton(
                        Icons.Outlined.Lightbulb,
                        "Menu",
                        iconSize = BTN_ICON + 2.dp,
                        iconPad = BTN_PAD - 1.dp
                    ) { expanded = true }
                    ShimMenu(
                        expanded = expanded,
                        onDismiss = { expanded = false }
                    ) {
                        ShimMenuItem("Import", Icons.AutoMirrored.Outlined.Login) {
                            importLauncher.launch("*/*")
                            expanded = false
                        }
                        ShimMenuItem(
                            "Save",
                            Icons.Outlined.Save,
                            iconSize = BTN_ICON + 2.dp,
                            iconPad = BTN_PAD - 1.dp
                        ) {
                            save(null)
                            expanded = false
                        }
                        ShimMenuItem("Settings", Icons.Outlined.Settings) {
                            startActivity(Intent(this@ShimMainActivity, ShimSettings::class.java))
                            expanded = false
                        }
                        ShimMenuItem("Version", Icons.Outlined.Park) {
                            showVersionDialog = true
                            expanded = false
                        }
                        ShimMenuItem("Exit", Icons.Default.Block, enabled = false) {}
                    }
                }
            }

            if (markedShims.isNotEmpty()) {
                val single = markedShims.singleOrNull()
                ShimDock {
                    ShimButton(Icons.Default.Create, "Edit", enabled = single != null) {
                        single?.let { s ->
                            editId = s.id
                            shimText = s.shimtext
                            hashtagText = TextFieldValue(s.shimhash, TextRange(s.shimhash.length))
                            searchQuery = ""
                            refreshList()
                            isSearchExpanded = false
                        }
                        marked = emptySet()
                    }
                    ShimButton(
                        Icons.Default.ContentCopy,
                        "Copy",
                        iconSize = BTN_ICON_SM,
                        enabled = single != null
                    ) {
                        single?.let { s ->
                            scope.launch {
                                clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("shim", s.shimtext)))
                            }
                        }
                        marked = emptySet()
                    }
                    ShimButton(Icons.Default.Share, "Share", enabled = single != null) {
                        single?.let { s ->
                            startActivity(
                                Intent.createChooser(
                                    Intent(Intent.ACTION_SEND).apply {
                                        type = "text/plain"
                                        putExtra(Intent.EXTRA_TEXT, s.shimtext)
                                    },
                                    null
                                )
                            )
                        }
                        marked = emptySet()
                    }
                    ShimButton(
                        if (armDel) Icons.Outlined.WarningAmber else Icons.Outlined.Delete,
                        "Delete",
                        iconSize = BTN_ICON_LG,
                        iconPad = BTN_PAD_LG,
                        bg = if (armDel) WARN_BG else Color.Unspecified
                    ) {
                        if (armDel) {
                            db.deleteShims(markedShims.map { it.id })
                            marked = emptySet()
                            reload()
                        } else armDel = true
                    }
                }
            }          

            if (!isSearchExpanded) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(Color.Black.copy(alpha = 0.8f))
                        .clickable { isSearchExpanded = true }
                )
            }

            if (!isSearchExpanded) {
                Box(
                    modifier = Modifier
                        .align(Alignment.BottomCenter)
                        .navigationBarsPadding()
                        .imePadding()
                ) {
                    InputBar(
                        shimText = shimText,
                        onShimTextChange = { shimText = it },
                        hashtagText = hashtagText,
                        allTags = allTags,
                        topTags = topTags,
                        tagAssociations = tagAssociations,
                        onHashtagTextChange = { hashtagText = it },
                        onAdd = { text, tags ->
                            val id = editId
                            if (id == null) db.addShim(text, tags.text, ShimOTCClock.getCurrentOTCTime())
                            else db.updateShim(id, text, tags.text)
                            editId = null
                            shimText = ""
                            hashtagText = TextFieldValue("")
                            refreshList()
                            val at = if (id == null) 0
                            else shimListState.value.indexOfFirst { it.id == id }.coerceAtLeast(0)
                            scope.launch { listState.scrollToItem(at) }
                        },
                        themeManager = themeManager
                    )
                }
            }

            if (showTagsDialog) {
                TagsDialog(
                    tagSets = tagSetsState.value,
                    onDismiss = { showTagsDialog = false },
                    onTagClick = { tag ->
                        searchQuery = tag
                        isSearchExpanded = true
                        shimListState.value = db.searchShims(tag)
                    }
                )
            }

            VersionDialog(
                show = showVersionDialog,
                onDismiss = { showVersionDialog = false },
                uriHandler = uriHandler
            )
        }
    }
}

private fun Set<Int>.toggle(id: Int): Set<Int> = if (id in this) this - id else this + id