// Copyright 2014 The Flutter 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 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';

import '../../base/file_system.dart';
import '../../base/logger.dart';
import '../../convert.dart';
import '../../globals.dart' as globals;
import '../../localizations/gen_l10n.dart';
import '../../localizations/gen_l10n_types.dart';
import '../../localizations/localizations_utils.dart';
import '../../project.dart';
import '../build_system.dart';
import '../depfile.dart';

const String _kDependenciesFileName = 'gen_l10n_inputs_and_outputs.json';

/// Run the localizations generation script with the configuration [options].
void generateLocalizations({
  @required Directory projectDir,
  @required Directory dependenciesDir,
  @required LocalizationOptions options,
  @required LocalizationsGenerator localizationsGenerator,
  @required Logger logger,
}) {
  // If generating a synthetic package, generate a warning if
  // flutter: generate is not set.
  final FlutterProject flutterProject = FlutterProject.fromDirectory(projectDir);
  if (options.useSyntheticPackage && !flutterProject.manifest.generateSyntheticPackage) {
    logger.printError(
      'Attempted to generate localizations code without having '
      'the flutter: generate flag turned on.'
      '\n'
      'Check pubspec.yaml and ensure that flutter: generate: true has '
      'been added and rebuild the project. Otherwise, the localizations '
      'source code will not be importable.'
    );
    throw Exception();
  }

  precacheLanguageAndRegionTags();

  final String inputPathString = options?.arbDirectory?.path ?? globals.fs.path.join('lib', 'l10n');
  final String templateArbFileName = options?.templateArbFile?.toFilePath() ?? 'app_en.arb';
  final String outputFileString = options?.outputLocalizationsFile?.toFilePath() ?? 'app_localizations.dart';

  try {
    localizationsGenerator
      ..initialize(
        inputsAndOutputsListPath: dependenciesDir.path,
        projectPathString: projectDir.path,
        inputPathString: inputPathString,
        templateArbFileName: templateArbFileName,
        outputFileString: outputFileString,
        outputPathString: options?.outputDirectory?.path,
        classNameString: options.outputClass ?? 'AppLocalizations',
        preferredSupportedLocale: options.preferredSupportedLocales,
        headerString: options.header,
        headerFile: options?.headerFile?.toFilePath(),
        useDeferredLoading: options.deferredLoading ?? false,
        useSyntheticPackage: options.useSyntheticPackage ?? true,
        areResourceAttributesRequired: options.areResourceAttributesRequired ?? false,
        untranslatedMessagesFile: options?.untranslatedMessagesFile?.toFilePath(),
      )
      ..loadResources()
      ..writeOutputFiles(logger);
  } on L10nException catch (e) {
    logger.printError(e.message);
    throw Exception();
  }
}

/// A build step that runs the generate localizations script from
/// dev/tool/localizations.
class GenerateLocalizationsTarget extends Target {
  const GenerateLocalizationsTarget();

  @override
  List<Target> get dependencies => <Target>[];

  @override
  List<Source> get inputs => <Source>[
    // This is added as a convenience for developing the tool.
    const Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/localizations.dart'),
    // TODO(jonahwilliams): once https://github.com/flutter/flutter/issues/56321 is
    // complete, we should add the artifact as a dependency here. Since the tool runs
    // this code from source, looking up each dependency will be cumbersome.
  ];

  @override
  String get name => 'gen_localizations';

  @override
  List<Source> get outputs => <Source>[];

  @override
  List<String> get depfiles => <String>['gen_localizations.d'];

  @override
  bool canSkip(Environment environment) {
    final File configFile = environment.projectDir.childFile('l10n.yaml');
    return !configFile.existsSync();
  }

  @override
  Future<void> build(Environment environment) async {
    final File configFile = environment.projectDir.childFile('l10n.yaml');
    assert(configFile.existsSync());

    final LocalizationOptions options = parseLocalizationsOptions(
      file: configFile,
      logger: globals.logger,
    );
    final DepfileService depfileService = DepfileService(
      logger: environment.logger,
      fileSystem: environment.fileSystem,
    );
    generateLocalizations(
      logger: environment.logger,
      options: options,
      projectDir: environment.projectDir,
      dependenciesDir: environment.buildDir,
      localizationsGenerator: LocalizationsGenerator(environment.fileSystem),
    );

    final Map<String, Object> dependencies = json.decode(
      environment.buildDir.childFile(_kDependenciesFileName).readAsStringSync()
    ) as Map<String, Object>;
    final Depfile depfile = Depfile(
      <File>[
        configFile,
        for (dynamic inputFile in dependencies['inputs'] as List<dynamic>)
          environment.fileSystem.file(inputFile)
      ],
      <File>[
        for (dynamic outputFile in dependencies['outputs'] as List<dynamic>)
          environment.fileSystem.file(outputFile)
      ]
    );
    depfileService.writeToFile(
      depfile,
      environment.buildDir.childFile('gen_localizations.d'),
    );
  }
}

/// Typed configuration from the localizations config file.
class LocalizationOptions {
  const LocalizationOptions({
    this.arbDirectory,
    this.templateArbFile,
    this.outputLocalizationsFile,
    this.untranslatedMessagesFile,
    this.header,
    this.outputClass,
    this.outputDirectory,
    this.preferredSupportedLocales,
    this.headerFile,
    this.deferredLoading,
    this.useSyntheticPackage = true,
    this.areResourceAttributesRequired = false,
  }) : assert(useSyntheticPackage != null);

