// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl;
import 'package:intl/date_symbols.dart' as intl;
import 'package:intl/date_symbol_data_custom.dart' as date_symbol_data_custom;
import 'l10n/date_localizations.dart' as date_localizations;

import 'l10n/localizations.dart' show TranslationBundle, translationBundleForLocale;
import 'widgets_localizations.dart';

// Watch out: the supported locales list in the doc comment below must be kept
// in sync with the list we test, see test/translations_test.dart, and of course
// the acutal list of supported locales in _MaterialLocalizationsDelegate.

/// Localized strings for the material widgets.
///
/// To include the localizations provided by this class in a [MaterialApp],
/// add [GlobalMaterialLocalizations.delegates] to
/// [MaterialApp.localizationsDelegates], and specify the locales your
/// app supports with [MaterialApp.supportedLocales]:
///
/// ```dart
/// new MaterialApp(
///   localizationsDelegates: GlobalMaterialLocalizations.delegates,
///   supportedLocales: [
///     const Locale('en', 'US'), // English
///     const Locale('he', 'IL'), // Hebrew
///     // ...
///   ],
///   // ...
/// )
/// ```
///
/// This class supports locales with the following [Locale.languageCode]s:
///
///   * ar - Arabic
///   * de - German
///   * en - English
///   * es - Spanish
///   * fa - Farsi
///   * fr - French
///   * he - Hebrew
///   * id - Indonesian
///   * it - Italian
///   * ja - Japanese
///   * ko - Korean
///   * nl - Dutch
///   * no - Norwegian
///   * pl - Polish
///   * ps - Pashto
///   * pt - Portuguese
///   * ro - Romanian
///   * ru - Russian
///   * th - Thai
///   * tr - Turkish
///   * ur - Urdu
///   * zh - Simplified Chinese
///
/// See also:
///
///  * The Flutter Internationalization Tutorial,
///    <https://flutter.io/tutorials/internationalization/>.
///  * [DefaultMaterialLocalizations], which only provides US English translations.
class GlobalMaterialLocalizations implements MaterialLocalizations {
  /// Constructs an object that defines the material widgets' localized strings
  /// for the given `locale`.
  ///
  /// [LocalizationsDelegate] implementations typically call the static [load]
  /// function, rather than constructing this class directly.
  GlobalMaterialLocalizations(this.locale)
      : assert(locale != null),
        _localeName = _computeLocaleName(locale) {
    _loadDateIntlDataIfNotLoaded();

    _translationBundle = translationBundleForLocale(locale);
    assert(_translationBundle != null);

    const String kMediumDatePattern = 'E, MMM\u00a0d';
    if (intl.DateFormat.localeExists(_localeName)) {
      _fullYearFormat = new intl.DateFormat.y(_localeName);
      _mediumDateFormat = new intl.DateFormat(kMediumDatePattern, _localeName);
      _longDateFormat = new intl.DateFormat.yMMMMEEEEd(_localeName);
      _yearMonthFormat = new intl.DateFormat('yMMMM', _localeName);
    } else if (intl.DateFormat.localeExists(locale.languageCode)) {
      _fullYearFormat = new intl.DateFormat.y(locale.languageCode);
      _mediumDateFormat = new intl.DateFormat(kMediumDatePattern, locale.languageCode);

      _longDateFormat = new intl.DateFormat.yMMMMEEEEd(locale.languageCode);
      _yearMonthFormat = new intl.DateFormat('yMMMM', locale.languageCode);
    } else {
      _fullYearFormat = new intl.DateFormat.y();
      _mediumDateFormat = new intl.DateFormat(kMediumDatePattern);
      _longDateFormat = new intl.DateFormat.yMMMMEEEEd();
      _yearMonthFormat = new intl.DateFormat('yMMMM');
    }

    if (intl.NumberFormat.localeExists(_localeName)) {
      _decimalFormat = new intl.NumberFormat.decimalPattern(_localeName);
      _twoDigitZeroPaddedFormat = new intl.NumberFormat('00', _localeName);
    } else if (intl.NumberFormat.localeExists(locale.languageCode)) {
      _decimalFormat = new intl.NumberFormat.decimalPattern(locale.languageCode);
      _twoDigitZeroPaddedFormat = new intl.NumberFormat('00', locale.languageCode);
    } else {
      _decimalFormat = new intl.NumberFormat.decimalPattern();
      _twoDigitZeroPaddedFormat = new intl.NumberFormat('00');
    }
  }

