// clang-format off
#include <libremidi/libremidi-c.h>
#include <libremidi/libremidi.hpp>
#include <libremidi/backends.hpp>
// clang-format on

#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <limits>

struct libremidi_midi_observer_handle
{
  libremidi::observer self;
};

struct libremidi_midi_in_handle
{
  libremidi::midi_in self;
};

struct libremidi_midi_out_handle
{
  libremidi::midi_out self;
};

namespace libremidi
{

static void assign_error_callback(const auto& src, auto& dst)
{
  if (src.callback)
  {
    dst = [src](std::string_view errorText, const source_location& loc) {
      src.callback(src.context, errorText.data(), errorText.size(), &loc);
    };
  }
}
}

extern "C" {
const char* libremidi_get_version(void)
{
  return LIBREMIDI_VERSION;
}

void libremidi_midi1_available_apis(void* ctx, void (*cb)(void* ctx, libremidi_api))
{
  if (!cb)
    return;
  libremidi::midi1::for_all_backends([=](auto b) { cb(ctx, b.API); });
}

void libremidi_midi2_available_apis(void* ctx, void (*cb)(void* ctx, libremidi_api))
{
  if (!cb)
    return;
  libremidi::midi2::for_all_backends([=](auto b) { cb(ctx, b.API); });
}

const char* libremidi_api_identifier(libremidi_api api)
{
  return libremidi::get_api_name(api).data();
}

const char* libremidi_api_display_name(libremidi_api api)
{
  return libremidi::get_api_display_name(api).data();
}

libremidi_api libremidi_get_compiled_api_by_identifier(const char* name)
{
  libremidi_api ret = libremidi_api::UNSPECIFIED;
  libremidi::midi_any::for_all_backends([&](auto& b) {
    if (name == b.name)
      ret = b.API;
  });
  return ret;
}

int libremidi_midi_api_configuration_init(libremidi_api_configuration* conf)
{
  memset(conf, 0, sizeof(*conf));
  return 0;
}

int libremidi_midi_observer_configuration_init(libremidi_observer_configuration* conf)
{
  memset(conf, 0, sizeof(*conf));
  return 0;
}

int libremidi_midi_configuration_init(libremidi_midi_configuration* conf)
{
  memset(conf, 0, sizeof(*conf));
  return 0;
}

int libremidi_midi_in_port_clone(const libremidi_midi_in_port* port, libremidi_midi_in_port** dst)
{
  if (!port || !dst)
    return -EINVAL;

  auto copied = new libremidi::input_port{*reinterpret_cast<const libremidi::input_port*>(port)};
  *dst = reinterpret_cast<libremidi_midi_in_port*>(copied);
  return 0;
}

int libremidi_midi_in_port_free(libremidi_midi_in_port* port)
{
  delete reinterpret_cast<libremidi::input_port*>(port);
  return 0;
}

int libremidi_midi_in_port_name(const libremidi_midi_in_port* port, const char** name, size_t* len)
{
  if (!port || !name || !len)
    return -EINVAL;

  auto& p = *reinterpret_cast<const libremidi::input_port*>(port);
  *name = p.port_name.data();
  *len = p.port_name.size();
  return 0;
}

int libremidi_midi_out_port_clone(
    const libremidi_midi_out_port* port, libremidi_midi_out_port** dst)
{
  if (!port || !dst)
    return -EINVAL;

  auto copied = new libremidi::output_port{*reinterpret_cast<const libremidi::output_port*>(port)};
  *dst = reinterpret_cast<libremidi_midi_out_port*>(copied);
  return 0;
}

int libremidi_midi_out_port_free(libremidi_midi_out_port* port)
{
  delete reinterpret_cast<libremidi::output_port*>(port);
  return 0;
}

int libremidi_midi_out_port_name(
    const libremidi_midi_out_port* port, const char** name, size_t* len)
{
  if (!port || !name || !len)
    return -EINVAL;

  auto& p = *reinterpret_cast<const libremidi::output_port*>(port);
  *name = p.port_name.data();
  *len = p.port_name.size();
  return 0;
}

int libremidi_midi_observer_new(
    const libremidi_observer_configuration* c, libremidi_api_configuration* api,
    libremidi_midi_observer_handle** out)
{
  if (!out || !c)
    return -EINVAL;

  libremidi::observer_configuration conf;
  libremidi::assign_error_callback(c->on_error, conf.on_error);
  libremidi::assign_error_callback(c->on_warning, conf.on_warning);

  conf.track_hardware = c->track_hardware;
  conf.track_virtual = c->track_virtual;
  conf.track_any = c->track_any;
  conf.notify_in_constructor = c->notify_in_constructor;

  auto api_conf = libremidi::observer_configuration_for(static_cast<libremidi::API>(api->api));

  if (c->input_added.callback)
  {
    conf.input_added = [cb = c->input_added](const auto& port) {
      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_in_port*>(&port));
    };
  }
  if (c->input_removed.callback)
  {
    conf.input_removed = [cb = c->input_removed](const auto& port) {
      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_in_port*>(&port));
    };
  }
  if (c->output_added.callback)
  {
    conf.output_added = [cb = c->output_added](const auto& port) {
      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_out_port*>(&port));
    };
  }
  if (c->output_removed.callback)
  {
    conf.output_removed = [cb = c->output_removed](const auto& port) {
      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_out_port*>(&port));
    };
  }

  try
  {
    auto ptr = new libremidi_midi_observer_handle{
        libremidi::observer{std::move(conf), std::move(api_conf)}};
    *out = ptr;
    return 0;
  }
  catch (...)
  {
    return -EINVAL;
  }
}

