// Copyright 2023 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:async';
import 'dart:isolate';
import 'dart:typed_data';

import 'package:native_synchronization/mailbox.dart';
import 'package:native_synchronization/sendable.dart';

/// The entrypoint for a [ReusableIsolate].
///
/// This must be a static global function. It's run when the isolate is spawned,
/// and is passed a [Mailbox] that receives messages from [ReusableIsolate.send]
/// and a [SendPort] that sends messages to the [ReceivePort] listened by
/// [ReusableIsolate.borrow].
///
/// If the [sendPort] sends a message before [ReusableIsolate.borrow] is called,
/// this will throw an unhandled [StateError].
typedef ReusableIsolateEntryPoint = FutureOr<void> Function(
    Mailbox mailbox, SendPort sink);

class ReusableIsolate {
  /// The wrapped isolate.
  final Isolate _isolate;

  /// The mailbox used to send messages to this isolate.
  final Mailbox _mailbox;

  /// The [ReceivePort] that receives messages from the wrapped isolate.
  final ReceivePort _receivePort;

  /// The subscription to [_receivePort].
  final StreamSubscription<dynamic> _subscription;

  /// Whether the current isolate has been borrowed.
  bool _borrowed = false;

  ReusableIsolate._(
    this._isolate,
    this._mailbox,
    this._receivePort, {
    Function? onError,
  }) : _subscription = _receivePort.listen(_defaultOnData, onError: onError);

  /// Spawns a [ReusableIsolate] that runs the given [entryPoint].
  static Future<ReusableIsolate> spawn(
    ReusableIsolateEntryPoint entryPoint, {
    Function? onError,
  }) async {
    var mailbox = Mailbox();
    var receivePort = ReceivePort();
    var isolate = await Isolate.spawn(_isolateMain, (
      entryPoint,
      mailbox.asSendable,
      receivePort.sendPort,
    ));
    return ReusableIsolate._(isolate, mailbox, receivePort, onError: onError);
  }

  /// Subscribe to messages from [_receivePort].
  void borrow(void onData(dynamic event)?) {
    if (_borrowed) {
      throw StateError('ReusableIsolate has already been borrowed.');
    }
    _borrowed = true;
    _subscription.onData(onData);
  }

  /// Unsubscribe to messages from [_receivePort].
  void release() {
    if (!_borrowed) {
      throw StateError('ReusableIsolate has not been borrowed.');
    }
    _borrowed = false;
    _subscription.onData(_defaultOnData);
  }

  /// Sends [message] to the isolate.
  ///
  /// Throws a [StateError] if this is called while the isolate isn't borrowed,
  /// or if a second message is sent before the isolate has processed the first
  /// one.
  void send(Uint8List message) {
    if (!_borrowed) {
      throw StateError('Cannot send a message before being borrowed.');
    }
    _mailbox.put(message);
  }

  /// Shuts down the isolate.
  void kill() {
    // If the isolate is blocking on [Mailbox.take], it won't even process a
    // kill event, so we closed the mailbox to nofity and wake it up.
    _mailbox.close();
    _isolate.kill(priority: Isolate.immediate);
    _receivePort.close();
  }
}

/// The default handler for data events from the wrapped isolate when it's not
/// borrowed.
void _defaultOnData(dynamic _) {
  throw StateError("Shouldn't receive a message before being borrowed.");
}

void _isolateMain(
  (ReusableIsolateEntryPoint, Sendable<Mailbox>, SendPort) message,
) {
  var (entryPoint, sendableMailbox, sendPort) = message;
  entryPoint(sendableMailbox.materialize(), sendPort);
}