  /// The locale for which the values of this class's localized resources
  /// have been translated.
  final Locale locale;

  final String _localeName;

  TranslationBundle _translationBundle;

  intl.NumberFormat _decimalFormat;

  intl.NumberFormat _twoDigitZeroPaddedFormat;

  intl.DateFormat _fullYearFormat;

  intl.DateFormat _mediumDateFormat;

  intl.DateFormat _longDateFormat;

  intl.DateFormat _yearMonthFormat;

  static String _computeLocaleName(Locale locale) {
    return intl.Intl.canonicalizedLocale(locale.toString());
  }

  @override
  String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat: false }) {
    switch (hourFormat(of: timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat))) {
      case HourFormat.HH:
        return _twoDigitZeroPaddedFormat.format(timeOfDay.hour);
      case HourFormat.H:
        return formatDecimal(timeOfDay.hour);
      case HourFormat.h:
        final int hour = timeOfDay.hourOfPeriod;
        return formatDecimal(hour == 0 ? 12 : hour);
    }
    return null;
  }

  @override
  String formatMinute(TimeOfDay timeOfDay) {
    return _twoDigitZeroPaddedFormat.format(timeOfDay.minute);
  }

  @override
  String formatYear(DateTime date) {
    return _fullYearFormat.format(date);
  }

  @override
  String formatMediumDate(DateTime date) {
    return _mediumDateFormat.format(date);
  }

  @override
  String formatFullDate(DateTime date) {
    return _longDateFormat.format(date);
  }

  @override
  String formatMonthYear(DateTime date) {
    return _yearMonthFormat.format(date);
  }

  @override
  List<String> get narrowWeekdays {
    return _fullYearFormat.dateSymbols.NARROWWEEKDAYS;
  }

  @override
  int get firstDayOfWeekIndex => (_fullYearFormat.dateSymbols.FIRSTDAYOFWEEK + 1) % 7;

  @override
  String formatDecimal(int number) {
    return _decimalFormat.format(number);
  }

  @override
  String formatTimeOfDay(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat: false }) {
    // Not using intl.DateFormat for two reasons:
    //
    // - DateFormat supports more formats than our material time picker does,
    //   and we want to be consistent across time picker format and the string
    //   formatting of the time of day.
    // - DateFormat operates on DateTime, which is sensitive to time eras and
    //   time zones, while here we want to format hour and minute within one day
    //   no matter what date the day falls on.
    final String hour = formatHour(timeOfDay, alwaysUse24HourFormat: alwaysUse24HourFormat);
    final String minute = formatMinute(timeOfDay);
    switch (timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat)) {
      case TimeOfDayFormat.h_colon_mm_space_a:
        return '$hour:$minute ${_formatDayPeriod(timeOfDay)}';
      case TimeOfDayFormat.H_colon_mm:
      case TimeOfDayFormat.HH_colon_mm:
        return '$hour:$minute';
      case TimeOfDayFormat.HH_dot_mm:
        return '$hour.$minute';
      case TimeOfDayFormat.a_space_h_colon_mm:
        return '${_formatDayPeriod(timeOfDay)} $hour:$minute';
      case TimeOfDayFormat.frenchCanadian:
        return '$hour h $minute';
    }

    return null;
  }

  String _formatDayPeriod(TimeOfDay timeOfDay) {
    switch (timeOfDay.period) {
      case DayPeriod.am:
        return anteMeridiemAbbreviation;
      case DayPeriod.pm:
        return postMeridiemAbbreviation;
    }
    return null;
  }

  @override
  String get openAppDrawerTooltip => _translationBundle.openAppDrawerTooltip;

  @override
  String get backButtonTooltip => _translationBundle.backButtonTooltip;

  @override
  String get closeButtonTooltip => _translationBundle.closeButtonTooltip;

  @override
  String get deleteButtonTooltip => _translationBundle.deleteButtonTooltip;

  @override
  String get nextMonthTooltip => _translationBundle.nextMonthTooltip;

  @override
  String get previousMonthTooltip => _translationBundle.previousMonthTooltip;

  @override
  String get nextPageTooltip => _translationBundle.nextPageTooltip;

  @override
  String get previousPageTooltip => _translationBundle.previousPageTooltip;

  @override
  String get showMenuTooltip => _translationBundle.showMenuTooltip;

  @override
  String get drawerLabel => _translationBundle.alertDialogLabel;

  @override
  String get popupMenuLabel => _translationBundle.popupMenuLabel;

  @override
  String get dialogLabel => _translationBundle.dialogLabel;

  @override
  String get alertDialogLabel => _translationBundle.alertDialogLabel;

  @override
  String aboutListTileTitle(String applicationName) {
    final String text = _translationBundle.aboutListTileTitle;
    return text.replaceFirst(r'$applicationName', applicationName);
  }

  @override
  String get licensesPageTitle => _translationBundle.licensesPageTitle;

  @override
  String pageRowsInfoTitle(int firstRow, int lastRow, int rowCount, bool rowCountIsApproximate) {
    String text = rowCountIsApproximate ? _translationBundle.pageRowsInfoTitleApproximate : null;
    text ??= _translationBundle.pageRowsInfoTitle;
    assert(text != null, 'A $locale localization was not found for pageRowsInfoTitle or pageRowsInfoTitleApproximate');
    // TODO(hansmuller): this could be more efficient.
    return text
      .replaceFirst(r'$firstRow', formatDecimal(firstRow))
      .replaceFirst(r'$lastRow', formatDecimal(lastRow))
      .replaceFirst(r'$rowCount', formatDecimal(rowCount));
  }

  @override
  String get rowsPerPageTitle => _translationBundle.rowsPerPageTitle;

  @override
  String tabLabel({int tabIndex, int tabCount}) {
    assert(tabIndex >= 1);
    assert(tabCount >= 1);
    final String template = _translationBundle.tabLabel;
    return template
      .replaceFirst(r'$tabIndex', formatDecimal(tabIndex))
      .replaceFirst(r'$tabCount', formatDecimal(tabCount));
  }

  @override
  String selectedRowCountTitle(int selectedRowCount) {
    // TODO(hmuller): the rules for mapping from an integer value to
    // "one" or "two" etc. are locale specific and an additional "few" category
    // is needed. See http://cldr.unicode.org/index/cldr-spec/plural-rules
    String text;
    if (selectedRowCount == 0)
      text = _translationBundle.selectedRowCountTitleZero;
    else if (selectedRowCount == 1)
      text = _translationBundle.selectedRowCountTitleOne;
    else if (selectedRowCount == 2)
      text = _translationBundle.selectedRowCountTitleTwo;
    else if (selectedRowCount > 2)
      text = _translationBundle.selectedRowCountTitleMany;
    text ??= _translationBundle.selectedRowCountTitleOther;
    assert(text != null);

    return text.replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount));
  }

  @override
  String get cancelButtonLabel => _translationBundle.cancelButtonLabel;

  @override
  String get closeButtonLabel => _translationBundle.closeButtonLabel;

  @override
  String get continueButtonLabel => _translationBundle.continueButtonLabel;

  @override
  String get copyButtonLabel => _translationBundle.copyButtonLabel;

  @override
  String get cutButtonLabel => _translationBundle.cutButtonLabel;

  @override
  String get okButtonLabel => _translationBundle.okButtonLabel;

  @override
  String get pasteButtonLabel => _translationBundle.pasteButtonLabel;

  @override
  String get selectAllButtonLabel => _translationBundle.selectAllButtonLabel;

  @override
  String get viewLicensesButtonLabel => _translationBundle.viewLicensesButtonLabel;

  @override
  String get anteMeridiemAbbreviation => _translationBundle.anteMeridiemAbbreviation;

  @override
  String get postMeridiemAbbreviation => _translationBundle.postMeridiemAbbreviation;

  @override
  String get timePickerHourModeAnnouncement => _translationBundle.timePickerHourModeAnnouncement;

  @override
  String get timePickerMinuteModeAnnouncement => _translationBundle.timePickerMinuteModeAnnouncement;

  @override
  String get modalBarrierDismissLabel => _translationBundle.modalBarrierDismissLabel;

  @override
  String get signedInLabel => _translationBundle.signedInLabel;

  @override
  String get hideAccountsLabel => _translationBundle.hideAccountsLabel;

  @override
  String get showAccountsLabel => _translationBundle.showAccountsLabel;

  /// The [TimeOfDayFormat] corresponding to one of the following supported
  /// patterns:
  ///
  ///  * `HH:mm`
  ///  * `HH.mm`
  ///  * `HH 'h' mm`
  ///  * `HH:mm น.`
  ///  * `H:mm`
  ///  * `h:mm a`
  ///  * `a h:mm`
  ///  * `ah:mm`
  ///
  /// See also:
  ///
  ///  * http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US shows the
  ///    short time pattern used in locale en_US
  @override
  TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat: false }) {
    final String icuShortTimePattern = _translationBundle.timeOfDayFormat;

    assert(() {
      if (!_icuTimeOfDayToEnum.containsKey(icuShortTimePattern)) {
        throw new FlutterError(
          '"$icuShortTimePattern" is not one of the ICU short time patterns '
          'supported by the material library. Here is the list of supported '
          'patterns:\n  ' +
          _icuTimeOfDayToEnum.keys.join('\n  ')
        );
      }
      return true;
    }());

    final TimeOfDayFormat icuFormat = _icuTimeOfDayToEnum[icuShortTimePattern];

    if (alwaysUse24HourFormat)
      return _get24HourVersionOf(icuFormat);

    return icuFormat;
  }

  /// Looks up text geometry defined in [MaterialTextGeometry].
  @override
  TextTheme get localTextGeometry => MaterialTextGeometry.forScriptCategory(_translationBundle.scriptCategory);

  /// Creates an object that provides localized resource values for the
  /// for the widgets of the material library.
  ///
  /// This method is typically used to create a [LocalizationsDelegate].
  /// The [MaterialApp] does so by default.
  static Future<MaterialLocalizations> load(Locale locale) {
    return new SynchronousFuture<MaterialLocalizations>(new GlobalMaterialLocalizations(locale));
  }

  /// A [LocalizationsDelegate] that uses [GlobalMaterialLocalizations.load]
  /// to create an instance of this class.
  ///
  /// Most internationalized apps will use [GlobalMaterialLocalizations.delegates]
  /// as the value of [MaterialApp.localizationsDelegates] to include
  /// the localizations for both the material and widget libraries.
  static const LocalizationsDelegate<MaterialLocalizations> delegate = const _MaterialLocalizationsDelegate();

  /// A value for [MaterialApp.localizationsDelegates] that's typically used by
  /// internationalized apps.
  ///
  /// To include the localizations provided by this class and by
  /// [GlobalWidgetsLocalizations] in a [MaterialApp],
  /// use [GlobalMaterialLocalizations.delegates] as the value of
  /// [MaterialApp.localizationsDelegates], and specify the locales your
  /// app supports with [MaterialApp.supportedLocales]:
  ///
  /// ```dart
  /// new MaterialApp(
  ///   localizationsDelegates: GlobalMaterialLocalizations.delegates,
  ///   supportedLocales: [
  ///     const Locale('en', 'US'), // English
  ///     const Locale('he', 'IL'), // Hebrew
  ///   ],
  ///   // ...
  /// )
  /// ```
  static const List<LocalizationsDelegate<dynamic>> delegates = const <LocalizationsDelegate<dynamic>>[
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ];
}

