// 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 'dart:io';

import 'package:args/args.dart';
import 'package:gen_web_keyboard_keymap/benchmark_planner.dart';
import 'package:gen_web_keyboard_keymap/common.dart';
import 'package:gen_web_keyboard_keymap/github.dart';
import 'package:gen_web_keyboard_keymap/layout_types.dart';
import 'package:path/path.dart' as path;

const String kEnvGithubToken = 'GITHUB_TOKEN';

String _renderTemplate(String template, Map<String, String> dictionary) {
  String result = template;
  dictionary.forEach((String key, String value) {
    final String localResult = result.replaceAll('@@@$key@@@', value);
    if (localResult == result) {
      print('Template key $key is not used.');
    }
    result = localResult;
  });
  return result;
}

void _writeFileTo(String outputDir, String outputFileName, String body) {
  final String outputPath = path.join(outputDir, outputFileName);
  Directory(outputDir).createSync(recursive: true);
  File(outputPath).writeAsStringSync(body);
}

String _readSharedSegment(String path) {
  const String kSegmentStartMark = '/*@@@ SHARED SEGMENT START @@@*/';
  const String kSegmentEndMark = '/*@@@ SHARED SEGMENT END @@@*/';
  final List<String> lines = File(path).readAsStringSync().split('\n');
  // Defining the two variables as `late final` ensures that each mark is found
  // once and only once, otherwise assertion errors will be thrown.
  late final int startLine;
  late final int endLine;
  for (int lineNo = 0; lineNo < lines.length; lineNo += 1) {
    if (lines[lineNo] == kSegmentStartMark) {
      startLine = lineNo;
    } else if (lines[lineNo] == kSegmentEndMark) {
      endLine = lineNo;
    }
  }
  assert(startLine < endLine);
  return lines.sublist(startLine + 1, endLine).join('\n').trimRight();
}

typedef _ForEachAction<V> = void Function(String key, V value);
void _sortedForEach<V>(Map<String, V> map, _ForEachAction<V> action) {
  map
    .entries
    .toList()
    ..sort((MapEntry<String, V> a, MapEntry<String, V> b) => a.key.compareTo(b.key))
    ..forEach((MapEntry<String, V> entry) {
      action(entry.key, entry.value);
    });
}

