package compose.widgets

import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import compose.utils.tooltip
import java.util.Locale


/**
 * A text field that validates and parses the value input by the user.
 */
@Composable
fun <T: Any> ValueTextField(
    value: T?,
    onValueChange: (T?) -> Unit,
    stringConverter: StringConverter<T>,
    textFieldProvider: TextFieldProvider<T>,
) {
    var internalValue: T? by remember { mutableStateOf(null) }
    var textValue by remember { mutableStateOf("") }

    if (value != internalValue) {
        internalValue = value
        textValue = if (value == null) "" else stringConverter.valueToString(value)
    }

    val (isError, errorText) = remember(stringConverter) {
        derivedStateOf {
            if (textValue == "") {
                Pair(true, null)
            } else runCatching { stringConverter.stringToValue(textValue) }.let { result ->
                Pair(result.isFailure, result.exceptionOrNull()?.message)
            }
        }
    }.value

    fun applyNewTextValue(text: String) {
        textValue = text
        val parsedValue = try {
            stringConverter.stringToValue(text)
        } catch (_: RuntimeException) {
            onValueChange(null)
            return
        }
        if (parsedValue != value) {
            internalValue = parsedValue
            onValueChange(parsedValue)
        }
    }

    textFieldProvider.textField(
        text = textValue,
        onTextChange = {
            applyNewTextValue(it)
        },
        value = internalValue,
        isError = isError,
        modifier = Modifier
            .onFocusChanged {
                // Reformat on focus loss
                if (!it.hasFocus && !isError) {
                    textValue = stringConverter.valueToString(internalValue ?: return@onFocusChanged)
                }
            }.then(
                if (errorText != null)
                    Modifier.tooltip(errorText)
                else
                    Modifier
            ),
    )
}


/**
 * Provides the textfield implementation for [ValueTextField].
 */
fun interface TextFieldProvider<T: Any> {

    /**
     * Emits the textfield.
     */
    @Composable
    fun textField(
        text: String,
        onTextChange: (String) -> Unit,
        value: T?,
        isError: Boolean,
        modifier: Modifier
    )

}


/**
 * Converts between a value of type [T] and a string representing it in [ValueTextField].
 *
 * [valueToString] and [stringToValue] should be inverse functions.
 */
interface StringConverter<T> {

    /**
     * Converts [value] to the string representing it.
     */
    fun valueToString(value: T): String

    /**
     * Parses [string] to the value it represents.
     */
    fun stringToValue(string: String): T

}


/**
 * Converts between an [Int] and its standard textual representation (digits only).
 */
object DecimalIntConverter : StringConverter<Int> {
    override fun valueToString(value: Int): String = value.toString()
    override fun stringToValue(string: String): Int = string.toInt()
}


/**
 * Formats a [Double] with the given number of decimal places, or up to the last nonzero decimal if [decimalPlaces]
 * is `null`.
 */
private fun Double.formatDecimal(decimalPlaces: Int?): String {
    return if (decimalPlaces == null)
        this.toString()
    else
        "%.${decimalPlaces}f".format(Locale.ROOT, this)
}


/**
 * Converts between a [Double] and its standard textual representation with the given number of decimal places, or up to
 * the last nonzero decimal if [decimalPlaces] is `null`.
 */
@Stable
fun DecimalDoubleConverter(decimalPlaces: Int?) = object: StringConverter<Double> {
    override fun valueToString(value: Double): String = value.formatDecimal(decimalPlaces)
    override fun stringToValue(string: String): Double = string.toDouble()
}


/**
 * Similar to [DecimalIntConverter], but an additional constraint is placed on the value.
 * If [constraint] returns `false` on the value, the textfield will display an error.
 */
@Stable
fun ConstrainedDecimalIntConverter(
    constraint: ((Int) -> Boolean)?,
    constraintFailureMessage: (String) -> String = { "Value $it is not legal here" }
) : StringConverter<Int> {
    return if (constraint == null)
        DecimalIntConverter
    else {
        object: StringConverter<Int> {

            override fun valueToString(value: Int): String = value.toString()

            override fun stringToValue(string: String): Int {
                val number = string.toIntOrNull()
                if (number == null)
                    throw IllegalArgumentException("$string is not a valid number")
                if (!constraint(number))
                    throw IllegalArgumentException(constraintFailureMessage(string))
                return number
            }

        }
    }
}


/**
 * Similar to [DecimalDoubleConverter], but an additional constraint is placed on the value.
 * If [constraint] returns `false` on the value, the textfield will display an error.
 */
@Suppress("unused")
@Stable
fun ConstrainedDecimalDoubleConverter(
    decimalPlaces: Int?,
    constraint: ((Double) -> Boolean)?,
    constraintFailureMessage: (String) -> String = { "Value $it is not legal here" }
) : StringConverter<Double> = object : StringConverter<Double> {

    override fun valueToString(value: Double): String = value.formatDecimal(decimalPlaces)

    override fun stringToValue(string: String): Double {
        val number = string.toDoubleOrNull()
        if (number == null)
            throw IllegalArgumentException("$string is not a valid number")
        if ((constraint != null) && !constraint(number))
            throw IllegalArgumentException(constraintFailureMessage(string))
        return number
    }

}