// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of 'number_format.dart';

// Suppress naming issues as changes would be breaking.
// ignore_for_file: constant_identifier_names

/// An abstract class for compact number styles.
abstract class _CompactStyleBase {
  /// The _CompactStyle for the [number].
  _CompactStyle styleForNumber(dynamic number, _CompactNumberFormat format);

  /// What should we divide the number by in order to print. Normally it is
  /// either `10^normalizedExponent` or 1 if we shouldn't divide at all.
  int get divisor;

  /// The iterable of all possible styles which we represent.
  ///
  /// Normally this will be either a list with just ourself, or of two elements
  /// for our positive and negative styles.
  Iterable<_CompactStyle> get allStyles;
}

/// A compact format with separate styles for plural forms.
class _CompactStyleWithPlurals extends _CompactStyleBase {
  int exponent;
  Map<String, _CompactStyleBase> styles;
  plural_rules.PluralCase Function()? _plural;
  late _CompactStyleBase _defaultStyle;

  _CompactStyleWithPlurals(this.styles, this.exponent, String? locale) {
    _plural = plural_rules.pluralRules[locale];
    _defaultStyle = styles['other']!;
  }

  @override
  Iterable<_CompactStyle> get allStyles =>
      styles.values.expand((x) => x.allStyles);

  @override
  int get divisor => _defaultStyle.divisor;

  @override
  _CompactStyle styleForNumber(dynamic number, _CompactNumberFormat format) {
    var value = number.abs();
    if (_plural == null) {
      return _defaultStyle.styleForNumber(number, format);
    }

    var displayed = value;
    var precision = format._minimumFractionDigits;

    if (format.significantDigitsInUse) {
      // Note: this is not 100% correct, but good enough for most cases.
      var integerPart = format._floor(value);
      var integerLength = NumberFormat.numberOfIntegerDigits(integerPart);
      if (format.minimumSignificantDigits != null) {
        precision = max(0, format.minimumSignificantDigits! - integerLength);
      }
    }

    // Round to the right precision.
    var factor = pow(10, precision);
    displayed = (displayed * factor).round() / factor;

    if (format.significantDigitsInUse &&
        !format.minimumSignificantDigitsStrict) {
      // Check for trailing 0.
      var fractionStr = format._floor(displayed * factor).toString();
      while (precision > 0 && fractionStr.endsWith('0')) {
        precision--;
        fractionStr = fractionStr.substring(0, fractionStr.length - 1);
      }
    }

    // Direct value? (French 1000 => "mille" has key "1".)
    if (number >= 0 && precision == 0) {
      var indexed = styles[format._floor(displayed).toString()];
      if (indexed != null) {
        return indexed.styleForNumber(number, format);
      }
    }

    plural_rules.startRuleEvaluation(displayed, precision);
    var pluralCase = _plural!();
    var style = _defaultStyle;
    switch (pluralCase) {
      case plural_rules.PluralCase.ZERO:
        style = styles['zero'] ?? _defaultStyle;
        break;
      case plural_rules.PluralCase.ONE:
        style = styles['one'] ?? _defaultStyle;
        break;
      case plural_rules.PluralCase.TWO:
        style = styles['two'] ?? styles['few'] ?? _defaultStyle;
        break;
      case plural_rules.PluralCase.FEW:
        style = styles['few'] ?? _defaultStyle;
        break;
      case plural_rules.PluralCase.MANY:
        style = styles['many'] ?? _defaultStyle;
        break;
      default:
      // Keep _defaultStyle;
    }
    return style.styleForNumber(number, format);
  }
}

/// A compact format with separate styles for positive and negative numbers.
class _CompactStyleWithNegative extends _CompactStyleBase {
  _CompactStyleWithNegative(this.positiveStyle, this.negativeStyle);
  final _CompactStyle positiveStyle;
  final _CompactStyle negativeStyle;

  @override
  _CompactStyle styleForNumber(dynamic number, _CompactNumberFormat format) =>
      number < 0 ? negativeStyle : positiveStyle;

  @override
  int get divisor => positiveStyle.divisor;

  @override
  List<_CompactStyle> get allStyles => [positiveStyle, negativeStyle];
}

