// SPDX-License-Identifier: Unlicense

#include "sfxPluginClap.h"

#ifdef SHR3D_SFX_PLUGIN_CLAP

#include "../deps/clap/include/clap/clap.h"
#include "file.h"
#include "global.h"
#include "settings.h"
#include "string_.h"
#include "version.h"
#include "window.h"
#include <filesystem>

#ifdef SHR3D_WINDOW_SDL
#include <SDL.h>
#include <SDL_syswm.h>
#endif // SHR3D_WINDOW_SDL

#if defined(PLATFORM_WINDOWS)
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
typedef HMODULE dlhandle;
typedef HWND ClapWindow;
#define dlopen(x, y) LoadLibrary(x)
#define dlsym(x, y) (void*)GetProcAddress(x, y)
#else // PLATFORM_WINDOWS
#include <dlfcn.h>
typedef ::Window ClapWindow; // there is a "class Window" in rubberband
typedef void* dlhandle;
#endif // PLATFORM_WINDOWS


#ifdef PLATFORM_WINDOWS
#define CLAP_WINDOW_API_OS CLAP_WINDOW_API_WIN32
#else // PLATFORM_WINDOWS
#define CLAP_WINDOW_API_OS CLAP_WINDOW_API_X11
#endif // PLATFORM_WINDOWS


static std::mutex loadSfxPluginClapInstanceMutex;

static const void* clapGetExtension(const clap_host_t* /*host*/, const char* /*extension_id*/)
{
  return nullptr;
}

static void clapRequestRestart(const clap_host_t* /*host*/)
{
}

static void clapRequestProcess(const clap_host_t* /*host*/)
{
}

static void clapRequestCallback(const clap_host_t* /*host*/)
{
}

static const clap_host host{
  .clap_version = CLAP_VERSION,
  .host_data = nullptr,
  .name = "Shr3D",
  .vendor = "Shr3D Industries",
  .url = "https://shr3d.app/",
  .version = VERSION_STR,
  .get_extension = clapGetExtension,
  .request_restart = clapRequestRestart,
  .request_process = clapRequestProcess,
  .request_callback = clapRequestCallback
};

struct EventList
{
  static constexpr u32 heapSize = 1024 * 1024;
  u8* heap;
  u8* tail;
  std::vector<u32> eventSize;

  EventList()
    : heap((u8*)malloc(heapSize))
    , tail(heap)
  {
  }
};

static u32 clapSize(const struct clap_input_events* list)
{
  EventList* eventList = static_cast<EventList*>(list->ctx);
  return u32(eventList->eventSize.size());
}

static const clap_event_header_t* clapGet(const struct clap_input_events* list, const uint32_t index)
{
  EventList* eventList = static_cast<EventList*>(list->ctx);

  ASSERT(index < eventList->eventSize.size());

  size_t offset = 0;
  for (uint32_t i = 0; i < index; ++i)
  {
    offset += eventList->eventSize[i];
  }

  const clap_event_header* header = reinterpret_cast<const clap_event_header*>(&eventList->heap[offset]);

  return header;
}

static bool clapTryPush(const struct clap_output_events* list, const clap_event_header* event)
{
  EventList* eventList = static_cast<EventList*>(list->ctx);

  ASSERT(eventList->tail - eventList->heap + event->size <= EventList::heapSize);

  memcpy(eventList->tail, event, event->size);
  eventList->eventSize.push_back(event->size);
  eventList->tail += event->size;

  return true;
}

struct PluginInstance
{
  const clap_plugin_t* plugin;
  bool sheduleEventFlush = false;

  clap_audio_buffer* audio_inputs = new clap_audio_buffer
  {
    .data32 = nullptr,
    .data64 = nullptr,
    .channel_count = 2,
    .latency = 0,
    .constant_mask = 0
  };

  clap_audio_buffer* audio_outputs = new clap_audio_buffer
  {
    .data32 = nullptr,
    .data64 = nullptr,
    .channel_count = 2,
    .latency = 0,
    .constant_mask = 0
  };

