Pith - grain
package com.vgmlr.grain

import android.app.Application
import android.content.ClipData
import android.content.ClipboardManager
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import androidx.compose.runtime.mutableFloatStateOf

private const val CLIP_LABEL = "grain"

// process scope
object GrainMemory {
    var value by mutableStateOf<Double?>(null)
}

enum class Pad(val label: String, val num: Boolean) {
    CLR("C", false), MEM("M", false), PAR("( )", false), PCT("%", false),
    N7("7", true), N8("8", true), N9("9", true), DIV("/", false),
    N4("4", true), N5("5", true), N6("6", true), MUL("×", false),
    N1("1", true), N2("2", true), N3("3", true), SUB("-", false),
    DOT(".", true), N0("0", true), ADD("+", false), EQ("=", false)
}

class GrainViewModel(app: Application) : AndroidViewModel(app) {

    val prefs = GrainPrefs(app)
    val bias: StateFlow<Bias> =
        prefs.bias.stateIn(viewModelScope, SharingStarted.Eagerly, Bias.LEFT)
    val edgePad: StateFlow<Int> =
        prefs.edgePad.stateIn(viewModelScope, SharingStarted.Eagerly, PAD_DEFAULT)
    val opsSide: StateFlow<Side> =
        prefs.opsSide.stateIn(viewModelScope, SharingStarted.Eagerly, Side.RIGHT)
    val decimals: StateFlow<Int> =
        prefs.decimals.stateIn(viewModelScope, SharingStarted.Eagerly, DEC_DEFAULT)
    private val places: Int get() = decimals.value
    val digits: StateFlow<Digits> =
        prefs.digits.stateIn(viewModelScope, SharingStarted.Eagerly, Digits.MONO)
    val keySize: StateFlow<Int> =
        prefs.keySize.stateIn(viewModelScope, SharingStarted.Eagerly, KEY_AUTO)
    var keypadW by mutableFloatStateOf(0f) // dp
    val historyMax: StateFlow<Int> =
        prefs.historyMax.stateIn(viewModelScope, SharingStarted.Eagerly, HISTORY_DEFAULT)
    val history: StateFlow<List<String>> =
        prefs.history.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    private val clip = app.getSystemService(ClipboardManager::class.java)

    // calc
    var expr by mutableStateOf("")
        private set
    var solved by mutableStateOf(false)
        private set
    var error by mutableStateOf(false)
        private set
    val display: String get() = if (error) ERR else expr
    val memText: String? get() = GrainMemory.value?.let { GrainCalc.fmt(it, places) }

    // convert
    var category by mutableStateOf<Category?>(null)
        private set
    var unitA by mutableIntStateOf(0)
        private set
    var unitB by mutableIntStateOf(0)
        private set
    var focusA by mutableStateOf(true)
        private set
    private var input by mutableStateOf("")
    private var pinned by mutableStateOf<String?>(null) // prior value
    private var fresh = false

    val output: String
        get() {
            val c = category ?: return ""
            pinned?.let { return it }
            val v = GrainCalc.parse(input) ?: return ""
            val (f, t) = if (focusA) unitA to unitB else unitB to unitA
            return GrainCalc.fmt(c.units[t].fromBase(c.units[f].toBase(v)), places)
        }
    val valueA: String get() = if (focusA) input else output
    val valueB: String get() = if (focusA) output else input

    fun press(k: Pad) = if (category != null) convKey(k) else calcKey(k)

    fun clearAll() {
        if (category != null) enterInput("") else reset()
    }

    // title tap
    fun recall() {
        val v = GrainMemory.value ?: return
        if (category != null) enterInput(GrainCalc.fmt(v), true)
        else insert(GrainCalc.fmt(v, places))
    }

    fun copy() {
        val s = resultText() ?: return
        clip?.setPrimaryClip(ClipData.newPlainText(CLIP_LABEL, s))
    }

    fun paste() {
        val item = clip?.primaryClip?.takeIf { it.itemCount > 0 }?.getItemAt(0) ?: return
        val raw = item.coerceToText(getApplication()).toString()
        val v = GrainCalc.parse(raw) ?: return
        val s = GrainCalc.fmt(v)
        if (category != null) enterInput(s, true) else insert(s)
    }

    fun openCategory(c: Category) {
        category = c
        unitA = c.defA
        unitB = c.defB
        focusA = true
        enterInput("")
    }

    fun closeCategory() {
        val c = category ?: return
        val a = GrainCalc.parse(valueA)
        val b = GrainCalc.parse(valueB)
        if (a != null && b != null && a != 0.0 && b != 0.0) {
            log(
                "${GrainCalc.fmt(a, places)} ${c.units[unitA].abbr} = " +
                        "${GrainCalc.fmt(b, places)} ${c.units[unitB].abbr}"
            )
        }
        category = null
        enterInput("")
    }

