// ignore_for_file: avoid_print

import 'dart:convert';
import 'dart:io';

import 'package:ansicolor/ansicolor.dart';
import 'package:args/args.dart';
import 'package:recase/recase.dart';
import 'package:version/version.dart';
import 'package:pub_semver/pub_semver.dart' as pub;

/// A map which adjusts icon ids starting with a number
///
/// Some icons cannot keep their id as identifier, as dart does not allow
/// numbers as the beginning of a variable names. The chosen solution is, to
/// write those parts out.
const Map<String, String> nameAdjustments = {
  "500px": "fiveHundredPx",
  "360-degrees": "threeHundredSixtyDegrees",
  "1": "one",
  "2": "two",
  "3": "three",
  "4": "four",
  "5": "five",
  "6": "six",
  "7": "seven",
  "8": "eight",
  "9": "nine",
  "0": "zero",
  "42-group": "fortyTwoGroup",
  "00": "zeroZero",
  // found in aliases
  "100": "hundred",
};

/// Some aliases clash with reserved words of dartlang. Those are ignored.
const List<String> ignoredAliases = ["try"];

/// Generated by [readAndPickMetadata] for each icon
class IconMetadata {
  final String name;
  final String label;
  final String unicode;
  final List<String> searchTerms;
  final List<String> styles;
  final List<String> aliases;

  IconMetadata(
    this.name,
    this.label,
    this.unicode,
    this.searchTerms,
    this.styles,
    this.aliases,
  );
}

final AnsiPen red = AnsiPen()..xterm(009);
final AnsiPen blue = AnsiPen()..xterm(012);
final AnsiPen yellow = AnsiPen()..xterm(011);

/// Utility program to customize font awesome flutter
///
/// For usage information see [displayHelp]
///
/// Steps:
/// 1. Check if icons.json exists in project root (or in lib/fonts)
/// if icons.json does not exist:
///   1.1 download official, free icons.json from github
///     https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/metadata/icons.json
///   1.2 download official, free icons and replace existing
///     https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/webfonts/fa-brands-400.ttf
///     https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/webfonts/fa-regular-400.ttf
///     https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/webfonts/fa-solid-900.ttf
/// 3. filter out unwanted icon styles
/// 4. build icons, example
/// if dynamic icons requested:
///   4.1 create map
/// 5. format all generated files
/// 6. if icons.json was downloaded by this tool, remove icons.json
void main(List<String> rawArgs) async {
  print(blue('''
####  #   #####################################################################
###  ###  ############ Font Awesome Flutter Configurator ######################
#   #   # #####################################################################
  '''));

  final argParser = setUpArgParser();
  final args = argParser.parse(rawArgs);

  if (args['help']) {
    displayHelp(argParser);
    exit(0);
  }

  await printVersionNotice('fluttercommunity/font_awesome_flutter');

  File iconsJson = File('lib/fonts/icons.json');
  final hasCustomIconsJson = iconsJson.existsSync();

  if (!hasCustomIconsJson) {
    print(blue('No icons.json found, updating free icons'));
    const repositoryName = 'FortAwesome/Font-Awesome';
    final defaultBranch = await getRepositoryDefaultBranch(repositoryName);
    print(blue(
        'Choosing branch "$defaultBranch" of repository https://github.com/$repositoryName'));
    await download(
        'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/$defaultBranch/metadata/icons.json',
        File('lib/fonts/icons.json'));
    await download(
        'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/$defaultBranch/webfonts/fa-brands-400.ttf',
        File('lib/fonts/fa-brands-400.ttf'));
    await download(
        'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/$defaultBranch/webfonts/fa-regular-400.ttf',
        File('lib/fonts/fa-regular-400.ttf'));
    await download(
        'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/$defaultBranch/webfonts/fa-solid-900.ttf',
        File('lib/fonts/fa-solid-900.ttf'));
  } else {
    print(blue('Custom icons.json found, generating files'));
  }

  // A list of all versions mentioned in the metadata
  final List<String> versions = [];
  final List<IconMetadata> metadata = [];
  final Set<String> styles = {};
  // duotone icons are no longer supported
  final List<String> excludedStyles = ['duotone', ...args['exclude']];
  var hasDuotoneIcons = readAndPickMetadata(
      iconsJson, metadata, styles, versions, excludedStyles);
  if (hasDuotoneIcons) {
    // Duotone are no longer supported - temporarily added notice to avoid
    // confusion
    print(red(
        'Duotone icons are no longer supported. Automatically disabled them.'));
  }
  hasDuotoneIcons = false;

  final highestVersion = calculateFontAwesomeVersion(versions);

  print(blue('\nGenerating icon definitions'));
  writeCodeToFile(
    () => generateIconDefinitionClass(metadata, highestVersion),
    'lib/font_awesome_flutter.dart',
  );

  print(blue('\nGenerating example code'));
  writeCodeToFile(
    () => generateExamplesListClass(metadata),
    'example/lib/icons.dart',
  );

  if (args['dynamic']) {
    writeCodeToFile(
      () => generateIconNameMap(metadata),
      'lib/name_icon_mapping.dart',
    );
  } else {
    // Remove file if dynamic is not requested. Helps things to stay consistent
    final iconNameMappingFile = File('lib/name_icon_mapping.dart');
    if (iconNameMappingFile.existsSync()) iconNameMappingFile.deleteSync();
  }

  adjustPubspecFontIncludes(styles);

  if (!hasCustomIconsJson) {
    iconsJson.deleteSync();
  }
}