int libremidi_midi_observer_enumerate_input_ports(
    libremidi_midi_observer_handle* ptr, void* context,
    void (*cb)(void* ctx, const libremidi_midi_in_port*))
{
  if (!ptr || !cb)
    return -EINVAL;

  for (const auto& port : ptr->self.get_input_ports())
  {
    cb(context, reinterpret_cast<const libremidi_midi_in_port*>(&port));
  }

  return 0;
}

int libremidi_midi_observer_enumerate_output_ports(
    libremidi_midi_observer_handle* ptr, void* context,
    void (*cb)(void* ctx, const libremidi_midi_out_port*))
{
  if (!ptr || !cb)
    return -EINVAL;

  for (const auto& port : ptr->self.get_output_ports())
  {
    cb(context, reinterpret_cast<const libremidi_midi_out_port*>(&port));
  }

  return 0;
}

int libremidi_midi_observer_free(libremidi_midi_observer_handle* ptr)
{
  delete ptr;
  return 0;
}

int libremidi_midi_in_new(
    const libremidi_midi_configuration* c, const libremidi_api_configuration* api,
    libremidi_midi_in_handle** out)
{
  if (!out || !c)
    return -EINVAL;
  if (!c->virtual_port && !c->in_port)
    return -EINVAL;

  *out = nullptr;

  auto api_conf = libremidi::midi_in_configuration_for(static_cast<libremidi::API>(api->api));

  // Create the MIDI object
  switch (c->version)
  {
    case libremidi_midi_configuration::MIDI1:
    case libremidi_midi_configuration::MIDI1_RAW: {
      libremidi::input_configuration conf;
      libremidi::assign_error_callback(c->on_error, conf.on_error);
      libremidi::assign_error_callback(c->on_warning, conf.on_warning);
      conf.ignore_sensing = c->ignore_sensing;
      conf.ignore_sysex = c->ignore_sysex;
      conf.ignore_timing = c->ignore_timing;
      conf.timestamps = c->timestamps;

      if (c->get_timestamp.callback)
      {
        conf.get_timestamp
            = [cb = c->get_timestamp](int64_t msg) { return cb.callback(cb.context, msg); };
      }

      if (c->version == libremidi_midi_configuration::MIDI1 && c->on_midi1_message.callback)
        conf.on_message = [cb = c->on_midi1_message](const libremidi::message& msg) {
          cb.callback(cb.context, msg.timestamp, msg.bytes.data(), msg.size());
        };
      else if (c->version == libremidi_midi_configuration::MIDI1_RAW && c->on_midi1_raw_data.callback)
      {
        conf.on_raw_data = [cb = c->on_midi1_raw_data](std::span<const uint8_t> msg, int64_t ts) {
          cb.callback(cb.context, ts, msg.data(), msg.size());
        };
      }
      else
      {
        return -EINVAL;
      }

      try
      {
        auto ptr = new libremidi_midi_in_handle{
            libremidi::midi_in{std::move(conf), std::move(api_conf)}};
        *out = ptr;
      }
      catch (...)
      {
        return -EINVAL;
      }
      break;
    }
    case libremidi_midi_configuration::MIDI2:
    case libremidi_midi_configuration::MIDI2_RAW: {
      libremidi::ump_input_configuration conf;
      libremidi::assign_error_callback(c->on_error, conf.on_error);
      libremidi::assign_error_callback(c->on_warning, conf.on_warning);
      conf.ignore_sensing = c->ignore_sensing;
      conf.ignore_sysex = c->ignore_sysex;
      conf.ignore_timing = c->ignore_timing;
      conf.timestamps = c->timestamps;

      if (c->get_timestamp.callback)
      {
        conf.get_timestamp
            = [cb = c->get_timestamp](int64_t msg) { return cb.callback(cb.context, msg); };
      }

      if (c->version == libremidi_midi_configuration::MIDI2 && c->on_midi2_message.callback)
        conf.on_message = [cb = c->on_midi2_message](const libremidi::ump& msg) {
          cb.callback(cb.context, msg.timestamp, msg.data, msg.size());
        };
      else if (c->version == libremidi_midi_configuration::MIDI2_RAW && c->on_midi2_raw_data.callback)
      {
        conf.on_raw_data = [cb = c->on_midi2_raw_data](std::span<const uint32_t> msg, int64_t ts) {
          cb.callback(cb.context, ts, msg.data(), msg.size());
        };
      }
      else
      {
        return -EINVAL;
      }

      try
      {
        auto ptr = new libremidi_midi_in_handle{
            libremidi::midi_in{std::move(conf), std::move(api_conf)}};
        *out = ptr;
      }
      catch (...)
      {
        return -EINVAL;
      }
      break;
    }
    default:
      return -EINVAL;
  }

  // Open the port
  auto ptr = *out;
  if (!ptr)
    return -EINVAL;

  if (c->virtual_port)
  {
    if (auto ret = ptr->self.open_virtual_port(c->port_name); ret != stdx::error{})
    {
      *out = nullptr;
      delete ptr;
      return -EIO;
    }
  }
  else
  {
    auto port = reinterpret_cast<const libremidi::input_port*>(c->in_port);
    if (auto ret = ptr->self.open_port(*port); ret != stdx::error{})
    {
      *out = nullptr;
      delete ptr;
      return -EIO;
    }
  }

  return 0;
}
int libremidi_midi_in_is_connected(const libremidi_midi_in_handle* in)
{
  if (!in)
    return -EINVAL;
  return in->self.is_port_connected() ? 1 : 0;
}

