// 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.

import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/server/dev_server.dart';
import 'package:analysis_server/src/server/diagnostic_server.dart';
import 'package:analysis_server/src/server/http_server.dart';
import 'package:analysis_server/src/server/stdio_server.dart';
import 'package:analysis_server/src/services/completion/dart/uri_contributor.dart'
    show UriContributor;
import 'package:analysis_server/src/socket_server.dart';
import 'package:analysis_server/starter.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/instrumentation/file_instrumentation.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/plugin/resolver_provider.dart';
import 'package:args/args.dart';
import 'package:linter/src/rules.dart' as linter;
import 'package:plugin/manager.dart';
import 'package:telemetry/crash_reporting.dart';
import 'package:telemetry/telemetry.dart' as telemetry;

/// Commandline argument parser. (Copied from analyzer/lib/options.dart)
/// TODO(pquitslund): replaces with a simple [ArgParser] instance
/// when the args package supports ignoring unrecognized
/// options/flags (https://github.com/dart-lang/args/issues/9).
/// TODO(devoncarew): Consider removing the ability to support unrecognized
/// flags for the analysis server.
class CommandLineParser {
  final List<String> _knownFlags;
  final bool _alwaysIgnoreUnrecognized;
  final ArgParser _parser;

  /// Creates a new command line parser
  CommandLineParser({bool alwaysIgnoreUnrecognized: false})
      : _knownFlags = <String>[],
        _alwaysIgnoreUnrecognized = alwaysIgnoreUnrecognized,
        _parser = new ArgParser(allowTrailingOptions: true);

  ArgParser get parser => _parser;

  /// Defines a flag.
  /// See [ArgParser.addFlag()].
  void addFlag(String name,
      {String abbr,
      String help,
      bool defaultsTo: false,
      bool negatable: true,
      void callback(bool value),
      bool hide: false}) {
    _knownFlags.add(name);
    _parser.addFlag(name,
        abbr: abbr,
        help: help,
        defaultsTo: defaultsTo,
        negatable: negatable,
        callback: callback,
        hide: hide);
  }

  /// Defines a value-taking option.
  /// See [ArgParser.addOption()].
  void addOption(String name,
      {String abbr,
      String help,
      List<String> allowed,
      Map<String, String> allowedHelp,
      String defaultsTo,
      void callback(value)}) {
    _knownFlags.add(name);
    _parser.addOption(name,
        abbr: abbr,
        help: help,
        allowed: allowed,
        allowedHelp: allowedHelp,
        defaultsTo: defaultsTo,
        callback: callback);
  }

  /// Generates a string displaying usage information for the defined options.
  /// See [ArgParser.usage].
  String getUsage() => _parser.usage;

  /// Parses [args], a list of command-line arguments, matches them against the
  /// flags and options defined by this parser, and returns the result. The
  /// values of any defined variables are captured in the given map.
  /// See [ArgParser].
  ArgResults parse(List<String> args, Map<String, String> definedVariables) =>
      _parser.parse(
          _filterUnknowns(parseDefinedVariables(args, definedVariables)));

  List<String> parseDefinedVariables(
      List<String> args, Map<String, String> definedVariables) {
    int count = args.length;
    List<String> remainingArgs = <String>[];
    for (int i = 0; i < count; i++) {
      String arg = args[i];
      if (arg == '--') {
        while (i < count) {
          remainingArgs.add(args[i++]);
        }
      } else if (arg.startsWith("-D")) {
        definedVariables[arg.substring(2)] = args[++i];
      } else {
        remainingArgs.add(arg);
      }
    }
    return remainingArgs;
  }

  List<String> _filterUnknowns(List<String> args) {
    // Only filter args if the ignore flag is specified, or if
    // _alwaysIgnoreUnrecognized was set to true
    if (_alwaysIgnoreUnrecognized ||
        args.contains('--ignore-unrecognized-flags')) {
      // Filter all unrecognized flags and options.
      List<String> filtered = <String>[];
      for (int i = 0; i < args.length; ++i) {
        String arg = args[i];
        if (arg.startsWith('--') && arg.length > 2) {
          String option = arg.substring(2);
          // remove any leading 'no-'
          if (option.startsWith('no-')) {
            option = option.substring(3);
          }
          // strip the last '=value'
          int equalsOffset = option.lastIndexOf('=');
          if (equalsOffset != -1) {
            option = option.substring(0, equalsOffset);
          }
          // check the option
          if (!_knownFlags.contains(option)) {
            //"eat" params by advancing to the next flag/option
            i = _getNextFlagIndex(args, i);
          } else {
            filtered.add(arg);
          }
        } else {
          filtered.add(arg);
        }
      }

      return filtered;
    } else {
      return args;
    }
  }

