Pith - shim
package com.vgmlr.shim

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardReturn
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Tag
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Lightbulb
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.platform.LocalDensity

private val SUGGEST_EDGE = 12.dp

private const val SUGGEST_Y = -70
private val TAG_GAP = 6.dp
private val TAG_PAD = PaddingValues(horizontal = 16.dp, vertical = 6.dp)
private val LIST_TAG_PAD = PaddingValues(horizontal = 10.dp, vertical = 2.dp)
private const val LIST_TAG_ALPHA = 0.9f

private val FIELD_ICON = 28.dp
private const val FIELD_ICON_ALPHA = 0.6f
private val FIELD_ICON_NUDGE = 0.dp
private val INPUT_DIVIDER = 1.dp
private val INPUT_ROUND = 6.dp

private const val FOCUS_DELAY = 120L
private const val SHOW_RETRY = 250L
private const val SHOW_TRIES = 4

private val SEARCH_ICON = 26.dp
private val SEARCH_ICON_PAD = 14.dp
private val SEARCH_TEXT_START = 48.dp
private val SEARCH_TEXT_END = 48.dp
private val SEARCH_TEXT_END_DL = 84.dp // with download
private val SEARCH_FS = 17.sp

private val SHIM_PAD = 20.dp

private const val NUM_ALPHA = 0.5f
private val NUM_SEL = Color(0xFFCCCCCC)
private val NUM_FS = 12.sp
private val NUM_PAD = PaddingValues(horizontal = 8.dp, vertical = 2.dp)
private val NUM_DY = 4.dp
internal const val NUM_CHIP_ALPHA = 0.3f

private const val DATE_ALPHA = 0.3f
private const val DATE_SHRINK = 1f

internal val RAIL_W = 1.dp
internal const val RAIL_ALPHA = 0.4f
private val RAIL_X_BASE = 22.dp
private val RAIL_GAP_BASE = 26.dp
private val RAIL_STEP = 2.dp

internal data class Rail(val x: Dp, val gap: Dp)

internal fun railFor(maxId: Int): Rail {
    val steps = (maxId.toString().length - 2).coerceAtLeast(0)
    return Rail(RAIL_X_BASE + RAIL_STEP * steps, RAIL_GAP_BASE + RAIL_STEP * steps)
}

