// Copyright (c) 2022, 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 'serialization.dart';
import 'serialization_extensions.dart';

/// Base class for types that need to be able to be traced back to a specific
/// instance on the server side.
abstract class RemoteInstance implements Serializable {
  /// The unique ID for this instance.
  final int id;

  /// The type of instance being encoded.
  RemoteInstanceKind get kind;

  /// Static, incrementing ids.
  static int _nextId = 0;

  /// Gets the next unique identifier.
  static int get uniqueId => _nextId++;

  /// On the client side [id]s are given and you should reconstruct objects with
  /// the given ID. On the server side ids should be created using
  /// [RemoteInstance.uniqueId].
  RemoteInstance(this.id);

  /// Retrieves a cached instance by ID, if present.
  static RemoteInstance? cached(int id) => _remoteInstanceCache[id];

  /// Adds [instance] to the cache for this zone.
  static void cache(RemoteInstance instance) =>
      _remoteInstanceCache[instance.id] = instance;

  /// Deserializes an instance based on the current [serializationMode].
  ///
  // TODO: Ideally this would be `T extends RemoteInstance` but that interacts
  // poorly with inference in other places, we end up with Never and null as
  // inferred types due to only the impl versions of objects extending
  // `RemoteInstance`.
  static T deserialize<T extends Object>(Deserializer deserializer) =>
      (deserializer..moveNext()).expectRemoteInstance();

  /// This method should be overridden by any subclasses, they should instead
  /// implement [serializeUncached].
  @override
  void serialize(Serializer serializer) {
    serializer.addInt(id);
    // We only send the ID if it's in the cache, it's only in our cache if it is
    // also in the remote cache.
    if (_remoteInstanceCache.containsKey(id)) return;

    serializeUncached(serializer);
  }

  /// This method should be overridden by all subclasses, which should on their
  /// first line call this super method.
  ///
  /// This method should not be directly invoked, instead only [serialize]
  /// should call it (if serializing an uncached value).
  ///
  /// Only new fields added by the subtype should be serialized here, rely on
  /// super classes to have their own implementations for their fields.
  void serializeUncached(Serializer serializer) {
    serializer.addInt(kind.index);

    // Now we can add it to the cache, we know the other side has a copy of it
    // and don't need to serialize it in the future.
    _remoteInstanceCache[id] = this;
  }

  @override
  bool operator ==(Object other) => other is RemoteInstance && id == other.id;

  @override
  int get hashCode => id;
}

/// A remote instance which is just a pointer to some server side instance of
/// a generic object.
///
/// The wrapped object is not serialized.
class RemoteInstanceImpl extends RemoteInstance {
  /// Always null on the client side, has an actual instance on the server side.
  final Object? instance;

  @override
  final RemoteInstanceKind kind;

  RemoteInstanceImpl({
    required int id,
    this.instance,
    required this.kind,
  }) : super(id);
}

// The kinds of instances.
enum RemoteInstanceKind {
  classDeclaration,
  constructorDeclaration,
  constructorMetadataAnnotation,
  declarationPhaseIntrospector,
  definitionPhaseIntrospector,
  enumDeclaration,
  enumValueDeclaration,
  extensionDeclaration,
  extensionTypeDeclaration,
  fieldDeclaration,
  formalParameter,
  formalParameterDeclaration,
  functionDeclaration,
  functionTypeAnnotation,
  identifier,
  identifierMetadataAnnotation,
  library,
  methodDeclaration,
  mixinDeclaration,
  namedStaticType,
  namedTypeAnnotation,
  omittedTypeAnnotation,
  recordField,
  recordTypeAnnotation,
  staticType,
  typeAliasDeclaration,
  typeParameter,
  typeParameterDeclaration,
  typePhaseIntrospector,
  variableDeclaration,

  // Exceptions.
  macroImplementationException,
  macroIntrospectionCycleException,
  unexpectedMacroException,
}

/// Creates a new zone with a remote instance cache and an id, which it uses to
/// avoid sending the same remote instances across the wire multiple times.
///
/// The lifecycle of one of these zones should be no longer than that of a
/// single full compile, at which point [destroyRemoteInstanceZone] should be
/// called.
///
/// In order to keep these caches in sync between the server and client, the
/// server always creates new zone IDs and passes those to the client.
int newRemoteInstanceZone<T>() {
  final int id = _nextSerializationZoneId++;
  final Zone zone = Zone.current.fork(zoneValues: {
    _remoteInstanceZoneCacheKey: <int, RemoteInstance>{},
  });
  _remoteInstanceCacheZones[id] = zone;
  return id;
}

/// Runs [fn] in the remote instance zone identified by [zoneId].
///
/// If [createIfMissing] is `true`, then a new zone will be created with
/// [zoneId] if one does not already exist (this should only be `true` in client
/// code).
T withRemoteInstanceZone<T>(int zoneId, T Function() fn,
    {bool createIfMissing = false}) {
  Zone? zone = _remoteInstanceCacheZones[zoneId];
  if (zone == null) {
    if (!createIfMissing) {
      throw StateError('No remote instance zone with id `$zoneId` exists.');
    }
    zone = _remoteInstanceCacheZones[zoneId] = Zone.current.fork(zoneValues: {
      _remoteInstanceZoneCacheKey: <int, RemoteInstance>{},
    });
  }
  return zone.run(fn);
}

/// Removes the remote instance zone identified by [zoneId] from the known list
/// of zones and forcibly clears its cache.
///
/// Throws if a zone identified by [zoneId] does not exist.
void destroyRemoteInstanceZone(int zoneId) {
  final Zone? zone = _remoteInstanceCacheZones.remove(zoneId);
  if (zone == null) {
    throw StateError('No remote instance zone with id `$zoneId` exists.');
  }
  (zone[_remoteInstanceZoneCacheKey] as Map<int, RemoteInstance>).clear();
}

/// The key used to store the remote instance cache in the current zone.
const Symbol _remoteInstanceZoneCacheKey = #_remoteInstanceCache;

/// We cache remote instances by their ID, which allows us to not repeatedly
/// send the same information over the wire.
///
/// These are a part of the current remote instance cache zone, which all
/// serialization and deserialization of remote instances must be done in.
Map<int, RemoteInstance> get _remoteInstanceCache =>
    Zone.current[_remoteInstanceZoneCacheKey] as Map<int, RemoteInstance>? ??
    (throw StateError('Not running in a remote instance cache zone, call '
        '`withRemoteInstanceZone` to set one up.'));

/// Remote instance cache zones by ID.
final _remoteInstanceCacheZones = <int, Zone>{};

/// Incrementing identifier for the serialization zone ids.
int _nextSerializationZoneId = 0;