  int _getNextFlagIndex(List<String> args, int i) {
    for (; i < args.length; ++i) {
      if (args[i].startsWith('--')) {
        return i;
      }
    }
    return i;
  }
}

/**
 * The [Driver] class represents a single running instance of the analysis
 * server application.  It is responsible for parsing command line options
 * and starting the HTTP and/or stdio servers.
 */
class Driver implements ServerStarter {
  /**
   * The name of the application that is used to start a server.
   */
  static const BINARY_NAME = "server";

  /**
   * The name of the option used to set the identifier for the client.
   */
  static const String CLIENT_ID = "client-id";

  /**
   * The name of the option used to set the version for the client.
   */
  static const String CLIENT_VERSION = "client-version";

  /**
   * The name of the option used to enable DartPad specific functionality.
   */
  static const String DARTPAD_OPTION = "dartpad";

  /**
   * The name of the option used to enable instrumentation.
   */
  static const String ENABLE_INSTRUMENTATION_OPTION = "enable-instrumentation";

  /**
   * The name of the option used to set the file read mode.
   */
  static const String FILE_READ_MODE = "file-read-mode";

  /**
   * The name of the option used to print usage information.
   */
  static const String HELP_OPTION = "help";

  /**
   * The name of the flag used to configure reporting analytics.
   */
  static const String ANALYTICS_FLAG = "analytics";

  /**
   * Suppress analytics for this session.
   */
  static const String SUPPRESS_ANALYTICS_FLAG = "suppress-analytics";

  /**
   * The name of the option used to cause instrumentation to also be written to
   * a local file.
   */
  static const String INSTRUMENTATION_LOG_FILE = "instrumentation-log-file";

  /**
   * The name of the option used to specify if [print] should print to the
   * console instead of being intercepted.
   */
  static const String INTERNAL_PRINT_TO_CONSOLE = "internal-print-to-console";

  /**
   * The name of the option used to describe the new analysis driver logger.
   */
  static const String NEW_ANALYSIS_DRIVER_LOG = 'new-analysis-driver-log';

  /**
   * The name of the flag used to enable version 2 of semantic highlight
   * notification.
   */
  static const String USE_ANALYSIS_HIGHLIGHT2 = "useAnalysisHighlight2";

  /**
   * The option for specifying the http diagnostic port.
   * If specified, users can review server status and performance information
   * by opening a web browser on http://localhost:<port>
   */
  static const String PORT_OPTION = "port";

  /**
   * The path to the SDK.
   */
  static const String SDK_OPTION = "sdk";

  /**
   * The path to the data cache.
   */
  static const String CACHE_FOLDER = "cache";

  /**
   * Whether to enable the Dart 2.0 preview.
   */
  static const String PREVIEW_DART2 = "preview-dart-2";

  /**
   * Whether to enable the Dart 2.0 Common Front End implementation.
   */
  static const String USE_CFE = "use-cfe";

  /**
   * Whether to enable parsing via the Fasta parser.
   */
  static const String USE_FASTA_PARSER = "use-fasta-parser";

  /**
   * A directory to analyze in order to train an analysis server snapshot.
   */
  static const String TRAIN_USING = "train-using";

  /**
   * The instrumentation server that is to be used by the analysis server.
   */
  InstrumentationServer instrumentationServer;

  /**
   * The file resolver provider used to override the way file URI's are
   * resolved in some contexts.
   */
  ResolverProvider fileResolverProvider;

  /**
   * The package resolver provider used to override the way package URI's are
   * resolved in some contexts.
   */
  ResolverProvider packageResolverProvider;

  SocketServer socketServer;

  HttpAnalysisServer httpServer;

  Driver();