  const clap_input_events* in_events = new clap_input_events
  {
    .ctx = new EventList,
    .size = clapSize,
    .get = clapGet
  };
  const clap_output_events* out_events = new clap_output_events
  {
    .ctx = in_events->ctx,
    .try_push = clapTryPush
  };

  clap_process* process = new clap_process
  {
    .steady_time = 0,
    .frames_count = 0,
    .transport = nullptr,
    .audio_inputs = audio_inputs,
    .audio_outputs = audio_outputs,
    .audio_inputs_count = 1,
    .audio_outputs_count = 1,
    .in_events = in_events,
    .out_events = out_events
  };
};

struct ClapPlugin
{
  std::vector<PluginInstance> pluginInstances;
  const clap_plugin_factory* pluginFactory;
};

static std::vector<ClapPlugin> clapPlugins;

static void loadPluginInstance(ClapPlugin& clapPlugin)
{
  //const i32 instance = i32(clapPlugin.pluginInstances.size());

  const clap_plugin_descriptor_t* desc = clapPlugin.pluginFactory->get_plugin_descriptor(clapPlugin.pluginFactory, 0);
  ASSERT(desc != nullptr);
  ASSERT(desc->clap_version.major >= 1);

  PluginInstance& pluginInstances = clapPlugin.pluginInstances.emplace_back();
  pluginInstances.plugin = clapPlugin.pluginFactory->create_plugin(clapPlugin.pluginFactory, &host, desc->id);

  const clap_plugin_t* plugin = pluginInstances.plugin;
  ASSERT(plugin != nullptr);

  if (!plugin->init(plugin))
    ASSERT(false);

  if (!plugin->activate(plugin, sampleRate(), blockSize(), blockSize()))
    ASSERT(false);

  if (!plugin->start_processing(plugin))
    ASSERT(false);
}

