// Copyright (c) 2019, 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.

/// Locale extensions as defined for [Unicode Locale
/// Identifiers](http://www.unicode.org/reports/tr35/#Unicode_locale_identifier).
///
/// These extensions cover Locale information that aren't captured by the
/// language, script, region and variants subtags of the Unicode Language
/// Identifier. Please see the Unicode Technical Standard linked above.
class LocaleExtensions {
  /// Constructor.
  ///
  /// Keys in each of the maps passed to this contructor must be syntactically
  /// valid extension keys, and must already be normalized (correct case).
  LocaleExtensions(
      Map<String, String>? uExtensions,
      Map<String, String>? tExtensions,
      Map<String, String>? otherExtensions,
      this._xExtensions)
      : _uExtensions = _sortedUnmodifiable(uExtensions),
        _tExtensions = _sortedUnmodifiable(tExtensions),
        _otherExtensions = _sortedUnmodifiable(otherExtensions) {
    // Debug-mode asserts to ensure all parameters are normalized and UTS #35
    // compliant.
    assert(
        uExtensions == null ||
            uExtensions.entries.every((e) {
              if (!_uExtensionsValidKeysRE.hasMatch(e.key)) return false;
              // TODO(hugovdm) reconsider this representation: "true" values are
              // suppressed in canonical Unicode BCP47 Locale Identifiers, but
              // we may choose to represent them as "true" in memory.
              if (e.value == '' && e.key != '') return true;
              if (!_uExtensionsValidValuesRE.hasMatch(e.value)) return false;
              return true;
            }),
        'uExtensions keys must match '
        'RegExp/${_uExtensionsValidKeysRE.pattern}/. '
        'uExtensions values must match '
        'RegExp/${_uExtensionsValidValuesRE.pattern}/. '
        'uExtensions.entries: ${uExtensions.entries}.');
    assert(
        tExtensions == null ||
            tExtensions.entries.every((e) {
              if (!_tExtensionsValidKeysRE.hasMatch(e.key)) return false;
              if (e.key == '') {
                if (!_validTlangRE.hasMatch(e.value)) return false;
              } else {
                if (!_tExtensionsValidValuesRE.hasMatch(e.value)) return false;
              }
              return true;
            }),
        'tExtensions keys must match '
        'RegExp/${_tExtensionsValidKeysRE.pattern}/. '
        'tExtensions values other than tlang must match '
        'RegExp/${_tExtensionsValidValuesRE.pattern}/. '
        'Entries: ${tExtensions.entries}.');
    assert(
        otherExtensions == null ||
            otherExtensions.entries.every((e) {
              if (!_otherExtensionsValidKeysRE.hasMatch(e.key)) return false;
              if (!_otherExtensionsValidValuesRE.hasMatch(e.value)) {
                return false;
              }
              return true;
            }),
        'otherExtensions keys must match '
        'RegExp/${_otherExtensionsValidKeysRE.pattern}. '
        'otherExtensions values must match '
        'RegExp/${_otherExtensionsValidValuesRE.pattern}. '
        'Entries: ${otherExtensions.entries}.');
    assert(
        _xExtensions == null || _validXExtensionsRE.hasMatch(_xExtensions),
        '_xExtensions must match RegExp/${_validXExtensionsRE.pattern}/ '
        'but is "$_xExtensions".');
  }

  /// For debug/assert-use only! Matches keys considered valid for
  /// [_uExtensions], does not imply keys are valid as per Unicode LDML spec!
  //
  // Must be static to get tree-shaken away in production code.
  static final _uExtensionsValidKeysRE = RegExp(r'^$|^[a-z\d][a-z]$');

  /// For debug/assert-use only! Matches values considered valid for
  /// [_uExtensions], does not imply values are valid as per Unicode LDML spec!
  //
  // Must be static to get tree-shaken away in production code.
  static final _uExtensionsValidValuesRE =
      RegExp(r'^[a-z\d]{3,8}([-][a-z\d]{3,8})*$');

  /// For debug/assert-use only! Matches keys considered valid for
  /// [_tExtensions], does not imply keys are valid as per Unicode LDML spec!
  //
  // Must be static to get tree-shaken away in production code.
  static final _tExtensionsValidKeysRE = RegExp(r'^$|^[a-z]\d$');

  /// For debug/assert-use only! With the exception of `tlang`, matches values
  /// considered valid for [_tExtensions], does not imply values are valid as
  /// per Unicode LDML spec!
  //
  // Must be static to get tree-shaken away in production code.
  static final _tExtensionsValidValuesRE =
      RegExp(r'^[a-z\d]{3,8}([-][a-z\d]{3,8})*$');