@OptIn(
    ExperimentalMaterial3Api::class,
    ExperimentalComposeUiApi::class,
    ExperimentalLayoutApi::class
)
@Composable
fun InputBar(
    shimText: String,
    themeManager: ShimThemeManager,
    onShimTextChange: (String) -> Unit,
    hashtagText: TextFieldValue,
    allTags: List<String>,
    topTags: List<String>,
    tagAssociations: Map<String, List<String>>,
    onHashtagTextChange: (TextFieldValue) -> Unit,
    onAdd: (String, TextFieldValue) -> Unit
) {
    val focusRequester = remember { FocusRequester() }
    val focusManager = LocalFocusManager.current
    val keyboard = LocalSoftwareKeyboardController.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val windowInfo = LocalWindowInfo.current
    val imeVisible = rememberUpdatedState(WindowInsets.isImeVisible)
    var showSuggestions by remember { mutableStateOf(false) }
    var lastSubmittedText by remember { mutableStateOf("") }
    var resumeKey by remember { mutableIntStateOf(0) }
    val words = hashtagText.text.split(" ")
    val currentWord = words.lastOrNull() ?: ""
    val previousWord = if (words.size >= 2) words[words.size - 2] else ""
    val canAdd = shimText.isNotBlank() && hashtagText.text.isNotBlank()

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_PAUSE -> focusManager.clearFocus(true)
                Lifecycle.Event.ON_RESUME -> resumeKey++
                else -> Unit
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }

    LaunchedEffect(resumeKey) {
        snapshotFlow { windowInfo.isWindowFocused }.first { it }
        delay(FOCUS_DELAY)
        repeat(SHOW_TRIES) {
            focusRequester.requestFocus()
            keyboard?.show()
            delay(SHOW_RETRY)
            if (imeVisible.value) return@LaunchedEffect
        }
    }

    val inputBg = colorResource(id = R.color.input_bg_color)

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .inputFrame(inputBg)
    ) {
        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            TextField(
                value = shimText,
                onValueChange = onShimTextChange,
                placeholder = {
                    Text(
                        "Logic",
                        color = colorResource(id = R.color.input_color).copy(alpha = 0.5f)
                    )
                },
                leadingIcon = {
                    Icon(
                        Icons.Outlined.Lightbulb,
                        contentDescription = null,
                        tint = colorResource(id = R.color.input_icon_color).copy(alpha = FIELD_ICON_ALPHA),
                        modifier = Modifier
                            .size(FIELD_ICON)
                            .offset(x = FIELD_ICON_NUDGE)
                    )
                },
                textStyle = TextStyle(
                    fontSize = 17.sp,
                    lineHeight = (themeManager.fontSize.floatValue * (themeManager.lineHeight.floatValue + themeManager.lineSpacing.floatValue)).sp,
                    color = colorResource(id = R.color.input_color)
                ),
                modifier = Modifier
                    .weight(1f)
                    .focusRequester(focusRequester)
                    .padding(horizontal = 8.dp, vertical = 4.dp),
                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
                keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
                colors = TextFieldDefaults.colors(
                    focusedContainerColor = Color.Transparent,
                    unfocusedContainerColor = Color.Transparent,
                    focusedTextColor = colorResource(id = R.color.input_color),
                    unfocusedTextColor = colorResource(id = R.color.input_color),
                    cursorColor = colorResource(id = R.color.input_color),
                    focusedIndicatorColor = Color.Transparent,
                    unfocusedIndicatorColor = Color.Transparent
                )
            )
        }
        HorizontalDivider(
            modifier = Modifier.fillMaxWidth(),
            thickness = INPUT_DIVIDER,
            color = themeManager.backgroundColor.value
        )
        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(modifier = Modifier.weight(1f)) {
                TextField(
                    value = hashtagText,
                    onValueChange = {
                        if (lastSubmittedText.isNotEmpty() && it.text == lastSubmittedText) {
                            return@TextField
                        }
                        if (it.text != lastSubmittedText && it.text.isNotEmpty()) {
                            lastSubmittedText = ""
                        }
                        if (it.text.length <= 70) {
                            val sanitized = it.copy(text = it.text.replace("#", ""))
                            onHashtagTextChange(sanitized)
                            val newWords = sanitized.text.split(" ")
                            val ongoingWord = newWords.lastOrNull() ?: ""
                            val prevWord =
                                if (newWords.size >= 2) newWords[newWords.size - 2] else ""

                            val isTyping = ongoingWord.isNotBlank()
                            val hasAssociations =
                                ongoingWord.isEmpty() && prevWord.isNotBlank() && !tagAssociations[prevWord].isNullOrEmpty()

                            showSuggestions =
                                isTyping || hasAssociations || sanitized.text.isBlank()
                        }
                    },
                    placeholder = {
                        Text(
                            "Tags",
                            color = colorResource(id = R.color.input_color).copy(alpha = 0.5f)
                        )
                    },
                    leadingIcon = {
                        Icon(
                            Icons.Default.Tag,
                            contentDescription = null,
                            tint = colorResource(id = R.color.input_icon_color).copy(alpha = FIELD_ICON_ALPHA),
                            modifier = Modifier
                                .size(FIELD_ICON)
                                .offset(x = FIELD_ICON_NUDGE)
                        )
                    },
                    modifier = Modifier
                        .fillMaxWidth()
                        .onFocusChanged { state ->
                            showSuggestions = state.isFocused && hashtagText.text.isBlank()
                        }
                        .padding(horizontal = 8.dp, vertical = 4.dp),
                    textStyle = TextStyle(
                        color = colorResource(id = R.color.input_color),
                        lineHeight = (themeManager.fontSize.floatValue * (themeManager.lineHeight.floatValue + themeManager.lineSpacing.floatValue)).sp,
                        fontSize = 17.sp
                    ),
                    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
                    keyboardActions = KeyboardActions(onDone = {
                        if (canAdd) {
                            lastSubmittedText = hashtagText.text
                            onAdd(shimText, hashtagText)
                            focusRequester.requestFocus()
                        }
                    }),
                    colors = TextFieldDefaults.colors(
                        focusedContainerColor = Color.Transparent,
                        unfocusedContainerColor = Color.Transparent,
                        focusedTextColor = colorResource(id = R.color.input_color),
                        unfocusedTextColor = colorResource(id = R.color.input_color),
                        cursorColor = colorResource(id = R.color.input_color),
                        focusedIndicatorColor = Color.Transparent,
                        unfocusedIndicatorColor = Color.Transparent
                    ),
                    singleLine = true
                )

                if (showSuggestions) {
                    val suggestions = if (currentWord.isNotBlank()) {
                        allTags.filter {
                            it.startsWith(
                                currentWord,
                                ignoreCase = true
                            ) && it != currentWord
                        }
                    } else if (previousWord.isNotBlank()) {
                        val alreadyTypedTags = words.filter { it.isNotBlank() }.toSet()
                        tagAssociations[previousWord]?.filter { it !in alreadyTypedTags }
                            ?: emptyList()
                    } else {
                        topTags
                    }
                    if (suggestions.isNotEmpty()) {
                        val scrollState = rememberScrollState()
                        val maxRowW = with(LocalDensity.current) {
                            windowInfo.containerSize.width.toDp()
                        }

                        LaunchedEffect(suggestions) { scrollState.scrollTo(0) }

                        Popup(alignment = Alignment.TopStart, offset = IntOffset(0, SUGGEST_Y)) {
                            Row(
                                modifier = Modifier
                                    .widthIn(max = maxRowW)
                                    .horizontalScroll(scrollState)
                                    .padding(horizontal = SUGGEST_EDGE),
                                horizontalArrangement = Arrangement.spacedBy(TAG_GAP)
                            ) {
                                suggestions.take(if (hashtagText.text.isBlank()) 5 else 3)
                                    .forEach { tag ->
                                        TagChip(tag) {
                                            val textWithoutLastWord =
                                                hashtagText.text.substringBeforeLast(
                                                    " ",
                                                    missingDelimiterValue = ""
                                                )
                                            val newText =
                                                if (textWithoutLastWord.isEmpty()) "$tag " else "$textWithoutLastWord $tag "

                                            onHashtagTextChange(
                                                TextFieldValue(
                                                    text = newText,
                                                    selection = TextRange(newText.length)
                                                )
                                            )
                                            val currentTags = newText.split(" ")
                                                .filter { it.isNotBlank() }.toSet()
                                            val availableAssoc =
                                                tagAssociations[tag]?.filter { it !in currentTags }
                                            showSuggestions =
                                                !availableAssoc.isNullOrEmpty()
                                        }
                                    }
                            }
                        }
                    }
                }
            }
            IconButton(
                onClick = {
                    if (canAdd) {
                        lastSubmittedText = hashtagText.text
                        onAdd(shimText, hashtagText)
                        focusRequester.requestFocus()
                    }
                },
                enabled = canAdd,
                modifier = Modifier
                    .padding(end = 14.dp)
                    .background(
                        Color.White.copy(alpha = if (canAdd) 0.15f else 0.0f),
                        shape = RoundedCornerShape(ROUND_END)
                    )
            ) {
                Icon(
                    Icons.AutoMirrored.Filled.KeyboardReturn,
                    contentDescription = "Add",
                    tint = colorResource(id = R.color.input_icon_color).copy(alpha = if (canAdd) 0.95f else 0.0f),
                    modifier = Modifier.size(26.dp)
                )
            }
        }
    }
}