void SfxPluginClap::init(std::vector<std::string>& sfxNames)
{
  const std::vector<std::string> paths = String::split(Settings::pathClap, '|');
  for (const std::string& path : paths)
  {
    if (!File::exists(path.c_str()))
      continue;

    for (const auto& file : File::filesInDirectory(path.c_str()))
    {
      if (File::extension(file.c_str()) != std::string(".clap"))
        continue;

#ifdef PLATFORM_WINDOWS
      void* hinstLib = LoadLibrary(String::ws_ExpandEnvironmentStrings(String::s2ws(file.c_str(), file.size())).c_str());
      if (hinstLib == nullptr)
        continue;
#else // PLATFORM_WINDOWS
      void* hinstLib = dlopen(reinterpret_cast<const char*>(file.c_str()), RTLD_LAZY | RTLD_LOCAL);
      if (hinstLib == nullptr)
      {
        const char* err = dlerror();
        printf("dlopen error: %s\n", err);
        continue;
      }
#endif // PLATFORM_WINDOWS

      const clap_plugin_entry* pluginEntry = reinterpret_cast<const struct clap_plugin_entry*>(dlsym((dlhandle)hinstLib, "clap_entry"));
      if (pluginEntry == nullptr)
        continue;

      pluginEntry->init(reinterpret_cast<const char*>(file.c_str()));

      ClapPlugin& clapPlugin = clapPlugins.emplace_back();
      clapPlugin.pluginFactory = static_cast<const clap_plugin_factory*>(pluginEntry->get_factory(CLAP_PLUGIN_FACTORY_ID));
      ASSERT(clapPlugin.pluginFactory != nullptr);

      //loadPluginInstance(clapPlugin);
      //const clap_plugin_t* plugin = clapPlugin.pluginInstances[0].plugin;

      clapPlugin.pluginFactory->get_plugin_count(clapPlugin.pluginFactory);
      //const u32 plugin_count = clapPlugin.pluginFactory->get_plugin_count(clapPlugin.pluginFactory);
      //ASSERT(plugin_count == 1);

      const clap_plugin_descriptor_t* plugin_descriptor = clapPlugin.pluginFactory->get_plugin_descriptor(clapPlugin.pluginFactory, 0);
      ASSERT(plugin_descriptor != nullptr);

      sfxNames.emplace_back(reinterpret_cast<const char*>(plugin_descriptor->name));

      //const clap_plugin_audio_ports* pluginAudioPorts = static_cast<const clap_plugin_audio_ports*>(plugin->get_extension(plugin, CLAP_EXT_AUDIO_PORTS));
      //const clap_plugin_gui* pluginGui = static_cast<const clap_plugin_gui*>(plugin->get_extension(plugin, CLAP_EXT_GUI));
      //const clap_plugin_latency* pluginLatency = static_cast<const clap_plugin_latency*>(plugin->get_extension(plugin, CLAP_EXT_LATENCY));
      //const clap_plugin_note_ports* pluginNotePorts = static_cast<const clap_plugin_note_ports*>(plugin->get_extension(plugin, CLAP_EXT_NOTE_PORTS));
      //const clap_plugin_params* pluginParams = static_cast<const clap_plugin_params*>(plugin->get_extension(plugin, CLAP_EXT_PARAMS));
      //const clap_plugin_posix_fd_support* pluginPosixFdSupport = static_cast<const clap_plugin_posix_fd_support*>(plugin->get_extension(plugin, CLAP_EXT_POSIX_FD_SUPPORT));
      //const clap_plugin_preset_load* pluginPresetLoad = static_cast<const clap_plugin_preset_load*>(plugin->get_extension(plugin, CLAP_EXT_PRESET_LOAD));
      //if (!pluginPresetLoad)
      //  pluginPresetLoad = static_cast<const clap_plugin_preset_load*>(plugin->get_extension(plugin, CLAP_EXT_PRESET_LOAD_COMPAT));
      //const clap_plugin_remote_controls* pluginRemoteControls = static_cast<const clap_plugin_remote_controls*>(plugin->get_extension(plugin, CLAP_EXT_REMOTE_CONTROLS));
      //if (!pluginRemoteControls)
      //  pluginRemoteControls = static_cast<const clap_plugin_remote_controls*>(plugin->get_extension(plugin, CLAP_EXT_REMOTE_CONTROLS_COMPAT));
      //const clap_plugin_render* pluginRender = static_cast<const clap_plugin_render*>(plugin->get_extension(plugin, CLAP_EXT_RENDER));
      //const clap_plugin_state* pluginState = static_cast<const clap_plugin_state*>(plugin->get_extension(plugin, CLAP_EXT_STATE));
      //const clap_plugin_tail* pluginTail = static_cast<const clap_plugin_tail*>(plugin->get_extension(plugin, CLAP_EXT_TAIL));
      //const clap_plugin_thread_pool* pluginThreadPool = static_cast<const clap_plugin_thread_pool*>(plugin->get_extension(plugin, CLAP_EXT_THREAD_POOL));
      //const clap_plugin_timer_support* pluginTimerSupport = static_cast<const clap_plugin_timer_support*>(plugin->get_extension(plugin, CLAP_EXT_TIMER_SUPPORT));
    }
  }
}

bool SfxPluginClap::hasSfxPluginWindow(SfxIndex index)
{
  ASSERT(index >= 0);
  ASSERT(index < clapPlugins.size());

  ClapPlugin& clapPlugin = clapPlugins[index];

  {
    const std::lock_guard<std::mutex> guard(loadSfxPluginClapInstanceMutex);
    while (clapPlugin.pluginInstances.size() <= 0)
      loadPluginInstance(clapPlugin);
  }

  const clap_plugin_t* plugin = clapPlugin.pluginInstances[0].plugin;
  const clap_plugin_gui* pluginGui = static_cast<const clap_plugin_gui*>(plugin->get_extension(plugin, CLAP_EXT_GUI));

  return pluginGui != nullptr;
}