  /// The `--arb-dir` argument.
  ///
  /// The directory where all input localization files should reside.
  final Uri arbDirectory;

  /// The `--template-arb-file` argument.
  ///
  /// This URI is relative to [arbDirectory].
  final Uri templateArbFile;

  /// The `--output-localization-file` argument.
  ///
  /// This URI is relative to [arbDirectory].
  final Uri outputLocalizationsFile;

  /// The `--untranslated-messages-file` argument.
  ///
  /// This URI is relative to [arbDirectory].
  final Uri untranslatedMessagesFile;

  /// The `--header` argument.
  ///
  /// The header to prepend to the generated Dart localizations.
  final String header;

  /// The `--output-class` argument.
  final String outputClass;

  /// The `--output-dir` argument.
  ///
  /// The directory where all output localization files should be generated.
  final Uri outputDirectory;

  /// The `--preferred-supported-locales` argument.
  final List<String> preferredSupportedLocales;

  /// The `--header-file` argument.
  ///
  /// A file containing the header to prepend to the generated
  /// Dart localizations.
  final Uri headerFile;

  /// The `--use-deferred-loading` argument.
  ///
  /// Whether to generate the Dart localization file with locales imported
  /// as deferred.
  final bool deferredLoading;

  /// The `--synthetic-package` argument.
  ///
  /// Whether to generate the Dart localization files in a synthetic package
  /// or in a custom directory.
  final bool useSyntheticPackage;

  /// The `required-resource-attributes` argument.
  ///
  /// Whether to require all resource ids to contain a corresponding
  /// resource attribute.
  final bool areResourceAttributesRequired;
}

/// Parse the localizations configuration options from [file].
///
/// Throws [Exception] if any of the contents are invalid. Returns a
/// [LocalizationOptions] with all fields as `null` if the config file exists
/// but is empty.
LocalizationOptions parseLocalizationsOptions({
  @required File file,
  @required Logger logger,
}) {
  final String contents = file.readAsStringSync();
  if (contents.trim().isEmpty) {
    return const LocalizationOptions();
  }
  final YamlNode yamlNode = loadYamlNode(file.readAsStringSync());
  if (yamlNode is! YamlMap) {
    logger.printError('Expected ${file.path} to contain a map, instead was $yamlNode');
    throw Exception();
  }
  final YamlMap yamlMap = yamlNode as YamlMap;
  return LocalizationOptions(
    arbDirectory: _tryReadUri(yamlMap, 'arb-dir', logger),
    templateArbFile: _tryReadUri(yamlMap, 'template-arb-file', logger),
    outputLocalizationsFile: _tryReadUri(yamlMap, 'output-localization-file', logger),
    untranslatedMessagesFile: _tryReadUri(yamlMap, 'untranslated-messages-file', logger),
    header: _tryReadString(yamlMap, 'header', logger),
    outputClass: _tryReadString(yamlMap, 'output-class', logger),
    outputDirectory: _tryReadUri(yamlMap, 'output-dir', logger),
    preferredSupportedLocales: _tryReadStringList(yamlMap, 'preferred-supported-locales', logger),
    headerFile: _tryReadUri(yamlMap, 'header-file', logger),
    deferredLoading: _tryReadBool(yamlMap, 'use-deferred-loading', logger),
    useSyntheticPackage: _tryReadBool(yamlMap, 'synthetic-package', logger) ?? true,
    areResourceAttributesRequired: _tryReadBool(yamlMap, 'required-resource-attributes', logger) ?? false,
  );
}

// Try to read a `bool` value or null from `yamlMap`, otherwise throw.
bool _tryReadBool(YamlMap yamlMap, String key, Logger logger) {
  final Object value = yamlMap[key];
  if (value == null) {
    return null;
  }
  if (value is! bool) {
    logger.printError('Expected "$key" to have a bool value, instead was "$value"');
    throw Exception();
  }
  return value as bool;
}

// Try to read a `String` value or null from `yamlMap`, otherwise throw.
String _tryReadString(YamlMap yamlMap, String key, Logger logger) {
  final Object value = yamlMap[key];
  if (value == null) {
    return null;
  }
  if (value is! String) {
    logger.printError('Expected "$key" to have a String value, instead was "$value"');
    throw Exception();
  }
  return value as String;
}

List<String> _tryReadStringList(YamlMap yamlMap, String key, Logger logger) {
  final Object value = yamlMap[key];
  if (value == null) {
    return null;
  }
  if (value is String) {
    return <String>[value];
  }
  if (value is Iterable) {
    return value.map((dynamic e) => e.toString()).toList();
  }
  logger.printError('"$value" must be String or List.');
  throw Exception();
}

// Try to read a valid `Uri` or null from `yamlMap`, otherwise throw.
Uri _tryReadUri(YamlMap yamlMap, String key, Logger logger) {
  final String value = _tryReadString(yamlMap, key, logger);
  if (value == null) {
    return null;
  }
  final Uri uri = Uri.tryParse(value);
  if (uri == null) {
    logger.printError('"$value" must be a relative file URI');
  }
  return uri;
}
