AmountScopeField.kt

/*
 * This file is part of GNU Taler
 * (C) 2024 Taler Systems S.A.
 *
 * GNU Taler is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3, or (at your option) any later version.
 *
 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

package net.taler.wallet.compose

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.filled.Check
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.taler.common.Amount
import net.taler.common.CurrencySpecification
import net.taler.wallet.R
import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.cleanExchange

data class AmountScope(
    val amount: Amount = Amount.zero(scope.currency),
    val scope: ScopeInfo,
    // whether fee calculation should be debounced
    val debounce: Boolean = false,
    // whether it originated from user input
    val userInput: Boolean = false,
)

@Composable
fun AmountScopeField(
    modifier: Modifier = Modifier,
    amount: AmountScope,
    editableScope: Boolean = true,
    scopes: List<ScopeInfo>,
    onAmountChanged: (amount: AmountScope) -> Unit,
    label: @Composable (() -> Unit)? = null,
    supportingText: @Composable (() -> Unit)? = null,
    isError: Boolean = false,
    readOnly: Boolean = false,
    keyboardActions: KeyboardActions = KeyboardActions.Default,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
    showAmount: Boolean = true,
    showScope: Boolean = true,
    showShortcuts: Boolean = false,
    onShortcutSelected: ((amount: AmountScope) -> Unit)? = null,
) {
    Column(modifier) {
        if (showScope) {
            ScopeDropdown(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 10.dp),
                scopes = scopes,
                onScopeChanged = { scope ->
                    onAmountChanged(amount.copy(
                        scope = scope,
                        amount = Amount.zero(scope.currency),
                    ))
                },
                initialScope = amount.scope,
                readOnly = readOnly || !editableScope,
            )
        }

        if (showAmount) {
            AmountInputFieldBase(
                modifier = Modifier
                    .fillMaxWidth(),
                amount = amount.amount,
                onAmountChanged = {
                    onAmountChanged(amount.copy(amount = it))
                },
                label = label,
                isError = isError,
                supportingText = supportingText,
                readOnly = readOnly,
                keyboardActions = keyboardActions,
                keyboardOptions = keyboardOptions,
                showSymbol = true,
            )

            val commonAmounts = amount.amount.spec?.commonAmounts
                ?.map { it.withSpec(amount.amount.spec) }
            AnimatedVisibility(showShortcuts && amount.amount.isZero() && commonAmounts != null) {
                if (commonAmounts != null) {
                    AmountInputShortcuts(
                        modifier = Modifier.padding(top = 10.dp),
                        amounts = commonAmounts,
                        onSelected = { shortcut ->
                            onShortcutSelected?.let {
                                it(amount.copy(amount = shortcut))
                            }
                        },
                    )
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScopeDropdown(
    scopes: List<ScopeInfo>,
    onScopeChanged: (ScopeInfo) -> Unit,
    modifier: Modifier = Modifier,
    initialScope: ScopeInfo? = null,
    readOnly: Boolean = false,
) {
    val initialIndex = scopes.indexOf(initialScope).let { if (it < 0) 0 else it }
    var selectedIndex by remember { mutableIntStateOf(initialIndex) }
    var expanded by remember { mutableStateOf(false) }
    Box(
        modifier = modifier,
    ) {
        val scope = scopes.getOrNull(selectedIndex)
            ?: initialScope
            ?: error("no scope available")

        val value = when (scope) {
            is ScopeInfo.Global -> scope.currency
            is ScopeInfo.Exchange -> cleanExchange(scope.url)
            is ScopeInfo.Auditor -> cleanExchange(scope.url)
            is ScopeInfo.ExchangeLegacyKeys -> cleanExchange(scope.url)
        }

        val colors = OutlinedTextFieldDefaults.colors()
        val singleLine = true
        val enabled = false
        val interactionSource = remember { MutableInteractionSource() }

        ExposedDropdownMenuBox(
            expanded = expanded,
            onExpandedChange = { expanded = it },
            modifier = Modifier,
        ) {
            BasicTextField(
                value = value,
                modifier = Modifier
                    .height(45.dp)
                    .clickable { if (!readOnly) expanded = true }
                    .fillMaxWidth(),
                onValueChange = { },
                enabled = enabled,
                readOnly = true,
                textStyle = TextStyle(color = colors.focusedTextColor),
                interactionSource = interactionSource,
                singleLine = singleLine,
                decorationBox =
                    @Composable { innerTextField ->
                        OutlinedTextFieldDefaults.DecorationBox(
                            value = value,
                            innerTextField = innerTextField,
                            singleLine = singleLine,
                            enabled = enabled,
                            visualTransformation = VisualTransformation.None,
                            interactionSource = interactionSource,
                            prefix = {
                                Text(
                                    modifier = Modifier.padding(end = 6.dp),
                                    text = stringResource(R.string.currency_via),
                                )
                            },
                            contentPadding = OutlinedTextFieldDefaults.contentPadding(
                                top = 0.dp,
                                bottom = 0.dp,
                            ),
                            colors = ExposedDropdownMenuDefaults.textFieldColors(),
                            trailingIcon = {
                                ExposedDropdownMenuDefaults.TrailingIcon(expanded)
                            }
                        )
                    },
            )
            
            ExposedDropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false },
                containerColor = MenuDefaults.containerColor,
                shape = MenuDefaults.shape,
            ) {
                scopes.forEachIndexed { index, s ->
                    DropdownMenuItem(
                        contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
                        leadingIcon = {
                            if (selectedIndex == index) {
                                Icon(Icons.Filled.Check, contentDescription = null)
                            }
                        },
                        text = {
                            Text(
                                text = when (s) {
                                    is ScopeInfo.Global -> s.currency
                                    is ScopeInfo.Exchange -> stringResource(
                                        R.string.currency_url,
                                        s.currency,
                                        cleanExchange(s.url),
                                    )

                                    is ScopeInfo.Auditor -> stringResource(
                                        R.string.currency_url,
                                        s.currency,
                                        cleanExchange(s.url),
                                    )

                                    is ScopeInfo.ExchangeLegacyKeys -> stringResource(
                                        R.string.currency_url,
                                        s.currency,
                                        cleanExchange(s.url),
                                    )
                                }
                            )
                        },
                        onClick = {
                            selectedIndex = index
                            onScopeChanged(scopes[index])
                            expanded = false
                        }
                    )
                }
            }
        }
    }
}

@Composable
private fun AmountInputShortcuts(
    modifier: Modifier = Modifier,
    amounts: List<Amount>,
    onSelected: (amount: Amount) -> Unit,
) {
    FlowRow(
        modifier = modifier
            .fillMaxWidth(),
        maxItemsInEachRow = 2,
        horizontalArrangement = Arrangement.SpaceEvenly,
    ) {
        amounts.forEach {
            SelectionChip (
                selected = false,
                label = { Text(it.toString()) },
                value = it,
                onSelected = onSelected,
            )
        }
    }
}

@Preview
@Composable
fun AmountInputFieldPreview() {
    TalerSurface {
        var amount by remember {
            mutableStateOf(AmountScope(
                amount = Amount.fromJSONString("KUDOS:0").withSpec(
                    CurrencySpecification(
                        name = "Kudos",
                        numFractionalInputDigits = 2,
                        numFractionalNormalDigits = 2,
                        numFractionalTrailingZeroDigits = 2,
                        altUnitNames = mapOf(),
                        commonAmounts = listOf(
                            Amount.fromJSONString("KUDOS:5"),
                            Amount.fromJSONString("KUDOS:10"),
                            Amount.fromJSONString("KUDOS:25"),
                            Amount.fromJSONString("KUDOS:50"),
                        ),
                    ),
                ),
                scope = ScopeInfo.Exchange("KUDOS", "https://exchange.demo.taler.net/"),
            ))
        }
        AmountScopeField(
            amount = amount,
            editableScope = true,
            scopes = listOf(
                ScopeInfo.Global("CHF"),
                ScopeInfo.Exchange("KUDOS", url = "https://exchange.demo.taler.net/"),
                ScopeInfo.Auditor("TESTKUDOS", url = "https://auditor.test.taler.net/"),
            ),
            onAmountChanged = { amount = it },
            label = { Text("Amount to withdraw") },
            isError = false,
            readOnly = false,
            showShortcuts = true,
            onShortcutSelected = { amount = it },
        )
    }
}