// Copyright (c) 2023, 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:ffi';
import 'dart:typed_data';

import 'package:ffi/ffi.dart';

import 'primitives.dart';
import 'sendable.dart';

final class _MailboxRepr extends Struct {
  external Pointer<Uint8> buffer;

  @Int32()
  external int bufferLength;

  @Int32()
  external int state;
}

class _SendableMailbox {
  final int address;
  final Sendable<Mutex> mutex;
  final Sendable<ConditionVariable> condVar;

  _SendableMailbox(
      {required this.address, required this.mutex, required this.condVar});
}

/// Mailbox communication primitive.
///
/// This synchronization primitive allows a single producer to send messages
/// to one or more consumers. Producer uses [put] to place a message into
/// a mailbox which consumers can then [take] out.
///
/// [Mailbox] object can not be directly sent to other isolates via a
/// `SendPort`, but it can be converted to a `Sendable<Mailbox>` via
/// `asSendable` getter.
///
/// [Mailbox] object is owned by an isolate which created them.
class Mailbox {
  final Pointer<_MailboxRepr> _mailbox;
  final Mutex _mutex;
  final ConditionVariable _condVar;

  static const _stateEmpty = 0;
  static const _stateFull = 1;
  static const _stateClosed = 2;

  static final finalizer = Finalizer((Pointer<_MailboxRepr> mailbox) {
    calloc.free(mailbox.ref.buffer);
    calloc.free(mailbox);
  });

  Mailbox()
      : _mailbox = calloc.allocate(sizeOf<_MailboxRepr>()),
        _mutex = Mutex(),
        _condVar = ConditionVariable() {
    finalizer.attach(this, _mailbox);
  }

  Mailbox._fromSendable(_SendableMailbox sendable)
      : _mailbox = Pointer.fromAddress(sendable.address),
        _mutex = sendable.mutex.materialize(),
        _condVar = sendable.condVar.materialize();

  /// Place a message into the mailbox if has space for it.
  ///
  /// If mailbox already contains a message or mailbox is closed then [put] will
  /// throw [StateError].
  void put(Uint8List message) {
    final buffer = message.isEmpty ? nullptr : _toBuffer(message);
    _mutex.runLocked(() {
      if (_mailbox.ref.state != _stateEmpty) {
        throw StateError('Mailbox is closed or full');
      }

      _mailbox.ref.state = _stateFull;
      _mailbox.ref.buffer = buffer;
      _mailbox.ref.bufferLength = message.length;

      _condVar.notify();
    });
  }

  /// Close a mailbox.
  ///
  /// If mailbox already contains a message then [close] will drop the message.
  void close() => _mutex.runLocked(() {
        if (_mailbox.ref.state == _stateFull && _mailbox.ref.bufferLength > 0) {
          malloc.free(_mailbox.ref.buffer);
        }

        _mailbox.ref.state = _stateClosed;
        _mailbox.ref.buffer = nullptr;
        _mailbox.ref.bufferLength = 0;

        _condVar.notify();
      });

  /// Take a message from the mailbox.
  ///
  /// If mailbox is empty then [take] will synchronously block until message
  /// is available or mailbox is closed. If mailbox is closed then [take] will
  /// throw [StateError].
  Uint8List take() => _mutex.runLocked(() {
        while (_mailbox.ref.state == _stateEmpty) {
          _condVar.wait(_mutex);
        }

        if (_mailbox.ref.state == _stateClosed) {
          throw StateError('Mailbox is closed');
        }

        final result = _toList(_mailbox.ref.buffer, _mailbox.ref.bufferLength);

        _mailbox.ref.state = _stateEmpty;
        _mailbox.ref.buffer = nullptr;
        _mailbox.ref.bufferLength = 0;
        return result;
      });

  static final _emptyResponse = Uint8List(0);

  static Uint8List _toList(Pointer<Uint8> buffer, int length) {
    if (length == 0) {
      return _emptyResponse;
    }

    // TODO: remove feature detection once 3.1 becomes stable.
    // ignore: omit_local_variable_types
    final Uint8List Function(int) asTypedList = buffer.asTypedList;
    if (asTypedList is Uint8List Function(int,
        {Pointer<NativeFinalizerFunction> finalizer})) {
      return asTypedList(length, finalizer: malloc.nativeFree);
    }

    final result = Uint8List(length);
    result.setRange(0, length, buffer.asTypedList(length));
    malloc.free(buffer);
    return result;
  }

  static Pointer<Uint8> _toBuffer(Uint8List list) {
    final buffer = malloc.allocate<Uint8>(list.length);
    buffer.asTypedList(list.length).setRange(0, list.length, list);
    return buffer;
  }

  Sendable<Mailbox> get asSendable => Sendable.wrap(
      Mailbox._fromSendable,
      _SendableMailbox(
          address: _mailbox.address,
          mutex: _mutex.asSendable,
          condVar: _condVar.asSendable));
}
