AmountEntryFragment.kt

/*
 * This file is part of GNU Taler
 * (C) 2026 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.merchantpos.amount

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels

import net.taler.common.Amount
import net.taler.common.CurrencySpecification
import net.taler.merchantpos.MainActivity
import net.taler.merchantpos.MainViewModel
import net.taler.merchantpos.PosDestination
import net.taler.merchantpos.R
import net.taler.merchantpos.compose.AmountKeypad
import net.taler.merchantpos.compose.PosTheme
import net.taler.merchantpos.config.ConfigProduct
import net.taler.merchantpos.order.Order
import net.taler.merchantpos.showPosError

private const val QUICK_AMOUNT_ORDER_ID = -1
private const val QUICK_AMOUNT_PRODUCT_ID = "quick_amount"
private const val STATE_AMOUNT_CURRENCY = "amount_entry.currency"
private const val STATE_AMOUNT_INTEGER = "amount_entry.integer"
private const val STATE_AMOUNT_FRACTION = "amount_entry.fraction"

internal data class AmountEntryDraft(
    val currency: String,
    val amount: Amount,
)

internal fun saveAmountEntryDraft(
    outState: Bundle,
    currency: String?,
    amount: Amount?,
)
{
    currency?.let { outState.putString(STATE_AMOUNT_CURRENCY, it) }
    amount?.let {
        outState.putLong(STATE_AMOUNT_INTEGER, it.value)
        outState.putInt(STATE_AMOUNT_FRACTION, it.fraction)
    }
}

internal fun restoreAmountEntryDraft(
    savedState: Bundle?,
    configuredCurrency: String,
    currencySpec: CurrencySpecification?,
): AmountEntryDraft?
{
    val state = savedState ?: return null
    val currency =
        state
            .getString(STATE_AMOUNT_CURRENCY)
            ?.takeIf { it == configuredCurrency } ?: return null
    if (!state.containsKey(STATE_AMOUNT_INTEGER) ||
        !state.containsKey(STATE_AMOUNT_FRACTION))
    {
        return null
    }
    return AmountEntryDraft(
        currency = currency,
        amount =
            Amount(
                currency = currency,
                value = state.getLong(STATE_AMOUNT_INTEGER),
                fraction = state.getInt(STATE_AMOUNT_FRACTION),
                spec = currencySpec,
            ),
    )
}

class AmountEntryFragment : Fragment()
{
    private val viewModel: MainViewModel by activityViewModels()
    private val paymentManager by lazy { viewModel.paymentManager }
    private var selectedCurrency by mutableStateOf<String?>(null)
    private var amount by mutableStateOf<Amount?>(null)

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View
    {
        initializeAmountState(savedInstanceState)
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                AmountEntryScreen(
                    amountText = amount?.toString(showSymbol = false) ?: "0.00",
                    selectedCurrency = selectedCurrency,
                    currencyOptions = viewModel.configManager.currency?.let(::listOf) ?: emptyList(),
                    chargeEnabled = amount?.isZero() == false,
                    onCurrencySelected = ::setCurrency,
                    onDigitPressed = ::onDigitPressed,
                    onClearPressed = ::clearAmount,
                    onBackspacePressed = ::onBackspacePressed,
                    onChargePressed = ::onChargePressed,
                )
            }
        }
    }

    override fun onSaveInstanceState(outState: Bundle)
    {
        super.onSaveInstanceState(outState)
        saveAmountEntryDraft(outState, selectedCurrency, amount)
    }

    override fun onStart()
    {
        super.onStart()
        val activity = requireActivity() as MainActivity
        val configManager = viewModel.configManager
        if (!configManager.config.isValid())
        {
            activity.navigateTo(PosDestination.Config)
        }
        else if (configManager.currency == null)
        {
            activity.navigateTo(PosDestination.ConfigFetcher)
        }
    }

    private fun initializeAmountState(savedInstanceState: Bundle?)
    {
        val configuredCurrency = viewModel.configManager.currency ?: return
        val currencySpec = viewModel.configManager.currencySpec
        val restored = restoreAmountEntryDraft(savedInstanceState, configuredCurrency, currencySpec)
        val currency = restored?.currency ?: configuredCurrency
        selectedCurrency = currency
        amount = restored?.amount ?: amount ?: Amount.zero(currency).withSpec(currencySpec)
    }

    private fun setCurrency(currency: String)
    {
        selectedCurrency = currency
        val currentAmount = amount
        val spec = viewModel.configManager.currencySpec
        amount =
            when
            {
                currentAmount == null -> Amount.zero(currency)
                currentAmount.currency == currency -> currentAmount
                else -> currentAmount.withCurrency(currency)
            }.withSpec(spec)
    }

    private fun onDigitPressed(digit: Char)
    {
        val currentAmount = amount ?: return
        amount = currentAmount.addInputDigit(digit)?.withSpec(currentAmount.spec) ?: currentAmount
    }

    private fun onBackspacePressed()
    {
        val currentAmount = amount ?: return
        amount = currentAmount.removeInputDigit()?.withSpec(currentAmount.spec) ?: currentAmount
    }

    private fun clearAmount()
    {
        val currency = selectedCurrency ?: return
        amount = Amount.zero(currency).withSpec(viewModel.configManager.currencySpec)
    }

    private fun onChargePressed()
    {
        val activity = requireActivity() as MainActivity
        val configuredCurrency =
            viewModel.configManager.currency ?: run {
                activity.navigateTo(PosDestination.ConfigFetcher)
                return
            }
        val enteredCurrency = selectedCurrency ?: configuredCurrency
        val enteredAmount =
            amount
                ?: Amount.zero(enteredCurrency).withSpec(viewModel.configManager.currencySpec)

        if (enteredAmount.isZero())
        {
            requireActivity().showPosError(R.string.amount_entry_error_zero)
            return
        }
        if (enteredCurrency != configuredCurrency)
        {
            requireActivity().showPosError(R.string.amount_entry_error_wrong_currency)
            return
        }

        val order =
            Order(
                id = QUICK_AMOUNT_ORDER_ID,
                currency = configuredCurrency,
                currencySpec = viewModel.configManager.currencySpec,
                availableCategories = emptyMap(),
            )
        val product =
            ConfigProduct(
                description = getString(R.string.amount_entry_product_description),
                productId = QUICK_AMOUNT_PRODUCT_ID,
                price = enteredAmount.withSpec(viewModel.configManager.currencySpec),
                categories = listOf(Int.MIN_VALUE),
            )
        val orderWithProduct = order + product

        // Backend doesn't require products; omit them for this "quick amount" flow.
        val checkoutManager = viewModel.checkoutManager
        if (checkoutManager.shouldUseCheckout())
        {
            checkoutManager.start(orderWithProduct, includeProducts = false)
            activity.navigateTo(PosDestination.Checkout)
        }
        else
        {
            paymentManager.createPayment(orderWithProduct, includeProducts = false)
            activity.navigateTo(PosDestination.ProcessPayment)
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AmountEntryScreen(
    amountText: String,
    selectedCurrency: String?,
    currencyOptions: List<String>,
    chargeEnabled: Boolean,
    onCurrencySelected: (String) -> Unit,
    onDigitPressed: (Char) -> Unit,
    onClearPressed: () -> Unit,
    onBackspacePressed: () -> Unit,
    onChargePressed: () -> Unit,
)
{
    PosTheme {
        val configuration = LocalConfiguration.current
        val isTabletLayout = configuration.smallestScreenWidthDp >= 600
        val portrait = configuration.screenHeightDp > configuration.screenWidthDp
        Box(
            modifier = Modifier.fillMaxSize(),
        ) {
            if (portrait)
            {
                BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
                    val contentHeight = maxHeight - 48.dp
                    val keypadHeight =
                        (contentHeight * if (isTabletLayout) 0.44f else 0.56f)
                            .coerceIn(
                                minimumValue = if (isTabletLayout) 460.dp else 280.dp,
                                maximumValue = if (isTabletLayout) 516.dp else 420.dp,
                            )
                    Column(
                        modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 24.dp),
                        horizontalAlignment = Alignment.CenterHorizontally,
                    ) {
                        val contentWidth =
                            if (isTabletLayout)
                            {
                                Modifier.widthIn(
                                    max = 560.dp,
                                )
                            }
                            else
                            {
                                Modifier.fillMaxWidth()
                            }
                        AmountPane(
                            amountText = amountText,
                            selectedCurrency = selectedCurrency,
                            currencyOptions = currencyOptions,
                            isTabletLayout = isTabletLayout,
                            portrait = true,
                            onCurrencySelected = onCurrencySelected,
                            modifier = contentWidth.weight(1f),
                        )
                        AmountKeypad(
                            isTabletLayout = isTabletLayout,
                            chargeEnabled = chargeEnabled,
                            onDigitPressed = onDigitPressed,
                            onClearPressed = onClearPressed,
                            onBackspacePressed = onBackspacePressed,
                            onChargePressed = onChargePressed,
                            modifier = contentWidth.height(keypadHeight),
                        )
                    }
                }
            }
            else if (!isTabletLayout)
            {
                Row(
                    modifier =
                        Modifier
                            .fillMaxSize()
                            .padding(8.dp),
                    horizontalArrangement = Arrangement.spacedBy(8.dp),
                ) {
                    AmountPane(
                        amountText = amountText,
                        selectedCurrency = selectedCurrency,
                        currencyOptions = currencyOptions,
                        isTabletLayout = false,
                        onCurrencySelected = onCurrencySelected,
                        modifier =
                            Modifier
                                .weight(0.32f)
                                .padding(horizontal = 8.dp, vertical = 4.dp),
                    )
                    AmountKeypad(
                        isTabletLayout = false,
                        chargeEnabled = chargeEnabled,
                        onDigitPressed = onDigitPressed,
                        onClearPressed = onClearPressed,
                        onBackspacePressed = onBackspacePressed,
                        onChargePressed = onChargePressed,
                        modifier =
                            Modifier
                                .weight(0.68f)
                                .padding(4.dp),
                    )
                }
            }
            else
            {
                Column(
                    modifier =
                        Modifier
                            .fillMaxSize()
                            .padding(16.dp),
                    verticalArrangement = Arrangement.spacedBy(12.dp),
                ) {
                    AmountPane(
                        amountText = amountText,
                        selectedCurrency = selectedCurrency,
                        currencyOptions = currencyOptions,
                        isTabletLayout = true,
                        onCurrencySelected = onCurrencySelected,
                        modifier =
                            Modifier
                                .fillMaxWidth()
                                .weight(0.35f),
                    )
                    Row(
                        modifier =
                            Modifier
                                .fillMaxWidth()
                                .weight(0.65f),
                        horizontalArrangement = Arrangement.Center,
                    ) {
                        AmountKeypad(
                            isTabletLayout = true,
                            chargeEnabled = chargeEnabled,
                            onDigitPressed = onDigitPressed,
                            onClearPressed = onClearPressed,
                            onBackspacePressed = onBackspacePressed,
                            onChargePressed = onChargePressed,
                            modifier = Modifier.fillMaxWidth(0.6f),
                        )
                    }
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AmountPane(
    amountText: String,
    selectedCurrency: String?,
    currencyOptions: List<String>,
    isTabletLayout: Boolean,
    onCurrencySelected: (String) -> Unit,
    modifier: Modifier = Modifier,
    portrait: Boolean = false,
)
{
    var expanded by remember { mutableStateOf(false) }
    val availableCurrencies = currencyOptions.distinct()

    val dropdownComposable: @Composable () -> Unit = {
        if (availableCurrencies.size <= 1)
        {
            Text(
                text = availableCurrencies.singleOrNull() ?: selectedCurrency.orEmpty(),
                style = MaterialTheme.typography.titleLarge.copy(fontSize = 24.sp, fontWeight = FontWeight.Medium),
                color = MaterialTheme.colorScheme.onSurface,
            )
        }
        else
        {
            ExposedDropdownMenuBox(
                expanded = expanded,
                onExpandedChange = { if (currencyOptions.isNotEmpty()) expanded = !expanded },
                modifier =
                    if (portrait)
                    {
                        Modifier.fillMaxWidth()
                    }
                    else
                    {
                        Modifier.wrapContentWidth(
                            Alignment.CenterHorizontally,
                        )
                    },
            ) {
                OutlinedTextField(
                    modifier =
                        Modifier
                            .menuAnchor(
                                type = ExposedDropdownMenuAnchorType.PrimaryNotEditable,
                                enabled = currencyOptions.isNotEmpty(),
                            ).then(if (portrait) Modifier.fillMaxWidth() else Modifier.widthIn(min = 96.dp)),
                    value = selectedCurrency.orEmpty(),
                    onValueChange = {},
                    readOnly = true,
                    singleLine = true,
                    label = { Text(stringResource(R.string.amount_entry_label)) },
                    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
                    shape = RoundedCornerShape(14.dp),
                    colors =
                        OutlinedTextFieldDefaults.colors(
                            focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,
                            unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,
                            disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,
                            focusedBorderColor = MaterialTheme.colorScheme.primary,
                            unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant,
                            focusedLabelColor = MaterialTheme.colorScheme.primary,
                            unfocusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
                            focusedTextColor = MaterialTheme.colorScheme.onSurface,
                            unfocusedTextColor = MaterialTheme.colorScheme.onSurface,
                            focusedTrailingIconColor = MaterialTheme.colorScheme.primary,
                            unfocusedTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
                        ),
                )

                DropdownMenu(
                    expanded = expanded,
                    onDismissRequest = { expanded = false },
                ) {
                    availableCurrencies.forEach { currency ->
                        DropdownMenuItem(
                            text = { Text(currency) },
                            onClick = {
                                expanded = false
                                onCurrencySelected(currency)
                            },
                        )
                    }
                }
            }
        }
    }

    if (portrait)
    {
        val compactPortrait = LocalConfiguration.current.screenHeightDp <= 680
        Column(
            modifier = modifier,
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
        ) {
            if (availableCurrencies.size > 1)
            {
                Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart) {
                    dropdownComposable()
                }
                Spacer(
                    modifier =
                        Modifier.height(
                            if (isTabletLayout)
                            {
                                44.dp
                            }
                            else if (compactPortrait)
                            {
                                8.dp
                            }
                            else
                            {
                                40.dp
                            },
                        ),
                )
            }
            Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
                Text(
                    amountText,
                    modifier = Modifier.weight(1f, fill = false).alignByBaseline(),
                    style =
                        MaterialTheme.typography.headlineLarge.copy(
                            fontSize =
                                if (isTabletLayout)
                                {
                                    64.sp
                                }
                                else if (compactPortrait)
                                {
                                    48.sp
                                }
                                else
                                {
                                    56.sp
                                },
                        ),
                    maxLines = 1,
                    softWrap = false,
                )
                if (availableCurrencies.size <= 1)
                {
                    Text(
                        text = availableCurrencies.singleOrNull() ?: selectedCurrency.orEmpty(),
                        modifier = Modifier.alignByBaseline(),
                        style =
                            MaterialTheme.typography.titleLarge.copy(
                                fontSize = 24.sp,
                                fontWeight = FontWeight.Medium,
                            ),
                        color = MaterialTheme.colorScheme.onSurface,
                    )
                }
            }
            Spacer(modifier = Modifier.height(if (compactPortrait) 8.dp else 24.dp))
            Text(stringResource(R.string.amount_entry_hint), style = MaterialTheme.typography.bodyLarge)
        }
    }
    else if (isTabletLayout)
    {
        Row(
            modifier = modifier,
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(
                text = amountText,
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.headlineLarge.copy(fontSize = 56.sp),
                maxLines = 1,
            )
            Spacer(modifier = Modifier.width(12.dp))
            dropdownComposable()
        }
    }
    else
    {
        val phoneAmountFontSize =
            when (amountText.length)
            {
                in 0..6 -> 56.sp
                in 7..8 -> 48.sp
                in 9..10 -> 40.sp
                in 11..12 -> 32.sp
                in 13..14 -> 26.sp
                else -> 22.sp
            }

        Column(
            modifier = modifier,
            verticalArrangement = Arrangement.Top,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text(
                text = amountText,
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.headlineLarge.copy(fontSize = phoneAmountFontSize),
                maxLines = 1,
                softWrap = false,
            )

            Spacer(modifier = Modifier.height(12.dp))
            dropdownComposable()
            Spacer(modifier = Modifier.weight(1f))
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun AmountEntryScreenContent(
    amountText: String,
    selectedCurrency: String,
    currencyOptions: List<String>,
    chargeEnabled: Boolean,
)
{
    AmountEntryScreen(
        amountText = amountText,
        selectedCurrency = selectedCurrency,
        currencyOptions = currencyOptions,
        chargeEnabled = chargeEnabled,
        onCurrencySelected = {},
        onDigitPressed = {},
        onClearPressed = {},
        onBackspacePressed = {},
        onChargePressed = {},
    )
}