#ifdef DITTOFFI_EXPERIMENTAL_BUS
#include <cstring>
#include <stdexcept>
#include <vector>

extern "C" {
#include "dittoffi.h"
#include "retainable.h"
}
#ifdef SWIG
%include <swigmove.i>
%include <std_string.i>
%feature("director", assumeoverride=1, allprotected=0) AcceptorSwig;
%feature("director", assumeoverride=1, allprotected=0) InboundCallbackSwig;
%feature("director", assumeoverride=1, allprotected=0) StreamCandidateCallbackSwig;
%feature("director", assumeoverride=1, allprotected=0) ConnectionResultCallbackSwig;
%feature("director", assumeoverride=1, allprotected=0) SendStatusCallbackSwig;
%feature("director", assumeoverride=1, allprotected=0) StreamStatusCallbackSwig;
%feature("director", assumeoverride=1, allprotected=0) BusSwig;
#endif
class PeerPubkeySwig {
  void_const_ptr_2_array_t pubkey;

public:
  explicit PeerPubkeySwig(char const *peer_key_str) noexcept(false)
      : pubkey({}) {
    slice_ref_uint8_t slice = {.ptr = (const uint8_t *)peer_key_str,
                               .len = strlen(peer_key_str)};
    auto parsed = dittoffi_peer_pubkey_from_str(slice);
    if (!parsed._0) {
      throw std::runtime_error("Failed to parse pubkey");
    }
    pubkey = parsed._1;
  }

  explicit PeerPubkeySwig(void_const_ptr_2_array_t key) noexcept
      : pubkey(key) {}

  ~PeerPubkeySwig() noexcept { dittoffi_peer_pubkey_delete(pubkey); }

  std::string to_string() noexcept {
    auto s = dittoffi_peer_pubkey_to_cstr(&pubkey);
    std::string result((const char *)s.ptr, s.len);
    return result;
  }
};

class TopicSwig {
  Topic_t topic;

public:
  explicit TopicSwig(Topic_t topic) noexcept : topic(topic) {}

  std::string to_string() noexcept {
    std::string result((const char *)&this->topic.buffer, this->topic.len);
    return result;
  }
};

class AcceptorSwig {
  Acceptor_t acceptor;

public:
  explicit AcceptorSwig(Acceptor_t acceptor) noexcept : acceptor(acceptor) {}

  AcceptorSwig(AcceptorSwig &&other) noexcept : acceptor(other.acceptor) {
    other.acceptor.inner.ptr = nullptr;
  }

  AcceptorSwig(AcceptorSwig const &other) noexcept : acceptor(other.acceptor) {
    acceptor.inner.vtable.retain_vptr(acceptor.inner.ptr);
  }

  virtual ~AcceptorSwig() noexcept {
    if (acceptor.inner.ptr != nullptr) {
      acceptor.inner.vtable.release_vptr(acceptor.inner.ptr);
    }
  }

  virtual TopicSwig topic() noexcept {
    return TopicSwig(acceptor.inner.vtable.topic(acceptor.inner.ptr));
  }

  virtual Reliability reliability() noexcept {
    return static_cast<Reliability>(
        acceptor.inner.vtable.reliability(acceptor.inner.ptr));
  }
};

class BytesSwig {
  Bytes_t bytes;

public:
  explicit BytesSwig(slice_ref_uint8_t slice) noexcept
      : bytes(dittoffi_bytes_copied_from_slice(slice)) {}

  explicit BytesSwig(Bytes_t bytes) noexcept : bytes(bytes) {}

  BytesSwig(BytesSwig &&other) noexcept : bytes(other.bytes) {
    other.bytes.vtable = nullptr;
  }

  BytesSwig(BytesSwig const &other) noexcept
      : bytes(dittoffi_bytes_clone(&other.bytes)) {}

  BytesSwig &operator=(BytesSwig const &other) noexcept = default;

  Bytes_t take() noexcept {
    auto result = bytes;
    bytes.vtable = nullptr;
    return result;
  }

  slice_ref_uint8_t data() const noexcept {
    return {.ptr = bytes.start, .len = bytes.len};
  }

  virtual ~BytesSwig() noexcept {
    if (bytes.vtable != nullptr) {
      dittoffi_bytes_drop(&bytes);
    }
  }
};

class SendStatusCallbackSwig : public Retainable {
public:
  virtual void call(SendStatus data) noexcept = 0;

  virtual void drop() noexcept = 0;

  static void invokeCall(void *ctx, SendStatus_t inbound) noexcept {
    auto inst = static_cast<SendStatusCallbackSwig *>(ctx);
    inst->call(static_cast<SendStatus>(inbound));
  }
};

