// SPDX-License-Identifier: Unlicense

#include "clap/clap.h"

#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

enum Parameter
{
  gain,
  COUNT
};

struct MyPlugin
{
  clap_plugin_t plugin;
  double parameter[Parameter::COUNT]{ 0.5 };
};

static const char* plugin_descriptor__feature[] = {
    CLAP_PLUGIN_FEATURE_INSTRUMENT,
    CLAP_PLUGIN_FEATURE_SYNTHESIZER,
    CLAP_PLUGIN_FEATURE_STEREO,
    NULL,
};

static const clap_plugin_descriptor_t plugin__descriptor = {
  .clap_version = CLAP_VERSION_INIT,
  .id = "shr3d.clap.gain",
  .name = "SfxPluginClapGain",
  .vendor = "Shr3D Industries",
  .url = "https://shr3d.app",
  .manual_url = "https://shr3d.app",
  .support_url = "https://shr3d.app",
  .version = "0.1.0",
  .description = "A Example Clap Plugin",
  .features = plugin_descriptor__feature
};

static uint32_t plugin_params__count(const clap_plugin_t* /*plugin*/)
{
  return Parameter::COUNT;
}

static bool plugin_params__get_info(const clap_plugin_t* /*plugin*/, uint32_t param_index, clap_param_info_t* param_info)
{
  switch (param_index)
  {
  case Parameter::gain:
    memset(param_info, 0, sizeof(clap_param_info_t));
    param_info->id = param_index;
    param_info->flags = CLAP_PARAM_IS_PERIODIC | CLAP_PARAM_IS_AUTOMATABLE;
    strcpy(param_info->name, "Gain");
    param_info->min_value = 0.0;
    param_info->max_value = 1.0;
    param_info->default_value = 0.5;
    return true;
  default:
    return false;
  }
}

static bool plugin_params__get_value(const clap_plugin_t* plugin, clap_id param_id, double* value)
{
  const MyPlugin* my_plugin = static_cast<const MyPlugin*>(plugin->plugin_data);

  switch (param_id)
  {
  case Parameter::gain:
    *value = my_plugin->parameter[param_id];
    return true;
  default:
    return false;
  }
}

static bool plugin_params__value_to_text(const clap_plugin_t* /*plugin*/, clap_id param_id, double value, char* display, uint32_t size)
{
  switch (param_id)
  {
  case Parameter::gain:
    snprintf(display, size, "%lf", value);
    return true;
  default:
    return false;
  }
}

static bool plugin_params__text_to_value(const clap_plugin_t* /*plugin*/, clap_id param_id, const char* display, double* value)
{
  switch (param_id)
  {
  case Parameter::gain:
      *value = 0.5 * atof(display);
      return true;
  default:
    return false;
  }
}

static void PluginProcessEvent(MyPlugin* my_plugin, const clap_event_header_t* event)
{
  switch (event->space_id)
  {
  case CLAP_CORE_EVENT_SPACE_ID:
    switch (event->type)
    {
    case CLAP_EVENT_PARAM_VALUE:
    {
      const clap_event_param_value_t* valueEvent = reinterpret_cast<const clap_event_param_value_t*>(event);
      my_plugin->parameter[valueEvent->param_id] = valueEvent->value;
    }
    break;
    default:
      break;
    }
    break;
  default:
    break;
  }
}

static void plugin_params__flush(const clap_plugin_t* plugin, const clap_input_events_t* in, const clap_output_events_t* /*out*/)
{
  MyPlugin* my_plugin = static_cast<MyPlugin*>(plugin->plugin_data);
  const uint32_t eventCount = in->size(in);

  for (uint32_t eventIndex = 0; eventIndex < eventCount; ++eventIndex)
  {
    const clap_event_header_t* event = in->get(in, eventIndex);
    PluginProcessEvent(my_plugin, event);
  }
}

static const clap_plugin_params_t extensionParams = {
  .count = plugin_params__count,
  .get_info = plugin_params__get_info,
  .get_value = plugin_params__get_value,
  .value_to_text = plugin_params__value_to_text,
  .text_to_value = plugin_params__text_to_value,
  .flush = plugin_params__flush
};

static uint32_t plugin_audio_ports__count(const clap_plugin_t* /*plugin*/, bool /*is_input*/)
{
  return 1;
}

static bool plugin_audio_ports__get(const clap_plugin_t* /*plugin*/, uint32_t index, bool is_input, clap_audio_port_info_t* info)
{
  if (index > 0)
    return false;
  info->id = 0;
  info->channel_count = 2;
  info->flags = CLAP_AUDIO_PORT_IS_MAIN;
  info->port_type = CLAP_PORT_STEREO;
  info->in_place_pair = CLAP_INVALID_ID;
  snprintf(info->name, sizeof(info->name), "%s", is_input ? "Audio Input" : "Audio Output");
  return true;
}

static const clap_plugin_audio_ports_t extensionAudioPorts = {
  .count = plugin_audio_ports__count,
  .get = plugin_audio_ports__get
};

static bool plugin__init(const clap_plugin* plugin)
{
  MyPlugin* my_plugin = static_cast<MyPlugin*>(plugin->plugin_data);

  for (int i = 0; i < Parameter::COUNT; ++i)
  {
    clap_param_info_t info = {};
    extensionParams.get_info(plugin, i, &info);
    my_plugin->parameter[i] = info.default_value;
  }

  return true;
}