const Map<String, TimeOfDayFormat> _icuTimeOfDayToEnum = const <String, TimeOfDayFormat>{
  'HH:mm': TimeOfDayFormat.HH_colon_mm,
  'HH.mm': TimeOfDayFormat.HH_dot_mm,
  "HH 'h' mm": TimeOfDayFormat.frenchCanadian,
  'HH:mm น.': TimeOfDayFormat.HH_colon_mm,
  'H:mm': TimeOfDayFormat.H_colon_mm,
  'h:mm a': TimeOfDayFormat.h_colon_mm_space_a,
  'a h:mm': TimeOfDayFormat.a_space_h_colon_mm,
  'ah:mm': TimeOfDayFormat.a_space_h_colon_mm,
};

/// Finds the [TimeOfDayFormat] to use instead of the `original` when the
/// `original` uses 12-hour format and [MediaQueryData.alwaysUse24HourFormat]
/// is true.
TimeOfDayFormat _get24HourVersionOf(TimeOfDayFormat original) {
  switch (original) {
    case TimeOfDayFormat.HH_colon_mm:
    case TimeOfDayFormat.HH_dot_mm:
    case TimeOfDayFormat.frenchCanadian:
    case TimeOfDayFormat.H_colon_mm:
      return original;
    case TimeOfDayFormat.h_colon_mm_space_a:
    case TimeOfDayFormat.a_space_h_colon_mm:
      return TimeOfDayFormat.HH_colon_mm;
  }
  return TimeOfDayFormat.HH_colon_mm;
}

