// 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:convert' show jsonDecode, jsonEncode;
import 'dart:io' show Directory, File, Platform, Process, exitCode;

import 'package:args/args.dart' show ArgParser;
import 'package:path/path.dart' show absolute;
import 'package:yaml/yaml.dart' show loadYaml;

const _dartdocDir = 'dartdoc-dir';
const _markdownBefore = 'before';
const _markdownAfter = 'after';
const _sdk = 'sdk';
const _help = 'help';

void main(List<String> arguments) {
  final parser = ArgParser()
    ..addSeparator('Usage: dartdoc-compare.dart [OPTIONS] <dart-package>')
    ..addOption(_dartdocDir, help: 'Directory of the dartdoc package')
    ..addOption(_markdownBefore, help: "Markdown package 'before' ref")
    ..addOption(
      _markdownAfter,
      defaultsTo: 'HEAD',
      help: "Markdown package 'after' ref (or 'local')",
    )
    ..addFlag(
      _sdk,
      negatable: false,
      help: 'Is the package the SDK?',
    )
    ..addFlag(_help, abbr: 'h', hide: true);

  final options = parser.parse(arguments);
  if (options[_help] as bool) {
    print(parser.usage);
    exitCode = 0;
    return;
  }
  if (options[_dartdocDir] == null || options[_markdownBefore] == null) {
    print(
      'Invalid arguments: Options --$_dartdocDir and --$_markdownBefore '
      'must be specified',
    );
    print(parser.usage);
    exitCode = 1;
    return;
  }
  final comparer = DartdocCompare(
    options[_dartdocDir] as String,
    options[_markdownBefore] as String,
    options[_markdownAfter] as String,
    absolute(options[_dartdocDir] as String, 'bin/dartdoc.dart'),
    absolute(options[_dartdocDir] as String, 'pubspec.yaml'),
    options[_sdk] as bool,
  );

  String? path;
  if (comparer.sdk) {
    if (options.rest.isNotEmpty) {
      path = options.rest.single;
    }
  } else {
    path = options.rest.single;
  }

  if (comparer.compare(path)) {
    exitCode = 0;
  } else {
    exitCode = 1;
  }
}

class DartdocCompare {
  final String dartdocDir;
  final String markdownBefore;
  final String markdownAfter;
  final String dartdocBin;
  final String dartdocPubspecPath;
  final bool sdk;
  final String markdownPath = File(Platform.script.path).parent.parent.path;

  DartdocCompare(
    this.dartdocDir,
    this.markdownBefore,
    this.markdownAfter,
    this.dartdocBin,
    this.dartdocPubspecPath,
    this.sdk,
  );

  bool compare(String? package) {
    // Generate docs with Markdown "Before".
    final outBefore = _runDartdoc(markdownBefore, package);

    // Generate docs with Markdown "After".
    final outAfter = _runDartdoc(markdownAfter, package);

    // Compare outputs
    final diffOptions = ['-r', '-B', outBefore, outAfter];
    final result = Process.runSync('diff', diffOptions, runInShell: true);
    final nlines = '\n'.allMatches(result.stdout as String).length;
    print('Diff lines: $nlines');
    print('diff ${diffOptions.join(" ")}');
    return result.exitCode == 0;
  }

  String _runDartdoc(String markdownRef, String? path) {
    print('==========================================================');
    print('Running dartdoc for $markdownRef...');
    print('==========================================================');
    _doInPath(dartdocDir, () {
      final returnCode = _updateDartdocPubspec(markdownRef);
      if (returnCode != 0) {
        throw Exception("Could not update dartdoc's pubspec!");
      }
    });
    return _doInPath(path, () {
      if (!sdk) {
        _system('pub', ['upgrade']);
      }
      final out = Directory.systemTemp
          .createTempSync('dartdoc-compare-${markdownRef}__');
      const cmd = 'dart';
      final args = [dartdocBin, '--output=${out.path}'];

      if (sdk) {
        args.add('--sdk-docs');
      }

      print('Command: $cmd ${args.join(' ')}');
      final startTime = DateTime.now();
      _system(cmd, args);
      final endTime = DateTime.now();
      final duration = endTime.difference(startTime).inSeconds;
      print('dartdoc generation for $markdownRef took $duration seconds.');
      print('');

      return out.path;
    });
  }

  int _updateDartdocPubspec(String markdownRef) {
    var dartdocPubspec =
        loadYaml(File(dartdocPubspecPath).readAsStringSync()) as Map;
    // make modifiable copy
    dartdocPubspec = jsonDecode(jsonEncode(dartdocPubspec)) as Map;

    final dependencies = dartdocPubspec['dependencies'] as Map;

    if (markdownRef == 'local') {
      dependencies['markdown'] = {
        'path': markdownPath,
      };
    } else {
      dependencies['markdown'] = {
        'git': {
          'url': 'git://github.com/dart-lang/markdown.git',
          'ref': markdownRef
        }
      };
    }

    File(dartdocPubspecPath).writeAsStringSync(jsonEncode(dartdocPubspec));
    return _system('pub', ['upgrade']);
  }
}

int _system(String cmd, List<String> args) {
  final result = Process.runSync(cmd, args);
  print(result.stdout);
  print(result.stderr);
  return result.exitCode;
}

T _doInPath<T>(String? path, T Function() f) {
  if (path == null) {
    return f();
  }

  final former = Directory.current.path;
  Directory.current = path;
  try {
    return f();
  } finally {
    Directory.current = former;
  }
}
