// Copyright (c) 2021, 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:isolate';

import '../api.dart';
import '../executor.dart';
import 'exception_impls.dart';
import 'introspection_impls.dart';
import 'protocol.dart';
import 'remote_instance.dart';
import 'serialization.dart';
import 'span.dart';

/// Base implementation for macro executors which communicate with some external
/// process to run macros.
///
/// Subtypes must extend this class and implement the [close] and [sendResult]
/// apis to handle communication with the external macro program.
abstract class ExternalMacroExecutorBase extends MacroExecutor {
  /// The stream on which we receive messages from the external macro executor.
  final Stream<Object> messageStream;

  /// The mode to use for serialization - must be a `server` variant.
  final SerializationMode serializationMode;

  /// A map of response completers by request id.
  final _responseCompleters = <int, Completer<Response>>{};

  bool isClosed = false;

  ExternalMacroExecutorBase(
      {required this.messageStream, required this.serializationMode}) {
    withSerializationMode(serializationMode, () {
      messageStream.listen((message) {
        // No need for a remote cache in this zone we only read a zone ID and
        // then immediately run in that zone.
        Deserializer deserializer = deserializerFactory(message);
        // Every object starts with a zone ID which dictates the zone in which
        // we should deserialize the message.
        deserializer.moveNext();
        int zoneId = deserializer.expectInt();
        withRemoteInstanceZone(zoneId, () async {
          deserializer.moveNext();
          MessageType messageType =
              MessageType.values[deserializer.expectInt()];
          // A response to a request we sent, everything else is a request from
          // the client.
          if (messageType == MessageType.response) {
            SerializableResponse response =
                SerializableResponse.deserialize(deserializer, zoneId);
            Completer<Response>? completer =
                _responseCompleters.remove(response.requestId);
            if (completer == null) {
              throw StateError('Got a response for an unrecognized request id '
                  '${response.requestId}');
            }
            completer.complete(response);
            return;
          }

          // These are initialized in the switch below.
          final Serializable result;
          final MessageType resultType;
          int? requestId;

          // Initialized after the switch or in the catch handler.
          late final SerializableResponse response;
          try {
            switch (messageType) {
              case MessageType.resolveIdentifierRequest:
                ResolveIdentifierRequest request =
                    ResolveIdentifierRequest.deserialize(deserializer, zoneId);
                requestId = request.id;
                result = await (request.introspector.instance
                            as TypePhaseIntrospector)
                        // ignore: deprecated_member_use_from_same_package
                        .resolveIdentifier(request.library, request.name)
                    as IdentifierImpl;
                resultType = MessageType.remoteInstance;
              case MessageType.resolveTypeRequest:
                ResolveTypeRequest request =
                    ResolveTypeRequest.deserialize(deserializer, zoneId);
                requestId = request.id;
                StaticType instance = await (request.introspector.instance
                        as DeclarationPhaseIntrospector)
                    .resolve(request.typeAnnotationCode);
                result = RemoteInstanceImpl(
                    id: RemoteInstance.uniqueId,
                    instance: instance,
                    kind: instance is NamedStaticType
                        ? RemoteInstanceKind.namedStaticType
                        : RemoteInstanceKind.staticType);
                resultType = instance is NamedStaticType
                    ? MessageType.namedStaticType
                    : MessageType.staticType;
              case MessageType.inferTypeRequest:
                InferTypeRequest request =
                    InferTypeRequest.deserialize(deserializer, zoneId);
                requestId = request.id;
                result = await (request.introspector.instance
                        as DefinitionPhaseIntrospector)
                    .inferType(request.omittedType) as TypeAnnotationImpl;
                resultType = MessageType.remoteInstance;
              case MessageType.isExactlyTypeRequest:
                IsExactlyTypeRequest request =
                    IsExactlyTypeRequest.deserialize(deserializer, zoneId);
                requestId = request.id;
                StaticType leftType = request.leftType.instance as StaticType;
                StaticType rightType = request.rightType.instance as StaticType;
                result = BooleanValue(await leftType.isExactly(rightType));
                resultType = MessageType.boolean;
              case MessageType.isSubtypeOfRequest:
                IsSubtypeOfRequest request =
                    IsSubtypeOfRequest.deserialize(deserializer, zoneId);
                requestId = request.id;
                StaticType leftType = request.leftType.instance as StaticType;
                StaticType rightType = request.rightType.instance as StaticType;
                result = BooleanValue(await leftType.isSubtypeOf(rightType));
                resultType = MessageType.boolean;
              case MessageType.declarationOfRequest:
                DeclarationOfRequest request = DeclarationOfRequest.deserialize(
                    deserializer, zoneId, messageType);
                requestId = request.id;
                DefinitionPhaseIntrospector introspector = request
                    .introspector.instance as DefinitionPhaseIntrospector;
                result = (await introspector.declarationOf(request.identifier))
                    // TODO: Consider refactoring to avoid the need for
                    //  this cast.
                    as Serializable;
                resultType = MessageType.remoteInstance;
              case MessageType.typeDeclarationOfRequest:
                DeclarationOfRequest request = DeclarationOfRequest.deserialize(
                    deserializer, zoneId, messageType);
                requestId = request.id;
                DeclarationPhaseIntrospector introspector = request
                    .introspector.instance as DeclarationPhaseIntrospector;
                result =
                    (await introspector.typeDeclarationOf(request.identifier))
                        // TODO: Consider refactoring to avoid the need for
                        //  this cast.
                        as Serializable;
                resultType = MessageType.remoteInstance;
              case MessageType.constructorsOfRequest:
                TypeIntrospectorRequest request =
                    TypeIntrospectorRequest.deserialize(
                        deserializer, messageType, zoneId);
                requestId = request.id;
                DeclarationPhaseIntrospector introspector = request
                    .introspector.instance as DeclarationPhaseIntrospector;
                result = DeclarationList((await introspector
                        .constructorsOf(request.declaration as TypeDeclaration))
                    // TODO: Consider refactoring to avoid the need for this.
                    .cast<ConstructorDeclarationImpl>());
                resultType = MessageType.declarationList;
              case MessageType.topLevelDeclarationsOfRequest:
                DeclarationsOfRequest request =
                    DeclarationsOfRequest.deserialize(deserializer, zoneId);
                requestId = request.id;
                DefinitionPhaseIntrospector introspector = request
                    .introspector.instance as DefinitionPhaseIntrospector;
                result = DeclarationList(// force newline
                    (await introspector.topLevelDeclarationsOf(request.library))
                        // TODO: Consider refactoring to avoid the need for
                        // this.
                        .cast<DeclarationImpl>());
                resultType = MessageType.declarationList;
              case MessageType.fieldsOfRequest:
                TypeIntrospectorRequest request =
                    TypeIntrospectorRequest.deserialize(
                        deserializer, messageType, zoneId);
                requestId = request.id;
                DeclarationPhaseIntrospector introspector = request
                    .introspector.instance as DeclarationPhaseIntrospector;
                result = DeclarationList((await introspector
                        .fieldsOf(request.declaration as TypeDeclaration))
                    // TODO: Consider refactoring to avoid the need for this.
                    .cast<FieldDeclarationImpl>());
                resultType = MessageType.declarationList;
              case MessageType.methodsOfRequest:
                TypeIntrospectorRequest request =
                    TypeIntrospectorRequest.deserialize(
                        deserializer, messageType, zoneId);
                requestId = request.id;
                DeclarationPhaseIntrospector introspector = request
                    .introspector.instance as DeclarationPhaseIntrospector;
                result = DeclarationList((await introspector
                        .methodsOf(request.declaration as TypeDeclaration))
                    // TODO: Consider refactoring to avoid the need for this.
                    .cast<MethodDeclarationImpl>());
                resultType = MessageType.declarationList;
              case MessageType.typesOfRequest:
                TypeIntrospectorRequest request =
                    TypeIntrospectorRequest.deserialize(
                        deserializer, messageType, zoneId);
                requestId = request.id;
                DeclarationPhaseIntrospector introspector = request
                    .introspector.instance as DeclarationPhaseIntrospector;
                result = DeclarationList((await introspector
                        .typesOf(request.declaration as Library))
                    // TODO: Consider refactoring to avoid the need for this.
                    .cast<TypeDeclarationImpl>());
                resultType = MessageType.declarationList;
              case MessageType.valuesOfRequest:
                TypeIntrospectorRequest request =
                    TypeIntrospectorRequest.deserialize(
                        deserializer, messageType, zoneId);
                requestId = request.id;
                DeclarationPhaseIntrospector introspector = request
                    .introspector.instance as DeclarationPhaseIntrospector;
                result = DeclarationList((await introspector
                        .valuesOf(request.declaration as EnumDeclaration))
                    // TODO: Consider refactoring to avoid the need for this.
                    .cast<EnumValueDeclarationImpl>());
                resultType = MessageType.declarationList;
              default:
                throw StateError('Unexpected message type $messageType');
            }
            response = SerializableResponse(
                response: result,
                requestId: requestId,
                responseType: resultType,
                serializationZoneId: zoneId);
          } catch (error, stackTrace) {
            // TODO: Something better here.
            if (requestId == null) rethrow;
            response = SerializableResponse(
                exception: MacroExceptionImpl.from(error, stackTrace),
                requestId: requestId,
                responseType: MessageType.exception,
                serializationZoneId: zoneId);
          }
          Serializer serializer = serializerFactory();
          response.serialize(serializer);
          sendResult(serializer);
        });
      });
    });
  }