void SfxPluginClap::openWindow(SfxIndex index, i32 instance)
{
  ClapPlugin& clapPlugin = clapPlugins[index];
  const clap_plugin_t* plugin = clapPlugin.pluginInstances[instance].plugin;
  const clap_plugin_gui* pluginGui = static_cast<const clap_plugin_gui*>(plugin->get_extension(plugin, CLAP_EXT_GUI));
  ASSERT(pluginGui != nullptr);

  ASSERT(pluginGui->is_api_supported(plugin, CLAP_WINDOW_API_OS, false));

  if (!pluginGui->create(plugin, CLAP_WINDOW_API_OS, false))
    ASSERT(false);
}

void SfxPluginClap::parentWindow(SfxIndex index, i32 instance, Shr3DWindow parentWindow)
{
  ClapPlugin& clapPlugin = clapPlugins[index];
  const clap_plugin_t* plugin = clapPlugin.pluginInstances[instance].plugin;
  const clap_plugin_gui* pluginGui = static_cast<const clap_plugin_gui*>(plugin->get_extension(plugin, CLAP_EXT_GUI));
  ASSERT(pluginGui != nullptr);

#ifdef SHR3D_WINDOW_SDL
  SDL_SysWMinfo wmInfo;
  SDL_VERSION(&wmInfo.version);
  SDL_GetWindowWMInfo(parentWindow, &wmInfo);
#endif // SHR3D_WINDOW_SDL

  const clap_window clapParentWindow
  {
    .api = CLAP_WINDOW_API_OS,
#ifdef PLATFORM_LINUX
    .x11 = (clap_xwnd)wmInfo.info.x11.window,
#endif // PLATFORM_LINUX
#ifdef PLATFORM_WINDOWS
    .win32 = parentWindow
#endif // PLATFORM_WINDOWS
  };

  pluginGui->set_parent(plugin, &clapParentWindow);
  pluginGui->show(plugin);

  //#ifdef SHR3D_OPENGL_ERROR_CHECK
  //  ASSERT(glGetError() == 0); // plugin corrupted opengl context
  //#endif // SHR3D_OPENGL_ERROR_CHECK
}

Size SfxPluginClap::getSfxPluginWindowSize(SfxIndex index, i32 instance)
{
  const clap_plugin_t* plugin = clapPlugins[index].pluginInstances[instance].plugin;
  const clap_plugin_gui* pluginGui = static_cast<const clap_plugin_gui*>(plugin->get_extension(plugin, CLAP_EXT_GUI));

  Size size;
  pluginGui->get_size(plugin, &size.width, &size.height);

  return size;
}

void SfxPluginClap::closeWindow(SfxIndex index, i32 instance)
{
  const clap_plugin_t* plugin = clapPlugins[index].pluginInstances[instance].plugin;
  const clap_plugin_gui* pluginGui = static_cast<const clap_plugin_gui*>(plugin->get_extension(plugin, CLAP_EXT_GUI));

  if (pluginGui != nullptr)
  {
    pluginGui->hide(plugin);
    pluginGui->destroy(plugin);
  }
}

i32 SfxPluginClap::numParams(SfxIndex index, i32 instance)
{
  const clap_plugin_t* plugin = clapPlugins[index].pluginInstances[instance].plugin;
  const clap_plugin_params* pluginParams = static_cast<const clap_plugin_params*>(plugin->get_extension(plugin, CLAP_EXT_PARAMS));

  //plugin->stop_processing(plugin);

  if (pluginParams != nullptr)
  {
    const u32 paramCount = pluginParams->count(plugin);
    return paramCount;
  }

  return {};
}

