/*
 * This file is part of Stack Wallet.
 *
 * Copyright (c) 2023 Cypher Stack
 * All Rights Reserved.
 * The code is distributed under GPLv3 license, see LICENSE file for details.
 * Generated by Cypher Stack on 2023-05-26
 *
 */

import 'dart:math' as math;

import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/util.dart';

// preserve index order as index is used to store value in preferences
enum AmountUnit {
  normal(0),
  milli(3),
  micro(6),
  nano(9),
  pico(12),
  femto(15),
  atto(18),
  zepto(21),
  yocto(24),
  ronto(27),
  quecto(30),
  ;

  const AmountUnit(this.shift);
  final int shift;

  static List<AmountUnit> valuesForCoin(Coin coin) {
    switch (coin) {
      case Coin.firo:
      case Coin.litecoin:
      case Coin.particl:
      case Coin.namecoin:
      case Coin.bitcoinFrost:
      case Coin.bitcoinFrostTestNet:
      case Coin.bitcoinTestNet:
      case Coin.litecoinTestNet:
      case Coin.bitcoincashTestnet:
      case Coin.dogecoinTestNet:
      case Coin.firoTestNet:
      case Coin.bitcoin:
      case Coin.bitcoincash:
      case Coin.dogecoin:
      case Coin.eCash:
      case Coin.epicCash:
      case Coin.stellar: // TODO: check if this is correct
      case Coin.stellarTestnet:
      case Coin.tezos:
      case Coin.solana:
        return AmountUnit.values.sublist(0, 4);

      case Coin.monero:
      case Coin.wownero:
        return AmountUnit.values.sublist(0, 5);

      case Coin.ethereum:
        return AmountUnit.values.sublist(0, 7);

      case Coin.nano:
      case Coin.banano:
        return AmountUnit.values;
    }
  }
}

extension AmountUnitExt on AmountUnit {
  String unitForCoin(Coin coin) {
    switch (this) {
      case AmountUnit.normal:
        return coin.ticker;
      case AmountUnit.milli:
        return "m${coin.ticker}";
      case AmountUnit.micro:
        return "µ${coin.ticker}";
      case AmountUnit.nano:
        if (coin == Coin.ethereum) {
          return "gwei";
        } else if (coin == Coin.wownero ||
            coin == Coin.monero ||
            coin == Coin.nano ||
            coin == Coin.banano) {
          return "n${coin.ticker}";
        } else {
          return "sats";
        }
      case AmountUnit.pico:
        if (coin == Coin.ethereum) {
          return "mwei";
        } else if (coin == Coin.wownero ||
            coin == Coin.monero ||
            coin == Coin.nano ||
            coin == Coin.banano) {
          return "p${coin.ticker}";
        } else {
          return "invalid";
        }
      case AmountUnit.femto:
        if (coin == Coin.ethereum) {
          return "kwei";
        } else if (coin == Coin.nano || coin == Coin.banano) {
          return "f${coin.ticker}";
        } else {
          return "invalid";
        }
      case AmountUnit.atto:
        if (coin == Coin.ethereum) {
          return "wei";
        } else if (coin == Coin.nano || coin == Coin.banano) {
          return "a${coin.ticker}";
        } else {
          return "invalid";
        }
      case AmountUnit.zepto:
        if (coin == Coin.nano || coin == Coin.banano) {
          return "z${coin.ticker}";
        } else {
          return "invalid";
        }
      case AmountUnit.yocto:
        if (coin == Coin.nano || coin == Coin.banano) {
          return "y${coin.ticker}";
        } else {
          return "invalid";
        }
      case AmountUnit.ronto:
        if (coin == Coin.nano || coin == Coin.banano) {
          return "r${coin.ticker}";
        } else {
          return "invalid";
        }
      case AmountUnit.quecto:
        if (coin == Coin.nano || coin == Coin.banano) {
          return "q${coin.ticker}";
        } else {
          return "invalid";
        }
    }
  }

  String unitForContract(EthContract contract) {
    switch (this) {
      case AmountUnit.normal:
        return contract.symbol;
      case AmountUnit.milli:
        return "m${contract.symbol}";
      case AmountUnit.micro:
        return "µ${contract.symbol}";
      case AmountUnit.nano:
        return "gwei";
      case AmountUnit.pico:
        return "mwei";
      case AmountUnit.femto:
        return "kwei";
      case AmountUnit.atto:
        return "wei";
      default:
        throw ArgumentError(
          "Does eth even allow more than 18 decimal places?",
        );
    }
  }

