// Copyright (c) 2017, 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:io';

import 'package:front_end/compiler_options.dart';
import 'package:front_end/dependency_grapher.dart';
import 'package:path/path.dart' as pathos;

main() async {
  exit(await new _SubpackageRelationshipsTest().run());
}

/// Map from subpackage name to the rules for what the subpackage is allowed to
/// depend directly on.
///
/// Each listed directory is considered a subpackage.  Each package contains all
/// of its descendant files that are not in a more deeply nested subpackage.
///
/// TODO(paulberry): stuff in lib/src shouldn't depend on lib; lib should just
/// re-export stuff in lib/src.
/// TODO(paulberry): remove dependencies on analyzer.
final subpackageRules = {
  'lib': new SubpackageRules(
      mayImportAnalyzer: true,
      allowedDependencies: ['lib/src', 'lib/src/base']),
  'lib/src': new SubpackageRules(
      mayImportAnalyzer: true,
      allowedDependencies: ['lib', 'lib/src/base', 'lib/src/scanner']),
  'lib/src/base': new SubpackageRules(
      mayImportAnalyzer: true, allowedDependencies: ['lib']),
  'lib/src/codegen': new SubpackageRules(),
  'lib/src/fasta':
      new SubpackageRules(mayImportAnalyzer: false, allowedDependencies: [
    'lib/src/fasta/builder',
    'lib/src/fasta/dill',
    'lib/src/fasta/kernel',
    'lib/src/fasta/parser',
    'lib/src/fasta/scanner',
    'lib/src/fasta/testing',
    'lib/src/fasta/util',
  ]),
  'lib/src/fasta/analyzer':
      new SubpackageRules(mayImportAnalyzer: true, allowedDependencies: [
    'lib/src/fasta',
    'lib/src/fasta/builder',
    'lib/src/fasta/dill',
    'lib/src/fasta/kernel',
    'lib/src/fasta/source',
  ]),
  'lib/src/fasta/builder': new SubpackageRules(allowedDependencies: [
    'lib/src/fasta',
    'lib/src/fasta/dill',
    'lib/src/fasta/parser',
    'lib/src/fasta/source',
    'lib/src/fasta/util',
  ]),
  'lib/src/fasta/dill': new SubpackageRules(allowedDependencies: [
    'lib/src/fasta',
    'lib/src/fasta/kernel',
  ]),
  'lib/src/fasta/kernel': new SubpackageRules(allowedDependencies: [
    'lib/src/fasta',
    'lib/src/fasta/builder',
    'lib/src/fasta/dill',
    'lib/src/fasta/parser',
    'lib/src/fasta/scanner',
    'lib/src/fasta/source',
    'lib/src/fasta/util',
  ]),
  'lib/src/fasta/parser': new SubpackageRules(allowedDependencies: [
    'lib/src/fasta',
    'lib/src/fasta/scanner',
    'lib/src/fasta/util',
  ]),
  'lib/src/fasta/scanner': new SubpackageRules(allowedDependencies: [
    'lib/src/fasta',
    // fasta scanner produces analyzer scanner tokens
    'lib/src/scanner',
    'lib/src/fasta/util',
  ]),
  'lib/src/fasta/source': new SubpackageRules(allowedDependencies: [
    'lib/src/fasta',
    'lib/src/fasta/builder',
    'lib/src/fasta/dill',
    'lib/src/fasta/kernel',
    'lib/src/fasta/parser',
    'lib/src/fasta/scanner',
    'lib/src/fasta/util',
  ]),
  'lib/src/fasta/testing':
      new SubpackageRules(mayImportAnalyzer: true, allowedDependencies: [
    'lib/src/fasta',
    'lib/src/fasta/dill',
    'lib/src/fasta/kernel',
    'lib/src/fasta/analyzer',
    'lib/src/fasta/scanner',
  ]),
  'lib/src/fasta/util': new SubpackageRules(),
  'lib/src/scanner': new SubpackageRules(allowedDependencies: [
    'lib/src/base',
    // For error codes.
    'lib/src/fasta',
    // fasta scanner produces analyzer scanner tokens
    'lib/src/fasta/scanner',
  ]),
};

/// Rules for what a subpackage may depend directly on.
class SubpackageRules {
  /// Indicates whether the subpackage may directly depend on analyzer.
  final bool mayImportAnalyzer;

  /// Indicates whether dart files may exist in subdirectories of this
  /// subpackage.
  ///
  /// If `false`, any subdirectory of this subpackage must be a separate
  /// subpackage.
  final bool allowSubdirs;

  /// Indicates which other subpackages a given subpackage may directly depend
  /// on.
  final List<String> allowedDependencies;

  var actuallyContainsFiles = false;

  var actuallyImportsAnalyzer = false;

  var actuallyHasSubdirs = false;

  var actualDependencies = new Set<String>();

  SubpackageRules(
      {this.mayImportAnalyzer: false,
      this.allowSubdirs: false,
      this.allowedDependencies: const []});
}

class _SubpackageRelationshipsTest {
  /// File uri of the front_end package's "lib" directory.
  final frontEndLibUri = Platform.script.resolve('../lib/');

