// Copyright (c) 2018, 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:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/src/channel/channel.dart';
import 'package:analysis_server/src/socket_server.dart';
import 'package:path/path.dart' as path;

/// Instances of the class [DevAnalysisServer] implement a simple analysis
/// server implementation, used to analyze one or more packages and then
/// terminate the server.
class DevAnalysisServer {
  static bool get _terminalSupportsAnsi {
    return stdout.supportsAnsiEscapes &&
        stdioType(stdout) == StdioType.terminal;
  }

  /// An object that can handle either a WebSocket connection or a connection
  /// to the client over stdio.
  final SocketServer socketServer;

  int _nextId = 0;
  late DevChannel _channel;

  /// Initialize a newly created stdio server.
  new(this.socketServer);

  void initServer() {
    _channel = DevChannel();
    socketServer.createAnalysisServer(_channel);
  }

  /// Analyze the given directories and display any results to stdout.
  ///
  /// Return a future that will be completed with an exit code when analysis
  /// finishes.
  Future<int> processDirectories(List<String> directories) async {
    var bold = _terminalSupportsAnsi ? '\u001b[1m' : '';
    var none = _terminalSupportsAnsi ? '\u001b[0m' : '';

    print('Analyzing ${directories.join(', ')}...');

    var timer = Stopwatch()..start();

    var whenComplete = Completer<int>();
    var beganAnalysis = false;

    var exitCode = 0;

    void handleStatusNotification(Notification notification) {
      var params = notification.params;
      if (params != null && params.containsKey('analysis')) {
        var isAnalyzing =
            (params['analysis'] as Map<String, Object>)['isAnalyzing'] as bool;
        if (isAnalyzing) {
          beganAnalysis = true;
        } else if (beganAnalysis) {
          beganAnalysis = false;
          timer.stop();
          var seconds = timer.elapsedMilliseconds / 1000.0;
          print('Completed in ${seconds.toStringAsFixed(1)}s.');
          whenComplete.complete(exitCode);
        }
      }
    }

    void handleErrorsNotification(Notification notification) {
      var params = notification.params!;
      var filePath = params['file'] as String;
      var errors = params['errors'] as List<Map>;

      if (errors.isEmpty) {
        return;
      }

      filePath = path.relative(filePath);

      for (var error in errors) {
        if (error['type'] == 'TODO') {
          continue;
        }

        var severity = (error['severity'] as String).toLowerCase();
        if (severity == 'warning' && exitCode < 1) {
          exitCode = 1;
        } else if (severity == 'error' && exitCode < 2) {
          exitCode = 2;
        }

        var message = error['message'] as String;
        if (message.endsWith('.')) {
          message = message.substring(0, message.length - 1);
        }
        var code = error['code'] as String;
        var location = error['location'] as Map<Object?, Object?>;
        var line = location['startLine'] as int;
        var column = location['startColumn'] as int;

        print(
          '  $severity • $bold$message$none at $filePath:$line:$column • '
          '($code)',
        );
      }
    }

    void handleServerError(Notification notification) {
      var params = notification.params!;
      var message = params['message'] as String;
      var stackTrace = params['stackTrace'] as String?;

      print(message);
      if (stackTrace != null) {
        print(stackTrace);
      }

      exitCode = 3;

      // Ensure we terminate training if we get an exception from the analysis
      // server.
      whenComplete.completeError(message);
    }

    var notificationSubscriptions = _channel.onNotification.listen((
      Notification notification,
    ) {
      if (notification.event == 'server.status') {
        handleStatusNotification(notification);
      } else if (notification.event == 'analysis.errors') {
        handleErrorsNotification(notification);
      } else if (notification.event == 'server.error') {
        handleServerError(notification);
      }
    });

    await _channel.simulateRequestFromClient(
      Request('${_nextId++}', 'server.setSubscriptions', {
        'subscriptions': ['STATUS'],
      }),
    );

    directories = directories
        .map((dir) => path.normalize(path.absolute(dir)))
        .toList();

    await _channel.simulateRequestFromClient(
      Request('${_nextId++}', 'analysis.setAnalysisRoots', {
        'included': directories,
        'excluded': [],
      }),
    );

    try {
      return await whenComplete.future;
    } finally {
      await notificationSubscriptions.cancel();

      await _channel.simulateRequestFromClient(
        Request('${_nextId++}', 'analysis.setAnalysisRoots', {
          'included': [],
          'excluded': [],
        }),
      );
    }
  }
}

class DevChannel implements ServerCommunicationChannel {
  final StreamController<RequestOrResponse> _requestController =
      StreamController.broadcast();

  final StreamController<Notification> _notificationController =
      StreamController.broadcast();

  final Map<String, Completer<Response>> _responseCompleters = {};

  Stream<Notification> get onNotification => _notificationController.stream;

  @override
  Stream<RequestOrResponse> get requests => _requestController.stream;

  @override
  void close() {
    _notificationController.close();
  }

  @override
  void sendNotification(Notification notification) {
    _notificationController.add(notification);
  }

  @override
  void sendRequest(Request request) {
    throw UnimplementedError(
      'sendRequest (did you mean simulateRequestFromClient?)',
    );
  }

  @override
  void sendResponse(Response response) {
    var completer = _responseCompleters.remove(response.id);
    completer?.complete(response);
  }

  Future<Response> simulateRequestFromClient(Request request) {
    var completer = Completer<Response>();
    _responseCompleters[request.id] = completer;
    _requestController.add(request);
    return completer.future;
  }
}
