// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
// for details. 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:collection';
import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;

import '../tool/expected_output.dart';
import 'stats_lib.dart';

final _configs = List<Config>.unmodifiable([
  Config.commonMarkConfig,
  Config.gfmConfig,
]);

Future<void> main(List<String> args) async {
  final parser = ArgParser()
    ..addOption(
      'section',
      help: 'Restrict tests to one section, provided after the option.',
    )
    ..addFlag(
      'raw',
      help: 'raw JSON format',
      negatable: false,
    )
    ..addFlag(
      'update-files',
      help: 'Update stats files in $toolDir',
      negatable: false,
    )
    ..addFlag(
      'verbose',
      help: 'Print details for failures and errors.',
      negatable: false,
    )
    ..addFlag(
      'verbose-loose',
      help: 'Print details for "loose" matches.',
      negatable: false,
    )
    ..addOption('flavor', allowed: _configs.map((c) => c.prefix))
    ..addFlag('help', negatable: false);

  ArgResults options;

  try {
    options = parser.parse(args);
  } on FormatException catch (e) {
    stderr.writeln(e);
    print(parser.usage);
    exitCode = 64; // unix standard improper usage
    return;
  }

  if (options['help'] as bool) {
    print(parser.usage);
    return;
  }

  final specifiedSection = options['section'] as String?;
  final raw = options['raw'] as bool;
  final verbose = options['verbose'] as bool;
  final verboseLooseMatch = options['verbose-loose'] as bool;
  final updateFiles = options['update-files'] as bool;

  if (updateFiles && (raw || verbose || (specifiedSection != null))) {
    stderr.writeln('The `update-files` flag must be used by itself');
    print(parser.usage);
    exitCode = 64; // unix standard improper usage
    return;
  }

  var testPrefix = options['flavor'] as String?;
  if (!updateFiles) {
    testPrefix = _configs.first.prefix;
  }

  final testPrefixes =
      testPrefix == null ? _configs.map((c) => c.prefix) : <String>[testPrefix];

  for (final testPrefix in testPrefixes) {
    await _processConfig(
      testPrefix,
      raw,
      updateFiles,
      verbose,
      specifiedSection,
      verboseLooseMatch,
    );
  }
}

final _sectionNameReplace = RegExp(r'[ \)\(]+');

String _unitOutput(Iterable<DataCase> cases) => cases.map((dataCase) => '''
>>> ${dataCase.front_matter}
${dataCase.input}<<<
${dataCase.expectedOutput}''').join();

/// Set this to `true` and rerun `--update-files` to ease finding easy strict
/// fixes.
const _improveStrict = false;

Future<void> _processConfig(
  String testPrefix,
  bool raw,
  bool updateFiles,
  bool verbose,
  String? specifiedSection,
  bool verboseLooseMatch,
) async {
  final config = _configs.singleWhere((c) => c.prefix == testPrefix);

  final sections = loadCommonMarkSections(testPrefix);

  final scores = SplayTreeMap<String, SplayTreeMap<int, CompareLevel>>(
      compareAsciiLowerCaseNatural);

  for (final entry in sections.entries) {
    if (specifiedSection != null && entry.key != specifiedSection) {
      continue;
    }

    final units = <DataCase>[];

    for (final e in entry.value) {
      final result = compareResult(
        config,
        e,
        verboseFail: verbose,
        verboseLooseMatch: verboseLooseMatch,
        extensions: e.extensions,
      );

      units.add(DataCase(
        front_matter: result.testCase.toString(),
        input: result.testCase.markdown,
        expectedOutput:
            (_improveStrict && result.compareLevel == CompareLevel.loose)
                ? result.testCase.html
                : result.result!,
      ));

      final nestedMap = scores.putIfAbsent(
        entry.key,
        SplayTreeMap<int, CompareLevel>.new,
      );
      nestedMap[e.example] = result.compareLevel;
    }

    if (updateFiles && units.isNotEmpty) {
      var fileName =
          entry.key.toLowerCase().replaceAll(_sectionNameReplace, '_');
      while (fileName.endsWith('_')) {
        fileName = fileName.substring(0, fileName.length - 1);
      }
      fileName = '$fileName.unit';
      File(p.join('test', testPrefix, fileName))
        ..createSync(recursive: true)
        ..writeAsStringSync(_unitOutput(units));
    }
  }

  if (raw || updateFiles) {
    await _printRaw(testPrefix, scores, updateFiles);
  }

  if (!raw || updateFiles) {
    await _printFriendly(testPrefix, scores, updateFiles);
  }
}

Object? _convert(Object? obj) {
  if (obj is CompareLevel) {
    return obj.name;
  }
  if (obj is Map) {
    return obj
        .map<String, Object?>((key, value) => MapEntry(key.toString(), value));
  }
  return obj;
}

Future<void> _printRaw(
  String testPrefix,
  Map<String, Map<int, CompareLevel>> scores,
  bool updateFiles,
) async {
  IOSink sink;
  if (updateFiles) {
    final file = getStatsFile(testPrefix);
    print('Updating ${file.path}');
    sink = file.openWrite();
  } else {
    sink = stdout;
  }

  const encoder = JsonEncoder.withIndent(' ', _convert);
  try {
    sink.writeln(encoder.convert(scores));
    // ignore: avoid_catching_errors
  } on JsonUnsupportedObjectError catch (e) {
    stderr.writeln(e.cause);
    stderr.writeln(e.unsupportedObject.runtimeType);
    rethrow;
  }

  await sink.flush();
  await sink.close();
}

String _pct(int value, int total, String section) =>
    '${value.toString().padLeft(4)} '
    'of ${total.toString().padLeft(4)} '
    '– ${(100 * value / total).toStringAsFixed(1).padLeft(5)}%  $section';

Future<void> _printFriendly(
  String testPrefix,
  SplayTreeMap<String, SplayTreeMap<int, CompareLevel>> scores,
  bool updateFiles,
) async {
  var totalValid = 0;
  var totalStrict = 0;
  var totalExamples = 0;

  IOSink sink;
  if (updateFiles) {
    final path = p.join(toolDir, '${testPrefix}_stats.txt');
    print('Updating $path');
    final file = File(path);
    sink = file.openWrite();
  } else {
    sink = stdout;
  }

  scores.forEach((section, Map<int, CompareLevel> map) {
    final total = map.values.length;
    totalExamples += total;

    final sectionStrictCount =
        map.values.where((val) => val == CompareLevel.strict).length;

    final sectionLooseCount =
        map.values.where((val) => val == CompareLevel.loose).length;

    final sectionValidCount = sectionStrictCount + sectionLooseCount;

    totalStrict += sectionStrictCount;
    totalValid += sectionValidCount;

    sink.writeln(_pct(sectionValidCount, total, section));
  });

  sink.writeln(_pct(totalValid, totalExamples, 'TOTAL'));
  sink.writeln(_pct(totalStrict, totalValid, 'TOTAL Strict'));

  await sink.flush();
  await sink.close();
}
