Pith - stave
package com.vgmlr.stave

import android.content.Intent
import android.graphics.Color as AndroidColor
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
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.automirrored.outlined.ListAlt
import androidx.compose.material.icons.filled.Apps
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.FrontHand
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Place
import androidx.compose.material.icons.outlined.Receipt
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.activity.compose.BackHandler
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

class StaveMainActivity : ComponentActivity() {

    private lateinit var db: StaveDatabase
    private lateinit var themeManager: StaveThemeManager
    private var tripState = mutableStateOf<StaveTrip?>(null)
    private var entriesState = mutableStateOf<List<StaveEntry>>(emptyList())

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

        enableEdgeToEdge(
            statusBarStyle = SystemBarStyle.dark(AndroidColor.TRANSPARENT),
            navigationBarStyle = SystemBarStyle.dark(AndroidColor.TRANSPARENT)
        )
        window.isNavigationBarContrastEnforced = false
        if (!ensureTrip()) return

        setContent {
            StaveTheme(themeManager) {
                MainScreen()
            }
        }
    }

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

    private fun ensureTrip(): Boolean {
        val trip = db.getTrip(StaveSession.currentTripId(this))
        if (trip == null) {
            StaveSession.clear(this)
            startActivity(Intent(this, StaveTrips::class.java))
            finish()
            return false
        }
        tripState.value = trip
        entriesState.value = db.getEntries(trip.id)
        return true
    }

    private fun releaseTrip() {
        StaveSession.clear(this)
        ensureTrip()
    }

    private fun refresh() {
        val id = tripState.value?.id ?: return
        tripState.value = db.getTrip(id)
        entriesState.value = db.getEntries(id)
    }

    private fun runningTotals(entries: List<StaveEntry>): Map<Int, Double> {
        var sum = 0.0
        val map = HashMap<Int, Double>(entries.size)
        entries.asReversed().forEach { sum += it.cost; map[it.id] = sum }
        return map
    }

    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun MainScreen() {
        val lifecycleOwner = LocalLifecycleOwner.current
        val trip = tripState.value
        val entries = entriesState.value

        var marked by remember { mutableStateOf<Int?>(null) }
        var filterCat by remember { mutableStateOf<String?>(null) }
        var inputActive by remember { mutableStateOf(true) }
        val listState = rememberLazyListState()
        val haptic = LocalHapticFeedback.current
        var addTick by remember { mutableIntStateOf(0) }

        var cost by remember { mutableStateOf("") }
        var category by remember { mutableStateOf(StaveCategory.DEFAULT) }
        var note by remember { mutableStateOf("") }
        var location by remember { mutableStateOf("") }
        var lat by remember { mutableStateOf<Double?>(null) }
        var lng by remember { mutableStateOf<Double?>(null) }

        val totals = remember(entries) { runningTotals(entries) }
        val categoryTotals = remember(entries) {
            entries.groupBy { StaveCategory.normalize(it.category) }
                .map { (cat, list) -> cat to list.sumOf { it.cost } }
                .sortedByDescending { it.second }
        }

        val newestFirst = themeManager.isNewestFirst.value
        val visible = remember(entries, filterCat, newestFirst) {
            val list = filterCat
                ?.let { c -> entries.filter { StaveCategory.normalize(it.category) == c } }
                ?: entries
            if (newestFirst) list else list.reversed()
        }
        
        LaunchedEffect(categoryTotals, filterCat) {
            if (filterCat != null && categoryTotals.none { it.first == filterCat }) filterCat = null
        }

        LaunchedEffect(addTick) {
            if (addTick == 0) return@LaunchedEffect
            val i = if (newestFirst) 0 else visible.lastIndex
            if (visible.isNotEmpty()) listState.animateScrollToItem(i)
        }

        BackHandler(enabled = marked != null) { marked = null }

        val saveEntry: () -> Unit = {
            val value = cost.toDoubleOrNull()
            val id = trip?.id
            if (value != null && value > 0.0 && id != null &&
                category.isNotBlank() && location.isNotBlank()
            ) {
                db.addEntry(
                    id, value, category, location.trim(),
                    lat, lng, StaveOTCClock.getCurrentOTCTime(), note.trim()
                )
                cost = ""
                note = ""
                location = ""
                lat = null
                lng = null
                refresh()
                addTick++
            }
        }

        DisposableEffect(lifecycleOwner) {
            val observer = LifecycleEventObserver { _, event ->
                if (event == Lifecycle.Event.ON_RESUME) {
                    marked = null
                    inputActive = true
                }
            }
            lifecycleOwner.lifecycle.addObserver(observer)
            onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
        }

        val txtExport = rememberLauncherForActivityResult(
            ActivityResultContracts.CreateDocument("text/tab-separated-values")
        ) { uri -> uri?.let { trip?.let { t -> db.exportFile(this, it, t, entries) } } }

        Box(Modifier.fillMaxSize()) {
            Scaffold(
                topBar = {
                    TopAppBar(
                        title = { },
                        colors = TopAppBarDefaults.topAppBarColors(
                            containerColor = Color.Transparent
                        ),
                        actions = {
                            Spacer(Modifier.width(BAR_EDGE))
                            StaveBlankButton(
                                Modifier.weight(1f),
                                label = trip?.name.orEmpty(),
                                placeholder = UNNAMED,
                                onRename = { name ->
                                    trip?.let { db.renameTrip(it.id, name); refresh() }
                                }
                            )
                            Spacer(Modifier.width(BAR_GAP))
                            StaveButton(
                                Icons.Outlined.Place,
                                "Place",
                                enabled = entries.isNotEmpty()
                            ) { trip?.let { StaveMap.open(this@StaveMainActivity, it.id) } }
                            Spacer(Modifier.width(BAR_GAP))
                            StaveButton(
                                Icons.Outlined.FrontHand,
                                "Release",
                                iconSize = BTN_ICON - 2.dp
                            ) { releaseTrip() }

                            Spacer(Modifier.width(BAR_GAP))

                            Box {
                                var expanded by remember { mutableStateOf(false) }
                                StaveButton(Icons.Outlined.Receipt, "Menu") { expanded = true }
                                StaveMenu(expanded = expanded, onDismiss = { expanded = false }) {
                                    StaveMenuItem("Staves", Icons.Default.Apps) {
                                        startActivity(
                                            Intent(this@StaveMainActivity, StaveTrips::class.java)
                                        )
                                        expanded = false
                                    }
                                    StaveMenuItem(
                                        "Entry",
                                        Icons.AutoMirrored.Outlined.ListAlt,
                                        iconSize = BTN_ICON + 2.dp
                                    ) {
                                        marked = null
                                        inputActive = true
                                        expanded = false
                                    }
                                    StaveMenuItem(
                                        "Share",
                                        Icons.Default.Share,
                                        enabled = entries.isNotEmpty()
                                    ) {
                                        trip?.let { shareStave(this@StaveMainActivity, it, entries) }
                                        expanded = false
                                    }
                                    StaveMenuItem(
                                        "Export",
                                        Icons.Outlined.Save,
                                        enabled = trip != null,
                                        iconSize = BTN_ICON + 2.dp
                                    ) {
                                        trip?.let { txtExport.launch(StaveText.fileName(it)) }
                                        expanded = false
                                    }
                                }
                            }
                            Spacer(Modifier.width(BAR_EDGE_ACTIONS))
                        }
                    )
                },
                contentWindowInsets = WindowInsets(0, 0, 0, 0),
                containerColor = themeManager.backgroundColor.value
            ) { p ->
                val topInset = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
                val navInset =
                    WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
                val density = LocalDensity.current
                val headerTop = topInset + TOP_BAR_H
                var graphH by remember { mutableStateOf(0.dp) }

                Box(Modifier.fillMaxSize()) {
                    LazyColumn(
                        state = listState,
                        contentPadding = PaddingValues(
                            top = 0.dp,
                            bottom = p.calculateBottomPadding() + navInset + BTN_SIZE + DOCK_PAD * 2
                        ),
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(top = headerTop + graphH)
                            .clipToBounds()
                            .background(themeManager.backgroundColor.value)
                            .clickable(
                                interactionSource = remember { MutableInteractionSource() },
                                indication = null
                            ) { marked = null }
                    ) {
                        items(visible, key = { it.id }) { entry ->
                            EntryRow(
                                entry = entry,
                                runningTotal = totals[entry.id] ?: 0.0,
                                themeManager = themeManager,
                                isSelected = marked == entry.id,
                                onClick = { marked = null },
                                onLongClick = {
                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)
                                    marked = entry.id
                                }
                            )
                        }
                    }
                    
                    Box(
                        Modifier
                            .align(Alignment.TopCenter)
                            .padding(top = headerTop)
                            .onGloballyPositioned { c ->
                                graphH = with(density) { c.size.height.toDp() }
                            }
                    ) {
                        CategoryDonut(
                            totals = categoryTotals,
                            themeManager = themeManager,
                            selected = filterCat,
                            onSelect = { cat ->
                                filterCat = if (filterCat == cat) null else cat
                                marked = null
                            }
                        )
                    }
                }
            }

            if (marked != null && !inputActive) {
                StaveDock {
                    StaveMenuItem("Place", Icons.Outlined.Place) {
                        trip?.let { StaveMap.open(this@StaveMainActivity, it.id, marked) }
                    }
                    StaveMenuItem(
                        "Delete",
                        Icons.Outlined.Delete,
                        iconSize = BTN_ICON + 2.dp
                    ) {
                        marked?.let { db.deleteEntry(it) }
                        marked = null
                        refresh()
                    }
                }
            }

            if (inputActive) {
                Box(
                    Modifier
                        .fillMaxSize()
                        .background(Color.Black.copy(alpha = 0.8f))
                        .clickable(
                            interactionSource = remember { MutableInteractionSource() },
                            indication = null
                        ) { inputActive = false }
                )
                Box(
                    Modifier
                        .align(Alignment.BottomCenter)
                        .navigationBarsPadding()
                        .imePadding()
                ) {
                    InputBar(
                        cost = cost,
                        category = category,
                        note = note,
                        location = location,
                        themeManager = themeManager,
                        onCostChange = { cost = it },
                        onCategoryChange = { category = it },
                        onNoteChange = { note = it },
                        onLocationChange = { location = it; lat = null; lng = null },
                        onPinned = { fix ->
                            location = fix.city
                            lat = fix.lat
                            lng = fix.lng
                        },
                        onAdd = saveEntry
                    )
                }
            }
        }
    }
}