libremidi_timestamp libremidi_midi_in_absolute_timestamp(libremidi_midi_in_handle* in)
{
  if (!in)
    return -EINVAL;
  return in->self.absolute_timestamp();
}

int libremidi_midi_in_free(libremidi_midi_in_handle* ptr)
{
  delete ptr;
  return 0;
}

int libremidi_midi_out_new(
    const libremidi_midi_configuration* c, const libremidi_api_configuration* api,
    libremidi_midi_out_handle** out)
{
  if (!out || !c)
    return -EINVAL;
  if (!c->virtual_port && !c->out_port)
    return -EINVAL;

  *out = nullptr;

  auto api_conf = libremidi::midi_out_configuration_for(static_cast<libremidi::API>(api->api));

  // Create the MIDI object
  libremidi::output_configuration conf;
  libremidi::assign_error_callback(c->on_error, conf.on_error);
  libremidi::assign_error_callback(c->on_warning, conf.on_warning);
  conf.timestamps = c->timestamps;
  try
  {
    auto ptr
        = new libremidi_midi_out_handle{libremidi::midi_out{std::move(conf), std::move(api_conf)}};
    *out = ptr;
  }
  catch (...)
  {
    return -EINVAL;
  }

  // Open the port
  auto ptr = *out;
  if (!ptr)
    return -EINVAL;

  if (c->virtual_port)
  {
    if (auto ret = ptr->self.open_virtual_port(c->port_name); ret != stdx::error{})
    {
      *out = nullptr;
      delete ptr;
      return -EIO;
    }
  }
  else
  {
    auto port = reinterpret_cast<const libremidi::output_port*>(c->in_port);
    if (auto ret = ptr->self.open_port(*port); ret != stdx::error{})
    {
      *out = nullptr;
      delete ptr;
      return -EIO;
    }
  }
  return 0;
}

int libremidi_midi_out_is_connected(const libremidi_midi_out_handle* out)
{
  if (!out)
    return -EINVAL;
  return out->self.is_port_connected() ? 1 : 0;
}

int libremidi_midi_out_send_message(
    libremidi_midi_out_handle* out, const libremidi_midi1_symbol* msg, size_t sz)
{
  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
    return -EINVAL;

  auto res = out->self.send_message(msg, sz);
  return res != stdx::error{} ? -EIO : 0;
}

int libremidi_midi_out_send_ump(libremidi_midi_out_handle* out, const libremidi_midi2_symbol* msg, size_t sz)
{
  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
    return -EINVAL;

  auto res = out->self.send_ump(msg, sz);
  return res != stdx::error{} ? -EIO : 0;
}

int libremidi_midi_out_schedule_message(
    libremidi_midi_out_handle* out, int64_t ts, const libremidi_midi1_symbol* msg, size_t sz)
{
  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
    return -EINVAL;

  auto res = out->self.schedule_message(ts, msg, sz);
  return res != stdx::error{} ? -EIO : 0;
}

int libremidi_midi_out_schedule_ump(
    libremidi_midi_out_handle* out, int64_t ts, const libremidi_midi2_symbol* msg, size_t sz)
{
  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
    return -EINVAL;

  auto res = out->self.schedule_ump(ts, msg, sz);
  return res != stdx::error{} ? -EIO : 0;
}

int libremidi_midi_out_free(libremidi_midi_out_handle* ptr)
{
  delete ptr;
  return 0;
}
}
