content: // Copyright 2019 Google Inc. Use of this source code is governed by an
  // MIT-style license that can be found in the LICENSE file or at
  // https://opensource.org/licenses/MIT.
  
  import 'dart:convert';
  import 'dart:io';
  import 'dart:isolate';
  import 'dart:typed_data';
  
  import 'package:native_synchronization/mailbox.dart';
  import 'package:path/path.dart' as p;
  import 'package:protobuf/protobuf.dart';
  import 'package:pub_semver/pub_semver.dart';
  import 'package:sass/sass.dart' as sass;
  import 'package:sass/src/importer/node_package.dart' as npi;
  
  import '../logger.dart';
  import '../value/function.dart';
  import '../value/mixin.dart';
  import 'embedded_sass.pb.dart';
  import 'opaque_registry.dart';
  import 'host_callable.dart';
  import 'importer/file.dart';
  import 'importer/host.dart';
  import 'logger.dart';
  import 'util/proto_extensions.dart';
  import 'utils.dart';
  
  /// The request ID used for all outbound requests.
  ///
  /// Since the dispatcher runs a single-threaded compilation, it will only ever
  /// have one active request at a time, so there's no need to vary the ID.
  final _outboundRequestId = 0;
  
  /// A class that dispatches messages to and from the host for a single
  /// compilation.
  final class CompilationDispatcher {
    /// The mailbox for receiving messages from the host.
    final Mailbox _mailbox;
  
    /// The send port for sending messages to the host.
    final SendPort _sendPort;
  
    /// The compilation ID for which this dispatcher is running.
    ///
    /// This is used in error messages.
    late int _compilationId;
  
    /// [_compilationId], serialized as a varint.
    ///
    /// This is used in outgoing messages.
    late Uint8List _compilationIdVarint;
  
    /// Creates a [CompilationDispatcher] that receives encoded protocol buffers
    /// through [_mailbox] and sends them through [_sendPort].
    CompilationDispatcher(this._mailbox, this._sendPort);
  
    /// Listens for incoming `CompileRequests` and runs their compilations.
    void listen() {
      while (true) {
        try {
          var (compilationId, messageBuffer) = parsePacket(_receive());
  
          _compilationId = compilationId;
          _compilationIdVarint = serializeVarint(compilationId);
  
          InboundMessage message;
          try {
            message = InboundMessage.fromBuffer(messageBuffer);
          } on InvalidProtocolBufferException catch (error) {
            throw parseError(error.message);
          }
  
          switch (message.whichMessage()) {
            case InboundMessage_Message.compileRequest:
              var request = message.compileRequest;
              var response = _compile(request);
              _send(OutboundMessage()..compileResponse = response);
  
            case InboundMessage_Message.versionRequest:
              throw paramsError("VersionRequest must have compilation ID 0.");
  
            case InboundMessage_Message.canonicalizeResponse ||
                  InboundMessage_Message.importResponse ||
                  InboundMessage_Message.fileImportResponse ||
                  InboundMessage_Message.functionCallResponse:
              throw paramsError(
                "Response ID ${message.id} doesn't match any outstanding requests"
                " in compilation $_compilationId.",
              );
  
            case InboundMessage_Message.notSet:
              throw parseError("InboundMessage.message is not set.");
  
            default: // ignore: unreachable_switch_default
              throw parseError(
                "Unknown message type: ${message.toDebugString()}",
              );
          }
        } catch (error, stackTrace) {
          _handleError(error, stackTrace);
        }
      }
    }
  
    OutboundMessage_CompileResponse _compile(
      InboundMessage_CompileRequest request,
    ) {
      var functions = OpaqueRegistry<SassFunction>();
      var mixins = OpaqueRegistry<SassMixin>();
  
      var style = request.style == OutputStyle.COMPRESSED
          ? sass.OutputStyle.compressed
          : sass.OutputStyle.expanded;
      var logger = request.silent
          ? Logger.quiet
          : EmbeddedLogger(
              this,
              color: request.alertColor,
              ascii: request.alertAscii,
            );
  
      Iterable<sass.Deprecation>? parseDeprecationsOrWarn(
        Iterable<String> deprecations, {
        bool supportVersions = false,
      }) {
        return () sync* {
          for (var item in deprecations) {
            var deprecation = sass.Deprecation.fromId(item);
            if (deprecation == null) {
              if (supportVersions) {
                try {
                  yield* sass.Deprecation.forVersion(Version.parse(item));
                } on FormatException {
                  logger.warn('Invalid deprecation id or version "$item".');
                }
              } else {
                logger.warn('Invalid deprecation id "$item".');
              }
            } else {
              yield deprecation;
            }
          }
        }();
      }
  
      var fatalDeprecations = parseDeprecationsOrWarn(
        request.fatalDeprecation,
        supportVersions: true,
      );
      var silenceDeprecations = parseDeprecationsOrWarn(
        request.silenceDeprecation,
      );
      var futureDeprecations = parseDeprecationsOrWarn(request.futureDeprecation);
  
      try {
        var importers = request.importers.map(
          (importer) =>
              _decodeImporter(importer) ??
              (throw mandatoryError("Importer.importer")),
        );
  
        var globalFunctions = request.globalFunctions.map(
          (signature) => hostCallable(this, functions, mixins, signature),
        );
  
        late sass.CompileResult result;
        switch (request.whichInput()) {
          case InboundMessage_CompileRequest_Input.string:
            var input = request.string;
            result = sass.compileStringToResult(
              input.source,
              color: request.alertColor,
              logger: logger,
              importers: importers,
              importer: _decodeImporter(input.importer) ??
                  (input.url.startsWith("file:") ? null : sass.Importer.noOp),
              functions: globalFunctions,
              syntax: syntaxToSyntax(input.syntax),
              style: style,
              url: input.url.isEmpty ? null : input.url,
              quietDeps: request.quietDeps,
              verbose: request.verbose,
              fatalDeprecations: fatalDeprecations,
              silenceDeprecations: silenceDeprecations,
              futureDeprecations: futureDeprecations,
              sourceMap: request.sourceMap,
              charset: request.charset,
            );
  
          case InboundMessage_CompileRequest_Input.path:
            if (request.path.isEmpty) {
              throw mandatoryError("CompileRequest.Input.path");
            }
  
            try {
              result = sass.compileToResult(
                request.path,
                color: request.alertColor,
                logger: logger,
                importers: importers,
                functions: globalFunctions,
                style: style,
                quietDeps: request.quietDeps,
                verbose: request.verbose,
                fatalDeprecations: fatalDeprecations,
                silenceDeprecations: silenceDeprecations,
                futureDeprecations: futureDeprecations,
                sourceMap: request.sourceMap,
                charset: request.charset,
              );
            } on FileSystemException catch (error) {
              return OutboundMessage_CompileResponse()
                ..failure = (OutboundMessage_CompileResponse_CompileFailure()
                  ..message = error.path == null
                      ? error.message
                      : "${error.message}: ${error.path}"
                  ..span = (SourceSpan()
                    ..start = SourceSpan_SourceLocation()
                    ..end = SourceSpan_SourceLocation()
                    ..url = p.toUri(request.path).toString()));
            }
  
          case InboundMessage_CompileRequest_Input.notSet:
            throw mandatoryError("CompileRequest.input");
        }
  
        var success = OutboundMessage_CompileResponse_CompileSuccess()
          ..css = result.css;
  
        var sourceMap = result.sourceMap;
        if (sourceMap != null) {
          success.sourceMap = json.encode(
            sourceMap.toJson(
              includeSourceContents: request.sourceMapIncludeSources,
            ),
          );
        }
        return OutboundMessage_CompileResponse()
          ..success = success
          ..loadedUrls.addAll(result.loadedUrls.map((url) => url.toString()));
      } on sass.SassException catch (error) {
        var formatted = withGlyphs(
          () => error.toString(color: request.alertColor),
          ascii: request.alertAscii,
        );
        return OutboundMessage_CompileResponse()
          ..failure = (OutboundMessage_CompileResponse_CompileFailure()
            ..message = error.message
            ..span = protofySpan(error.span)
            ..stackTrace = error.trace.toString()
            ..formatted = formatted)
          ..loadedUrls.addAll(error.loadedUrls.map((url) => url.toString()));
      }
    }
  
    /// Converts [importer] into a [sass.Importer].
    sass.Importer? _decodeImporter(
      InboundMessage_CompileRequest_Importer importer,
    ) {
      switch (importer.whichImporter()) {
        case InboundMessage_CompileRequest_Importer_Importer.path:
          _checkNoNonCanonicalScheme(importer);
          return sass.FilesystemImporter(importer.path);
  
        case InboundMessage_CompileRequest_Importer_Importer.importerId:
          return HostImporter(
            this,
            importer.importerId,
            importer.nonCanonicalScheme,
          );
  
        case InboundMessage_CompileRequest_Importer_Importer.fileImporterId:
          _checkNoNonCanonicalScheme(importer);
          return FileImporter(this, importer.fileImporterId);
  
        case InboundMessage_CompileRequest_Importer_Importer.nodePackageImporter:
          return npi.NodePackageImporter(
            importer.nodePackageImporter.entryPointDirectory,
          );
  
        case InboundMessage_CompileRequest_Importer_Importer.notSet:
          _checkNoNonCanonicalScheme(importer);
          return null;
      }
    }
  
    /// Throws a [ProtocolError] if [importer] contains one or more
    /// `nonCanonicalScheme`s.
    void _checkNoNonCanonicalScheme(
      InboundMessage_CompileRequest_Importer importer,
    ) {
      if (importer.nonCanonicalScheme.isEmpty) return;
      throw paramsError(
        "Importer.non_canonical_scheme may only be set along "
        "with Importer.importer.importer_id",
      );
    }
  
    /// Sends [event] to the host.
    void sendLog(OutboundMessage_LogEvent event) =>
        _send(OutboundMessage()..logEvent = event);
  
    /// Sends [error] to the host and exit.
    ///
    /// This is used during compilation by other classes like host callable.
    Never sendError(ProtocolError error) {
      Isolate.exit(_sendPort, _serializePacket(OutboundMessage()..error = error));
    }
  
    InboundMessage_CanonicalizeResponse sendCanonicalizeRequest(
      OutboundMessage_CanonicalizeRequest request,
    ) =>
        _sendRequest<InboundMessage_CanonicalizeResponse>(
          OutboundMessage()..canonicalizeRequest = request,
        );
  
    InboundMessage_ImportResponse sendImportRequest(
      OutboundMessage_ImportRequest request,
    ) =>
        _sendRequest<InboundMessage_ImportResponse>(
          OutboundMessage()..importRequest = request,
        );
  
    InboundMessage_FileImportResponse sendFileImportRequest(
      OutboundMessage_FileImportRequest request,
    ) =>
        _sendRequest<InboundMessage_FileImportResponse>(
          OutboundMessage()..fileImportRequest = request,
        );
  
    InboundMessage_FunctionCallResponse sendFunctionCallRequest(
      OutboundMessage_FunctionCallRequest request,
    ) =>
        _sendRequest<InboundMessage_FunctionCallResponse>(
          OutboundMessage()..functionCallRequest = request,
        );
  
    /// Sends [request] to the host and returns the message sent in response.
    T _sendRequest<T extends GeneratedMessage>(OutboundMessage message) {
      message.id = _outboundRequestId;
      _send(message);
  
      try {
        var messageBuffer = Uint8List.sublistView(
          _receive(),
          _compilationIdVarint.length,
        );
  
        InboundMessage message;
        try {
          message = InboundMessage.fromBuffer(messageBuffer);
        } on InvalidProtocolBufferException catch (error) {
          throw parseError(error.message);
        }
  
        var response = switch (message.whichMessage()) {
          InboundMessage_Message.canonicalizeResponse =>
            message.canonicalizeResponse,
          InboundMessage_Message.importResponse => message.importResponse,
          InboundMessage_Message.fileImportResponse => message.fileImportResponse,
          InboundMessage_Message.functionCallResponse =>
            message.functionCallResponse,
          InboundMessage_Message.compileRequest => throw paramsError(
              "A CompileRequest with compilation ID $_compilationId is already "
              "active.",
            ),
          InboundMessage_Message.versionRequest =>
            throw paramsError("VersionRequest must have compilation ID 0."),
          InboundMessage_Message.notSet =>
            throw parseError("InboundMessage.message is not set."),
        };
  
        if (message.id != _outboundRequestId) {
          throw paramsError(
            "Response ID ${message.id} doesn't match any outstanding requests "
            "in compilation $_compilationId.",
          );
        } else if (response is! T) {
          throw paramsError(
            "Request ID $_outboundRequestId doesn't match response type "
            "${response.runtimeType} in compilation $_compilationId.",
          );
        }
  
        return response;
      } catch (error, stackTrace) {
        _handleError(error, stackTrace);
      }
    }
  
    /// Handles an error thrown by the dispatcher or code it dispatches to.
    ///
    /// The [messageId] indicate the IDs of the message being responded to, if
    /// available.
    Never _handleError(Object error, StackTrace stackTrace, {int? messageId}) {
      sendError(handleError(error, stackTrace, messageId: messageId));
    }
  
    /// Sends [message] to the host with the given [wireId].
    void _send(OutboundMessage message) {
      _sendPort.send(_serializePacket(message));
    }
  
    /// Serialize [message] to [Uint8List].
    Uint8List _serializePacket(OutboundMessage message) {
      var protobufWriter = CodedBufferWriter();
      message.writeToCodedBufferWriter(protobufWriter);
  
      // Add one additional byte to the beginning to indicate whether or not the
      // compilation has finished (1) or encountered a fatal error (2), so the
      // [IsolateDispatcher] knows whether to treat this isolate as inactive or
      // close out entirely.
      var packet = Uint8List(
        1 + _compilationIdVarint.length + protobufWriter.lengthInBytes,
      );
      packet[0] = switch (message.whichMessage()) {
        OutboundMessage_Message.compileResponse => 1,
        OutboundMessage_Message.error => 2,
        _ => 0,
      };
      packet.setAll(1, _compilationIdVarint);
      protobufWriter.writeTo(packet, 1 + _compilationIdVarint.length);
      return packet;
    }
  
    /// Receive a packet from the host.
    Uint8List _receive() {
      try {
        return _mailbox.take();
      } on StateError catch (_) {
        // The [_mailbox] has been closed, exit the current isolate immediately
        // to avoid bubble the error up as [SassException] during [_sendRequest].
        Isolate.exit();
      }
    }
  }
  