// 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:path/path.dart' as path;

/// Scans the dartdoc HTML output in the provided `htmlOutputPath` for
/// unresolved dartdoc directives (`{@foo x y}`).
///
/// Dartdoc usually replaces those directives with other content. However,
/// if the directive is misspelled (or contains other errors) it is placed
/// verbatim into the HTML output. That's not desirable and this check verifies
/// that no directives appear verbatim in the output by checking that the
/// string `{@` does not appear in the HTML output outside of <code> sections.
///
/// The string `{@` is allowed in <code> sections, because those may contain
/// sample code where the sequence is perfectly legal, e.g. for required named
/// parameters of a method:
///
/// ```
/// void foo({@required int bar});
/// ```
void checkForUnresolvedDirectives(String htmlOutputPath) {
  final Directory dartDocDir = Directory(htmlOutputPath);
  if (!dartDocDir.existsSync()) {
    throw Exception('Directory with dartdoc output (${dartDocDir.path}) does not exist.');
  }

  // Makes sure that the path we were given contains some of the expected
  // libraries and HTML files.
  final List<String> canaryLibraries = <String>[
    'animation',
    'cupertino',
    'material',
    'widgets',
    'rendering',
    'flutter_driver',
  ];
  final List<String> canaryFiles = <String>[
    'Widget-class.html',
    'Material-class.html',
    'Canvas-class.html',
  ];

  print('Scanning for unresolved dartdoc directives...');

  final List<FileSystemEntity> toScan = dartDocDir.listSync();
  int count = 0;

  while (toScan.isNotEmpty) {
    final FileSystemEntity entity = toScan.removeLast();
    if (entity is File) {
      if (path.extension(entity.path) != '.html') {
        continue;
      }
      canaryFiles.remove(path.basename(entity.path));
      count += _scanFile(entity);
    } else if (entity is Directory) {
      canaryLibraries.remove(path.basename(entity.path));
      toScan.addAll(entity.listSync());
    } else {
      throw Exception('$entity is neither file nor directory.');
    }
  }

  if (canaryLibraries.isNotEmpty) {
    throw Exception('Did not find docs for the following libraries: ${canaryLibraries.join(', ')}.');
  }
  if (canaryFiles.isNotEmpty) {
    throw Exception('Did not find docs for the following files: ${canaryFiles.join(', ')}.');
  }
  if (count > 0) {
    throw Exception('Found $count unresolved dartdoc directives (see log above).');
  }
  print('No unresolved dartdoc directives detected.');
}

int _scanFile(File file) {
  assert(path.extension(file.path) == '.html');
  final Iterable<String> matches = _pattern.allMatches(file.readAsStringSync())
      .map((RegExpMatch m ) => m.group(0)!);

  if (matches.isNotEmpty) {
    stderr.writeln('Found unresolved dartdoc directives in ${file.path}:');
    for (final String match in matches) {
      stderr.writeln('  $match');
    }
  }
  return matches.length;
}

// Matches all `{@` that are not within `<code></code>` sections.
//
// This regex may lead to false positives if the docs ever contain nested tags
// inside <code> sections. Since we currently don't do that, doing the matching
// with a regex is a lot faster than using an HTML parser to strip out the
// <code> sections.
final RegExp _pattern = RegExp(r'({@[^}\n]*}?)(?![^<>]*</code)');

// Usually, the checker is invoked directly from `dartdoc.dart`. Main method
// is included for convenient local runs without having to regenerate
// the dartdocs every time.
//
// Provide the path to the dartdoc HTML output as an argument when running the
// program.
void main(List<String> args) {
  if (args.length != 1) {
    throw Exception('Must provide the path to the dartdoc HTML output as argument.');
  }
  if (!Directory(args.single).existsSync()) {
    throw Exception('The dartdoc HTML output directory ${args.single} does not exist.');
  }
  checkForUnresolvedDirectives(args.single);
}