/// Represents a compact format for a particular base
///
/// For example, 10K can be used to represent 10,000.  Corresponds to one of the
/// patterns in COMPACT_DECIMAL_SHORT_FORMAT. So, for example, in en_US we have
/// the pattern
///
///       4: '00K'
/// which matches
///
///       _CompactStyle(pattern: '00K', divisor: 1000,
///           prefix: '', suffix: 'K');
class _CompactStyle extends _CompactStyleBase {
  _CompactStyle(
      {this.pattern,
      this.divisor = 1,
      this.positivePrefix = '',
      this.negativePrefix = '',
      this.positiveSuffix = '',
      this.negativeSuffix = '',
      this.isDirectValue = false});

  /// The pattern on which this is based.
  ///
  /// We don't actually need this, but it makes debugging easier.
  String? pattern;

  /// What should we divide the number by in order to print. Normally is either
  /// 10^normalizedExponent or 1 if we shouldn't divide at all.
  @override
  int divisor;

  // Prefixes / suffixes.
  String positivePrefix;
  String negativePrefix;
  String positiveSuffix;
  String negativeSuffix;

  /// Whether this pattern omits numbers. Ex: "mille" for 1000 in fr.
  bool isDirectValue;

  /// Return true if this is the fallback compact pattern, printing the number
  /// un-compacted. e.g. 1200 might print as '1.2K', but 12 just prints as '12'.
  ///
  /// For currencies, with the fallback pattern we use the super implementation
  /// so that we will respect things like the default number of decimal digits
  /// for a particular currency (e.g. two for USD, zero for JPY)
  bool get isFallback => pattern == null || pattern == '0';

  @override
  _CompactStyle styleForNumber(dynamic number, _CompactNumberFormat format) =>
      this;

  @override
  List<_CompactStyle> get allStyles => [this];

  static final _regex = RegExp('([^0]*)(0+)(.*)');

  static final _justZeros = RegExp(r'^0*$');

  /// Does pattern have any additional characters or is it just zeros.
  static bool _hasNonZeroContent(String pattern) =>
      !_justZeros.hasMatch(pattern);

  /// Creates a [_CompactStyle] instance for pattern with [normalizedExponent].
  static _CompactStyle createStyle(
      NumberSymbols symbols, String pattern, int normalizedExponent,
      {bool isSigned = false, bool explicitSign = false}) {
    var prefix = '';
    var suffix = '';
    var divisor = 1;
    var isDirectValue = false;
    var match = _regex.firstMatch(pattern);
    if (match != null) {
      prefix = match.group(1)!;
      suffix = match.group(3)!;
      // If the pattern is just zeros, with no suffix, then we shouldn't divide
      // by the number of digits. e.g. for 'af', the pattern for 3 is '0', but
      // it doesn't mean that 4321 should print as 4. But if the pattern was
      // '0K', then it should print as '4K'. So we have to check if the pattern
      // has a suffix. This seems extremely hacky, but I don't know how else to
      // encode that. Check what other things are doing.
      if (_hasNonZeroContent(pattern)) {
        var integerDigits = match.group(2)!.length;
        divisor = pow(10, normalizedExponent - integerDigits + 1) as int;
      }
    } else {
      if (pattern.isNotEmpty && !pattern.contains('0')) {
        // "Direct" pattern: no numbers.
        divisor = pow(10, normalizedExponent) as int;
        isDirectValue = true;
      }
    }

    final positivePrefix =
        (explicitSign && !isSigned) ? '${symbols.PLUS_SIGN}$prefix' : prefix;
    final negativePrefix =
        (!isSigned) ? '${symbols.MINUS_SIGN}$prefix' : prefix;
    final positiveSuffix = suffix;
    final negativeSuffix = suffix;

    return _CompactStyle(
        pattern: pattern,
        positivePrefix: positivePrefix,
        negativePrefix: negativePrefix,
        positiveSuffix: positiveSuffix,
        negativeSuffix: negativeSuffix,
        divisor: divisor,
        isDirectValue: isDirectValue);
  }
}

/// Enumerates the different formats supported.
enum _CompactFormatType {
  COMPACT_DECIMAL_SHORT_PATTERN,
  COMPACT_DECIMAL_LONG_PATTERN,
  COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN
}

