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

import 'package:analysis_server/src/utilities/stream.dart';
import 'package:collection/collection.dart';

class InvalidEncodingError {
  final String headers;
  new(this.headers);

  @override
  String toString() =>
      'Encoding in supplied headers is not supported.\n\nHeaders:\n$headers';
}

class LspHeaders {
  final String rawHeaders;
  final int contentLength;
  final String? encoding;
  new(this.rawHeaders, this.contentLength, this.encoding);
}

/// Transforms a stream of LSP data in the form:
///
///     Content-Length: xxx\r\n
///     Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n
///     \r\n
///     { JSON payload }
///
/// into just the JSON payload, decoded with the specified encoding. Line endings
/// for headers must be \r\n on all platforms as defined in the LSP spec.
class LspPacketTransformer extends StreamTransformerBase<List<int>, String> {
  @override
  Stream<String> bind(Stream<List<int>> stream) {
    LspHeaders? headersState;
    var buffer = <int>[];
    var controller =
        MoreTypedStreamController<
          String,
          _LspPacketTransformerListenData,
          _LspPacketTransformerPauseData
        >(
          onListen: (controller) {
            var input = stream
                .expand((b) => b)
                .listen(
                  (codeUnit) {
                    buffer.add(codeUnit);
                    var headers = headersState;
                    if (headers == null && _endsWithCrLfCrLf(buffer)) {
                      headersState = _parseHeaders(buffer);
                      buffer.clear();
                    } else if (headers != null &&
                        buffer.length >= headers.contentLength) {
                      // UTF-8 is the default - and only supported - encoding for LSP.
                      // The string 'utf8' is valid since it was published in the original spec.
                      // Any other encodings should be rejected with an error.
                      if ([
                        null,
                        'utf-8',
                        'utf8',
                      ].contains(headers.encoding?.toLowerCase())) {
                        controller.add(utf8.decode(buffer));
                      } else {
                        controller.addError(
                          InvalidEncodingError(headers.rawHeaders),
                        );
                      }
                      buffer.clear();
                      headersState = null;
                    }
                  },
                  onError: controller.addError,
                  onDone: controller.close,
                );
            return _LspPacketTransformerListenData(input);
          },
          onPause: (listenData) {
            listenData.input.pause();
            return _LspPacketTransformerPauseData();
          },
          onResume: (listenData, pauseData) => listenData.input.resume(),
          onCancel: (listenData) => listenData.input.cancel(),
        );
    return controller.controller.stream;
  }

  /// Whether [buffer] ends in '\r\n\r\n'.
  static bool _endsWithCrLfCrLf(List<int> buffer) {
    var l = buffer.length;
    return l > 4 &&
        buffer[l - 1] == 10 &&
        buffer[l - 2] == 13 &&
        buffer[l - 3] == 10 &&
        buffer[l - 4] == 13;
  }

  static String? _extractEncoding(String? header) {
    var charset = header
        ?.split(';')
        .map((s) => s.trim().toLowerCase())
        .firstWhereOrNull((s) => s.startsWith('charset='));

    return charset?.split('=')[1];
  }

  /// Decodes [buffer] into a String and returns the 'Content-Length' header value.
  static LspHeaders _parseHeaders(List<int> buffer) {
    // Headers are specified as always ASCII in LSP.
    var asString = ascii.decode(buffer);
    var headers = asString.split('\r\n');
    var lengthHeader = headers.firstWhere(
      (h) => h.startsWith('Content-Length'),
    );
    var length = lengthHeader.split(':').last.trim();
    var contentTypeHeader = headers.firstWhereOrNull(
      (h) => h.startsWith('Content-Type'),
    );
    var encoding = _extractEncoding(contentTypeHeader);
    return LspHeaders(asString, int.parse(length), encoding);
  }
}

/// The data class for [StreamController.onListen].
class _LspPacketTransformerListenData {
  final StreamSubscription<int> input;

  new(this.input);
}

/// The marker class for [StreamController.onPause].
class _LspPacketTransformerPauseData {}