/// Returns this package's current version
String getPackageVersion() {
  var pubspecFile = File('pubspec.yaml');
  var pubspec = pubspecFile.readAsLinesSync();
  for (final line in pubspec) {
    if (line.startsWith('version:')) {
      return line.substring('version'.length + 1).trim();
    }
  }
  return 'no version found';
}

/// Automatically (un)comments font definitions in pubspec.yaml
void adjustPubspecFontIncludes(Set<String> styles) {
  var pubspecFile = File('pubspec.yaml');
  var pubspec = pubspecFile.readAsLinesSync();
  String styleName;
  Set<String> enabledStyles = {};

  var startFlutterSection = pubspec.indexOf('flutter:');
  String line;
  for (var i = startFlutterSection; i < pubspec.length; i++) {
    line = uncommentYamlLine(pubspec[i]);
    if (!line.trimLeft().startsWith('- family:')) continue;

    styleName = line.substring(25).toLowerCase(); // - family: FontAwesomeXXXXXX
    if (styles.contains(styleName)) {
      pubspec[i] = uncommentYamlLine(pubspec[i]);
      pubspec[i + 1] = uncommentYamlLine(pubspec[i + 1]);
      pubspec[i + 2] = uncommentYamlLine(pubspec[i + 2]);
      pubspec[i + 3] = uncommentYamlLine(pubspec[i + 3]);
      enabledStyles.add(styleName);
    } else {
      pubspec[i] = commentYamlLine(pubspec[i]);
      pubspec[i + 1] = commentYamlLine(pubspec[i + 1]);
      pubspec[i + 2] = commentYamlLine(pubspec[i + 2]);
      pubspec[i + 3] = commentYamlLine(pubspec[i + 3]);
    }
    i = i + 3; // + 4 with i++
  }

  pubspecFile.writeAsStringSync(pubspec.join('\n'));

  print(blue('\nFound and enabled the following icon styles:'));
  enabledStyles.isEmpty
      ? print(red("None"))
      : print(blue(enabledStyles.join(', ')));

  print(blue('\nRunning "flutter pub get"'));
  final result = Process.runSync('flutter', ['pub', 'get'], runInShell: true);
  stdout.write(result.stdout);
  stderr.write(red(result.stderr));

  print(blue('\nDone'));
}

/// Comments out a line of yaml code. Does nothing if already commented
String commentYamlLine(String line) {
  if (line.startsWith('#')) return line;
  return '#$line';
}

/// Uncomments a line of yaml code. Does nothing if not commented.
///
/// Expects the rest of the line to be valid yaml and to have the correct
/// indention after removing the first #.
String uncommentYamlLine(String line) {
  if (!line.startsWith('#')) return line;
  return line.substring(1);
}

/// Writes lines of code created by a [generator] to [filePath] and formats it
void writeCodeToFile(List<String> Function() generator, String filePath) {
  List<String> generated = generator();
  File(filePath).writeAsStringSync(generated.join('\n'));
  final result = Process.runSync('dart', ['format', filePath]);
  stdout.write(result.stdout);
  stderr.write(red(result.stderr));
}