class _CompactNumberFormat extends NumberFormat {
  /// A default, using the decimal pattern, for the `getPattern` constructor parameter.
  static String _forDecimal(NumberSymbols symbols) => symbols.DECIMAL_PATTERN;

  // Map exponent => style.
  final Map<int, _CompactStyleBase> _styles;

  // Whether positive sign should be explicitly printed.
  final bool _explicitSign;

  factory _CompactNumberFormat(
      {String? locale,
      _CompactFormatType? formatType,
      String? name,
      String? currencySymbol,
      String? Function(NumberSymbols) getPattern = _forDecimal,
      int? decimalDigits,
      bool explicitSign = false,
      bool lookupSimpleCurrencySymbol = false,
      bool isForCurrency = false}) {
    // Initialization copied from `NumberFormat` constructor.
    // TODO(davidmorgan): deduplicate.
    locale = helpers.verifiedLocale(locale, NumberFormat.localeExists, null)!;
    var symbols = numberFormatSymbols[locale]!;
    var localeZero = symbols.ZERO_DIGIT.codeUnitAt(0);
    var zeroOffset = localeZero - constants.asciiZeroCodeUnit;
    name ??= symbols.DEF_CURRENCY_CODE;
    if (currencySymbol == null && lookupSimpleCurrencySymbol) {
      currencySymbol = constants.simpleCurrencySymbols[name];
    }
    currencySymbol ??= name;
    var pattern = getPattern(symbols);

    // CompactNumberFormat initialization.

    /// Map from magnitude to formatting pattern for that magnitude.
    ///
    /// The magnitude is the exponent when using the normalized scientific
    /// notation (so numbers from 1000 to 9999 correspond to magnitude 3).
    ///
    /// These patterns are taken from the appropriate CompactNumberSymbols
    /// instance's COMPACT_DECIMAL_SHORT_PATTERN, COMPACT_DECIMAL_LONG_PATTERN,
    /// or COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN members.
    Map<int, Map<String, String>> patterns;

    var compactSymbols = compactNumberSymbols[locale]!;

    var styles = <int, _CompactStyleBase>{};
    switch (formatType) {
      case _CompactFormatType.COMPACT_DECIMAL_SHORT_PATTERN:
        patterns = compactSymbols.COMPACT_DECIMAL_SHORT_PATTERN;
        break;
      case _CompactFormatType.COMPACT_DECIMAL_LONG_PATTERN:
        patterns = compactSymbols.COMPACT_DECIMAL_LONG_PATTERN ??
            compactSymbols.COMPACT_DECIMAL_SHORT_PATTERN;
        break;
      case _CompactFormatType.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN:
        patterns = compactSymbols.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN;
        break;
      default:
        throw ArgumentError.notNull('formatType');
    }

    patterns.forEach((int exponent, Map<String, String> patterns) {
      _CompactStyleBase style;
      if (patterns.keys.length == 1 && patterns.keys.single == 'other') {
        // No plural.
        var pattern = patterns.values.single;
        style = _styleFromPattern(pattern, exponent, explicitSign, symbols);
      } else {
        style = _CompactStyleWithPlurals(
            patterns.map((key, value) => MapEntry(key,
                _styleFromPattern(value, exponent, explicitSign, symbols))),
            exponent,
            locale);
      }
      styles[exponent] = style;
    });

    return _CompactNumberFormat._(
        name,
        currencySymbol,
        isForCurrency,
        locale,
        localeZero,
        pattern,
        symbols,
        zeroOffset,
        NumberFormatParser.parse(symbols, pattern, isForCurrency,
            currencySymbol, name, decimalDigits),
        styles,
        explicitSign);
  }