void SfxPluginClap::getParameterProperties(SfxIndex index, i32 instance, i32 param, SfxParameterProperties& parameterProperties)
{
  const clap_plugin_t* plugin = clapPlugins[index].pluginInstances[instance].plugin;
  const clap_plugin_params* pluginParams = static_cast<const clap_plugin_params*>(plugin->get_extension(plugin, CLAP_EXT_PARAMS));

  //const u32 paramCount = pluginParams->count(plugin);

  clap_param_info_t paramInfo;
  pluginParams->get_info(plugin, param, &paramInfo);

  memcpy(parameterProperties.name, paramInfo.name, sizeof(parameterProperties.name));

  f64 value = F64::inf;
  pluginParams->get_value(plugin, paramInfo.id, &value);
  ASSERT(value != F64::inf);

  parameterProperties.label[0] = '\0'; // not used on clap

  pluginParams->value_to_text(plugin, paramInfo.id, value, parameterProperties.display, sizeof(parameterProperties.display));
}

f32 SfxPluginClap::getParameter(SfxIndex index, i32 instance, i32 param)
{
  const clap_plugin_t* plugin = clapPlugins[index].pluginInstances[instance].plugin;
  const clap_plugin_params* pluginParams = static_cast<const clap_plugin_params*>(plugin->get_extension(plugin, CLAP_EXT_PARAMS));

  //const u32 paramCount = pluginParams->count(plugin);

  clap_param_info_t paramInfo;
  pluginParams->get_info(plugin, param, &paramInfo);

  f64 value;
  pluginParams->get_value(plugin, paramInfo.id, &value);

  return f32(value);
}

void SfxPluginClap::setParameter(SfxIndex index, i32 instance, i32 param, f32 value)
{
  ClapPlugin& clapPlugin = clapPlugins[index];
  PluginInstance& pluginInstances = clapPlugin.pluginInstances[instance];
  const clap_plugin_t* plugin = pluginInstances.plugin;
  const clap_plugin_params* pluginParams = static_cast<const clap_plugin_params*>(plugin->get_extension(plugin, CLAP_EXT_PARAMS));

  //const u32 paramCount = pluginParams->count(plugin);

  clap_param_info_t paramInfo;
  pluginParams->get_info(plugin, param, &paramInfo);

  const clap_event_param_value ev{
    .header {
      .size = sizeof(ev),
      .time = 0,
      .space_id = CLAP_CORE_EVENT_SPACE_ID,
      .type = CLAP_EVENT_PARAM_VALUE,
      .flags = 0
    },
    .param_id = paramInfo.id,
    .cookie = nullptr,
    .note_id = -1,
    .port_index = 0,
    .channel = -1,
    .key = -1,
    .value = value
  };

  clapTryPush(pluginInstances.out_events, &ev.header);
  pluginInstances.sheduleEventFlush = true;
}

void SfxPluginClap::processBlock(SfxIndex index, i32 instance, f32** inBlock, f32** outBlock, i32 blockSize)
{
  ClapPlugin& clapPlugin = clapPlugins[index];

  {
    const std::lock_guard<std::mutex> guard(loadSfxPluginClapInstanceMutex);
    while (clapPlugin.pluginInstances.size() <= instance)
      loadPluginInstance(clapPlugin);
  }

  PluginInstance& pluginInstances = clapPlugin.pluginInstances[instance];

  pluginInstances.audio_inputs->data32 = inBlock;
  pluginInstances.audio_outputs->data32 = outBlock;
  pluginInstances.process->frames_count = u32(blockSize);
  //pluginInstances.process.audio_inputs = &pluginInstances.audioIn;
  //pluginInstances.process.audio_outputs = &pluginInstances.audioOut;
  //pluginInstances.process.in_events = &pluginInstances.inputEvents;
  //pluginInstances.process.out_events = &pluginInstances.outputEvents;

  const clap_plugin_t* plugin = pluginInstances.plugin;

  if (pluginInstances.sheduleEventFlush)
  {
    pluginInstances.sheduleEventFlush = false;
    plugin->stop_processing(plugin);
    const clap_plugin_params* pluginParams = static_cast<const clap_plugin_params*>(plugin->get_extension(plugin, CLAP_EXT_PARAMS));
    if (pluginParams != nullptr)
      pluginParams->flush(plugin, pluginInstances.in_events, pluginInstances.out_events);
    plugin->start_processing(plugin);
  }

  if (plugin->process(plugin, pluginInstances.process) != CLAP_PROCESS_CONTINUE)
  {
    //ASSERT(false);
  }

  {
    EventList* eventList = static_cast<EventList*>(pluginInstances.in_events->ctx);
    eventList->eventSize.clear();
    eventList->tail = eventList->heap;
  }

  pluginInstances.process->steady_time += blockSize;
}