  /**
   * Use the given command-line [arguments] to start this server.
   *
   * At least temporarily returns AnalysisServer so that consumers of the
   * starter API can then use the server, this is done as a stopgap for the
   * angular plugin until the official plugin API is finished.
   */
  AnalysisServer start(List<String> arguments) {
    CommandLineParser parser = _createArgParser();
    ArgResults results = parser.parse(arguments, <String, String>{});

    AnalysisServerOptions analysisServerOptions = new AnalysisServerOptions();
    analysisServerOptions.useAnalysisHighlight2 =
        results[USE_ANALYSIS_HIGHLIGHT2];
    analysisServerOptions.fileReadMode = results[FILE_READ_MODE];
    analysisServerOptions.newAnalysisDriverLog =
        results[NEW_ANALYSIS_DRIVER_LOG];
    analysisServerOptions.clientId = results[CLIENT_ID];
    analysisServerOptions.clientVersion = results[CLIENT_VERSION];
    analysisServerOptions.cacheFolder = results[CACHE_FOLDER];
    if (results.wasParsed(PREVIEW_DART2)) {
      analysisServerOptions.previewDart2 = results[PREVIEW_DART2];
    } else {
      analysisServerOptions.previewDart2 = true;
    }
    analysisServerOptions.useCFE = results[USE_CFE];
    analysisServerOptions.useFastaParser = results[USE_FASTA_PARSER];

    bool disableAnalyticsForSession = results[SUPPRESS_ANALYTICS_FLAG];
    if (results.wasParsed(TRAIN_USING)) {
      disableAnalyticsForSession = true;
    }

    telemetry.Analytics analytics = telemetry.createAnalyticsInstance(
        'UA-26406144-29', 'analysis-server',
        disableForSession: disableAnalyticsForSession);
    analysisServerOptions.analytics = analytics;

    if (analysisServerOptions.clientId != null) {
      // Record the client name as the application installer ID.
      analytics.setSessionValue('aiid', analysisServerOptions.clientId);
    }
    if (analysisServerOptions.clientVersion != null) {
      analytics.setSessionValue('cd1', analysisServerOptions.clientVersion);
    }

    // TODO(devoncarew): Replace with the real crash product ID.
    analysisServerOptions.crashReportSender =
        new CrashReportSender('Dart_analysis_server', analytics);

    if (telemetry.SHOW_ANALYTICS_UI) {
      if (results.wasParsed(ANALYTICS_FLAG)) {
        analytics.enabled = results[ANALYTICS_FLAG];
        print(telemetry.createAnalyticsStatusMessage(analytics.enabled));
        return null;
      }
    }

    if (results[DARTPAD_OPTION]) {
      UriContributor.suggestFilePaths = false;
    }

    if (results[HELP_OPTION]) {
      _printUsage(parser.parser, analytics, fromHelp: true);
      return null;
    }

    String trainDirectory = results[TRAIN_USING];
    if (trainDirectory != null) {
      if (!FileSystemEntity.isDirectorySync(trainDirectory)) {
        print("Training directory '$trainDirectory' not found.\n");
        exitCode = 1;
        return null;
      }
    }

    int port;
    bool serve_http = false;
    if (results[PORT_OPTION] != null) {
      try {
        port = int.parse(results[PORT_OPTION]);
        serve_http = true;
      } on FormatException {
        print('Invalid port number: ${results[PORT_OPTION]}');
        print('');
        _printUsage(parser.parser, analytics);
        exitCode = 1;
        return null;
      }
    }

    //
    // Process all of the plugins so that extensions are registered.
    //
    ExtensionManager manager = new ExtensionManager();
    manager.processPlugins(AnalysisEngine.instance.requiredPlugins);
    linter.registerLintRules();

    String defaultSdkPath;
    if (results[SDK_OPTION] != null) {
      defaultSdkPath = results[SDK_OPTION];
    } else {
      // No path to the SDK was provided.
      // Use FolderBasedDartSdk.defaultSdkDirectory, which will make a guess.
      defaultSdkPath = FolderBasedDartSdk.defaultSdkDirectory(
              PhysicalResourceProvider.INSTANCE)
          .path;
    }
    // TODO(brianwilkerson) It would be nice to avoid creating an SDK that
    // cannot be re-used, but the SDK is needed to create a package map provider
    // in the case where we need to run `pub` in order to get the package map.
    DartSdk defaultSdk = _createDefaultSdk(defaultSdkPath, true);
    //
    // Initialize the instrumentation service.
    //
    String logFilePath = results[INSTRUMENTATION_LOG_FILE];
    if (logFilePath != null) {
      _rollLogFiles(logFilePath, 5);
      FileInstrumentationServer fileBasedServer =
          new FileInstrumentationServer(logFilePath);
      instrumentationServer = instrumentationServer != null
          ? new MulticastInstrumentationServer(
              [instrumentationServer, fileBasedServer])
          : fileBasedServer;
    }
    InstrumentationService instrumentationService =
        new InstrumentationService(instrumentationServer);
    instrumentationService.logVersion(
        _readUuid(instrumentationService),
        analysisServerOptions.clientId,
        analysisServerOptions.clientVersion,
        AnalysisServer.VERSION,
        defaultSdk.sdkVersion);
    AnalysisEngine.instance.instrumentationService = instrumentationService;

    _DiagnosticServerImpl diagnosticServer = new _DiagnosticServerImpl();

    // Ping analytics with our initial call.
    analytics.sendScreenView('home');

    //
    // Create the sockets and start listening for requests.
    //
    socketServer = new SocketServer(
        analysisServerOptions,
        new DartSdkManager(defaultSdkPath, true),
        defaultSdk,
        instrumentationService,
        diagnosticServer,
        fileResolverProvider,
        packageResolverProvider);
    httpServer = new HttpAnalysisServer(socketServer);

    diagnosticServer.httpServer = httpServer;
    if (serve_http) {
      diagnosticServer.startOnPort(port);
    }

    if (trainDirectory != null) {
      Directory tempDriverDir =
          Directory.systemTemp.createTempSync('analysis_server_');
      analysisServerOptions.cacheFolder = tempDriverDir.path;

      DevAnalysisServer devServer = new DevAnalysisServer(socketServer);
      devServer.initServer();

      () async {
        // We first analyze code with an empty driver cache.
        print('Analyzing with an empty driver cache:');
        int exitCode = await devServer.processDirectories([trainDirectory]);

        print('');

        // Then again with a populated cache.
        print('Analyzing with a populated driver cache:');
        exitCode = await devServer.processDirectories([trainDirectory]);

        if (serve_http) {
          httpServer.close();
        }
        await instrumentationService.shutdown();

        try {
          tempDriverDir.deleteSync(recursive: true);
        } catch (_) {
          // ignore any exception
        }

        exit(exitCode);
      }();
    } else {
      _captureExceptions(instrumentationService, () {
        StdioAnalysisServer stdioServer = new StdioAnalysisServer(socketServer);
        stdioServer.serveStdio().then((_) async {
          // TODO(brianwilkerson) Determine whether this await is necessary.
          await null;
          if (serve_http) {
            httpServer.close();
          }
          await instrumentationService.shutdown();
          exit(0);
        });
      },
          print: results[INTERNAL_PRINT_TO_CONSOLE]
              ? null
              : httpServer.recordPrint);
    }

    return socketServer.analysisServer;
  }