static void plugin__destroy(const clap_plugin* plugin)
{
  const MyPlugin* my_plugin = static_cast<const MyPlugin*>(plugin->plugin_data);
  delete my_plugin;
}

static bool plugin__activate(const clap_plugin* /*plugin*/, double /*sampleRate*/, uint32_t /*minimumFramesCount*/, uint32_t /*maximumFramesCount*/)
{
  return true;
}

static void plugin__deactivate(const clap_plugin* /*plugin*/)
{
}

static bool plugin__start_processing(const clap_plugin* /*plugin*/)
{
  return true;
}

static void plugin__stop_processing(const clap_plugin* /*plugin*/)
{
}

static void plugin__reset(const clap_plugin* /*plugin*/)
{
}

static float distortion(const float x)
{
  return (x >= 0.0f ? 1.0f - expf(-x) : -1.0f + expf(x));
}

static clap_process_status plugin__process(const clap_plugin* plugin, const clap_process_t* process)
{
  MyPlugin* my_plugin = (MyPlugin*)plugin->plugin_data;

  assert(process->audio_outputs_count == 1);
  assert(process->audio_inputs_count == 1);

  const float gain = float(2.0 * my_plugin->parameter[Parameter::gain]);

  const uint32_t frameCount = process->frames_count;
  const uint32_t inputEventCount = process->in_events->size(process->in_events);
  uint32_t eventIndex = 0;
  uint32_t nextEventFrame = inputEventCount ? 0 : frameCount;

  for (uint32_t i = 0; i < frameCount; )
  {
    while (eventIndex < inputEventCount && nextEventFrame == i)
    {
      const clap_event_header_t* event = process->in_events->get(process->in_events, eventIndex);

      if (event->time != i)
      {
        nextEventFrame = event->time;
        break;
      }

      PluginProcessEvent(my_plugin, event);
      ++eventIndex;

      if (eventIndex == inputEventCount)
      {
        nextEventFrame = frameCount;
        break;
      }
    }

    for (uint32_t index = i; index < frameCount; index++)
    {
      process->audio_outputs[0].data32[0][index] = distortion(process->audio_inputs[0].data32[0][index] * gain);
      process->audio_outputs[0].data32[1][index] = distortion(process->audio_inputs[0].data32[1][index] * gain);
    }

    i = nextEventFrame;
  }

  return CLAP_PROCESS_CONTINUE;
}

static const void* plugin__get_extension(const clap_plugin* /*plugin*/, const char* id)
{
  if (0 == strcmp(id, CLAP_EXT_AUDIO_PORTS))
    return &extensionAudioPorts;
  if (0 == strcmp(id, CLAP_EXT_PARAMS))
    return &extensionParams;
  return nullptr;
}

static void plugin__on_main_thread(const clap_plugin* /*plugin*/)
{
}

static const clap_plugin_t pluginClass{
  .desc = &plugin__descriptor,
  .plugin_data = nullptr,
  .init = plugin__init,
  .destroy = plugin__destroy,
  .activate = plugin__activate,
  .deactivate = plugin__deactivate,
  .start_processing = plugin__start_processing,
  .stop_processing = plugin__stop_processing,
  .reset = plugin__reset,
  .process = plugin__process,
  .get_extension = plugin__get_extension,
  .on_main_thread = plugin__on_main_thread
};

static uint32_t plugin_factory__get_plugin_count(const struct clap_plugin_factory* /*factory*/)
{
  return 1;
}

static const clap_plugin_descriptor_t* plugin_factory__get_plugin_descriptor(const clap_plugin_factory* /*factory*/, uint32_t index)
{
  switch (index)
  {
  case 0:
    return &plugin__descriptor;
  default:
    return nullptr;
  }
}

static const clap_plugin_t* plugin_factory__create_plugin(const clap_plugin_factory* /*factory*/, const clap_host_t* host, const char* pluginID)
{
  if (!clap_version_is_compatible(host->clap_version) || strcmp(pluginID, plugin__descriptor.id))
    return nullptr;

  MyPlugin* my_plugin = new MyPlugin;
  my_plugin->plugin = pluginClass;
  my_plugin->plugin.plugin_data = my_plugin;
  return &my_plugin->plugin;
}

static const clap_plugin_factory_t plugin_factory{
  .get_plugin_count = plugin_factory__get_plugin_count,
  .get_plugin_descriptor = plugin_factory__get_plugin_descriptor,
  .create_plugin = plugin_factory__create_plugin
};

static bool plugin_entry__init(const char* /*plugin_path*/)
{
  return true;
}

static void plugin_entry__deinit()
{
}

static const void* plugin_entry__get_factory(const char* factoryID)
{
  if (strcmp(factoryID, CLAP_PLUGIN_FACTORY_ID) == 0)
    return &plugin_factory;
  return nullptr;
}

const clap_plugin_entry_t clap_entry{
  .clap_version = CLAP_VERSION_INIT,
  .init = plugin_entry__init,
  .deinit = plugin_entry__deinit,
  .get_factory = plugin_entry__get_factory
};