String _escapeStringToDart(String origin) {
  // If there is no `'`, we can use the raw string surrounded by `'`.
  if (!origin.contains("'")) {
    return "r'$origin'";
  } else {
    // If there is no `"`, we can use the raw string surrounded by `"`.
    if (!origin.contains('"')) {
      return 'r"$origin"';
    } else {
      // If there are both kinds of quotes, we have to use non-raw string
      // and escape necessary characters.
      final String beforeQuote = origin
        .replaceAll(r'\', r'\\')
        .replaceAll(r'$', r'\$')
        .replaceAll("'", r"\'");
      return "'$beforeQuote'";
    }
  }
}

typedef _ValueCompare<T> = void Function(T?, T?, String path);
void _mapForEachEqual<T>(Map<String, T> a, Map<String, T> b, _ValueCompare<T> body, String path) {
  assert(a.length == b.length, '$path.length: ${a.length} vs ${b.length}');
  for (final String key in a.keys) {
    body(a[key], b[key], key);
  }
}

// Ensure that two maps are deeply equal.
//
// Differences will be thrown as assertion.
bool _verifyMap(Map<String, Map<String, int>> a, Map<String, Map<String, int>> b) {
  _mapForEachEqual(a, b, (Map<String, int>? aMap, Map<String, int>? bMap, String path) {
    _mapForEachEqual(aMap!, bMap!, (int? aValue, int? bValue, String path) {
      assert(aValue == bValue && aValue != null, '$path: $aValue vs $bValue');
    }, path);
  }, '');
  return true;
}

String _buildMapString(Iterable<Layout> layouts) {
  final Map<String, Map<String, int>> originalMap = combineLayouts(layouts);
  final List<String> compressed = marshallMappingData(originalMap);
  final Map<String, Map<String, int>> uncompressed = unmarshallMappingData(compressed.join());
  assert(_verifyMap(originalMap, uncompressed));
  return '  return unmarshallMappingData(\n'
         '${compressed.map((String line) => '    ${_escapeStringToDart(line)}\n').join()}'
         '  ); // ${compressed.join().length} characters';
}

String _buildTestCasesString(List<Layout> layouts) {
  final List<String> layoutsString = <String>[];
  for (final Layout layout in layouts) {
    final List<String> layoutEntries = <String>[];
    _sortedForEach(planLayout(layout.entries), (String eventCode, int logicalKey) {
      final LayoutEntry entry = layout.entries[eventCode]!;
      layoutEntries.add("    verifyEntry(mapping, '$eventCode', <String>["
          '${entry.printables.map(_escapeStringToDart).join(', ')}'
          "], '${String.fromCharCode(logicalKey)}');");
    });
    layoutsString.add('''
  group('${layout.language}', () {
${layoutEntries.join('\n')}
  });
''');
  }
  return layoutsString.join('\n').trimRight();
}

Future<void> main(List<String> rawArguments) async {
  final Map<String, String> env = Platform.environment;
  final ArgParser argParser = ArgParser();
  argParser.addFlag(
    'force',
    abbr: 'f',
    negatable: false,
    help: 'Make a new request to GitHub even if a cache is detected',
  );
  argParser.addFlag(
    'help',
    abbr: 'h',
    negatable: false,
    help: 'Print help for this command.',
  );

  final ArgResults parsedArguments = argParser.parse(rawArguments);

  if (parsedArguments['help'] as bool) {
    print(argParser.usage);
    exit(0);
  }

  bool enabledAssert = false;
  assert(() {
    enabledAssert = true;
    return true;
  }());
  if (!enabledAssert) {
    print('Error: This script must be run with assert enabled. Please rerun with --enable-asserts.');
    exit(1);
  }

  final String? envGithubToken = env[kEnvGithubToken];
  if (envGithubToken == null) {
    print('Error: Environment variable $kEnvGithubToken not found.\n\n'
          'Set the environment variable $kEnvGithubToken as a GitHub personal access\n'
          'token for authentication. This token is only used for quota controlling\n'
          'and does not need any scopes. Create one at\n'
          'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token.',
    );
    exit(1);
  }

  // The root of this package. The folder that is called
  // 'gen_web_locale_keymap' and contains 'pubspec.yaml'.
  final Directory packageRoot = Directory(path.dirname(Platform.script.toFilePath())).parent;

  // The root of the output package. The folder that is called
  // 'web_locale_keymap' and contains 'pubspec.yaml'.
  final String outputRoot = path.join(packageRoot.parent.parent.path,
        'third_party', 'web_locale_keymap');

  final GithubResult githubResult = await fetchFromGithub(
    githubToken: envGithubToken,
    force: parsedArguments['force'] as bool,
    cacheRoot: path.join(packageRoot.path, '.cache'),
  );

  final List<Layout> winLayouts = githubResult.layouts.where((Layout layout) =>
            layout.platform == LayoutPlatform.win).toList();
  final List<Layout> linuxLayouts = githubResult.layouts.where((Layout layout) =>
            layout.platform == LayoutPlatform.linux).toList();
  final List<Layout> darwinLayouts = githubResult.layouts.where((Layout layout) =>
            layout.platform == LayoutPlatform.darwin).toList();

  // Generate the definition file.
  _writeFileTo(
    path.join(outputRoot, 'lib', 'web_locale_keymap'),
    'key_mappings.g.dart',
    _renderTemplate(
      File(path.join(packageRoot.path, 'data', 'key_mappings.dart.tmpl')).readAsStringSync(),
      <String, String>{
        'COMMIT_URL': githubResult.url,
        'WIN_MAPPING': _buildMapString(winLayouts),
        'LINUX_MAPPING': _buildMapString(linuxLayouts),
        'DARWIN_MAPPING': _buildMapString(darwinLayouts),
        'COMMON': _readSharedSegment(path.join(packageRoot.path, 'lib', 'common.dart')),
      },
    ),
  );

  // Generate the test cases.
  _writeFileTo(
    path.join(outputRoot, 'test'),
    'test_cases.g.dart',
    _renderTemplate(
      File(path.join(packageRoot.path, 'data', 'test_cases.dart.tmpl')).readAsStringSync(),
      <String, String>{
        'WIN_CASES': _buildTestCasesString(winLayouts),
        'LINUX_CASES': _buildTestCasesString(linuxLayouts),
        'DARWIN_CASES': _buildTestCasesString(darwinLayouts),
      },
    ),
  );
}