  /**
   * Execute the given [callback] within a zone that will capture any unhandled
   * exceptions and both report them to the client and send them to the given
   * instrumentation [service]. If a [print] function is provided, then also
   * capture any data printed by the callback and redirect it to the function.
   */
  dynamic _captureExceptions(InstrumentationService service, dynamic callback(),
      {void print(String line)}) {
    void errorFunction(Zone self, ZoneDelegate parent, Zone zone,
        dynamic exception, StackTrace stackTrace) {
      service.logPriorityException(exception, stackTrace);
      AnalysisServer analysisServer = socketServer.analysisServer;
      analysisServer.sendServerErrorNotification(
          'Captured exception', exception, stackTrace);
      throw exception;
    }

    var printFunction = print == null
        ? null
        : (Zone self, ZoneDelegate parent, Zone zone, String line) {
            // Note: we don't pass the line on to stdout, because that is
            // reserved for communication to the client.
            print(line);
          };
    ZoneSpecification zoneSpecification = new ZoneSpecification(
        handleUncaughtError: errorFunction, print: printFunction);
    return runZoned(callback, zoneSpecification: zoneSpecification);
  }

  /**
   * Create and return the parser used to parse the command-line arguments.
   */
  CommandLineParser _createArgParser() {
    CommandLineParser parser =
        new CommandLineParser(alwaysIgnoreUnrecognized: true);
    parser.addOption(CLIENT_ID,
        help: "an identifier used to identify the client");
    parser.addOption(CLIENT_VERSION, help: "the version of the client");
    parser.addFlag(DARTPAD_OPTION,
        help: 'enable DartPad specific functionality',
        defaultsTo: false,
        hide: true);
    parser.addFlag(ENABLE_INSTRUMENTATION_OPTION,
        help: "enable sending instrumentation information to a server",
        defaultsTo: false,
        negatable: false);
    parser.addFlag(HELP_OPTION,
        help: "print this help message without starting a server",
        abbr: 'h',
        defaultsTo: false,
        negatable: false);
    parser.addOption(INSTRUMENTATION_LOG_FILE,
        help: "write instrumentation data to the given file");
    parser.addFlag(INTERNAL_PRINT_TO_CONSOLE,
        help: "enable sending `print` output to the console",
        defaultsTo: false,
        negatable: false);
    parser.addOption(NEW_ANALYSIS_DRIVER_LOG,
        help: "set a destination for the new analysis driver's log");
    parser.addFlag(ANALYTICS_FLAG,
        help: 'enable or disable sending analytics information to Google',
        hide: !telemetry.SHOW_ANALYTICS_UI);
    parser.addFlag(SUPPRESS_ANALYTICS_FLAG,
        negatable: false,
        help: 'suppress analytics for this session',
        hide: !telemetry.SHOW_ANALYTICS_UI);
    parser.addOption(PORT_OPTION,
        help: "the http diagnostic port on which the server provides"
            " status and performance information");
    parser.addOption(SDK_OPTION, help: "[path] the path to the sdk");
    parser.addFlag(USE_ANALYSIS_HIGHLIGHT2,
        help: "enable version 2 of semantic highlight",
        defaultsTo: false,
        negatable: false);
    parser.addOption(FILE_READ_MODE,
        help: "an option for reading files (some clients normalize eol "
            "characters, which make the file offset and range information "
            "incorrect)",
        allowed: ["as-is", "normalize-eol-always"],
        allowedHelp: {
          "as-is": "file contents are read as-is",
          "normalize-eol-always":
              r"eol characters normalized to the single character new line ('\n')"
        },
        defaultsTo: "as-is");
    parser.addOption(CACHE_FOLDER,
        help: "[path] path to the location where to cache data");
    parser.addFlag(PREVIEW_DART2, help: "Enable the Dart 2.0 preview");
    parser.addFlag(USE_CFE,
        help: "Enable the Dart 2.0 Common Front End implementation");
    parser.addFlag(USE_FASTA_PARSER,
        help: "Whether to enable parsing via the Fasta parser");
    parser.addOption(TRAIN_USING,
        help: "Pass in a directory to analyze for purposes of training an "
            "analysis server snapshot.");

    return parser;
  }