    fun focus(a: Boolean) {
        if (a == focusA) return
        val o = output
        val prior = input
        focusA = a
        input = o
        fresh = true
        pinned = prior
    }

    fun pickUnit(a: Boolean, i: Int) {
        if (a) unitA = i else unitB = i
        pinned = null
    }

    // calc keys

    private fun calcKey(k: Pad) {
        when (k) {
            Pad.CLR -> backspace()
            Pad.MEM -> store(if (error) null else GrainCalc.eval(expr))
            Pad.PAR -> paren()
            Pad.EQ -> solve()
            Pad.PCT -> pct()
            Pad.DOT -> dot()
            Pad.ADD, Pad.SUB, Pad.MUL, Pad.DIV -> op(k.label[0])
            else -> digit(k.label[0])
        }
    }

    private fun reset(s: String = "") {
        expr = s
        solved = false
        error = false
    }

    private fun digit(c: Char) {
        if (solved || error) return reset(c.toString())
        if (expr.lastOrNull() == ')') return
        val t = GrainCalc.tail(expr)
        if ('%' in t || 'E' in t) return
        expr = if (t == "0") expr.dropLast(1) + c else expr + c
    }

    private fun dot() {
        if (solved || error) return reset("0.")
        if (expr.lastOrNull() == ')') return
        val t = GrainCalc.tail(expr)
        if ('.' in t || 'E' in t || '%' in t) return
        expr += if (t.isEmpty()) "0." else "."
    }

    private fun op(c: Char) {
        if (error) return
        solved = false
        val last = expr.lastOrNull()
        val prev = expr.getOrNull(expr.length - 2)
        expr = when {
            last == null || last == '(' -> if (c == '-') expr + c else return // unary
            GrainCalc.isOp(last) -> if (prev == null || prev == '(') return else expr.dropLast(1) + c
            else -> expr + c
        }
    }

    // open/close
    private fun paren() {
        if (solved || error) return reset("(")
        val last = expr.lastOrNull()
        val operand = last != null && (last.isDigit() || last in ".%)")
        expr = when {
            GrainCalc.openCount(expr) <= 0 -> expr + if (operand) "×(" else "("
            operand -> "$expr)"
            else -> return
        }
    }

    private fun pct() {
        if (error) return
        solved = false
        val t = GrainCalc.tail(expr)
        if (t.isEmpty() || t == "." || '%' in t) return
        expr += "%"
    }

    private fun backspace() {
        if (solved || error) return reset()
        expr = expr.dropLast(1)
    }

    private fun solve() {
        if (error || solved) return
        val v = GrainCalc.eval(expr) ?: return
        val r = GrainCalc.fmt(v, places)
        if (r == ERR) {
            expr = ""
            error = true
            return
        }
        if (GrainCalc.isCompound(expr)) {
            log("${GrainCalc.close(expr)} = $r")
        }
        expr = r
        solved = true
    }

    // replace trailing
    private fun insert(s: String) {
        if (solved || error || expr.isEmpty()) return reset(s)
        val last = expr.last()
        expr = if (GrainCalc.isOp(last) || last == '(') expr + s
        else if (last == ')') "$expr×$s"
        else expr.dropLast(GrainCalc.tail(expr).length).let { if (it == "-") s else it + s }
    }

    // convert keys

    private fun convKey(k: Pad) {
        when (k) {
            Pad.CLR -> enterInput(if (fresh) "" else input.dropLast(1))
            Pad.MEM -> store(GrainCalc.parse(output))
            Pad.SUB -> enterInput(
                if (input.startsWith("-")) input.drop(1) else "-$input", fresh
            )
            Pad.DOT -> when {
                fresh -> enterInput("0.")
                '.' in input -> Unit
                input.isEmpty() || input == "-" -> enterInput(input + "0.")
                else -> enterInput("$input.")
            }
            Pad.ADD, Pad.MUL, Pad.DIV, Pad.PCT, Pad.EQ, Pad.PAR -> Unit
            else -> {
                val c = k.label
                enterInput(
                    when {
                        fresh || input == "0" -> c
                        input == "-0" -> "-$c"
                        else -> input + c
                    }
                )
            }
        }
    }

    private fun enterInput(s: String, isFresh: Boolean = false) {
        input = s
        fresh = isFresh
        pinned = null
    }

    // shared

    private fun store(v: Double?) {
        v?.takeIf { it.isFinite() }?.let { GrainMemory.value = it }
    }

    private fun resultText(): String? = when {
        category != null -> output.takeIf { it.isNotEmpty() && it != ERR }
        error -> null
        solved -> expr
        else -> GrainCalc.eval(expr)?.let { GrainCalc.fmt(it, places) }?.takeIf { it != ERR }
    }

    private fun log(line: String) {
        viewModelScope.launch { prefs.addHistory(line) }
    }
}