class SendHandleSwig {
  VirtualPtr__Erased_ptr_ISendHandleVTable_t inner;

public:
  explicit SendHandleSwig(
      VirtualPtr__Erased_ptr_ISendHandleVTable_t inner) noexcept
      : inner(inner) {}

  SendHandleSwig(SendHandleSwig &&other) noexcept : inner(other.inner) {
    other.inner.ptr = nullptr;
  }

  SendHandleSwig(SendHandleSwig const &other) noexcept : inner(other.inner) {
    inner.vtable.retain_vptr(inner.ptr);
  }

  virtual ~SendHandleSwig() noexcept {
    if (inner.ptr != nullptr) {
      inner.vtable.release_vptr(inner.ptr);
    }
  }

  virtual SendStatus current_status() noexcept {
    return static_cast<SendStatus>(inner.vtable.poll(inner.ptr));
  }

  virtual void cancel() noexcept { inner.vtable.cancel(inner.ptr); }

  virtual void onChange(SendStatusCallbackSwig *callback) {
    callback->java_retain();
    inner.vtable.set_on_change(inner.ptr,
                               {.env_ptr = callback,
                                .call = callback->invokeCall,
                                .free = callback->invokeRelease},
                               false);
  }
};

class StreamStatusCallbackSwig : public Retainable {
public:
  virtual void call(StreamStatus data) noexcept = 0;

  virtual void drop() noexcept = 0;

  static void invokeCall(void *ctx, StreamStatus_t inbound) noexcept {
    auto inst = static_cast<StreamStatusCallbackSwig *>(ctx);
    inst->call(static_cast<StreamStatus>(inbound));
  }
};

class StreamSwig {
  Stream_t stream;

public:
  explicit StreamSwig(Stream_t stream) noexcept : stream(stream) {}

  StreamSwig(StreamSwig &&other) noexcept : stream(other.stream) {
    other.stream.inner.ptr = nullptr;
  }

  StreamSwig(StreamSwig const &other) noexcept : stream(other.stream) {
    stream.inner.vtable.retain_vptr(stream.inner.ptr);
  }

  virtual ~StreamSwig() noexcept {
    if (stream.inner.ptr != nullptr) {
      stream.inner.vtable.release_vptr(stream.inner.ptr);
      stream.inner.ptr = nullptr;
    }
  }

  virtual SendHandleSwig send(BytesSwig data) noexcept {
    return SendHandleSwig(
        stream.inner.vtable.send(stream.inner.ptr, data.take()));
  }

  virtual TopicSwig topic() noexcept {
    return TopicSwig(stream.inner.vtable.topic(stream.inner.ptr));
  }

  virtual PeerPubkeySwig peer() noexcept {
    return PeerPubkeySwig(stream.inner.vtable.peer_pubkey(stream.inner.ptr));
  }

  virtual void addOnClose(StreamStatusCallbackSwig *callback) noexcept {
    callback->java_retain();
    stream.inner.vtable.add_on_close(stream.inner.ptr,
                                     {.env_ptr = callback,
                                      .call = callback->invokeCall,
                                      .free = callback->invokeRelease});
  }

  virtual unsigned size_t max_send_size() noexcept {
    return stream.inner.vtable.max_send_size(stream.inner.ptr);
  }
};

class InboundSwig {
public:
  BytesSwig payload;

  explicit InboundSwig(Inbound_t inbound) noexcept : payload(inbound.payload) {}

  InboundSwig(InboundSwig &&other) noexcept
      : payload(std::move(other.payload)) {}

  InboundSwig(InboundSwig const &other) noexcept = default;

  InboundSwig &operator=(InboundSwig const &other) noexcept = default;
};

class InboundCallbackSwig : public Retainable {
public:
  virtual void call(InboundSwig data) noexcept = 0;

  virtual void drop() noexcept = 0;

  static void invokeCall(void *ctx, Inbound_t inbound) noexcept {
    auto inst = static_cast<InboundCallbackSwig *>(ctx);
    inst->call(InboundSwig(inbound));
  }
};

class StreamCandidateSwig {
  StreamCandidate_t candidate;

public:
  explicit StreamCandidateSwig(StreamCandidate_t candidate) noexcept
      : candidate(candidate) {}

  StreamCandidateSwig(StreamCandidateSwig &&other) noexcept
      : candidate(other.candidate) {
    other.candidate.inner.ptr = nullptr;
  }

  StreamCandidateSwig(StreamCandidateSwig const &other) noexcept
      : candidate(other.candidate) {
    candidate.inner.vtable.retain_vptr(candidate.inner.ptr);
  }

  virtual ~StreamCandidateSwig() noexcept {
    if (candidate.inner.ptr != nullptr) {
      candidate.inner.vtable.release_vptr(candidate.inner.ptr);
      candidate.inner.ptr = nullptr;
    }
  }