/// Enables the use of a map to dynamically load icons by their name
///
/// To use, import:
/// `import 'package:font_awesome_flutter/name_icon_mapping.dart'`
/// And then either use faIconNameMapping directly to look up specific icons,
/// or use the getIconFromCss helper function.
List<String> generateIconNameMap(List<IconMetadata> icons) {
  print(yellow('''

------------------------------- IMPORTANT NOTICE -------------------------------
Dynamic icon retrieval by name disables icon tree shaking. This means unused
icons will not be automatically removed and thus make the overall app size
larger. It is highly recommended to use this option only in combination with
the "exclude" option, to remove styles which are not needed.
You may need to pass --no-tree-shake-icons to the flutter build command for it
to complete successfully.
--------------------------------------------------------------------------------
'''));

  print(blue('Generating name to icon mapping'));

  List<String> output = [
    'library font_awesome_flutter;',
    '',
    "import 'package:flutter/widgets.dart';",
    "import 'package:font_awesome_flutter/font_awesome_flutter.dart';",
    '',
    '// THIS FILE IS AUTOMATICALLY GENERATED!',
    '',
    '/// Utility function retrieve icons based on their css classes',
    '///',
    '/// [cssClasses] may contain other classes as well. This tool searches',
    '/// for the known font awesome classes (far, fas, fab, ...) and an icon',
    '/// name starting with `fa-`. Should multiple classes fulfill these',
    '/// requirements, the first occurrence is chosen.',
    '/// ',
    '/// Returns null if no icon matches.',
    'IconData? getIconFromCss(String cssClasses) {',
    '  const Map<String, String> cssStyles = {',
    "    'far': 'regular', 'fas': 'solid', 'fab': 'brands',",
    "    'fad': 'duotone', 'fal': 'light', 'fat': 'thin',",
    '  };',
    '',
    "var separatedCssClasses = cssClasses.split(' ');",
    'try {',
    '  var style = separatedCssClasses.firstWhere((c) => cssStyles.containsKey(c));',
    '  // fas -> solid',
    '  style = cssStyles[style]!;',
    '',
    "  var icon = separatedCssClasses.firstWhere((c) => c.startsWith('fa-'));",
    "  icon = icon.replaceFirst('fa-', '');",
    '',
    "  return faIconNameMapping['\$style \$icon'];",
    '  } on StateError {',
    '  return null;',
    '  }',
    '}',
    '',
    '/// Icon name to icon mapping for font awesome icons',
    '///',
    '/// Keys are in the following format: "style iconName"',
    'const Map<String, IconData> faIconNameMapping = {',
  ];

  String iconName;
  for (var icon in icons) {
    for (var style in icon.styles) {
      iconName = normalizeIconName(icon.name, style, icon.styles.length);
      output.add("'$style ${icon.name}': FontAwesomeIcons.$iconName,");
    }
  }

  output.add('};');

  return output;
}

/// Builds the class with icon definitions and returns the output
List<String> generateIconDefinitionClass(
    List<IconMetadata> metadata, Version version) {
  final List<String> output = [
    'library font_awesome_flutter;',
    '',
    "import 'package:flutter/widgets.dart';",
    "import 'package:font_awesome_flutter/src/icon_data.dart';",
    "export 'package:font_awesome_flutter/src/fa_icon.dart';",
    "export 'package:font_awesome_flutter/src/icon_data.dart';",
  ];

  output.addAll([
    '',
    '// THIS FILE IS AUTOMATICALLY GENERATED!',
    '',
    '/// Icons based on font awesome $version',
    '@staticIconProvider',
    'class FontAwesomeIcons {',
  ]);

  for (var icon in metadata) {
    for (String style in icon.styles) {
      output.add(generateIconDocumentation(icon, style));
      output.add(generateIconDefinition(icon, style));
      output.add(generateIconAliases(icon, style));
    }
  }

  output.add('}');
  return output;
}

/// Builds the example icons
List<String> generateExamplesListClass(List<IconMetadata> metadata) {
  final List<String> output = [
    "import 'package:font_awesome_flutter/font_awesome_flutter.dart';",
    "import 'package:font_awesome_flutter_example/example_icon.dart';",
    '',
    '// THIS FILE IS AUTOMATICALLY GENERATED!',
    '',
    'final icons = <ExampleIcon>[',
  ];

  for (var icon in metadata) {
    for (String style in icon.styles) {
      output.add(generateExampleIcon(icon, style));
    }
  }

  output.add('];');

  return output;
}

/// Generates an icon for the example app. Used by [generateExamplesListClass]
String generateExampleIcon(IconMetadata icon, String style) {
  var iconName = normalizeIconName(icon.name, style, icon.styles.length);

  return "ExampleIcon(FontAwesomeIcons.$iconName, '$iconName'),";
}

