// SPDX-License-Identifier: Unlicense

#include <X11/Xlib.h>
#include <X11/Xaw/Form.h>
#include <X11/Xaw/Label.h>
#include <X11/Xaw/Scrollbar.h>
#include <X11/Xaw/Command.h>
#include <X11/Xaw/Cardinals.h>
#include <X11/StringDefs.h>

#include "clap/clap.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <math.h>

#define GUI_WIDTH 300
#define GUI_HEIGHT 360

enum Parameter
{
  onOff,
  drive,
  gain,
  COUNT
};

struct MyPlugin
{
  clap_plugin_t plugin;
  double parameter[Parameter::COUNT]{ 1.0, 0.5, 0.5 };
  Window window;
  Display* display;
  GC mygc;
};

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.distortion.gui.x11",
  .name = "SfxPluginClapDistortionGuiX11",
  .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 Gui X11",
  .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::onOff:
      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, "On/Off");
      param_info->min_value = 0.0;
      param_info->max_value = 1.0;
      param_info->default_value = 1.0;
      return true;
    case Parameter::drive:
      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, "Drive");
      param_info->min_value = 0.0;
      param_info->max_value = 1.0;
      param_info->default_value = 0.5;
      return true;
    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::onOff:
      *value = my_plugin->parameter[param_id];
      return true;
    case Parameter::drive:
      *value = my_plugin->parameter[param_id];
      return true;
    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::onOff:
      snprintf(display, size, value > 0.5 ? "On" : "Off");
      return true;
    case Parameter::drive:
      snprintf(display, size, "%f", value);
      return true;
    case Parameter::gain:
      snprintf(display, size, "%f", 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::onOff:
      *value = strcmp(display, "On") == 0 ? 1.0 : 0.0;
      return true;
    case Parameter::drive:
      *value = 0.5 * atof(display);
      return true;
    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 bool plugin_gui__is_api_supported(const clap_plugin_t* /*plugin*/, const char* api, bool is_floating)
{
  return 0 == strcmp(api, CLAP_WINDOW_API_X11) && !is_floating;
}

static bool plugin_gui__get_preferred_api(const clap_plugin_t* /*plugin*/, const char** api, bool* is_floating)
{
  *api = CLAP_WINDOW_API_X11;
  *is_floating = false;
  return true;
}

// static void update_value_label(MyPlugin* plugin, int param, double value) {
//   char buffer[16];
//   switch (param) {
//   case onOff:
//     snprintf(buffer, sizeof(buffer), value > 0.5 ? "On" : "Off");
//     break;
//   default:
//     snprintf(buffer, sizeof(buffer), "%.2f", value);
//     break;
//   }
//   XtVaSetValues(plugin->values[param], XtNlabel, buffer, NULL);
// }
//
// static void slider_callback(Widget widget, XtPointer client_data, XtPointer call_data) {
//   MyPlugin* plugin = (MyPlugin*)client_data;
//
//   for (int i = 0; i < 3; ++i) {
//     if (plugin->sliders[i] == widget) {
//       float fraction;
//       XtVaGetValues(widget, XtNtopOfThumb, &fraction, NULL);
//       double value = fraction;
//       if (i == onOff)
//         value = value > 0.5 ? 1.0 : 0.0;
//
//       plugin->parameter[i] = value;
//       update_value_label(plugin, i, value);
//     }
//   }
// }



// static void GUIOnPOSIXFD(MyPlugin *plugin) {
//   XFlush(plugin->display);
//
//   if (XPending(plugin->display)) {
//     XEvent event;
//     XNextEvent(plugin->display, &event);
//
//     while (XPending(plugin->display)) {
//       XEvent event0;
//       XNextEvent(plugin-->display, &event0);
//
//       if (event.type == MotionNotify && event0.type == MotionNotify) {
//         // Merge adjacent mouse motion events.
//       } else {
//         GUIX11ProcessEvent(plugin, &event);
//         XFlush(plugin->display);
//       }
//
//       event = event0;
//     }
//
//     GUIX11ProcessEvent(plugin, &event);
//     XFlush(plugin->display);
//     GUIPaint(plugin, true);
//   }
//
//   XFlush(plugin->display);
// }



static void create_gui(MyPlugin* plugin)
{
  plugin->display = XOpenDisplay(NULL);
  XSetWindowAttributes attributes {};
  plugin->window = XCreateWindow(plugin->display, DefaultRootWindow(plugin->display), 0, 0, GUI_WIDTH, GUI_HEIGHT, 0, 0, InputOutput, CopyFromParent, CWOverrideRedirect, &attributes);
  XStoreName(plugin->display, plugin->window, plugin__descriptor.name);

  Atom embedInfoAtom = XInternAtom(plugin->display, "_XEMBED_INFO", 0);
  uint32_t embedInfoData[2] = { 0 /* version */, 0 /* not mapped */ };
  XChangeProperty(plugin->display, plugin->window, embedInfoAtom, embedInfoAtom, 32, PropModeReplace, (uint8_t*) embedInfoData, 2);

  XSizeHints sizeHints = {};
  sizeHints.flags = PMinSize | PMaxSize;
  sizeHints.min_width = sizeHints.max_width = GUI_WIDTH;
  sizeHints.min_height = sizeHints.max_height = GUI_HEIGHT;
    XSetWMNormalHints(plugin->display, plugin->window, &sizeHints);

  XSelectInput(plugin->display, plugin->window, SubstructureNotifyMask | ExposureMask | PointerMotionMask
          | ButtonPressMask | ButtonReleaseMask | KeyPressMask | KeyReleaseMask | StructureNotifyMask
          | EnterWindowMask | LeaveWindowMask | ButtonMotionMask | KeymapStateMask | FocusChangeMask | PropertyChangeMask);

  // plugin->image = XCreateImage(plugin->display, DefaultVisual(plugin->display, 0), 24, ZPixmap, 0, NULL, 10, 10, 32, 0);
  // plugin->bits = (uint32_t *) calloc(1, GUI_WIDTH * GUI_HEIGHT * 4);
  // plugin->image->width = GUI_WIDTH;
  // plugin->image->height = GUI_HEIGHT;
  // plugin->image->bytes_per_line = GUI_WIDTH * 4;
  // plugin->image->data = (char *) plugin->gui->bits;
  //
  // // Register the file descriptor we'll receive events from.
  // if (plugin->hostPOSIXFDSupport && plugin->hostPOSIXFDSupport->register_fd) {
  //   plugin->hostPOSIXFDSupport->register_fd(plugin->host, ConnectionNumber(plugin->gui->display), CLAP_POSIX_FD_READ);
  // }



  //   char hello[] = "Hello World!";
  //   char hi[] = "hi!";
  //
  //   XEvent myevent;
  //   KeySym mykey;
  //
  //   XSizeHints myhint;
  //
  //   int myscreen;
  //   unsigned long myforeground, mybackground;
  //   int i;
  //   char text[10];
  //   int done;
  //
  //   /* setup display/screen */
  //   plugin->display = XOpenDisplay("");
  //
  //   myscreen = DefaultScreen(plugin->display);
  //
  //   /* drawing contexts for a window */
  //   myforeground = BlackPixel(plugin->display, myscreen);
  //   mybackground = WhitePixel(plugin->display, myscreen);
  //   myhint.x = 200;
  //   myhint.y = 300;
  //   myhint.width = GUI_WIDTH;
  //   myhint.height = GUI_HEIGHT;
  //   myhint.flags = PPosition|PSize;
  //
  //   /* create window */
  //   plugin->window = XCreateSimpleWindow(plugin->display, DefaultRootWindow(plugin->display),
  //                                  myhint.x, myhint.y,
  //                                  myhint.width, myhint.height,
  //                                  5, myforeground, mybackground);
  //
  //   /* window manager properties (yes, use of StdProp is obsolete) */
  //   XSetStandardProperties(plugin->display, plugin->window, hello, hello, None, nullptr, 0, &myhint);
  //
  //   /* graphics context */
  //   plugin->mygc = XCreateGC(plugin->display, plugin->window, 0, 0);
  //   XSetBackground(plugin->display, plugin->mygc, mybackground);
  //   XSetForeground(plugin->display, plugin->mygc, myforeground);
  //
  //   /* allow receiving mouse events */
  //   XSelectInput(plugin->display,plugin->window, ButtonPressMask|KeyPressMask|ExposureMask);
  //
  //   /* show up window */
  //   XMapRaised(plugin->display, plugin->window);
  //
  // /* event loop */
  // done = 0;
  // while(done==0){
  //
  //   /* fetch event */
  //   XNextEvent(plugin->display, &myevent);
  //
  //   switch(myevent.type){
  //
  //     case Expose:
  //       /* Window was showed. */
  //       if(myevent.xexpose.count==0)
  //         XDrawImageString(myevent.xexpose.display,
  //                          myevent.xexpose.window,
  //                          plugin->mygc,
  //                          50, 50,
  //                          hello, strlen(hello));
  //       break;
  //     case MappingNotify:
  //       /* Modifier key was up/down. */
  //       XRefreshKeyboardMapping(&myevent.xmapping);
  //       break;
  //     case ButtonPress:
  //       /* Mouse button was pressed. */
  //       XDrawImageString(myevent.xbutton.display,
  //                        myevent.xbutton.window,
  //                        plugin->mygc,
  //                        myevent.xbutton.x, myevent.xbutton.y,
  //                        hi, strlen(hi));
  //       break;
  //     case KeyPress:
  //       /* Key input. */
  //       i = XLookupString(&myevent.xkey, text, 10, &mykey, 0);
  //       if(i==1 && text[0]=='q') done = 1;
  //       break;
  //   }
  // }

  /* finalization */
  // XFreeGC(mydisplay,mygc);
  // XDestroyWindow(mydisplay, mywindow);
  // XCloseDisplay(mydisplay);
  //
  // exit(0);
}

static bool plugin_gui__create(const clap_plugin_t* plugin, const char* api, bool is_floating)
{
  if (strcmp(api, CLAP_WINDOW_API_X11) != 0 || is_floating)
    return false;

  MyPlugin* my_plugin = (MyPlugin*)plugin->plugin_data;

  create_gui(my_plugin);
  return true;
}

static void plugin_gui__destroy(const clap_plugin_t* plugin)
{
  MyPlugin* my_plugin = (MyPlugin*)plugin->plugin_data;

  /* finalization */
  XFreeGC(my_plugin->display, my_plugin->mygc);
  XDestroyWindow(my_plugin->display, my_plugin->window);
  XCloseDisplay(my_plugin->display);
}

static bool plugin_gui__set_scale(const clap_plugin_t* /*plugin*/, const double /*scale*/)
{
  return false;
}

static bool plugin_gui__get_size(const clap_plugin_t* /*plugin*/, uint32_t* width, uint32_t* height)
{
  *width = GUI_WIDTH;
  *height = GUI_HEIGHT;
  return true;
}

static bool plugin_gui__can_resize(const clap_plugin_t* /*plugin*/)
{
  return false;
}

static bool plugin_gui__get_resize_hints(const clap_plugin_t* /*plugin*/, clap_gui_resize_hints_t* /*hints*/)
{
  return false;
}

static bool plugin_gui__adjust_size(const clap_plugin_t* plugin, uint32_t* width, uint32_t* height)
{
  return plugin_gui__get_size(plugin, width, height);
}

static bool plugin_gui__set_size(const clap_plugin_t* /*plugin*/, uint32_t /*width*/, uint32_t /*height*/)
{
  return true;
}

static bool plugin_gui__set_parent(const clap_plugin_t* plugin, const clap_window_t* window)
{
  assert(0 == strcmp(window->api, CLAP_WINDOW_API_X11));

  MyPlugin* my_plugin = (MyPlugin*)plugin->plugin_data;

  XReparentWindow(my_plugin->display, my_plugin->window, window->x11, 0, 0);
  //XMapWindow(my_plugin->display, my_plugin->window);
  XFlush(my_plugin->display);

  return true;
}

static bool plugin_gui__set_transient(const clap_plugin_t* /*plugin*/, const clap_window_t* /*window*/)
{
  return false;
}

static void plugin_gui__suggest_title(const clap_plugin_t* /*plugin*/, const char* /*title*/)
{
}

static bool plugin_gui__show(const clap_plugin_t* plugin)
{
  MyPlugin* my_plugin = (MyPlugin*)plugin->plugin_data;
  // XtRealizeWidget(my_plugin->top_widget);
  // XtAppMainLoop(XtWidgetToApplicationContext(my_plugin->top_widget));
  return true;
}

static bool plugin_gui__hide(const clap_plugin_t* plugin)
{
  MyPlugin* my_plugin = (MyPlugin*)plugin->plugin_data;
  //XtUnrealizeWidget(my_plugin->top_widget);
  return true;
}

static const clap_plugin_gui_t extensionGUI{
  .is_api_supported = plugin_gui__is_api_supported,
  .get_preferred_api = plugin_gui__get_preferred_api,
  .create = plugin_gui__create,
  .destroy = plugin_gui__destroy,
  .set_scale = plugin_gui__set_scale,
  .get_size = plugin_gui__get_size,
  .can_resize = plugin_gui__can_resize,
  .get_resize_hints = plugin_gui__get_resize_hints,
  .adjust_size = plugin_gui__adjust_size,
  .set_size = plugin_gui__set_size,
  .set_parent = plugin_gui__set_parent,
  .set_transient = plugin_gui__set_transient,
  .suggest_title = plugin_gui__suggest_title,
  .show = plugin_gui__show,
  .hide = plugin_gui__hide,
};

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 bool onOff = my_plugin->parameter[Parameter::onOff] > 0.5;
  const float drive_ = float(1.0 + my_plugin->parameter[Parameter::drive] * 100.0f);
  const float gain = float(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;
      }
    }

    if (onOff)
    {
      for (uint32_t index = i; index < frameCount; index++)
      {
        process->audio_outputs[0].data32[0][index] = distortion(process->audio_inputs[0].data32[0][index] * drive_) * gain;
        process->audio_outputs[0].data32[1][index] = distortion(process->audio_inputs[0].data32[1][index] * drive_) * gain;
      }
    }
    else
    {
      memcpy(process->audio_outputs[0].data32[0], process->audio_inputs[0].data32[0], sizeof(float) * frameCount);
      memcpy(process->audio_outputs[0].data32[1], process->audio_inputs[0].data32[1], sizeof(float) * frameCount);
    }

    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;
  if (0 == strcmp(id, CLAP_EXT_GUI))
    return &extensionGUI;
  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
};
