// Copyright 2019 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 'dart:convert';
import 'dart:io';

import 'package:args/args.dart' as argslib;
import 'package:path/path.dart' as path;

import 'localizations_utils.dart';

const String defaultFileTemplate = '''
import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart';

import 'messages_all.dart';

/// Callers can lookup localized strings with an instance of @className returned
/// by `@className.of(context)`.
///
/// Applications need to include `@className.delegate()` in their app\'s
/// localizationDelegates list, and the locales they support in the app\'s
/// supportedLocales list. For example:
///
/// ```
/// import '@importFile';
///
/// return MaterialApp(
///   localizationsDelegates: @className.localizationsDelegates,
///   supportedLocales: @className.supportedLocales,
///   home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```
/// dependencies:
///   # Internationalization support.
///   flutter_localizations:
///     sdk: flutter
///   intl: 0.16.0
///   intl_translation: 0.17.7
///
///   # rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, you’ll need to edit this
/// file.
///
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// project’s Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the @className.supportedLocales
/// property.
class @className {
  @className(Locale locale) : _localeName = Intl.canonicalizedLocale(locale.toString());

  final String _localeName;

  static Future<@className> load(Locale locale) {
    return initializeMessages(locale.toString())
      .then<@className>((_) => @className(locale));
  }

  static @className of(BuildContext context) {
    return Localizations.of<@className>(context, @className);
  }

  static const LocalizationsDelegate<@className> delegate = _@classNameDelegate();

  /// A list of this localizations delegate along with the default localizations
  /// delegates.
  ///
  /// Returns a list of localizations delegates containing this delegate along with
  /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
  /// and GlobalWidgetsLocalizations.delegate.
  static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
    delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ];

  /// A list of this localizations delegate's supported locales.
  @supportedLocales

@classMethods
}

class _@classNameDelegate extends LocalizationsDelegate<@className> {
  const _@classNameDelegate();

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

  @override
  bool isSupported(Locale locale) => <String>[@supportedLanguageCodes].contains(locale.languageCode);

  @override
  bool shouldReload(_@classNameDelegate old) => false;
}
''';

const String getterMethodTemplate = '''
  String get @methodName {
    return Intl.message(
      @message,
      locale: _localeName,
      @intlMethodArgs
    );
  }
''';

const String simpleMethodTemplate = '''
  String @methodName(@methodParameters) {
    return Intl.message(
      @message,
      locale: _localeName,
      @intlMethodArgs
    );
  }
''';

const String pluralMethodTemplate = '''
  String @methodName(@methodParameters) {
    return Intl.plural(
      @intlMethodArgs
    );
  }
''';

int sortFilesByPath (FileSystemEntity a, FileSystemEntity b) {
  return a.path.compareTo(b.path);
}

List<String> genMethodParameters(Map<String, dynamic> bundle, String key, String type) {
  final Map<String, dynamic> attributesMap = bundle['@$key'];
  if (attributesMap != null && attributesMap.containsKey('placeholders')) {
    final Map<String, dynamic> placeholders = attributesMap['placeholders'];
    return placeholders.keys.map((String parameter) => '$type $parameter').toList();
  }
  return <String>[];
}

List<String> genIntlMethodArgs(Map<String, dynamic> bundle, String key) {
  final List<String> attributes = <String>['name: \'$key\''];
  final Map<String, dynamic> attributesMap = bundle['@$key'];
  if (attributesMap != null) {
    if (attributesMap.containsKey('description')) {
      final String description = attributesMap['description'];
      attributes.add('desc: ${generateString(description)}');
    }
    if (attributesMap.containsKey('placeholders')) {
      final Map<String, dynamic> placeholders = attributesMap['placeholders'];
      if (placeholders.isNotEmpty) {
        final String args = placeholders.keys.join(', ');
        attributes.add('args: <Object>[$args]');
      }
    }
  }
  return attributes;
}

String genSimpleMethod(Map<String, dynamic> bundle, String key) {
  String genSimpleMethodMessage(Map<String, dynamic> bundle, String key) {
    String message = bundle[key];
    final Map<String, dynamic> attributesMap = bundle['@$key'];
    final Map<String, dynamic> placeholders = attributesMap['placeholders'];
    for (String placeholder in placeholders.keys)
      message = message.replaceAll('{$placeholder}', '\$$placeholder');
    return generateString(message);
  }

  final Map<String, dynamic> attributesMap = bundle['@$key'];
  if (attributesMap == null)
    exitWithError(
      'Resource attribute "@$key" was not found. Please ensure that each '
      'resource id has a corresponding resource attribute.'
    );

  if (attributesMap.containsKey('placeholders')) {
    return simpleMethodTemplate
      .replaceAll('@methodName', key)
      .replaceAll('@methodParameters', genMethodParameters(bundle, key, 'Object').join(', '))
      .replaceAll('@message', '${genSimpleMethodMessage(bundle, key)}')
      .replaceAll('@intlMethodArgs', genIntlMethodArgs(bundle, key).join(',\n      '));
  }

  return getterMethodTemplate
    .replaceAll('@methodName', key)
    .replaceAll('@message', '${generateString(bundle[key])}')
    .replaceAll('@intlMethodArgs', genIntlMethodArgs(bundle, key).join(',\n      '));
}