/// Generates the icon's doc comment. Used by [generateIconDefinitionClass]
String generateIconDocumentation(IconMetadata icon, String style) {
  var doc = '/// ${style.sentenceCase} ${icon.label} icon\n'
      '///\n'
      '/// https://fontawesome.com/icons/${icon.name}?style=$style';

  if (icon.searchTerms.isNotEmpty) {
    doc += '\n/// ${icon.searchTerms.join(", ")}';
  }

  return doc;
}

/// Generates the icon's constant. Used by [generateIconDefinitionClass]
String generateIconDefinition(IconMetadata icon, String style) {
  var iconName = normalizeIconName(icon.name, style, icon.styles.length);

  String iconDataSource = styleToDataSource(style);

  return 'static const IconData $iconName = $iconDataSource(0x${icon.unicode});';
}

/// Generates aliases which link to the original icon. Used by
/// [generateIconDefinitionClass]
String generateIconAliases(IconMetadata icon, String style) {
  var iconName = normalizeIconName(icon.name, style, icon.styles.length);
  final List<String> lines = [];

  for (String alias in icon.aliases) {
    if (ignoredAliases.contains(alias)) continue;

    var aliasName = normalizeIconName(alias, style, icon.styles.length);
    lines.add('/// Alias $alias for icon [$iconName]');
    lines.add('@Deprecated(\'Use "$iconName" instead.\')');
    lines.add('static const IconData $aliasName = $iconName;');
  }

  return lines.join('\n');
}

/// Returns a normalized version of [iconName] which can be used as const name
///
/// [nameAdjustments] lists some icons which need special treatment to be valid
/// const identifiers, as they cannot start with a number.
/// The [style] name is automatically appended if necessary - deemed by the
/// number of [styleCompetitors] (number of styles) for this icon.
String normalizeIconName(String iconName, String style, int styleCompetitors) {
  iconName = nameAdjustments[iconName] ?? iconName;

  if (styleCompetitors > 1 && style != "regular") {
    iconName = "${style}_$iconName";
  }

  return iconName.camelCase;
}

/// Utility function to generate the correct 'IconData' subclass for a [style]
String styleToDataSource(String style) {
  return 'IconData${style[0].toUpperCase()}${style.substring(1)}';
}

/// Gets the default branch from github's metadata
///
/// Font awesome no longer uses the master branch, but instead version specific
/// ones, like 5.x and 6.x. Master is no longer updated. In the spirit of always
/// using the latest version, this tool always selects the default branch.
Future<String> getRepositoryDefaultBranch(String repositoryName) async {
  final tmpFile = File('fa-repo-metadata.tmp');
  await download('https://api.github.com/repos/$repositoryName', tmpFile);
  try {
    String rawGithubMetadata = await tmpFile.readAsString();
    Map<String, dynamic> githubMetadata = json.decode(rawGithubMetadata);
    return githubMetadata["default_branch"];
  } catch (_) {
    print(red('Error while getting font awesome\'s default branch. Aborting.'));
  } finally {
    tmpFile.delete();
  }
  exit(1);
}

/// Prints a notice should the current font_awesome_flutter version not be the
/// latest.
Future printVersionNotice(String repositoryName) async {
  final tmpFile = File('faf-releases-metadata.tmp');

  try {
    final packageVersion = pub.Version.parse(getPackageVersion());

    print(blue('Using font_awesome_flutter version $packageVersion'));

    await download(
        'https://api.github.com/repos/$repositoryName/releases', tmpFile);

    String rawReleasesData = await tmpFile.readAsString();
    List releasesData = json.decode(rawReleasesData);
    List<pub.Version> releases = [];
    List<pub.Version> preReleases = [];
    for (final Map<String, dynamic> release in releasesData) {
      var releaseName = release["name"] as String;
      releaseName = releaseName.isEmpty ? release["tag_name"] : releaseName;
      // remove possible prefixes
      releaseName = releaseName
          .toLowerCase()
          .replaceAll('version', '')
          .replaceAll('v.', '')
          .replaceAll('v', '')
          .trim();
      final version = pub.Version.parse(releaseName);
      if (version.isPreRelease) {
        preReleases.add(version);
      } else {
        releases.add(version);
      }
    }

    final primaryRelease = pub.Version.primary(releases);
    final primaryPreRelease = pub.Version.primary(preReleases);

    if (primaryRelease > packageVersion) {
      print(red(
          'A new version ($primaryRelease) of font_awesome_flutter is available. Please update before reporting any errors. You can update via `git pull` or by downloading the source code from github. (https://github.com/$repositoryName)'));
    }
    if (primaryPreRelease > packageVersion &&
        primaryPreRelease > primaryRelease) {
      print(yellow(
          'A pre-release version ($primaryPreRelease) of font_awesome_flutter is available. Should you encounter any problems, have a look if it fixes them.'));
    }
  } catch (_) {
    print(red(
        'Error while getting font awesome flutter\'s version information. Could not determine whether you are using the latest version.'));
  } finally {
    tmpFile.delete();
  }
  // do not exit
  print('');
}