  DartSdk _createDefaultSdk(String defaultSdkPath, bool useSummaries) {
    PhysicalResourceProvider resourceProvider =
        PhysicalResourceProvider.INSTANCE;
    FolderBasedDartSdk sdk = new FolderBasedDartSdk(
        resourceProvider, resourceProvider.getFolder(defaultSdkPath));
    sdk.useSummary = useSummaries;
    return sdk;
  }

  /**
   * Print information about how to use the server.
   */
  void _printUsage(ArgParser parser, telemetry.Analytics analytics,
      {bool fromHelp: false}) {
    print('Usage: $BINARY_NAME [flags]');
    print('');
    print('Supported flags are:');
    print(parser.usage);

    if (telemetry.SHOW_ANALYTICS_UI) {
      // Print analytics status and information.
      if (fromHelp) {
        print('');
        print(telemetry.analyticsNotice);
      }
      print('');
      print(telemetry.createAnalyticsStatusMessage(analytics.enabled,
          command: ANALYTICS_FLAG));
    }
  }

  /**
   * Read the UUID from disk, generating and storing a new one if necessary.
   */
  String _readUuid(InstrumentationService service) {
    File uuidFile = new File(PhysicalResourceProvider.INSTANCE
        .getStateLocation('.instrumentation')
        .getChild('uuid.txt')
        .path);
    try {
      if (uuidFile.existsSync()) {
        String uuid = uuidFile.readAsStringSync();
        if (uuid != null && uuid.length > 5) {
          return uuid;
        }
      }
    } catch (exception, stackTrace) {
      service.logPriorityException(exception, stackTrace);
    }
    int millisecondsSinceEpoch = new DateTime.now().millisecondsSinceEpoch;
    int random = new Random().nextInt(0x3fffffff);
    String uuid = '$millisecondsSinceEpoch$random';
    try {
      uuidFile.parent.createSync(recursive: true);
      uuidFile.writeAsStringSync(uuid);
    } catch (exception, stackTrace) {
      service.logPriorityException(exception, stackTrace);
      // Slightly alter the uuid to indicate it was not persisted
      uuid = 'temp-$uuid';
    }
    return uuid;
  }

  /**
   * Perform log files rolling.
   *
   * Rename existing files with names `[path].(x)` to `[path].(x+1)`.
   * Keep at most [numOld] files.
   * Rename the file with the given [path] to `[path].1`.
   */
  static void _rollLogFiles(String path, int numOld) {
    for (int i = numOld - 1; i >= 0; i--) {
      try {
        String oldPath = i == 0 ? path : '$path.$i';
        new File(oldPath).renameSync('$path.${i + 1}');
      } catch (e) {}
    }
  }
}

/**
 * Implements the [DiagnosticServer] class by wrapping an [HttpAnalysisServer].
 */
class _DiagnosticServerImpl extends DiagnosticServer {
  HttpAnalysisServer httpServer;

  _DiagnosticServerImpl();

  @override
  Future<int> getServerPort() => httpServer.serveHttp();

  Future startOnPort(int port) {
    return httpServer.serveHttp(port);
  }
}
