// Copyright (c) 2014, 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.

library domain.execution;

import 'dart:async';
import 'dart:collection';
import 'dart:core';

import 'package:analysis_server/plugin/protocol/protocol.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/constants.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';

/**
 * Instances of the class [ExecutionDomainHandler] implement a [RequestHandler]
 * that handles requests in the `execution` domain.
 */
class ExecutionDomainHandler implements RequestHandler {
  /**
   * The analysis server that is using this handler to process requests.
   */
  final AnalysisServer server;

  /**
   * The next execution context identifier to be returned.
   */
  int nextContextId = 0;

  /**
   * A table mapping execution context id's to the root of the context.
   */
  Map<String, String> contextMap = new HashMap<String, String>();

  /**
   * The subscription to the 'onAnalysisComplete' events,
   * used to send notifications when
   */
  StreamSubscription onFileAnalyzed;

  /**
   * Initialize a newly created handler to handle requests for the given [server].
   */
  ExecutionDomainHandler(this.server);

  /**
   * Implement the `execution.createContext` request.
   */
  Response createContext(Request request) {
    String file =
        new ExecutionCreateContextParams.fromRequest(request).contextRoot;
    String contextId = (nextContextId++).toString();
    contextMap[contextId] = file;
    return new ExecutionCreateContextResult(contextId).toResponse(request.id);
  }

  /**
   * Implement the `execution.deleteContext` request.
   */
  Response deleteContext(Request request) {
    String contextId = new ExecutionDeleteContextParams.fromRequest(request).id;
    contextMap.remove(contextId);
    return new ExecutionDeleteContextResult().toResponse(request.id);
  }

  @override
  Response handleRequest(Request request) {
    try {
      String requestName = request.method;
      if (requestName == EXECUTION_CREATE_CONTEXT) {
        return createContext(request);
      } else if (requestName == EXECUTION_DELETE_CONTEXT) {
        return deleteContext(request);
      } else if (requestName == EXECUTION_MAP_URI) {
        return mapUri(request);
      } else if (requestName == EXECUTION_SET_SUBSCRIPTIONS) {
        return setSubscriptions(request);
      }
    } on RequestFailure catch (exception) {
      return exception.response;
    }
    return null;
  }

  /**
   * Implement the 'execution.mapUri' request.
   */
  Response mapUri(Request request) {
    ExecutionMapUriParams params =
        new ExecutionMapUriParams.fromRequest(request);
    String contextId = params.id;
    String path = contextMap[contextId];
    if (path == null) {
      return new Response.invalidParameter(request, 'id',
          'There is no execution context with an id of $contextId');
    }

    SourceFactory sourceFactory;
    AnalysisDriver driver;
    if (server.options.enableNewAnalysisDriver) {
      driver = server.getAnalysisDriver(path);
      if (driver == null) {
        return new Response.invalidExecutionContext(request, contextId);
      }
      sourceFactory = driver.sourceFactory;
    } else {
      AnalysisContext context = server.getContainingContext(path);
      if (context == null) {
        return new Response.invalidExecutionContext(request, contextId);
      }
      sourceFactory = context.sourceFactory;
    }

    String file = params.file;
    String uri = params.uri;
    if (file != null) {
      if (uri != null) {
        return new Response.invalidParameter(request, 'file',
            'Either file or uri must be provided, but not both');
      }
      Resource resource = server.resourceProvider.getResource(file);
      if (!resource.exists) {
        return new Response.invalidParameter(request, 'file', 'Must exist');
      } else if (resource is! File) {
        return new Response.invalidParameter(
            request, 'file', 'Must not refer to a directory');
      }

      Source source;
      if (server.options.enableNewAnalysisDriver) {
        source = driver.fsState.getFileForPath(file).source;
      } else {
        ContextSourcePair contextSource = server.getContextSourcePair(file);
        source = contextSource.source;
      }
      if (source.uriKind != UriKind.FILE_URI) {
        uri = source.uri.toString();
      } else {
        uri = sourceFactory.restoreUri(source).toString();
      }
      return new ExecutionMapUriResult(uri: uri).toResponse(request.id);
    } else if (uri != null) {
      Source source = sourceFactory.forUri(uri);
      if (source == null) {
        return new Response.invalidParameter(request, 'uri', 'Invalid URI');
      }
      file = source.fullName;
      return new ExecutionMapUriResult(file: file).toResponse(request.id);
    }
    return new Response.invalidParameter(
        request, 'file', 'Either file or uri must be provided');
  }