  static _CompactStyleBase _styleFromPattern(
      String pattern, int exponent, bool explicitSign, NumberSymbols symbols) {
    if (pattern.contains(';')) {
      var patterns = pattern.split(';');
      var positivePattern = patterns.first;
      var negativePattern = patterns.last;
      if (explicitSign &&
          !positivePattern.contains(symbols.PLUS_SIGN) &&
          negativePattern.contains(symbols.MINUS_SIGN) &&
          positivePattern ==
              negativePattern.replaceAll(symbols.MINUS_SIGN, '')) {
        // Re-use the negative pattern, with plus sign.
        positivePattern =
            negativePattern.replaceAll(symbols.MINUS_SIGN, symbols.PLUS_SIGN);
      }
      return _CompactStyleWithNegative(
          _CompactStyle.createStyle(symbols, positivePattern, exponent,
              isSigned: positivePattern.contains(symbols.PLUS_SIGN)),
          _CompactStyle.createStyle(symbols, negativePattern, exponent,
              isSigned: true));
    } else {
      return _CompactStyle.createStyle(symbols, pattern, exponent,
          explicitSign: explicitSign);
    }
  }

  _CompactNumberFormat._(
      String super.currencyName,
      super.currencySymbol,
      super.isForCurrency,
      super.locale,
      super.localeZero,
      super.pattern,
      super.symbols,
      super.zeroOffset,
      super.result,
      // Fields introduced in this class.
      this._styles,
      this._explicitSign)
      : super._() {
    significantDigits = 3;
    turnOffGrouping();
  }

  @override
  set significantDigits(int? x) {
    // Replicate ICU behavior: set only the minimumSignificantDigits and
    // do not force trailing 0 in fractional part.
    _explicitMinimumFractionDigits = false;
    minimumSignificantDigits = x;
    maximumSignificantDigits = null;
    minimumSignificantDigitsStrict = false;
  }

  @override
  int get minimumFractionDigits =>
      _style != null && !_style!.isFallback && !_explicitMinimumFractionDigits
          ? 0
          : super.minimumFractionDigits;

  /// The style in which we will format a particular number.
  ///
  /// This is a temporary variable that is only valid within a call to format
  /// and parse.
  _CompactStyle? _style;

  // We delegate prefixes to current _style.
  @override
  String get positivePrefix =>
      _style!.isFallback ? super.positivePrefix : _style!.positivePrefix;
  @override
  String get negativePrefix =>
      _style!.isFallback ? super.negativePrefix : _style!.negativePrefix;
  @override
  String get positiveSuffix =>
      _style!.isFallback ? super.positiveSuffix : _style!.positiveSuffix;
  @override
  String get negativeSuffix =>
      _style!.isFallback ? super.negativeSuffix : _style!.negativeSuffix;

  @override
  String format(dynamic number) {
    var style = _styleFor(number);
    _style = style;
    final divisor = style.isFallback ? 1 : style.divisor;
    final numberToFormat = _divide(number, divisor);
    var formatted = style.isDirectValue
        ? '${_signPrefix(number)}${style.pattern}${_signSuffix(number)}'
        : super.format(numberToFormat);
    if (_explicitSign &&
        style.isFallback &&
        number >= 0 &&
        !formatted.contains(symbols.PLUS_SIGN)) {
      formatted = '${symbols.PLUS_SIGN}$formatted';
    }
    if (_isForCurrency && !style.isFallback) {
      formatted = formatted.replaceFirst('\u00a4', currencySymbol);
    }
    _style = null;
    return formatted;
  }

  @override
  bool _useDefaultSignificantDigits() {
    // For non-currencies, or for currencies if the numbers are large enough to
    // compact, always use the number of significant digits and ignore
    // decimalDigits.
    return !_isForCurrency || !_style!.isFallback;
  }

  /// Divide numbers that may not have a division operator (e.g. Int64).
  ///
  /// Only used for powers of 10, so we require an integer denominator.
  static num _divide(numerator, int denominator) {
    if (numerator is num) {
      return numerator / denominator;
    }
    // If it doesn't fit in a JS int after division, we're not going to be able
    // to meaningfully print a compact representation for it.
    var divided = numerator ~/ denominator;
    var integerPart = divided.toInt();
    if (divided != integerPart) {
      throw FormatException(
          'Number too big to use with compact format', numerator);
    }
    var remainder = numerator.remainder(denominator).toInt();
    var originalFraction = numerator - (numerator ~/ 1);
    var fraction = originalFraction == 0 ? 0 : originalFraction / denominator;
    return integerPart + (remainder / denominator) + fraction;
  }