  Amount? tryParse(
    String value, {
    required String locale,
    required Coin coin,
    EthContract? tokenContract,
    bool overrideWithDecimalPlacesFromString = false,
  }) {
    final precisionLost = value.startsWith("~");

    final parts = (precisionLost ? value.substring(1) : value).split(" ");

    if (parts.first.isEmpty) {
      return null;
    }

    String str = parts.first;
    if (str.startsWith(RegExp(r'[+-]'))) {
      str = str.substring(1);
    }

    if (str.isEmpty) {
      return null;
    }

    // get number symbols for decimal place and group separator
    final numberSymbols = Util.getSymbolsFor(locale: locale);

    final groupSeparator = numberSymbols?.GROUP_SEP ?? ",";
    final decimalSeparator = numberSymbols?.DECIMAL_SEP ?? ".";

    str = str.replaceAll(groupSeparator, "");

    final decimalString = str.replaceFirst(decimalSeparator, ".");
    final Decimal? decimal = Decimal.tryParse(decimalString);

    if (decimal == null) {
      return null;
    }

    final decimalPlaces = overrideWithDecimalPlacesFromString
        ? decimal.scale
        : tokenContract?.decimals ?? coin.decimals;
    final realShift = math.min(shift, decimalPlaces);

    return decimal.shift(0 - realShift).toAmount(fractionDigits: decimalPlaces);
  }

  String displayAmount({
    required Amount amount,
    required String locale,
    required Coin coin,
    required int maxDecimalPlaces,
    bool withUnitName = true,
    bool indicatePrecisionLoss = true,
    String? overrideUnit,
    EthContract? tokenContract,
  }) {
    assert(maxDecimalPlaces >= 0);

    // ensure we don't shift past minimum atomic value
    final realShift = math.min(shift, amount.fractionDigits);

    // shifted to unit
    final Decimal shifted = amount.decimal.shift(realShift);

    // get shift int value without fractional value
    final BigInt wholeNumber = shifted.toBigInt();

    // get decimal places to display
    final int places = math.max(0, amount.fractionDigits - realShift);

    // start building the return value with just the whole value
    String returnValue = wholeNumber.toString();

    // get number symbols for decimal place and group separator
    final numberSymbols = Util.getSymbolsFor(locale: locale);

    // insert group separator
    final regex = RegExp(r'\B(?=(\d{3})+(?!\d))');
    returnValue = returnValue.replaceAllMapped(
      regex,
      (m) => "${m.group(0)}${numberSymbols?.GROUP_SEP ?? ","}",
    );

    // if true and withUnitName is true, we will show "~" prepended on amount
    bool didLosePrecision = false;

    // if any decimal places should be shown continue building the return value
    if (places > 0) {
      // get the fractional value
      final Decimal fraction = shifted - shifted.truncate();

      // get final decimal based on max precision wanted while ensuring that
      // maxDecimalPlaces doesn't exceed the max per coin
      final int updatedMax;
      if (tokenContract != null) {
        updatedMax = maxDecimalPlaces > tokenContract.decimals
            ? tokenContract.decimals
            : maxDecimalPlaces;
      } else {
        updatedMax =
            maxDecimalPlaces > coin.decimals ? coin.decimals : maxDecimalPlaces;
      }
      final int actualDecimalPlaces = math.min(places, updatedMax);

      // get remainder string without the prepending "0."
      final fractionString = fraction.toString();
      String remainder;
      if (fractionString.length > 2) {
        remainder = fraction.toString().substring(2);
      } else {
        remainder = "0";
      }

      if (remainder.length > actualDecimalPlaces) {
        // check for loss of precision
        final remainingRemainder =
            BigInt.tryParse(remainder.substring(actualDecimalPlaces));
        if (remainingRemainder != null) {
          didLosePrecision = remainingRemainder > BigInt.zero;
        }

        // trim unwanted trailing digits
        remainder = remainder.substring(0, actualDecimalPlaces);
      } else if (remainder.length < actualDecimalPlaces) {
        // pad with zeros to achieve requested precision
        for (int i = remainder.length; i < actualDecimalPlaces; i++) {
          remainder += "0";
        }
      }

      // get decimal separator based on locale
      final String separator = numberSymbols?.DECIMAL_SEP ?? ".";

      // append separator and fractional amount
      returnValue += "$separator$remainder";
    }

    if (!withUnitName && !indicatePrecisionLoss) {
      return returnValue;
    }

    if (didLosePrecision && indicatePrecisionLoss) {
      returnValue = "~$returnValue";
    }

    if (!withUnitName && indicatePrecisionLoss) {
      return returnValue;
    }

    // return the value with the proper unit symbol
    if (tokenContract != null) {
      overrideUnit = unitForContract(tokenContract);
    }

    return "$returnValue ${overrideUnit ?? unitForCoin(coin)}";
  }
}