  /**
   * Implement the 'execution.setSubscriptions' request.
   */
  Response setSubscriptions(Request request) {
    if (server.options.enableNewAnalysisDriver) {
      // Under the analysis driver, setSubscriptions() becomes a no-op.
      return new ExecutionSetSubscriptionsResult().toResponse(request.id);
    } else {
      List<ExecutionService> subscriptions =
          new ExecutionSetSubscriptionsParams.fromRequest(request)
              .subscriptions;
      if (subscriptions.contains(ExecutionService.LAUNCH_DATA)) {
        if (onFileAnalyzed == null) {
          onFileAnalyzed = server.onFileAnalyzed.listen(_fileAnalyzed);
          _reportCurrentFileStatus();
        }
      } else {
        if (onFileAnalyzed != null) {
          onFileAnalyzed.cancel();
          onFileAnalyzed = null;
        }
      }
      return new ExecutionSetSubscriptionsResult().toResponse(request.id);
    }
  }

  void _fileAnalyzed(ChangeNotice notice) {
    ServerPerformanceStatistics.executionNotifications.makeCurrentWhile(() {
      Source source = notice.source;
      String filePath = source.fullName;
      // check files
      bool isDartFile = notice.resolvedDartUnit != null;
      if (!isDartFile) {
        return;
      }
      // prepare context
      AnalysisContext context = server.getContainingContext(filePath);
      if (context == null) {
        return;
      }
      // analyze the file
      if (isDartFile) {
        ExecutableKind kind = ExecutableKind.NOT_EXECUTABLE;
        if (context.isClientLibrary(source)) {
          kind = ExecutableKind.CLIENT;
          if (context.isServerLibrary(source)) {
            kind = ExecutableKind.EITHER;
          }
        } else if (context.isServerLibrary(source)) {
          kind = ExecutableKind.SERVER;
        }
        server.sendNotification(
            new ExecutionLaunchDataParams(filePath, kind: kind)
                .toNotification());
      }
    });
  }

  /**
   * Return `true` if the given [filePath] represents a file that is in an
   * analysis root.
   */
  bool _isInAnalysisRoot(String filePath) =>
      server.contextManager.isInAnalysisRoot(filePath);

  void _reportCurrentFileStatus() {
    for (AnalysisContext context in server.analysisContexts) {
      List<Source> librarySources = context.librarySources;
      List<Source> clientSources = context.launchableClientLibrarySources;
      List<Source> serverSources = context.launchableServerLibrarySources;
      for (Source source in clientSources) {
        if (serverSources.remove(source)) {
          _sendKindNotification(source.fullName, ExecutableKind.EITHER);
        } else {
          _sendKindNotification(source.fullName, ExecutableKind.CLIENT);
        }
        librarySources.remove(source);
      }
      for (Source source in serverSources) {
        _sendKindNotification(source.fullName, ExecutableKind.SERVER);
        librarySources.remove(source);
      }
      for (Source source in librarySources) {
        _sendKindNotification(source.fullName, ExecutableKind.NOT_EXECUTABLE);
      }
      for (Source source in context.htmlSources) {
        String filePath = source.fullName;
        if (_isInAnalysisRoot(filePath)) {
          List<Source> libraries =
              context.getLibrariesReferencedFromHtml(source);
          server.sendNotification(new ExecutionLaunchDataParams(filePath,
                  referencedFiles: _getFullNames(libraries))
              .toNotification());
        }
      }
    }
  }

  /**
   * Send a notification indicating the [kind] of the file with the given
   * [filePath], but only if the file is in an analysis root.
   */
  void _sendKindNotification(String filePath, ExecutableKind kind) {
    if (_isInAnalysisRoot(filePath)) {
      server.sendNotification(
          new ExecutionLaunchDataParams(filePath, kind: kind).toNotification());
    }
  }

  static List<String> _getFullNames(List<Source> sources) {
    return sources.map((Source source) => source.fullName).toList();
  }
}
