// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program 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 of the License, or
// any later version.
//
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.

import 'package:flutter/services.dart' show AssetBundle, rootBundle;
import 'package:flutter/widgets.dart';
import 'package:wyatt_i18n/src/core/utils/assets_utils.dart';
import 'package:wyatt_i18n/src/core/utils/intl_utils.dart';
import 'package:wyatt_i18n/src/core/utils/parsers/locale_parser.dart';
import 'package:wyatt_i18n/wyatt_i18n.dart';

/// {@template assets_i18n_data_source_impl}
/// Implementation of [I18nDataSource] that loads i18n files from the assets.
///
/// The [basePath] is the folder where the i18n files are located.
/// The [baseName] is the name of the i18n files without the extension.
/// The [format] is the format of the i18n files.
/// The [defaultLocale] is the default locale to use when the locale is not
/// specified.
/// The [separator] is the separator to use when the locale is specified.
///
/// For example, if the i18n files are located in the `assets/l10n/` and are
/// named `i18n.en.arb`, `i18n.fr.arb`, etc., then the [basePath]
/// is `l10n` and the [baseName] is `i18n` and the [format] is [Format.arb].
/// {@endtemplate}
class AssetsI18nDataSourceImpl extends I18nDataSource {
  /// {@macro assets_i18n_data_source_impl}
  AssetsI18nDataSourceImpl({
    this.basePath = '',
    this.baseName = 'i18n',
    super.format = Format.arb,
    super.defaultLocale = const Locale('en'),
    super.separator = '.',
    super.localeTransformer,
  }) : super();

  /// Creates a new instance of [AssetsI18nDataSourceImpl] with the system
  /// locale as the default locale.
  /// So, the [defaultLocale] is the system locale and you don't need to
  /// specify it when you load the i18n file.
  // ignore: long-parameter-list
  static Future<AssetsI18nDataSourceImpl> withSystemLocale({
    String basePath = '',
    String baseName = 'i18n',
    Format format = Format.arb,
    String separator = '.',
    LocaleTransformer? localeTransformer,
  }) async {
    final locale = await IntlUtils.getSystemLocale();

    return AssetsI18nDataSourceImpl(
      basePath: basePath,
      baseName: baseName,
      format: format,
      defaultLocale: const LocaleParser().parse(locale),
      separator: separator,
      localeTransformer: localeTransformer,
    );
  }

  /// The folder where the i18n files are located.
  ///
  /// Note: The path is relative to the `assets` folder. So, `assets/l10n/`
  /// is `l10n`.
  final String basePath;

  /// The name of the i18n files without the extension.
  final String baseName;

  /// The asset bundle used to load the i18n files.
  final AssetBundle assetBundle = rootBundle;

  /// Tries to load the i18n file from the given [locale].
  Future<I18n> _tryLoad(Locale? locale) async {
    String? content;
    final ext = format.name;

    final defaultLocaleString = localeTransformer?.call(defaultLocale) ??
        const LocaleParser().serialize(defaultLocale);

    final localeString = locale == null
        ? defaultLocaleString
        : localeTransformer?.call(locale) ??
            const LocaleParser().serialize(locale);

    final path =
        AssetsUtils.cleanPath('$basePath/$baseName.$localeString.$ext');

    try {
      if (await AssetsUtils.assetExists(path)) {
        content = await assetBundle.loadString(path);
      }
    } catch (_) {
      content = null;
    }

    /// If the i18n file is not found, then we try to load the
    /// default i18n file.
    if (content == null && locale != null) {
      try {
        final fallbackPath = AssetsUtils.cleanPath(
          '$basePath/$baseName.$defaultLocaleString.$ext',
        );
        content = await assetBundle.loadString(fallbackPath);
      } catch (_) {
        throw SourceNotFoundException(path, format: format);
      }
    }

    /// If the default i18n file is not found, then we throw an exception.
    /// This case should happen only if the locale is null.
    if (content == null) {
      throw SourceNotFoundException(path, format: format);
    }

    /// Parse the i18n file.
    final parsedData = format.parser.parse(content);

    return I18n(
      unparsedData: content,
      data: parsedData,
      locale: locale,
    );
  }

  /// Tries to load the i18n file from a given [uri].
  /// This method is used when the [uri] is not null.
  Future<I18n> _tryLoadUri(Uri uri) async {
    String? content;
    try {
      content = await assetBundle.loadString(uri.toString());
    } catch (_) {
      throw SourceNotFoundException(uri.toString(), format: format);
    }

    /// Parse the i18n file.
    final parsedData = format.parser.parse(content);

    return I18n(
      unparsedData: content,
      data: parsedData,
    );
  }

  /// Loads the i18n file from Assets folder.
  ///
  /// The i18n file must be in [basePath], named [baseName] +
  /// `.<locale>.<extension>` and must be specified in the `pubspec.yaml` file.
  @override
  Future<I18n> load({Locale? locale}) async =>
      super.currentI18nFile = await _tryLoad(locale);

  /// Loads the i18n file from Assets folder.
  ///
  /// If the [uri] is not null, then we try to load the i18n file
  /// from the given [uri]. In this case, the [basePath] and the
  /// [baseName] are ignored. And there is no fallback.
  @override
  Future<I18n> loadFrom(Uri uri) async =>
      super.currentI18nFile = await _tryLoadUri(uri);
}