String genPluralMethod(Map<String, dynamic> bundle, String key) {
  final Map<String, dynamic> attributesMap = bundle['@$key'];
  assert(attributesMap != null && attributesMap.containsKey('placeholders'));
  final Iterable<String> placeholders = attributesMap['placeholders'].keys;

  // To make it easier to parse the plurals message, temporarily replace each
  // "{placeholder}" parameter with "#placeholder#".
  String message = bundle[key];
  for (String placeholder in placeholders)
    message = message.replaceAll('{$placeholder}', '#$placeholder#');

  final Map<String, String> pluralIds = <String, String>{
    '=0': 'zero',
    '=1': 'one',
    '=2': 'two',
    'few': 'few',
    'many': 'many',
    'other': 'other'
  };

  final List<String> methodArgs = <String>[
    ...placeholders,
    'locale: _localeName',
    ...genIntlMethodArgs(bundle, key),
  ];

  for(String pluralKey in pluralIds.keys) {
    final RegExp expRE = RegExp('($pluralKey){([^}]+)}');
    final RegExpMatch match = expRE.firstMatch(message);
    if (match.groupCount == 2) {
      String argValue = match.group(2);
      for (String placeholder in placeholders)
        argValue = argValue.replaceAll('#$placeholder#', '\$$placeholder');

      methodArgs.add("${pluralIds[pluralKey]}: '$argValue'");
    }
  }

  return pluralMethodTemplate
    .replaceAll('@methodName', key)
    .replaceAll('@methodParameters', genMethodParameters(bundle, key, 'int').join(', '))
    .replaceAll('@intlMethodArgs', methodArgs.join(',\n      '));
}

String genSupportedLocaleProperty(Set<LocaleInfo> supportedLocales) {
  const String prefix = 'static const List<Locale> supportedLocales = <Locale>[\n    Locale(''';
  const String suffix = '),\n  ];';

  String resultingProperty = prefix;
  for (LocaleInfo locale in supportedLocales) {
    final String languageCode = locale.languageCode;
    final String countryCode = locale.countryCode;

    resultingProperty += '\'$languageCode\'';
    if (countryCode != null)
      resultingProperty += ', \'$countryCode\'';
    resultingProperty += '),\n    Locale(';
  }
  resultingProperty = resultingProperty.substring(0, resultingProperty.length - '),\n    Locale('.length);
  resultingProperty += suffix;

  return resultingProperty;
}

bool _isValidClassName(String className) {
  // Dart class name cannot contain non-alphanumeric symbols
  if (className.contains(RegExp(r'[^a-zA-Z\d]')))
    return false;
  // Dart class name must start with upper case character
  if (className[0].contains(RegExp(r'[a-z]')))
    return false;
  // Dart class name cannot start with a number
  if (className[0].contains(RegExp(r'\d')))
    return false;
  return true;
}

bool _isValidGetterAndMethodName(String name) {
  // Dart getter and method name cannot contain non-alphanumeric symbols
  if (name.contains(RegExp(r'[^a-zA-Z\d]')))
    return false;
  // Dart class name must start with lower case character
  if (name[0].contains(RegExp(r'[A-Z]')))
    return false;
  // Dart class name cannot start with a number
  if (name[0].contains(RegExp(r'\d')))
    return false;
  return true;
}

bool _isDirectoryReadableAndWritable(String statString) {
  if (statString[0] == '-' || statString[1] == '-')
    return false;
  return true;
}

String _importFilePath(String path, String fileName) {
  final String replaceLib = path.replaceAll('lib/', '');
  return '$replaceLib/$fileName';
}

