// 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:math' as math;

import 'package:analysis_server/src/status/diagnostics.dart';
import 'package:analysis_server/src/status/pages.dart';
import 'package:analysis_server/src/status/utilities/library_cycle_extensions.dart';
import 'package:analysis_server/src/status/utilities/string_extensions.dart';
import 'package:analyzer/src/context/source.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart';
import 'package:analyzer/src/dart/analysis/library_graph.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/workspace/pub.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:path/path.dart' as path;

class ContextsPage extends DiagnosticPageWithNav {
  new(DiagnosticsSite site)
    : super(
        site,
        'contexts',
        'Contexts',
        description:
            'An analysis context defines a set of sources for which URIs are '
            'all resolved in the same way.',
      );

  @override
  String get navDetail => '${server.driverMap.length}';

  @override
  Future<void> generateContent(Map<String, String> params) async {
    if (driverMap.isEmpty) {
      blankslate('No contexts.');
      return;
    }

    var (folder: folder, driver: driver) = currentContext(params);
    var contextPath = folder.path;

    writeContextNavigationTabs(folder);
    buf.writeln(formatOption('Context location', escape(contextPath)));
    buf.writeln(
      formatOption('SDK root', escape(driver.analysisContext?.sdkRoot?.path)),
    );

    h3('Analysis options');

    // Display analysis options entries inside this context root.
    var optionsList = getOptionsList(folder, driver);
    var foldersInContextRoot = [
      for (var options in optionsList) options.file!.path,
    ];
    var separator = folder.provider.pathContext.separator;
    ul(foldersInContextRoot, (folderPath) {
      buf.write(escape(folderPath));
      buf.write('$separator<wbr>');
      var optionsPath = path.join(folderPath, file_paths.analysisOptionsYaml);
      buf.writeln(
        formatContentsLink(optionsPath, file_paths.analysisOptionsYaml),
      );
    }, classes: 'scroll-table');

    h3('Workspace');
    var workspace = driver.analysisContext!.contextRoot.workspace;
    buf.writeln('<p>');
    buf.writeln(formatOption('Workspace root', escape(workspace.root)));
    var workspaceFolder = folder.provider.getFolder(workspace.root);

    void writePackage(WorkspacePackageImpl package) {
      buf.writeln(formatOption('Package root', escape(package.root.path)));
      if (package is PubPackage) {
        buf.write('pubspec file: ');
        buf.write(escape(workspaceFolder.path));
        buf.write('$separator<wbr>');
        var pubspecPath = workspaceFolder.getFile(file_paths.pubspecYaml).path;
        buf.writeln(formatContentsLink(pubspecPath, file_paths.pubspecYaml));
      }
    }

    var packageConfig = workspaceFolder
        .getFolder(file_paths.dotDartTool)
        .getFile(file_paths.packageConfigJson);
    buf.writeln(
      formatOption('Has package_config.json file', packageConfig.exists),
    );

    String lenCounter(int length) {
      return '<span class="counter" style="float: right;">$length</span>';
    }

    if (workspace is PackageConfigWorkspace) {
      var packages = workspace.allPackages;
      h4('Packages ${lenCounter(packages.length)}', raw: true);
      ul(packages, writePackage, classes: 'scroll-table');
    }
    buf.writeln('</p>');

    buf.writeln('</div>');

    h3('Plugins');
    var optionsData = collectOptionsData(driver);
    p(optionsData.plugins.toList().join(', '));

    var priorityFiles = driver.priorityFiles;
    var addedFiles = driver.addedFiles.toList();
    var knownFiles = driver.knownFiles.map((f) => f.path).toSet();
    var implicitFiles = knownFiles.difference(driver.addedFiles).toList();
    addedFiles.sort();
    implicitFiles.sort();

    h3('Context files');

    void writeFile(String file) {
      var astPath = '/ast?file=${Uri.encodeQueryComponent(file)}';
      var elementPath = '/element-model?file=${Uri.encodeQueryComponent(file)}';
      var contentsPath = '/contents?file=${Uri.encodeQueryComponent(file)}';
      var hasOverlay = server.resourceProvider.hasOverlay(file);

      buf.write(file.wordBreakOnSlashes);
      buf.writeln(' <a href="$astPath">ast</a>');
      buf.writeln(' <a href="$elementPath">element</a>');
      buf.writeln(
        ' <a href="$contentsPath">contents${hasOverlay ? '*' : ''}</a>',
      );
    }

    h4('Priority files ${lenCounter(priorityFiles.length)}', raw: true);
    ul(priorityFiles, writeFile, classes: 'scroll-table');

    h4('Added files ${lenCounter(addedFiles.length)}', raw: true);
    ul(addedFiles, writeFile, classes: 'scroll-table');

    h4('Implicit files ${lenCounter(implicitFiles.length)}', raw: true);
    ul(implicitFiles, writeFile, classes: 'scroll-table');

    var sourceFactory = driver.sourceFactory;
    if (sourceFactory is SourceFactoryImpl) {
      h3('Resolvers');
      for (var resolver in sourceFactory.resolvers) {
        h4(resolver.runtimeType.toString());
        buf.write('<p class="scroll-table">');
        if (resolver is DartUriResolver) {
          var sdk = resolver.dartSdk;
          buf.write(' (sdk = ');
          buf.write(sdk.runtimeType);
          if (sdk is FolderBasedDartSdk) {
            buf.write(' (path = ');
            buf.write(sdk.directory.path);
            buf.write(')');
          } else if (sdk is EmbedderSdk) {
            buf.write(' (map = ');
            writeMap(sdk.urlMappings);
            buf.write(')');
          }
          buf.write(')');
        } else if (resolver is PackageMapUriResolver) {
          writeMap(resolver.packageMap);
        } else if (resolver is PackageConfigPackageUriResolver) {
          writeMap(resolver.packageMap);
        }
        buf.write('</p>');
      }
    }

    h3('Dartdoc template info');
    var info = driver.dartdocDirectiveInfo;
    buf.write('<p class="scroll-table">');
    writeMap(info.templateMap);
    buf.write('</p>');

    h3('Largest library cycles');
    Set<LibraryCycle> cycles = {};
    var contextRoot = driver.analysisContext!.contextRoot;
    var pathContext = contextRoot.resourceProvider.pathContext;
    for (var filePath in contextRoot.analyzedFiles()) {
      if (!file_paths.isDart(pathContext, filePath)) continue;
      var fileState = driver.fsState.getFileForPath(filePath);
      var kind = fileState.kind;
      if (kind is LibraryFileKind) {
        cycles.add(kind.libraryCycle);
      }
    }
    var sortedMultiLibraryCycles =
        cycles.where((cycle) => cycle.size > 1).toList()
          ..sort((first, second) => second.size - first.size);
    var cyclesToDisplay = math.min(sortedMultiLibraryCycles.length, 10);
    var initialPathLength = contextRoot.root.path.length + 1;
    buf.write(
      'A library cycle is a '
      '<a href="https://en.wikipedia.org/wiki/Cycle_(graph_theory)">cycle</a> '
      'in the import/export graph. If library <em>a</em> imports library '
      '<em>b</em> and library <em>b</em> imports library <em>a</em>, they are '
      'in the same cycle. If library <em>a</em> imports library <em>b</em> and '
      'exports library <em>c</em>, and library <em>b</em> imports library '
      '<em>a</em>, then all three are in the same cycle. In some cases, '
      'library cycles are statically analyzed as a unit; reducing the size of '
      'the largest library cycles may result in faster incremental analysis.',
    );
    buf.write('<p>There are ${cycles.length} library cycles. ');
    if (cyclesToDisplay < 10) {
      buf.write('$cyclesToDisplay of these have more than one library.');
      if (cyclesToDisplay > 0) {
        buf.write(' They contain');
      }
      buf.write('</p>');
    } else {
      buf.write('The $cyclesToDisplay largest contain</p>');
    }
    buf.write('<ul>');
    for (var i = 0; i < cyclesToDisplay; i++) {
      var cycle = sortedMultiLibraryCycles[i];
      var libraries = cycle.libraries;
      var cycleSize = cycle.size;
      var libraryCount = math.min(cycleSize, 8);
      buf.write('<li>$cycleSize libraries, including');
      buf.write('<ul>');
      for (var j = 0; j < libraryCount; j++) {
        var library = libraries[j];
        buf.write('<li>');
        buf.write(library.file.path.substring(initialPathLength));
        buf.write('</li>');
      }
      if (cycleSize > libraryCount) {
        buf.write('<li>${cycleSize - libraryCount} more...</li>');
      }
      buf.write('</ul>');
      buf.write('</li>');
    }
    buf.write('</ul>');
  }

  void writeList<E>(List<E> list) {
    buf.writeln('[${list.join(', ')}]');
  }

  void writeMap<V>(Map<String, V> map) {
    var keys = map.keys.toList();
    keys.sort();
    var length = keys.length;
    buf.write('{');
    for (var i = 0; i < length; i++) {
      buf.write('<br>');
      var key = keys[i];
      var value = map[key];
      buf.write(key);
      buf.write(' = ');
      if (value is List) {
        writeList(value);
      } else {
        buf.write(value);
      }
      buf.write(',');
    }
    buf.write('<br>}');
  }
}