  virtual StreamSwig open() noexcept(false) {
    auto stream = candidate.inner.vtable.open(
        candidate.inner.ptr,
        Tuple2_bool_BoxDynFnMut1_void_Inbound_t{._0 = false, ._1 = {}});
    if (!stream._0) {
      throw std::runtime_error("Attempted to open StreamCandidate twice");
    }
    return StreamSwig(stream._1);
  }

  virtual StreamSwig open(InboundCallbackSwig *callback) noexcept(false) {
    callback->java_retain();
    auto stream = candidate.inner.vtable.open(
        candidate.inner.ptr, Tuple2_bool_BoxDynFnMut1_void_Inbound_t{
                                 ._0 = true,
                                 ._1 = {.env_ptr = callback,
                                        .call = callback->invokeCall,
                                        .free = callback->invokeRelease}});
    if (!stream._0) {
      throw std::runtime_error("Attempted to open StreamCandidate twice");
    }
    return StreamSwig(stream._1);
  }

  virtual TopicSwig topic() noexcept {
    return TopicSwig(candidate.inner.vtable.topic(candidate.inner.ptr));
  }

  virtual PeerPubkeySwig peer() noexcept {
    return PeerPubkeySwig(
        candidate.inner.vtable.peer_pubkey(candidate.inner.ptr));
  }
};

class StreamCandidateCallbackSwig : public Retainable {
public:
  virtual void call(StreamCandidateSwig data) noexcept = 0;

  virtual void drop() noexcept = 0;

  static void invokeCall(void *ctx, StreamCandidate_t candidate) noexcept {
    auto inst = static_cast<StreamCandidateCallbackSwig *>(ctx);
    inst->call(StreamCandidateSwig(candidate));
  }
};

class ConnectionResultCallbackSwig : public Retainable {
public:
  virtual void onSuccess(StreamCandidateSwig data) noexcept = 0;

  virtual void onFailure() noexcept = 0;

  virtual void drop() noexcept = 0;

  static void invokeCall(void *ctx,
                         ConnectionResult_Layout_t candidate) noexcept {
    auto inst = static_cast<ConnectionResultCallbackSwig *>(ctx);
    if (candidate.accepted) {
      inst->onSuccess(StreamCandidateSwig(candidate.stream));
    } else {
      inst->onFailure();
    }
  }
};

class BusSwig {
  Bus_t bus;

public:
  explicit BusSwig(CDitto_t const *ditto) noexcept
      : bus(dittoffi_get_experimental_bus(ditto)) {}

  BusSwig(BusSwig const &other) noexcept : bus(other.bus) {
    bus.inner.vtable.retain_vptr(bus.inner.ptr);
  };

  BusSwig(BusSwig &&other) noexcept : bus(other.bus) {
    other.bus.inner.ptr = nullptr;
  }

  virtual ~BusSwig() noexcept {
    if (bus.inner.ptr != nullptr) {
      bus.inner.vtable.release_vptr(bus.inner.ptr);
      bus.inner.ptr = nullptr;
    }
  }

  virtual AcceptorSwig
  bind_topic(const char *topic, Reliability reliability,
             StreamCandidateCallbackSwig *callback) noexcept(false) {
    if (callback == nullptr) {
      throw std::runtime_error("Callback must not be null");
    }
    callback->java_retain();
    auto acceptor = bus.inner.vtable.bind_topic(
        bus.inner.ptr,
        (slice_ref_uint8_t){.ptr = (const uint8_t *)topic,
                            .len = strlen(topic)},
        reliability,
        {.env_ptr = callback,
         .call = callback->invokeCall,
         .release = callback->invokeRelease,
         .retain = callback->invokeRetain});
    if (!acceptor.ok) {
      throw std::runtime_error("Failed to bind topic");
    }
    return AcceptorSwig(acceptor.acceptor);
  }

  virtual void connect(const char *peer_key_str, const char *topic,
                       Reliability reliability,
                       ConnectionResultCallbackSwig *callback) noexcept {
    callback->java_retain();
    slice_ref_uint8_t peer_key_slice = {.ptr = (const uint8_t *)peer_key_str,
                                        .len = strlen(peer_key_str)};
    auto parsed = dittoffi_peer_pubkey_from_str(peer_key_slice);
    if (!parsed._0) {
      callback->onFailure();
      callback->java_release();
      return;
    }
    bus.inner.vtable.connect(bus.inner.ptr, parsed._1,
                             (slice_ref_uint8_t){.ptr = (const uint8_t *)topic,
                                                 .len = strlen(topic)},
                             reliability,
                             {.env_ptr = callback,
                              .call = callback->invokeCall,
                              .free = callback->invokeRelease});
  }
};
#endif