  _CompactStyle _styleFor(number) {
    if (number.abs() < 10) {
      // Cannot be compacted.
      return _defaultCompactStyle;
    }
    var rounded = number.toDouble(); // No rounding yet...
    var digitLength = NumberFormat.numberOfIntegerDigits(number);
    var divisor = 1; // Default.

    void updateRounding() {
      var fractionDigits = maximumFractionDigits;
      if (significantDigitsInUse) {
        var divisorLength = NumberFormat.numberOfIntegerDigits(divisor);
        // We have to round the number based on the number of significant
        // digits so that we pick the right style based on the rounded form
        // and format 999999 as 1M rather than 1000K.
        fractionDigits =
            (maximumSignificantDigits ?? minimumSignificantDigits ?? 0) -
                digitLength +
                divisorLength -
                1;
        if (maximumSignificantDigits == null) {
          // Keep all digits of the integer part.
          fractionDigits = max(0, fractionDigits);
        }
      }
      var fractionMultiplier = pow(10, fractionDigits);
      rounded = (rounded * fractionMultiplier / divisor).round() *
          divisor /
          fractionMultiplier;
      digitLength = NumberFormat.numberOfIntegerDigits(rounded);
    }

    updateRounding();

    _CompactStyleBase? style;
    for (var entry in _styles.entries) {
      var exponent = entry.key + 1;
      if (exponent > digitLength) {
        break;
      }
      style = entry.value;
      // Recompute digits length based on new exponent.
      divisor = style.divisor;
      updateRounding();
    }
    return style?.styleForNumber(_divide(number, divisor), this) ??
        _defaultCompactStyle;
  }

  Iterable<_CompactStyle> get _stylesForSearching =>
      _styles.values.expand((x) => x.allStyles);

  String _normalize(String input) {
    return input
        .replaceAll('\u200e', '') // LEFT-TO-RIGHT MARK.
        .replaceAll('\u200f', '') // RIGHT-TO-LEFT MARK.
        .replaceAll('\u0020', '') // SPACE.
        .replaceAll('\u00a0', '') // NO-BREAK SPACE.
        .replaceAll('\u202f', '') // NARROW NO-BREAK SPACE.
        .replaceAll('\u2212', '-'); // MINUS SIGN.
  }

  @override
  num parse(final String inputText) {
    for (var style in [_defaultCompactStyle, ..._stylesForSearching]) {
      _style = style;
      var text = _normalize(inputText);
      var negative = false;
      var negativePrefix = _normalize(style.negativePrefix);
      var negativeSuffix = _normalize(style.negativeSuffix);
      var positivePrefix = _normalize(style.positivePrefix);
      var positiveSuffix = _normalize(style.positiveSuffix);
      if (!style.isFallback) {
        if (text.startsWith(negativePrefix) && text.endsWith(negativeSuffix)) {
          text = text.substring(
              negativePrefix.length, text.length - negativeSuffix.length);
          negative = true;
        } else if (text.startsWith(positivePrefix) &&
            text.endsWith(positiveSuffix)) {
          text = text.substring(
              positivePrefix.length, text.length - positiveSuffix.length);
        } else {
          continue;
        }
      }
      if (style.isDirectValue) {
        // "Direct formatting" pattern (1000 => "mille").
        if (text == style.pattern!) {
          _style = null;
          return style.divisor * (negative ? -1 : 1);
        } else {
          // Do not attempt parsing: no number.
          continue;
        }
      }
      var number = _tryParsing(text);
      if (number == null && _zeroOffset != 0) {
        // Locale has non-roman numerals.
        // Try simple number parse, in case input contains roman numerals.
        number = num.tryParse(text);
      }
      if (number != null) {
        _style = null;
        return number * style.divisor * (negative ? -1 : 1);
      }
    }
    _style = null;

    throw FormatException(
        "Cannot parse compact number in locale '$locale'", inputText);
  }

  /// Returns text parsed into a number if possible, else returns null.
  num? _tryParsing(String text) {
    try {
      return super.parse(text);
    } on FormatException {
      return null;
    }
  }
}

final _defaultCompactStyle = _CompactStyle();