  /// These calls are handled by the higher level executor.
  @override
  String buildAugmentationLibrary(
          Uri augmentedLibraryUri,
          Iterable<MacroExecutionResult> macroResults,
          TypeDeclaration Function(Identifier) resolveDeclaration,
          ResolvedIdentifier Function(Identifier) resolveIdentifier,
          TypeAnnotation? Function(OmittedTypeAnnotation) inferOmittedType,
          {Map<OmittedTypeAnnotation, String>? omittedTypes,
          List<Span>? spans}) =>
      throw StateError('Unreachable');

  @override
  Future<MacroExecutionResult> executeDeclarationsPhase(
          MacroInstanceIdentifier macro,
          MacroTarget target,
          DeclarationPhaseIntrospector introspector) =>
      _sendRequest((zoneId) => ExecuteDeclarationsPhaseRequest(
          macro,
          target as RemoteInstance,
          RemoteInstanceImpl(
              instance: introspector,
              id: RemoteInstance.uniqueId,
              kind: RemoteInstanceKind.declarationPhaseIntrospector),
          serializationZoneId: zoneId));

  @override
  Future<MacroExecutionResult> executeDefinitionsPhase(
          MacroInstanceIdentifier macro,
          MacroTarget target,
          DefinitionPhaseIntrospector introspector) =>
      _sendRequest((zoneId) => ExecuteDefinitionsPhaseRequest(
          macro,
          target as RemoteInstance,
          RemoteInstanceImpl(
              instance: introspector,
              id: RemoteInstance.uniqueId,
              kind: RemoteInstanceKind.definitionPhaseIntrospector),
          serializationZoneId: zoneId));

