// Copyright (c) 2021 Giorgos Vougioukas
//
// The license can be found in the LICENSE file.

#include "thalia/plugin.h"

#if defined(_WIN32)
  #define THA_PLUGIN_EXTENSION ".dll"
#elif defined(__APPLE__)
  #define THA_PLUGIN_EXTENSION ".dylib"
#elif defined(__linux__)
  #define THA_PLUGIN_EXTENSION ".so"
#else
  #error "Plugin extension not right"
#endif

#include "WDL/assocarray.h"
#include "WDL/ptrlist.h"
#include "WDL/dirscan.h"
#include "WDL/wdlcstring.h"

static THA_PluginInfo plugin_info;
static WDL_StringKeyedArray<HINSTANCE> modules;

static int RegisterCalled(const char *name, void *info_struct)
{
  if (name == NULL || info_struct == NULL)
  {
    return 0;
  }

  g_pluginimport.Insert((INT_PTR)info_struct, strdup(name));

  return 1;
}

static void *GetFuncCalled(const char *name)
{
  if (!name) return NULL;
  return g_pluginexport.Get(name); // if not found returns 0
}

static void PrepareFuncCalls()
{
  g_pluginexport.Insert("THA_GetAudioDeviceSamplerate", (void *)&THA_GetAudioDeviceSamplerate);
  g_pluginexport.Insert("THA_GetAudioDeviceBitDepth", (void *)&THA_GetAudioDeviceBitDepth);
  g_pluginexport.Insert("THA_GetAudioDeviceOutputChannels", (void *)&THA_GetAudioDeviceOutputChannels);
  g_pluginexport.Insert("THA_GetDiskReadMode", (void *)&THA_GetDiskReadMode);
  g_pluginexport.Insert("THA_GetDiskWriteMode", (void *)&THA_GetDiskWriteMode);
  g_pluginexport.Insert("THA_GetDiskIOPriority", (void *)&THA_GetDiskIOPriority);
  g_pluginexport.Insert("THA_GetDiskIOSleepStep", (void *)&THA_GetDiskIOSleepStep);
  g_pluginexport.Insert("THA_GetAntidoteBitDepth", (void *)&THA_GetAntidoteBitDepth);
  g_pluginexport.Insert("THA_GetAudioSystem", (void *)&THA_GetAudioSystem);
  g_pluginexport.Insert("THA_GetResampleMode", (void *)&THA_GetResampleMode);
  g_pluginexport.Insert("THA_GetWASAPIExclusiveMode", (void *)&THA_GetWASAPIExclusiveMode);
}

THA_IFilePic *CreateFilePic(const char *filename)
{
  for (int i = 0; i < g_pluginimport.GetSize(); i++)
  {
    INT_PTR key;

    if (!strcmp(g_pluginimport.Enumerate(i, &key), "apic"))
    {
      THA_FilePicRegister *fpr = (THA_FilePicRegister *)g_pluginimport.ReverseLookup(g_pluginimport.Enumerate(i));

      if (fpr)
      {
        THA_IFilePic *p = fpr->CreateFromFile(filename);

        if (p)
        {
          return p;
        }
        else
        {
          delete p;
        }
      }
    }
  }

  return NULL;
}

THA_IFileInput *CreateFileInput(const char *filename)
{
  for (int i = 0; i < g_pluginimport.GetSize(); i++)
  {
    INT_PTR key;

    if (!strcmp(g_pluginimport.Enumerate(i, &key), "input"))
    {
      THA_FileInputRegister *fir = (THA_FileInputRegister *)g_pluginimport.ReverseLookup(g_pluginimport.Enumerate(i));

      if (fir)
      {
        THA_IFileInput *p = fir->CreateFromFile(filename);

        if (p)
        {
          return p;
        }
        else
        {
          delete p;
        }
      }
    }
  }

  return NULL;
}

THA_IFileTag *CreateFileTag(const char *filename)
{
  for (int i = 0; i < g_pluginimport.GetSize(); i++)
  {
    if (!strcmp(g_pluginimport.Enumerate(i), "tag"))
    {
      THA_FileTagRegister *ftr = (THA_FileTagRegister *)g_pluginimport.ReverseLookup(g_pluginimport.Enumerate(i));

      if (ftr)
      {
        THA_IFileTag *p = ftr->CreateFromFile(filename);

        if (p)
        {
          return p;
        }
        else
        {
          delete p;
        }
      }
    }
  }

  return NULL;
}

THA_IFileTag *CreateEditFileTag(const char *filename)
{
  for (int i = 0; i < g_pluginimport.GetSize(); i++)
  {
    if (!strcmp(g_pluginimport.Enumerate(i), "edit_tag"))
    {
      THA_FileTagRegister *ftr = (THA_FileTagRegister *)g_pluginimport.ReverseLookup(g_pluginimport.Enumerate(i));

      if (ftr)
      {
        THA_IFileTag *p = ftr->CreateFromFile(filename);

        if (p)
        {
          return p;
        }
        else
        {
          delete p;
        }
      }
    }
  }

  return NULL;
}

THA_IAudioStreamer *CreateAudioStreamer()
{
  INT_PTR key;

  for (int i = 0; i < g_pluginimport.GetSize(); i++)
  {
    if (!strcmp(g_pluginimport.Enumerate(i, &key), "audio_streamer"))
    {
      THA_AudioStreamerRegister *asr = (THA_AudioStreamerRegister *)key;

      if (asr)
      {
        THA_IAudioStreamer *p = asr->CreateAudioStreamer();

        if (p)
        {
          return p;
        }
        else
        {
          delete p;
        }
      }
    }
  }

  return NULL;
}