private fun Modifier.inputFrame(bg: Color): Modifier =
    clip(RoundedCornerShape(INPUT_ROUND))
        .background(bg)
        .border(EDGE, RoundedCornerShape(INPUT_ROUND))

@Composable
private fun TagChip(
    tag: String,
    bg: Color = colorResource(id = R.color.suggestion_bg_color),
    border: BorderStroke? = null,
    alpha: Float = 1f,
    pad: PaddingValues = TAG_PAD,
    onClick: () -> Unit
) {
    Surface(
        color = bg,
        shape = RoundedCornerShape(ROUND_END),
        border = border,
        shadowElevation = 6.dp
    ) {
        val interactionSource = remember { MutableInteractionSource() }
        Text(
            text = tag,
            color = colorResource(id = R.color.input_color).copy(alpha = alpha),
            maxLines = 1,
            modifier = Modifier
                .clickable(
                    interactionSource = interactionSource,
                    indication = null,
                    onClick = onClick
                )
                .padding(pad)
        )
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchField(
    query: String,
    status: String? = null,
    onQueryChange: (String) -> Unit,
    onClose: () -> Unit,
    onDownload: () -> Unit
) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(SEARCH_FIELD_H)
            .clip(RoundedCornerShape(ROUND_END))
            .background(colorResource(id = R.color.search_bg_color))
            .border(EDGE, RoundedCornerShape(ROUND_END))
    ) {
        Icon(
            Icons.Outlined.Search,
            contentDescription = null,
            tint = Color.White.copy(alpha = 0.6f),
            modifier = Modifier
                .align(Alignment.CenterStart)
                .padding(start = SEARCH_ICON_PAD)
                .size(SEARCH_ICON)
        )
        BasicTextField(
            value = query,
            onValueChange = onQueryChange,
            modifier = Modifier
                .fillMaxWidth()
                .padding(
                    start = SEARCH_TEXT_START,
                    end = if (query.isEmpty()) SEARCH_TEXT_END else SEARCH_TEXT_END_DL
                )
                .align(Alignment.Center),
            textStyle = TextStyle(fontSize = SEARCH_FS, color = Color.White),
            cursorBrush = SolidColor(Color.White),
            singleLine = true,
            decorationBox = { inner ->
                Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart) {
                    if (query.isEmpty() || status != null) {
                        Text(
                            status ?: "Search",
                            color = Color.White.copy(alpha = 0.5f),
                            fontSize = SEARCH_FS
                        )
                    }
                    Box(Modifier.alpha(if (status != null) 0f else 1f)) { inner() }
                }
            }
        )
        Row(
            modifier = Modifier
                .align(Alignment.CenterEnd)
                .padding(end = 3.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            if (query.isNotEmpty()) {
                IconButton(onClick = onDownload) {
                    Icon(
                        Icons.Outlined.FileDownload,
                        contentDescription = "Download",
                        tint = Color.White.copy(alpha = 0.6f)
                    )
                }
            }
            IconButton(onClick = { if (query.isEmpty()) onClose() else onQueryChange("") }) {
                Icon(
                    Icons.Default.Clear,
                    contentDescription = "Clear",
                    tint = Color.White.copy(alpha = 0.6f)
                )
            }
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ShimItem(
    shim: ShimClass,
    themeManager: ShimThemeManager,
    railX: Dp,
    railGap: Dp,
    isSelected: Boolean = false,
    onHashtagClick: (String) -> Unit,
    onNumClick: () -> Unit = {}
) {
    
    val textStyle = TextStyle(
        fontSize = themeManager.fontSize.floatValue.sp,
        lineHeight = (themeManager.fontSize.floatValue * (themeManager.lineHeight.floatValue + themeManager.lineSpacing.floatValue)).sp,
        fontFamily = if (themeManager.isMonospace.value) FontFamily.Monospace else FontFamily.Default
    )

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = themeManager.shimSpacing.intValue.dp)
    ) {
        Column(
            Modifier
                .fillMaxWidth()
                .padding(start = railX + railGap, end = SHIM_PAD)
        ) {
            FlowRow(
                horizontalArrangement = Arrangement.spacedBy(TAG_GAP),
                verticalArrangement = Arrangement.spacedBy(TAG_GAP)
            ) {
                shim.shimhash.split(" ").filter { it.isNotBlank() }.forEach { tag ->
                    TagChip(
                        tag,
                        bg = themeManager.tagBgColor.value,
                        alpha = LIST_TAG_ALPHA,
                        pad = LIST_TAG_PAD
                    ) { onHashtagClick(tag) }
                }
            }

            Spacer(modifier = Modifier.height(10.dp))

            Text(
                text = shim.shimtext,
                style = textStyle.copy(
                    color = themeManager.textColor.value,
                    letterSpacing = 0.6.sp
                )
            )
            if (themeManager.showDates.value) {
                Spacer(modifier = Modifier.height(6.dp))
                Text(
                    text = shim.shimtime,
                    style = textStyle.copy(
                        color = Color.White.copy(alpha = DATE_ALPHA),
                        fontSize = (themeManager.fontSize.floatValue - DATE_SHRINK).sp
                    )
                )
            }
        }
        val rail = themeManager.textColor.value
        Surface(
            color = if (isSelected) NUM_SEL else themeManager.backgroundColor.value,
            shape = RoundedCornerShape(ROUND_LESS),
            border = BorderStroke(RAIL_W, themeManager.textColor.value.copy(alpha = NUM_CHIP_ALPHA)),
            modifier = Modifier
                .offset(y = NUM_DY)
                .centerOn(railX)
                .clickable(
                    interactionSource = remember { MutableInteractionSource() },
                    indication = null,
                    onClick = onNumClick
                )
        ) {
            Text(
                text = shim.id.toString(),
                style = textStyle.copy(
                    color = if (isSelected) themeManager.backgroundColor.value
                    else rail.copy(alpha = NUM_ALPHA),
                    fontSize = NUM_FS,
                    lineHeight = NUM_FS
                ),
                modifier = Modifier.padding(NUM_PAD)
            )
        }
    }
}

private fun Modifier.centerOn(x: Dp): Modifier = layout { m, c ->
    val p = m.measure(c.copy(minWidth = 0, minHeight = 0))
    layout(p.width, p.height) { p.place(x.roundToPx() - p.width / 2, 0) }
}