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

import 'package:analysis_server/plugin/protocol/protocol.dart' as server;
import 'package:analysis_server/src/channel/channel.dart';
import 'package:analysis_server/src/plugin/result_collector.dart';
import 'package:analysis_server/src/plugin/result_converter.dart';
import 'package:analysis_server/src/plugin/result_merger.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer_plugin/protocol/protocol.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_constants.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;

/**
 * The object used to coordinate the results of notifications from the analysis
 * server and multiple plugins.
 */
class NotificationManager {
  /**
   * The identifier used to identify results from the server.
   */
  static const String serverId = 'server';

  /**
   * The channel used to send notifications to the client.
   */
  final ServerCommunicationChannel channel;

  /**
   * The resource provider used to get the path context.
   */
  final ResourceProvider provider;

  /**
   * A list of the paths of files and directories that are included for analysis.
   */
  List<String> includedPaths = <String>[];

  /**
   * A list of the paths of files and directories that are excluded from
   * analysis.
   */
  List<String> excludedPaths = <String>[];

  /**
   * The current set of subscriptions to which the client has subscribed.
   */
  Map<server.AnalysisService, Set<String>> currentSubscriptions =
      <server.AnalysisService, Set<String>>{};

  /**
   * The collector being used to collect the analysis errors from the plugins.
   */
  ResultCollector<List<server.AnalysisError>> errors;

  /**
   * The collector being used to collect the folding regions from the plugins.
   */
  ResultCollector<List<server.FoldingRegion>> folding;

  /**
   * The collector being used to collect the highlight regions from the plugins.
   */
  ResultCollector<List<server.HighlightRegion>> highlights;

  /**
   * The collector being used to collect the navigation parameters from the
   * plugins.
   */
  ResultCollector<server.AnalysisNavigationParams> navigation;

  /**
   * The collector being used to collect the occurrences from the plugins.
   */
  ResultCollector<List<server.Occurrences>> occurrences;

  /**
   * The collector being used to collect the outlines from the plugins.
   */
  ResultCollector<List<server.Outline>> outlines;

  /**
   * The object used to convert results.
   */
  ResultConverter converter = new ResultConverter();

  /**
   * The object used to merge results.
   */
  ResultMerger merger = new ResultMerger();

  /**
   * Initialize a newly created notification manager.
   */
  NotificationManager(this.channel, this.provider) {
    errors = new ResultCollector<List<server.AnalysisError>>(serverId,
        predicate: _isIncluded);
    folding = new ResultCollector<List<server.FoldingRegion>>(serverId);
    highlights = new ResultCollector<List<server.HighlightRegion>>(serverId);
    navigation = new ResultCollector<server.AnalysisNavigationParams>(serverId);
    occurrences = new ResultCollector<List<server.Occurrences>>(serverId);
    outlines = new ResultCollector<List<server.Outline>>(serverId);
  }

  /**
   * Handle the given [notification] from the plugin with the given [pluginId].
   */
  void handlePluginNotification(
      String pluginId, plugin.Notification notification) {
    String event = notification.event;
    switch (event) {
      case plugin.ANALYSIS_NOTIFICATION_ERRORS:
        plugin.AnalysisErrorsParams params =
            new plugin.AnalysisErrorsParams.fromNotification(notification);
        recordAnalysisErrors(
            pluginId,
            params.file,
            params.errors
                .map((plugin.AnalysisError error) =>
                    converter.convertAnalysisError(error))
                .toList());
        break;
      case plugin.ANALYSIS_NOTIFICATION_FOLDING:
        plugin.AnalysisFoldingParams params =
            new plugin.AnalysisFoldingParams.fromNotification(notification);
        recordFoldingRegions(
            pluginId,
            params.file,
            params.regions
                .map((plugin.FoldingRegion region) =>
                    converter.convertFoldingRegion(region))
                .toList());
        break;
      case plugin.ANALYSIS_NOTIFICATION_HIGHLIGHTS:
        plugin.AnalysisHighlightsParams params =
            new plugin.AnalysisHighlightsParams.fromNotification(notification);
        recordHighlightRegions(
            pluginId,
            params.file,
            params.regions
                .map((plugin.HighlightRegion region) =>
                    converter.convertHighlightRegion(region))
                .toList());
        break;
      case plugin.ANALYSIS_NOTIFICATION_NAVIGATION:
        plugin.AnalysisNavigationParams params =
            new plugin.AnalysisNavigationParams.fromNotification(notification);
        recordNavigationParams(pluginId, params.file,
            converter.convertAnalysisNavigationParams(params));
        break;
      case plugin.ANALYSIS_NOTIFICATION_OCCURRENCES:
        plugin.AnalysisOccurrencesParams params =
            new plugin.AnalysisOccurrencesParams.fromNotification(notification);
        recordOccurrences(
            pluginId,
            params.file,
            params.occurrences
                .map((plugin.Occurrences occurrences) =>
                    converter.convertOccurrences(occurrences))
                .toList());
        break;
      case plugin.ANALYSIS_NOTIFICATION_OUTLINE:
        plugin.AnalysisOutlineParams params =
            new plugin.AnalysisOutlineParams.fromNotification(notification);
        recordOutlines(
            pluginId,
            params.file,
            params.outline
                .map((plugin.Outline outline) =>
                    converter.convertOutline(outline))
                .toList());
        break;
    }
  }