THA_IAudioStreamerDevice *CreateAudioStreamerDevice(const char *name)
{
  INT_PTR key;

  WDL_FastString str("audio_device:");
  str.Append(name);

  for (int i = 0; i < g_pluginimport.GetSize(); i++)
  {
    if (!strcmp(g_pluginimport.Enumerate(i, &key), str.Get()))
    {
      THA_AudioStreamerDeviceRegister *asdr = (THA_AudioStreamerDeviceRegister *)key;

      if (asdr)
      {
        THA_IAudioStreamerDevice *p = asdr->CreateAudioStreamerDevice();

        if (p)
        {
          return p;
        }
        else
        {
          delete p;
        }
      }
    }
  }

  return NULL;
}

THA_IPitchShift *CreatePitchShift(const char *name)
{
  INT_PTR key;

  WDL_FastString str("pitch_shift:");
  str.Append(name);

  for (int i = 0; i < g_pluginimport.GetSize(); i++)
  {
    if (!strcmp(g_pluginimport.Enumerate(i, &key), str.Get()))
    {
      THA_PitchShiftRegister *psr = (THA_PitchShiftRegister *)key;

      if (psr)
      {
        THA_IPitchShift *p = psr->CreatePitchShift();

        if (p)
        {
          return p;
        }
        else
        {
          delete p;
        }
      }
    }
  }

  return NULL;
}

THA_IFXProcessor *CreateFXProcessor()
{
  INT_PTR key;

  WDL_FastString str("fx");

  for (int i = 0; i < g_pluginimport.GetSize(); i++)
  {
    if (!strcmp(g_pluginimport.Enumerate(i, &key), str.Get()))
    {
      THA_FXProcessorRegister *fxpr = (THA_FXProcessorRegister *)key;

      if (fxpr)
      {
        THA_IFXProcessor *p = fxpr->CreateFXProcessor();

        if (p)
        {
          return p;
        }
        else
        {
          delete p;
        }
      }
    }
  }

  return NULL;
}

void LoadPlugins()
{
  WDL_ASSERT(g_pluginimport.GetSize() == 0);
  WDL_ASSERT(g_pluginexport.GetSize() == 0);
  WDL_ASSERT(modules.GetSize() == 0);

  plugin_info.caller_version = 120;
  plugin_info.Register = &RegisterCalled;
  plugin_info.GetFunc = &GetFuncCalled;

  PrepareFuncCalls();

  WDL_DirScan dir;
  WDL_FastString fn;

  WDL_FastString path(g_modpath);
  path.Append("ext" WDL_DIRCHAR_STR);

  WDL_PtrList_DeleteOnDestroy<WDL_FastString> file_list;

  if (!dir.First(path.Get()))
  {
    do
    {
      if (!dir.GetCurrentIsDirectory())
      {
        dir.GetCurrentFullFN(&fn);

        if (!strcmp(fn.get_fileext(), THA_PLUGIN_EXTENSION))
        {
          file_list.Add(new WDL_FastString(fn.Get()));
        }
      }
    }
    while (!dir.Next());
  }

  if (file_list.GetSize() == 0)
  {
    MessageBox(NULL, "Thalia could not find any plugin. Press OK to exit Thalia.",
      "Plugin error", MB_OK);
    exit(1);
  }

  for (int i = 0; i < file_list.GetSize(); i++)
  {
    WDL_FastString *file = file_list.Get(i);

    if (!file) continue;

    HINSTANCE inst = NULL;
    inst = LoadLibrary(file->Get());

    if (inst)
    {
      modules.Insert(file->Get(), inst);
    }
    else
    {
      WDL_FastString liberr;
      liberr.SetFormatted(1024, "Thalia could not load the following library:\n\n%s",
        file->Get());
      MessageBox(NULL, liberr.Get(), "Library error", MB_OK);
    }
  }

  typedef int (*PluginEntry)(THA_PLUGIN_HINSTANCE instance, THA_PluginInfo *rec);

  WDL_FastString err;
  bool display_error = false;
  err.Set("Thalia could not load the following plugin:\n\n");

  for (int i = 0; i < modules.GetSize(); i++)
  {
    PluginEntry plugin_entry = (PluginEntry)GetProcAddress(modules.Enumerate(i),
      THA_PLUGIN_ENTRYPOINT_NAME);

#if defined(_WIN32)
    HINSTANCE inst = g_inst;
    //HINSTANCE inst = GetModuleHandle(NULL);
#else
    HINSTANCE inst = NULL;
#endif

    //int success = plugin_entry(inst, &plugin_info);
    int success = plugin_entry(modules.Enumerate(i), &plugin_info);

    if (!success)
    {
      display_error = true;
      err.AppendFormatted(2048, "%s\n",
        WDL_get_filepart(modules.ReverseLookup(modules.Enumerate(i))));
    }
  }

  if (display_error)
  {
    err.Append("\nPress OK to continue with limited functionality.");
    MessageBox(NULL, err.Get(), "Plugin error", MB_OK);
  }

  WDL_ASSERT(g_pluginimport.GetSize() > 0);
  WDL_ASSERT(g_pluginexport.GetSize() > 0);
}

void ReleasePlugins()
{
  for (int i = 0; i < modules.GetSize(); i++)
  {
    FreeLibrary(modules.Enumerate(i));
  }

  modules.DeleteAll();
  g_pluginimport.DeleteAll();
  g_pluginexport.DeleteAll();
}