Future<void> main(List<String> arguments) async {
  final argslib.ArgParser parser = argslib.ArgParser();
  parser.addFlag(
    'help',
    defaultsTo: false,
    negatable: false,
    help: 'Print this help message.',
  );
  parser.addOption(
    'arb-dir',
    defaultsTo: path.join('lib', 'l10n'),
    help: 'The directory where all localization files should reside. For '
      'example, the template and translated arb files should be located here. '
      'Also, the generated output messages Dart files for each locale and the '
      'generated localizations classes will be created here.',
  );
  parser.addOption(
    'template-arb-file',
    defaultsTo: 'app_en.arb',
    help: 'The template arb file that will be used as the basis for '
      'generating the Dart localization and messages files.',
  );
  parser.addOption(
    'output-localization-file',
    defaultsTo: 'app_localizations.dart',
    help: 'The filename for the output localization and localizations '
      'delegate classes.',
  );
  parser.addOption(
    'output-class',
    defaultsTo: 'AppLocalizations',
    help: 'The Dart class name to use for the output localization and '
      'localizations delegate classes.',
  );

  final argslib.ArgResults results = parser.parse(arguments);
  if (results['help'] == true) {
    print(parser.usage);
    exit(0);
  }

  final String arbPathString = results['arb-dir'];
  final String outputFileString = results['output-localization-file'];

  final Directory l10nDirectory = Directory(arbPathString);
  final File templateArbFile = File(path.join(l10nDirectory.path, results['template-arb-file']));
  final File outputFile = File(path.join(l10nDirectory.path, outputFileString));
  final String stringsClassName = results['output-class'];

  if (!l10nDirectory.existsSync())
    exitWithError(
      "The 'arb-dir' directory, $l10nDirectory, does not exist.\n"
      'Make sure that the correct path was provided.'
    );
  final String l10nDirectoryStatModeString = l10nDirectory.statSync().modeString();
  if (!_isDirectoryReadableAndWritable(l10nDirectoryStatModeString))
    exitWithError(
      "The 'arb-dir' directory, $l10nDirectory, doesn't allow reading and writing.\n"
      'Please ensure that the user has read and write permissions.'
    );
  final String templateArbFileStatModeString = templateArbFile.statSync().modeString();
  if (templateArbFileStatModeString[0] == '-')
    exitWithError(
      "The 'template-arb-file', $templateArbFile, is not readable.\n"
      'Please ensure that the user has read permissions.'
    );
  if (!_isValidClassName(stringsClassName))
    exitWithError(
      "The 'output-class', $stringsClassName, is not valid Dart class name.\n"
    );

  final List<String> arbFilenames = <String>[];
  final Set<String> supportedLanguageCodes = <String>{};
  final Set<LocaleInfo> supportedLocales = <LocaleInfo>{};

  for (FileSystemEntity entity in l10nDirectory.listSync().toList()..sort(sortFilesByPath)) {
    final String entityPath = entity.path;

    if (FileSystemEntity.isFileSync(entityPath)) {
      final RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$');
      if (arbFilenameRE.hasMatch(entityPath)) {
        final File arbFile = File(entityPath);
        final Map<String, dynamic> arbContents = json.decode(arbFile.readAsStringSync());
        String localeString = arbContents['@@locale'];

        if (localeString == null) {
          final RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$');
          final RegExpMatch arbFileMatch = arbFilenameLocaleRE.firstMatch(entityPath);
          if (arbFileMatch == null) {
            exitWithError(
              "The following .arb file's locale could not be determined: \n"
              '$entityPath \n'
              "Make sure that the locale is specified in the '@@locale' "
              'property or as part of the filename (ie. file_en.arb)'
            );
          }

          localeString = arbFilenameLocaleRE.firstMatch(entityPath)[1];
        }

        arbFilenames.add(entityPath);
        final LocaleInfo localeInfo = LocaleInfo.fromString(localeString);
        if (supportedLocales.contains(localeInfo))
          exitWithError(
            'Multiple arb files with the same locale detected. \n'
            'Ensure that there is exactly one arb file for each locale.'
          );
        supportedLocales.add(localeInfo);
        supportedLanguageCodes.add('\'${localeInfo.languageCode}\'');
      }
    }
  }

  final List<String> classMethods = <String>[];

  Map<String, dynamic> bundle;
  try {
    bundle = json.decode(templateArbFile.readAsStringSync());
  } on FileSystemException catch (e) {
    exitWithError('Unable to read input arb file: $e');
  } on FormatException catch (e) {
    exitWithError('Unable to parse arb file: $e');
  }

  final RegExp pluralValueRE = RegExp(r'^\s*\{[\w\s,]*,\s*plural\s*,');

  for (String key in bundle.keys.toList()..sort()) {
    if (key.startsWith('@'))
      continue;
    if (!_isValidGetterAndMethodName(key))
      exitWithError(
        'Invalid key format: $key \n It has to be in camel case, cannot start '
        'with a number, and cannot contain non-alphanumeric characters.'
      );
    if (pluralValueRE.hasMatch(bundle[key]))
      classMethods.add(genPluralMethod(bundle, key));
    else
      classMethods.add(genSimpleMethod(bundle, key));
  }

  outputFile.writeAsStringSync(
    defaultFileTemplate
      .replaceAll('@className', stringsClassName)
      .replaceAll('@classMethods', classMethods.join('\n'))
      .replaceAll('@importFile', _importFilePath(arbPathString, outputFileString))
      .replaceAll('@supportedLocales', genSupportedLocaleProperty(supportedLocales))
      .replaceAll('@supportedLanguageCodes', supportedLanguageCodes.toList().join(', '))
  );

  final ProcessResult pubGetResult = await Process.run('flutter', <String>['pub', 'get']);
  if (pubGetResult.exitCode != 0) {
    stderr.write(pubGetResult.stderr);
    exit(1);
  }

  final ProcessResult generateFromArbResult = await Process.run('flutter', <String>[
    'pub',
    'pub',
    'run',
    'intl_translation:generate_from_arb',
    '--output-dir=${l10nDirectory.path}',
    '--no-use-deferred-loading',
    outputFile.path,
    ...arbFilenames,
  ]);
  if (generateFromArbResult.exitCode != 0) {
    stderr.write(generateFromArbResult.stderr);
    exit(1);
  }
}