  /**
   * Record error information from the plugin with the given [pluginId] for the
   * file with the given [filePath].
   */
  void recordAnalysisErrors(
      String pluginId, String filePath, List<server.AnalysisError> errorData) {
    if (errors.isCollectingFor(filePath)) {
      errors.putResults(filePath, pluginId, errorData);
      List<List<server.AnalysisError>> unmergedErrors =
          errors.getResults(filePath);
      List<server.AnalysisError> mergedErrors =
          merger.mergeAnalysisErrors(unmergedErrors);
      channel.sendNotification(
          new server.AnalysisErrorsParams(filePath, mergedErrors)
              .toNotification());
    }
  }

  /**
   * Record folding information from the plugin with the given [pluginId] for
   * the file with the given [filePath].
   */
  void recordFoldingRegions(String pluginId, String filePath,
      List<server.FoldingRegion> foldingData) {
    if (folding.isCollectingFor(filePath)) {
      folding.putResults(filePath, pluginId, foldingData);
      List<List<server.FoldingRegion>> unmergedFolding =
          folding.getResults(filePath);
      List<server.FoldingRegion> mergedFolding =
          merger.mergeFoldingRegions(unmergedFolding);
      channel.sendNotification(
          new server.AnalysisFoldingParams(filePath, mergedFolding)
              .toNotification());
    }
  }

  /**
   * Record highlight information from the plugin with the given [pluginId] for
   * the file with the given [filePath].
   */
  void recordHighlightRegions(String pluginId, String filePath,
      List<server.HighlightRegion> highlightData) {
    if (highlights.isCollectingFor(filePath)) {
      highlights.putResults(filePath, pluginId, highlightData);
      List<List<server.HighlightRegion>> unmergedHighlights =
          highlights.getResults(filePath);
      List<server.HighlightRegion> mergedHighlights =
          merger.mergeHighlightRegions(unmergedHighlights);
      channel.sendNotification(
          new server.AnalysisHighlightsParams(filePath, mergedHighlights)
              .toNotification());
    }
  }

  /**
   * Record navigation information from the plugin with the given [pluginId] for
   * the file with the given [filePath].
   */
  void recordNavigationParams(String pluginId, String filePath,
      server.AnalysisNavigationParams navigationData) {
    if (navigation.isCollectingFor(filePath)) {
      navigation.putResults(filePath, pluginId, navigationData);
      List<server.AnalysisNavigationParams> unmergedNavigations =
          navigation.getResults(filePath);
      server.AnalysisNavigationParams mergedNavigations =
          merger.mergeNavigation(unmergedNavigations);
      channel.sendNotification(mergedNavigations.toNotification());
    }
  }