  @override
  Future<MacroExecutionResult> executeTypesPhase(MacroInstanceIdentifier macro,
          MacroTarget target, TypePhaseIntrospector introspector) =>
      _sendRequest((zoneId) => ExecuteTypesPhaseRequest(
          macro,
          target as RemoteInstance,
          RemoteInstanceImpl(
              instance: introspector,
              id: RemoteInstance.uniqueId,
              kind: RemoteInstanceKind.typePhaseIntrospector),
          serializationZoneId: zoneId));

  @override
  Future<MacroInstanceIdentifier> instantiateMacro(
          Uri library, String name, String constructor, Arguments arguments) =>
      _sendRequest((zoneId) => InstantiateMacroRequest(
          library, name, constructor, arguments, RemoteInstance.uniqueId,
          serializationZoneId: zoneId));

  @override
  void disposeMacro(MacroInstanceIdentifier instance) => _sendRequest(
      (zoneId) => DisposeMacroRequest(instance, serializationZoneId: zoneId));

  /// Sends [serializer.result] to [sendPort], possibly wrapping it in a
  /// [TransferableTypedData] object.
  void sendResult(Serializer serializer);

  /// Creates a [Request] with a given serialization zone ID, and handles the
  /// response, casting it to the expected type or throwing the error provided.
  Future<T> _sendRequest<T>(Request Function(int) requestFactory) {
    if (isClosed) {
      throw UnexpectedMacroExceptionImpl(
          "Can't send request - $runtimeType is closed!");
    }
    return withSerializationMode(serializationMode, () {
      final int zoneId = newRemoteInstanceZone();
      return withRemoteInstanceZone(zoneId, () async {
        Request request = requestFactory(zoneId);
        Serializer serializer = serializerFactory();
        // It is our responsibility to add the zone ID header.
        serializer.addInt(zoneId);
        request.serialize(serializer);
        sendResult(serializer);
        Completer<Response> completer = Completer<Response>();
        _responseCompleters[request.id] = completer;
        try {
          Response response = await completer.future;
          T? result = response.response as T?;
          if (result != null) return result;
          throw response.exception!;
        } finally {
          // Clean up the zone after the request is done.
          destroyRemoteInstanceZone(zoneId);
          // Tell the remote client to clean it up as well.
          Serializer serializer = serializerFactory();
          serializer.addInt(zoneId);
          DestroyRemoteInstanceZoneRequest(serializationZoneId: zoneId)
              .serialize(serializer);
          sendResult(serializer);
        }
      });
    });
  }
}
