Pith - grain
package com.vgmlr.grain

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

internal const val HISTORY_MIN = 0
internal const val HISTORY_MAX = 50
internal const val HISTORY_DEFAULT = 25
internal const val PAD_MIN = 0
internal const val PAD_MAX = 150
internal const val PAD_DEFAULT = 0
internal const val DEC_MIN = 0
internal const val DEC_MAX = 12
internal const val DEC_DEFAULT = 4
internal const val KEY_AUTO = 0
internal const val KEY_RANGE = 15
private const val HISTORY_SEP = "\n"

enum class Bias(val label: String) {
    LEFT("Left"), CENTER("Center"), RIGHT("Right"), FULL("Full");

    val padded: Boolean get() = this == LEFT || this == RIGHT
}
enum class Side(val label: String) { LEFT("Left"), RIGHT("Right") }
enum class Digits(val label: String) { MONO("Mono"), SANS("Sans") }

private val Context.store: DataStore<Preferences> by preferencesDataStore("grain")

class GrainPrefs(context: Context) {
    private val ds = context.applicationContext.store

    private val kBias = stringPreferencesKey("bias")
    private val kDigits = stringPreferencesKey("digits")
    private val kMax = intPreferencesKey("history_max")
    private val kHistory = stringPreferencesKey("history")
    private val kPad = intPreferencesKey("edge_pad")
    private val kOps = stringPreferencesKey("ops_side")
    private val kDec = intPreferencesKey("decimals")
    private val kKey = intPreferencesKey("key_size")

    val bias: Flow<Bias> = ds.data.map { p ->
        Bias.entries.firstOrNull { it.name == p[kBias] } ?: Bias.LEFT
    }
    val digits: Flow<Digits> = ds.data.map { p ->
        Digits.entries.firstOrNull { it.name == p[kDigits] } ?: Digits.MONO
    }
    val historyMax: Flow<Int> = ds.data.map { it[kMax] ?: HISTORY_DEFAULT }
    val history: Flow<List<String>> = ds.data.map { read(it) }
    val edgePad: Flow<Int> = ds.data.map {
        (it[kPad] ?: PAD_DEFAULT).coerceIn(PAD_MIN, PAD_MAX)
    }
    val opsSide: Flow<Side> = ds.data.map { p ->
        Side.entries.firstOrNull { it.name == p[kOps] } ?: Side.RIGHT
    }
    
    val decimals: Flow<Int> = ds.data.map {
        (it[kDec] ?: DEC_DEFAULT).coerceIn(DEC_MIN, DEC_MAX)
    }
    
    val keySize: Flow<Int> = ds.data.map {
        (it[kKey] ?: KEY_AUTO).coerceAtLeast(KEY_AUTO)
    }

    suspend fun setBias(b: Bias) {
        ds.edit { it[kBias] = b.name }
    }

    suspend fun setEdgePad(n: Int) {
        ds.edit { it[kPad] = n.coerceIn(PAD_MIN, PAD_MAX) }
    }

    suspend fun setKeySize(n: Int) {
        ds.edit { it[kKey] = n.coerceAtLeast(KEY_AUTO) }
    }

    suspend fun setOpsSide(s: Side) {
        ds.edit { it[kOps] = s.name }
    }

    suspend fun setDecimals(n: Int) {
        ds.edit { it[kDec] = n.coerceIn(DEC_MIN, DEC_MAX) }
    }

    suspend fun setDigits(d: Digits) {
        ds.edit { it[kDigits] = d.name }
    }

    suspend fun setHistoryMax(n: Int) {
        ds.edit { p ->
            val max = n.coerceIn(HISTORY_MIN, HISTORY_MAX)
            p[kMax] = max
            p[kHistory] = read(p).takeLast(max).joinToString(HISTORY_SEP)
        }
    }

    suspend fun addHistory(line: String) {
        ds.edit { p ->
            val max = p[kMax] ?: HISTORY_DEFAULT
            if (max == 0) return@edit
            p[kHistory] = (read(p) + line).takeLast(max).joinToString(HISTORY_SEP)
        }
    }

    private fun read(p: Preferences): List<String> =
        p[kHistory]?.split(HISTORY_SEP)?.filter { it.isNotEmpty() } ?: emptyList()
}