/// Tracks if date i18n data has been loaded.
bool _dateIntlDataInitialized = false;

/// Loads i18n data for dates if it hasn't be loaded yet.
///
/// Only the first invocation of this function has the effect of loading the
/// data. Subsequent invocations have no effect.
void _loadDateIntlDataIfNotLoaded() {
  if (!_dateIntlDataInitialized) {
    date_localizations.dateSymbols.forEach((String locale, dynamic data) {
      assert(date_localizations.datePatterns.containsKey(locale));
      final intl.DateSymbols symbols = new intl.DateSymbols.deserializeFromMap(data);
      date_symbol_data_custom.initializeDateFormattingCustom(
        locale: locale,
        symbols: symbols,
        patterns: date_localizations.datePatterns[locale],
      );
    });
    _dateIntlDataInitialized = true;
  }
}

class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
  const _MaterialLocalizationsDelegate();

  // Watch out: this list must match the one in the GlobalMaterialLocalizations
  // class doc and the list we test, see test/translations_test.dart.
  static const List<String> _supportedLanguages = const <String>[
    'ar', // Arabic
    'de', // German
    'en', // English
    'es', // Spanish
    'fa', // Farsi (Persian)
    'fr', // French
    'he', // Hebrew
    'id', // Indonesian
    'it', // Italian
    'ja', // Japanese
    'ko', // Korean
    'nl', // Dutch
    'no', // Norwegian
    'pl', // Polish
    'ps', // Pashto
    'pt', // Portugese
    'ro', // Romanian
    'ru', // Russian
    'th', // Thai
    'tr', // Turkish
    'ur', // Urdu
    'zh', // Chinese (simplified)
  ];

  @override
  bool isSupported(Locale locale) => _supportedLanguages.contains(locale.languageCode);

  @override
  Future<MaterialLocalizations> load(Locale locale) => GlobalMaterialLocalizations.load(locale);

  @override
  bool shouldReload(_MaterialLocalizationsDelegate old) => false;
}