  /// For debug/assert-use only! Matches keys considered valid for
  /// [_otherExtensions], does not imply keys are valid as per Unicode LDML
  /// spec!
  //
  // Must be static to get tree-shaken away in production code.
  static final _otherExtensionsValidKeysRE = RegExp(r'^[a-svwyz]$');

  /// For debug/assert-use only! Matches values considered valid for
  /// [_otherExtensions], does not imply values are valid as per Unicode LDML
  /// spec!
  //
  // Must be static to get tree-shaken away in production code.
  static final _otherExtensionsValidValuesRE =
      RegExp(r'^[a-z\d]{2,8}([-][a-z\d]{2,8})*$');

  /// For debug/assert-use only! Matches values valid for [_xExtensions].
  //
  // Must be static to get tree-shaken away in production code.
  static final _validXExtensionsRE =
      RegExp(r'^[a-z\d]{1,8}([-][a-z\d]{1,8})*$');

  /// For debug/assert-use only! Matches values valid for tlang.
  //
  // Must be static to get tree-shaken away in production code.
  static final _validTlangRE = RegExp(
      // Full string match start
      r'^'

      // Language is required in a tlang identifier.
      r'([a-z]{2,3}|[a-z]{5,8})' // Language

      // Optional script
      r'(-[a-z]{4})?'

      // Optional region
      r'(-[a-z]{2}|-\d{3})?'

      // Any number of variant subtags
      r'(-([a-z\d]{5,8}|\d[a-z\d]{3}))*'

      // Full string match end
      r'$');

  /// `-u-` extension, with keys in sorted order. Attributes are stored under
  /// the zero-length string as key. Keywords (consisting of `key` and `type`)
  /// are stored under normalized (lowercased) `key`. See
  /// http://www.unicode.org/reports/tr35/#unicode_locale_extensions for
  /// details.
  final Map<String, String> _uExtensions;

  /// `-t-` extension, with keys in sorted order. tlang attributes are stored
  /// under the zero-length string as key. See
  /// http://www.unicode.org/reports/tr35/#transformed_extensions for
  /// details.
  final Map<String, String> _tExtensions;

  /// Other extensions, with keys in sorted order. See
  /// http://www.unicode.org/reports/tr35/#other_extensions for details.
  final Map<String, String> _otherExtensions;

  /// -x- extension values. See
  /// http://www.unicode.org/reports/tr35/#pu_extensions for details.
  final String? _xExtensions;

  /// List of subtags in the [Unicode Locale
  /// Identifier](https://www.unicode.org/reports/tr35/#Unicode_locale_identifier)
  /// extensions, including private use extensions.
  ///
  /// This covers everything after the unicode_language_id. If there are no
  /// extensions (i.e. the Locale Identifier has only language, script, region
  /// and/or variants), this will be an empty list.
  ///
  /// These subtags are sorted and normalized, ready for joining with a
  /// unicode_language_id and '-' as delimiter to provide a UTS #35 compliant
  /// normalized Locale Identifier.
  List<String> get subtags {
    final result = <String>[];
    final resultVWYZ = <String>[];

    _otherExtensions.forEach((singleton, value) {
      final letter = (singleton.codeUnitAt(0) - 0x61) & 0xFFFF;
      // 't', 'u' and 'x' are handled by other members.
      assert(letter < 26 && letter != 19 && letter != 20 && letter != 23);
      if (letter < 19) {
        result.addAll([singleton, value]);
      } else {
        resultVWYZ.addAll([singleton, value]);
      }
    });
    if (_tExtensions.isNotEmpty) {
      result.add('t');
      _tExtensions.forEach((key, value) {
        if (key != '') result.add(key);
        result.add(value);
      });
    }
    if (_uExtensions.isNotEmpty) {
      result.add('u');
      _uExtensions.forEach((key, value) {
        if (key != '') result.add(key);
        if (value != '') result.add(value);
      });
    }

    if (resultVWYZ.isNotEmpty) {
      result.addAll(resultVWYZ);
    }
    if (_xExtensions != null) {
      result.add('x-$_xExtensions');
    }
    return result;
  }
}

/// Creates an unmodifiable and sorted version of `unsorted`.
Map<String, String> _sortedUnmodifiable(Map<String, String>? unsorted) {
  if (unsorted == null) {
    return const {};
  }
  var map = <String, String>{};
  for (var key in unsorted.keys.toList()..sort()) {
    map[key] = unsorted[key]!;
  }
  return Map.unmodifiable(map);
}