std::string SfxPluginClap::saveParameters(SfxIndex index, i32 instance)
{
  std::string params;

  const clap_plugin_t* plugin = clapPlugins[index].pluginInstances[instance].plugin;
  const clap_plugin_params* pluginParams = static_cast<const clap_plugin_params*>(plugin->get_extension(plugin, CLAP_EXT_PARAMS));

  if (pluginParams != nullptr)
  {
    plugin->stop_processing(plugin);

    const u32 paramCount = pluginParams->count(plugin);

    for (u32 i = 0; i < paramCount; ++i)
    {
      clap_param_info_t paramInfo;
      pluginParams->get_info(plugin, i, &paramInfo);

      char substr[256];
      f64 value;
      pluginParams->get_value(plugin, paramInfo.id, &value);
      pluginParams->value_to_text(plugin, paramInfo.id, value, (char*)substr, sizeof(substr));
      params += std::string(substr) + ',';
    }
    params.pop_back();
  }

  return params;
}

void SfxPluginClap::loadParameters(SfxIndex index, i32 instance, const std::string& parameters)
{
  ClapPlugin& clapPlugin = clapPlugins[index];

  {
    const std::lock_guard<std::mutex> guard(loadSfxPluginClapInstanceMutex);
    while (clapPlugin.pluginInstances.size() == instance)
      loadPluginInstance(clapPlugin);
  }

  PluginInstance& pluginInstances = clapPlugin.pluginInstances[instance];
  const clap_plugin_t* plugin = pluginInstances.plugin;
  const clap_plugin_params* pluginParams = static_cast<const clap_plugin_params*>(plugin->get_extension(plugin, CLAP_EXT_PARAMS));
  if (pluginParams != nullptr)
  {
    const u32 paramCount = pluginParams->count(plugin);

    u64 strLength = parameters.size();
    if (strLength >= 1)
    {
      char substr[256];
      u64 substrIndex = 0;
      u32 paramIndex = 0;
      for (u64 i = 0; i <= strLength; ++i) // i is out of bound at the end
      {
        if (parameters[i] == ',' || i == strLength)
        {
          ASSERT(paramIndex < paramCount);

          clap_param_info_t paramInfo;
          pluginParams->get_info(plugin, paramIndex, &paramInfo);
          ++paramIndex;

          substr[i - substrIndex] = '\0';

          f64 value = F64::inf;
          if (!pluginParams->text_to_value(plugin, paramInfo.id, (char*)substr, &value))
            value = atof((char*)substr);
          ASSERT(value != F64::inf);

          clap_event_param_value ev;
          ev.header.time = 0;
          ev.header.type = CLAP_EVENT_PARAM_VALUE;
          ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID;
          ev.header.flags = 0;
          ev.header.size = sizeof(ev);
          ev.param_id = paramInfo.id;
          ev.cookie = nullptr;
          ev.port_index = 0;
          ev.key = -1;
          ev.channel = -1;
          ev.note_id = -1;
          ev.value = value;

          clapTryPush(pluginInstances.out_events, &ev.header);

          substrIndex = i + 1;
        }
        substr[i - substrIndex] = parameters[i]; // carefull overrun at the end. It is not accessed so it should be fine.
      }
    }
  }

  pluginInstances.sheduleEventFlush = true;
}

#endif // SHR3D_SFX_PLUGIN_CLAP