  /// Indicates whether any problems have been reported yet.
  bool problemsReported = false;

  /// Check for problems resulting from URI [src] having a direct dependency on
  /// URI [dst].
  void checkDependency(Uri src, Uri dst) {
    if (dst.scheme == 'dart') return;
    if (dst.scheme != 'package') {
      problem('$src depends on $dst, which is neither a package: or dart: URI');
      return;
    }
    var srcSubpackage = subpackageForUri(src);
    if (srcSubpackage == null) return;
    var srcSubpackageRules = subpackageRules[srcSubpackage];
    if (srcSubpackageRules == null) {
      problem('$src is in subpackage "$srcSubpackage", which is not found in '
          'subpackageRules');
      return;
    }
    srcSubpackageRules.actuallyContainsFiles = true;
    if (dst.pathSegments[0] == 'analyzer') {
      if (srcSubpackageRules.mayImportAnalyzer) {
        srcSubpackageRules.actuallyImportsAnalyzer = true;
      } else {
        problem('$src depends on $dst, but subpackage "$srcSubpackage" may not '
            'import analyzer');
      }
    }
    var dstSubPackage = subpackageForUri(dst);
    if (dstSubPackage == null) return;
    if (dstSubPackage == srcSubpackage) return;
    if (srcSubpackageRules.allowedDependencies.contains(dstSubPackage)) {
      srcSubpackageRules.actualDependencies.add(dstSubPackage);
    } else {
      problem('$src depends on $dst, but subpackage "$srcSubpackage" is not '
          'allowed to depend on subpackage "$dstSubPackage"');
    }
  }

  /// Finds all files in the front_end's "lib" directory and returns their Uris
  /// (as "package:" URIs).
  List<Uri> findFrontEndUris() {
    var frontEndUris = <Uri>[];
    var frontEndLibPath = pathos.fromUri(frontEndLibUri);
    for (var entity in new Directory(frontEndLibPath)
        .listSync(recursive: true, followLinks: false)) {
      if (entity is File && entity.path.endsWith('.dart')) {
        var posixRelativePath = pathos.url.joinAll(
            pathos.split(pathos.relative(entity.path, from: frontEndLibPath)));
        frontEndUris.add(Uri.parse('package:front_end/$posixRelativePath'));
      }
    }
    return frontEndUris;
  }

  /// Reports a single problem.
  void problem(String description) {
    print(description);
    problemsReported = true;
  }

  /// Tests all subpackage relationships in the front end, and returns an
  /// appropriate exit code.
  Future<int> run() async {
    var frontEndUris = await findFrontEndUris();
    var packagesFileUri = frontEndLibUri.resolve('../../../.packages');
    var graph = await graphForProgram(
        frontEndUris,
        new CompilerOptions()
          ..packagesFileUri = packagesFileUri
          ..chaseDependencies = true);
    for (var i = 0; i < graph.topologicallySortedCycles.length; i++) {
      for (var library in graph.topologicallySortedCycles[i].libraries.values) {
        for (var dependency in library.dependencies) {
          checkDependency(library.uri, dependency.uri);
        }
      }
    }
    subpackageRules.forEach((subpackage, rule) {
      if (!rule.actuallyContainsFiles) {
        problem("$subpackage contains no files");
      }
      if (rule.mayImportAnalyzer && !rule.actuallyImportsAnalyzer) {
        problem("$subpackage is allowed to import analyzer, but doesn't");
      }
      if (rule.allowSubdirs && !rule.actuallyHasSubdirs) {
        problem("$subpackage is allowed to have subdirectories, but doesn't");
      }
      for (var dep in rule.allowedDependencies
          .toSet()
          .difference(rule.actualDependencies)) {
        problem("$subpackage lists $dep as a dependency, but doesn't use it");
      }
    });
    return problemsReported ? 1 : 0;
  }

  /// Determines which subpackage [src] is in.
  ///
  /// If [src] is not part of the front end, `null` is returned.
  String subpackageForUri(Uri src) {
    if (src.scheme != 'package') return null;
    if (src.pathSegments[0] != 'front_end') return null;
    var pathWithLib = 'lib/${src.pathSegments.skip(1).join('/')}';
    String subpackage;
    String pathWithinSubpackage;
    for (var subpackagePath in subpackageRules.keys) {
      var subpackagePathWithSlash = '$subpackagePath/';
      if (pathWithLib.startsWith(subpackagePathWithSlash) &&
          (subpackage == null || subpackage.length < subpackagePath.length)) {
        subpackage = subpackagePath;
        pathWithinSubpackage =
            pathWithLib.substring(subpackagePathWithSlash.length);
      }
    }
    if (subpackage == null) {
      problem('Uri $src is inside package:front_end but is not in any known '
          'subpackage');
    } else if (pathWithinSubpackage.contains('/')) {
      if (subpackageRules[subpackage].allowSubdirs) {
        subpackageRules[subpackage].actuallyHasSubdirs = true;
      } else {
        problem('Uri $src is in a subfolder of $subpackage, but that '
            'subpackage does not allow dart files in subdirectories.');
      }
    }
    return subpackage;
  }
}