  /**
   * Record occurrences information from the plugin with the given [pluginId]
   * for the file with the given [filePath].
   */
  void recordOccurrences(String pluginId, String filePath,
      List<server.Occurrences> occurrencesData) {
    if (occurrences.isCollectingFor(filePath)) {
      occurrences.putResults(filePath, pluginId, occurrencesData);
      List<List<server.Occurrences>> unmergedOccurrences =
          occurrences.getResults(filePath);
      List<server.Occurrences> mergedOccurrences =
          merger.mergeOccurrences(unmergedOccurrences);
      channel.sendNotification(
          new server.AnalysisOccurrencesParams(filePath, mergedOccurrences)
              .toNotification());
    }
  }

  /**
   * Record outline information from the plugin with the given [pluginId] for
   * the file with the given [filePath].
   */
  void recordOutlines(
      String pluginId, String filePath, List<server.Outline> outlineData) {
    if (outlines.isCollectingFor(filePath)) {
      outlines.putResults(filePath, pluginId, outlineData);
      List<List<server.Outline>> unmergedOutlines =
          outlines.getResults(filePath);
      List<server.Outline> mergedOutlines =
          merger.mergeOutline(unmergedOutlines);
      channel.sendNotification(new server.AnalysisOutlineParams(
              filePath, server.FileKind.LIBRARY, mergedOutlines[0])
          .toNotification());
    }
  }

  /**
   * Set the lists of [included] and [excluded] files.
   */
  void setAnalysisRoots(List<String> included, List<String> excluded) {
    includedPaths = included;
    excludedPaths = excluded;
  }

  /**
   * Set the current subscriptions to the given set of [newSubscriptions].
   */
  void setSubscriptions(
      Map<server.AnalysisService, Set<String>> newSubscriptions) {
    /**
     * Return the collector associated with the given service, or `null` if the
     * service is not handled by this manager.
     */
    ResultCollector collectorFor(server.AnalysisService service) {
      switch (service) {
        case server.AnalysisService.FOLDING:
          return folding;
        case server.AnalysisService.HIGHLIGHTS:
          return highlights;
        case server.AnalysisService.NAVIGATION:
          return navigation;
        case server.AnalysisService.OCCURRENCES:
          return occurrences;
        case server.AnalysisService.OUTLINE:
          return outlines;
      }
      return null;
    }

    Set<server.AnalysisService> services =
        new HashSet<server.AnalysisService>();
    services.addAll(currentSubscriptions.keys);
    services.addAll(newSubscriptions.keys);
    services.forEach((server.AnalysisService service) {
      ResultCollector collector = collectorFor(service);
      if (collector != null) {
        Set<String> currentPaths = currentSubscriptions[service];
        Set<String> newPaths = newSubscriptions[service];
        if (currentPaths == null) {
          if (newPaths == null) {
            // This should not happen.
            return;
          }
          // All of the [newPaths] need to be added.
          newPaths.forEach((String filePath) {
            collector.startCollectingFor(filePath);
          });
        } else if (newPaths == null) {
          // All of the [currentPaths] need to be removed.
          currentPaths.forEach((String filePath) {
            collector.stopCollectingFor(filePath);
          });
        } else {
          // Compute the difference of the two sets.
          newPaths.forEach((String filePath) {
            if (!currentPaths.contains(filePath)) {
              collector.startCollectingFor(filePath);
            }
          });
          currentPaths.forEach((String filePath) {
            if (!newPaths.contains(filePath)) {
              collector.stopCollectingFor(filePath);
            }
          });
        }
      }
    });
    currentSubscriptions = newSubscriptions;
  }

  /**
   * Return `true` if errors should be collected for the file with the given
   * [path] (because it is being analyzed).
   */
  bool _isIncluded(String path) {
    bool isIncluded() {
      for (String includedPath in includedPaths) {
        if (provider.pathContext.isWithin(includedPath, path) ||
            provider.pathContext.equals(includedPath, path)) {
          return true;
        }
      }
      return false;
    }

    bool isExcluded() {
      for (String excludedPath in excludedPaths) {
        if (provider.pathContext.isWithin(excludedPath, path)) {
          return true;
        }
      }
      return false;
    }

    // TODO(brianwilkerson) Return false if error notifications are globally
    // disabled.
    return isIncluded() && !isExcluded();
  }
}