/// Reads the [iconsJson] metadata and picks out relevant data
///
/// Relevant data includes search-terms, label, unicode, styles, changes and is
/// saved to [metadata] as [IconMetadata].
/// Changes versions are all put into the [versions] list to calculate the
/// latest font awesome version.
/// [excludedStyles], which can be set in the program arguments, are removed.
/// Returns whether the dataset contains duotone icons.
bool readAndPickMetadata(File iconsJson, List<IconMetadata> metadata,
    Set<String> styles, List<String> versions, List<String> excludedStyles) {
  var hasDuotoneIcons = false;

  dynamic rawMetadata;
  try {
    final content = iconsJson.readAsStringSync();
    rawMetadata = json.decode(content);
  } catch (_) {
    print(
        'Error: Invalid icons.json. Please make sure you copied the correct file.');
    exit(1);
  }

  Map<String, dynamic> icon;
  for (var iconName in rawMetadata.keys) {
    icon = rawMetadata[iconName];

    // Add all changes to the list
    for (var v in icon['changes'] as List) {
      versions.add(v);
    }

    List<String> iconStyles = (icon['styles'] as List).cast<String>();

    //TODO: Remove line once duotone support discontinuation notice is removed
    if (iconStyles.contains('duotone')) hasDuotoneIcons = true;

    for (var excluded in excludedStyles) {
      iconStyles.remove(excluded);
    }

    if (iconStyles.isEmpty) continue;

    if (icon.containsKey('private') && icon['private']) continue;

    styles.addAll(iconStyles);

    final List searchTermsRaw = (icon['search']?['terms'] ?? []);
    final searchTerms = searchTermsRaw.map((e) => e.toString()).toList();

    final List aliasesRaw = (icon['aliases']?['names'] ?? []);
    final aliases = aliasesRaw.map((e) => e.toString()).toList();

    metadata.add(IconMetadata(
      iconName,
      icon['label'],
      icon['unicode'],
      searchTerms,
      iconStyles,
      aliases,
    ));
  }

  return hasDuotoneIcons;
}

/// Calculates the highest version number found in the metadata
///
/// Expects a list of all versions listed in the metadata.
/// See [readAndPickMetadata].
Version calculateFontAwesomeVersion(List<String> versions) {
  final sortedVersions = versions.map((version) {
    try {
      return Version.parse(version);
    } on FormatException {
      return Version(0, 0, 0);
    }
  }).toList()
    ..sort();

  return sortedVersions.last;
}

/// Downloads the content from [url] and saves it to [target]
Future download(String url, File target) async {
  print('Downloading $url');
  final request = await HttpClient().getUrl(Uri.parse(url));
  final response = await request.close();
  return response.pipe(target.openWrite());
}

/// Defines possible command line arguments for this program
ArgParser setUpArgParser() {
  final argParser = ArgParser();

  argParser.addFlag('help',
      abbr: 'h',
      defaultsTo: false,
      negatable: false,
      help: 'display program options and usage information');

  argParser.addMultiOption('exclude',
      abbr: 'e',
      defaultsTo: [],
      allowed: ['brands', 'regular', 'solid', 'duotone', 'light', 'thin'],
      help: 'icon styles which are excluded by the generator');

  argParser.addFlag('dynamic',
      abbr: 'd',
      defaultsTo: false,
      negatable: false,
      help: 'builds a map, which allows to dynamically retrieve icons by name');

  return argParser;
}

/// Displays the program help page. Accessible via the --help command line arg
void displayHelp(ArgParser argParser) {
  var fileType = Platform.isWindows ? 'bat' : 'sh';
  print('''
This script helps you to customize the font awesome flutter package to fit your
individual needs. Please follow the "customizing font awesome flutter" guide on
github.

By default, this tool acts as an updater. It retrieves the newest version of
free font awesome icons from the web and generates all necessary files.
If an icons.json exists within the lib/fonts folder, no update is performed and
files in this folder are used for generation instead.
To exclude styles from generation, pass the "exclude" option with a comma
separated list of styles to ignore.

Usage:
configurator.$fileType [options]

Options:''');
  print(argParser.usage);
}
