#include "sfxPluginVst3.h"

#ifdef SHR3D_SFX_PLUGIN_VST3

// MIT License
// 
// Copyright (c) 2025, Steinberg Media Technologies GmbH
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#include "file.h"
#include "string_.h"

#include "public.sdk/source/vst/utility/optional.h"
#include "public.sdk/source/vst/utility/stringconvert.h"
#include "public.sdk/source/vst/hosting/module.h"
#include "public.sdk/source/vst/vstpresetfile.h"
#include "base/source/fbuffer.h"

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <shlobj.h>
#ifdef SHR3D_SFX_PLUGIN_VST3_BENCHMARK
#include <Psapi.h>
#endif // SHR3D_SFX_PLUGIN_VST3_BENCHMARK

#include <algorithm>
#include <iostream>

#if SMTG_CPP17
#if __has_include(<filesystem>)
#define USE_FILESYSTEM 1
#elif __has_include(<experimental/filesystem>)
#define USE_FILESYSTEM 0
#endif
#else
#define USE_FILESYSTEM 0
#endif

#if USE_FILESYSTEM == 1
#include <filesystem>
namespace filesystem = std::filesystem;
#else
// The <experimental/filesystem> header is deprecated. It is superseded by the C++17 <filesystem>
// header. You can define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING to silence the
// warning, otherwise the build will fail in VS2020 16.3.0
#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING
#include <experimental/filesystem>
namespace filesystem = std::experimental::filesystem;
#endif

#pragma comment(lib, "Shell32")

//------------------------------------------------------------------------
extern "C" {
  using InitModuleFunc = bool (PLUGIN_API*) ();
  using ExitModuleFunc = bool (PLUGIN_API*) ();
}

//------------------------------------------------------------------------
namespace VST3 {
  namespace Hosting {

    //------------------------------------------------------------------------
    namespace {

#if SMTG_OS_WINDOWS_ARM
#define USE_OLE 0
#if SMTG_PLATFORM_64
      constexpr auto architectureString = "arm_64-win";
#else
      constexpr auto architectureString = "arm-win";
#endif
#else
#define USE_OLE !USE_FILESYSTEM
#if SMTG_PLATFORM_64
      constexpr auto architectureString = "x86_64-win";
#else
      constexpr auto architectureString = "x86-win";
#endif
#endif

#if USE_OLE
      //------------------------------------------------------------------------
      struct Ole
      {
        static Ole& instance()
        {
          static Ole gInstance;
          return gInstance;
        }

      private:
        Ole() { OleInitialize(nullptr); }
        ~Ole() { OleUninitialize(); }
      };
#endif // USE_OLE

      //------------------------------------------------------------------------
      class Win32Module : public Module
      {
      public:
        template <typename T>
        T getFunctionPointer(const char* name)
        {
          return reinterpret_cast<T> (GetProcAddress(mModule, name));
        }

        ~Win32Module() override
        {
          factory = PluginFactory(nullptr);

          if (mModule)
          {
            // ExitDll is optional
            if (auto dllExit = getFunctionPointer<ExitModuleFunc>("ExitDll"))
              dllExit();

            FreeLibrary((HMODULE)mModule);
          }
        }

        bool load(const std::string& inPath, std::string& errorDescription) override
        {
          filesystem::path p(inPath);
          auto filename = p.filename();
          p /= "Contents";
          p /= architectureString;
          p /= filename;
          auto wideStr = StringConvert::convert(p.string());
          mModule = LoadLibraryW(reinterpret_cast<LPCWSTR> (wideStr.data()));
          if (!mModule)
          {
            wideStr = StringConvert::convert(inPath);
            mModule = LoadLibraryW(reinterpret_cast<LPCWSTR> (wideStr.data()));
            if (!mModule)
            {
              auto lastError = GetLastError();
              LPVOID lpMessageBuffer;
              FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                nullptr, lastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                (LPSTR)&lpMessageBuffer, 0, nullptr);
              errorDescription = "LoadLibray failed: " + std::string((char*)lpMessageBuffer);
              LocalFree(lpMessageBuffer);

              return false;
            }
          }
          auto factoryProc = getFunctionPointer<GetFactoryProc>("GetPluginFactory");
          if (!factoryProc)
          {
            errorDescription = "The dll does not export the required 'GetPluginFactory' function";
            return false;
          }
          // InitDll is optional
          auto dllEntry = getFunctionPointer<InitModuleFunc>("InitDll");
          if (dllEntry && !dllEntry())
          {
            errorDescription = "Calling 'InitDll' failed";
            return false;
          }
          auto f = Steinberg::FUnknownPtr<Steinberg::IPluginFactory>(owned(factoryProc()));
          if (!f)
          {
            errorDescription = "Calling 'GetPluginFactory' returned nullptr";
            return false;
          }
          factory = PluginFactory(f);
          return true;
        }

        HINSTANCE mModule{ nullptr };
      };

      //------------------------------------------------------------------------
      bool checkVST3Package(const filesystem::path& p, filesystem::path* result = nullptr)
      {
        auto path = p;
        path /= "Contents";
        path /= architectureString;
        path /= p.filename();
        auto hFile = CreateFileW(reinterpret_cast<LPCWSTR> (path.c_str()), GENERIC_READ, FILE_SHARE_READ,
          nullptr, OPEN_EXISTING, 0, nullptr);
        if (hFile != INVALID_HANDLE_VALUE)
        {
          CloseHandle(hFile);
          if (result)
            *result = path;
          return true;
        }
        return false;
      }

      //------------------------------------------------------------------------
      bool isFolderSymbolicLink(const filesystem::path& p)
      {
#if USE_FILESYSTEM
        if (/*filesystem::exists (p) &&*/ filesystem::is_symlink(p))
          return true;
#else
        std::wstring wString = p.generic_wstring();
        auto attrib = GetFileAttributesW(reinterpret_cast<LPCWSTR> (wString.c_str()));
        if (attrib & FILE_ATTRIBUTE_REPARSE_POINT)
        {
          auto hFile = CreateFileW(reinterpret_cast<LPCWSTR> (wString.c_str()), GENERIC_READ,
            FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
          if (hFile == INVALID_HANDLE_VALUE)
            return true;
          else
            CloseHandle(hFile);
        }
#endif
        return false;
      }

      //------------------------------------------------------------------------
      Optional<std::string> getKnownFolder(REFKNOWNFOLDERID folderID)
      {
        PWSTR wideStr{};
        if (FAILED(SHGetKnownFolderPath(folderID, 0, nullptr, &wideStr)))
          return {};
        return StringConvert::convert(Steinberg::wscast(wideStr));
      }

      //------------------------------------------------------------------------
      VST3::Optional<filesystem::path> resolveShellLink(const filesystem::path& p)
      {
#if USE_FILESYSTEM
        return { filesystem::read_symlink(p).lexically_normal() };
#else
#if USE_OLE
        Ole::instance();

        IShellLink* shellLink = nullptr;
        if (!SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
          IID_IShellLink, reinterpret_cast<LPVOID*> (&shellLink))))
          return {};

        IPersistFile* persistFile = nullptr;
        if (!SUCCEEDED(
          shellLink->QueryInterface(IID_IPersistFile, reinterpret_cast<void**> (&persistFile))))
          return {};

        if (!SUCCEEDED(persistFile->Load(p.wstring().data(), STGM_READ)))
          return {};

        if (!SUCCEEDED(shellLink->Resolve(nullptr, MAKELONG(SLR_NO_UI, 500))))
          return {};

        WCHAR resolvedPath[MAX_PATH];
        if (!SUCCEEDED(shellLink->GetPath(resolvedPath, MAX_PATH, nullptr, SLGP_SHORTPATH)))
          return {};

        std::wstring longPath;
        longPath.resize(MAX_PATH);
        auto numChars =
          GetLongPathNameW(resolvedPath, const_cast<wchar_t*> (longPath.data()), MAX_PATH);
        if (!numChars)
          return {};
        longPath.resize(numChars);

        persistFile->Release();
        shellLink->Release();

        return { filesystem::path(longPath) };
#else
        // TODO for ARM
        return {};
#endif
#endif
      }

      //------------------------------------------------------------------------
      void findFilesWithExt(const filesystem::path& path, const std::string& ext,
        Module::PathList& pathList, bool recursive = true)
      {
        for (auto& p : filesystem::directory_iterator(path))
        {
#if USE_FILESYSTEM
          filesystem::path finalPath(p);
          if (isFolderSymbolicLink(p))
          {
            if (auto res = resolveShellLink(p))
            {
              finalPath = *res;
              if (!filesystem::exists(finalPath))
                continue;
            }
            else
              continue;
          }
          const auto& cpExt = finalPath.extension();
          if (cpExt == ext)
          {
            filesystem::path vstPath(finalPath);
            if (checkVST3Package(vstPath))
            {
              pathList.push_back(vstPath.generic_string());
              continue;
            }
          }

          if (filesystem::is_directory(finalPath))
          {
            if (recursive)
              findFilesWithExt(finalPath, ext, pathList, recursive);
          }
          else if (cpExt == ext)
            pathList.push_back(finalPath.generic_string());
#else
          const auto& cp = p.path();
          const auto& cpExt = cp.extension();
          if (cpExt == ext)
          {
            if ((p.status().type() == filesystem::file_type::directory) ||
              isFolderSymbolicLink(p))
            {
              filesystem::path finalPath(p);
              if (checkVST3Package(finalPath))
              {
                pathList.push_back(finalPath.generic_string());
                continue;
              }
              findFilesWithExt(cp, ext, pathList, recursive);
            }
            else
              pathList.push_back(cp.generic_string());
          }
          else if (recursive)
          {
            if (p.status().type() == filesystem::file_type::directory)
            {
              findFilesWithExt(cp, ext, pathList, recursive);
            }
            else if (cpExt == ".lnk")
            {
              if (auto resolvedLink = resolveShellLink(cp))
              {
                if (resolvedLink->extension() == ext)
                {
                  if (filesystem::is_directory(*resolvedLink) ||
                    isFolderSymbolicLink(*resolvedLink))
                  {
                    filesystem::path finalPath(*resolvedLink);
                    if (checkVST3Package(finalPath))
                    {
                      pathList.push_back(finalPath.generic_string());
                      continue;
                    }
                    findFilesWithExt(*resolvedLink, ext, pathList, recursive);
                  }
                  else
                    pathList.push_back(resolvedLink->generic_string());
                }
                else if (filesystem::is_directory(*resolvedLink))
                {
                  const auto& str = resolvedLink->generic_string();
                  if (cp.generic_string().compare(0, str.size(), str.data(),
                    str.size()) != 0)
                    findFilesWithExt(*resolvedLink, ext, pathList, recursive);
                }
              }
            }
          }
#endif
        }
      }

      //------------------------------------------------------------------------
      void findModules(const filesystem::path& path, Module::PathList& pathList)
      {
        if (filesystem::exists(path))
          findFilesWithExt(path, ".vst3", pathList);
      }

      //------------------------------------------------------------------------
      Optional<filesystem::path> getContentsDirectoryFromModuleExecutablePath(
        const std::string& modulePath)
      {
        filesystem::path path(modulePath);

        path = path.parent_path();
        if (path.filename() != architectureString)
          return {};
        path = path.parent_path();
        if (path.filename() != "Contents")
          return {};

        return Optional<filesystem::path> {std::move(path)};
      }

      //------------------------------------------------------------------------
    } // anonymous

    //------------------------------------------------------------------------
    Module::Ptr Module::create(const std::string& path, std::string& errorDescription)
    {
      auto _module = std::make_shared<Win32Module>();
      if (_module->load(path, errorDescription))
      {
        _module->path = path;
        auto it = std::find_if(path.rbegin(), path.rend(),
          [](const std::string::value_type& c) { return c == '/'; });
        if (it != path.rend())
          _module->name = { it.base(), path.end() };
        return _module;
      }
      return nullptr;
    }

    //------------------------------------------------------------------------
    Module::PathList Module::getModulePaths()
    {
      // find plug-ins located in common/VST3
      PathList list;
      if (auto knownFolder = getKnownFolder(FOLDERID_UserProgramFilesCommon))
      {
        filesystem::path p(*knownFolder);
        p.append("VST3");
        findModules(p, list);
      }

      if (auto knownFolder = getKnownFolder(FOLDERID_ProgramFilesCommon))
      {
        filesystem::path p(*knownFolder);
        p.append("VST3");
        findModules(p, list);
      }

      // find plug-ins located in VST3 (application folder)
      WCHAR modulePath[MAX_PATH + 1];
      GetModuleFileNameW(nullptr, modulePath, MAX_PATH);
      auto appPath = StringConvert::convert(Steinberg::wscast(modulePath));
      filesystem::path path(appPath);
      path = path.parent_path();
      path = path.append("VST3");
      findModules(path, list);

      return list;
    }

    //------------------------------------------------------------------------
    Optional<std::string> Module::getModuleInfoPath(const std::string& modulePath)
    {
      auto path = getContentsDirectoryFromModuleExecutablePath(modulePath);
      if (!path)
      {
        filesystem::path p;
        if (!checkVST3Package({ modulePath }, &p))
          return {};
        p = p.parent_path();
        p = p.parent_path();
        path = Optional<filesystem::path>{ p };
      }
      *path /= "moduleinfo.json";
      if (filesystem::exists(*path))
      {
        return { path->generic_string() };
      }
      return {};
    }

    //------------------------------------------------------------------------
    Module::SnapshotList Module::getSnapshots(const std::string& modulePath)
    {
      SnapshotList result;
      auto path = getContentsDirectoryFromModuleExecutablePath(modulePath);
      if (!path)
      {
        filesystem::path p;
        if (!checkVST3Package({ modulePath }, &p))
          return result;
        p = p.parent_path();
        p = p.parent_path();
        path = Optional<filesystem::path>(p);
      }

      *path /= "Resources";
      *path /= "Snapshots";

      if (filesystem::exists(*path) == false)
        return result;

      PathList pngList;
      findFilesWithExt(*path, ".png", pngList, false);
      for (auto& png : pngList)
      {
        filesystem::path p(png);
        auto filename = p.filename().generic_string();
        auto uid = Snapshot::decodeUID(filename);
        if (!uid)
          continue;
        auto scaleFactor = 1.;
        if (auto decodedScaleFactor = Snapshot::decodeScaleFactor(filename))
          scaleFactor = *decodedScaleFactor;

        Module::Snapshot::ImageDesc desc;
        desc.scaleFactor = scaleFactor;
        desc.path = std::move(png);
        bool found = false;
        for (auto& entry : result)
        {
          if (entry.uid != *uid)
            continue;
          found = true;
          entry.images.emplace_back(std::move(desc));
          break;
        }
        if (found)
          continue;
        Module::Snapshot snapshot;
        snapshot.uid = *uid;
        snapshot.images.emplace_back(std::move(desc));
        result.emplace_back(std::move(snapshot));
      }
      return result;
    }

    //------------------------------------------------------------------------
  } // Hosting
} // VST3

#include "global.h"
#include "version.h"

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

#include <pluginterfaces/gui/iplugview.h>
#include <pluginterfaces/vst/ivstaudioprocessor.h>
#include <pluginterfaces/vst/ivstcomponent.h>
#include <pluginterfaces/vst/ivsteditcontroller.h>
#include <pluginterfaces/vst/ivstparameterchanges.h>
#include <public.sdk/source/vst/hosting/hostclasses.h>
#include <public.sdk/source/vst/hosting/module.h>

#include <filesystem>
#include <functional>
#include <thread>

class ParameterValueQueue final : public Steinberg::Vst::IParamValueQueue
{
  struct Point
  {
    Steinberg::int32 sampleOffset;
    Steinberg::Vst::ParamValue paramValue;

    bool operator <(const Point& other) const
    {
      return sampleOffset < other.sampleOffset;
    }
  };

  bool mInitialized{ false };
  Steinberg::Vst::ParamID mId;
  std::vector<Point> mPoints;

public:

  ParameterValueQueue()
  {
    FUNKNOWN_CTOR
  }
  virtual ~ParameterValueQueue()
  {
    ASSERT(!mInitialized);
    FUNKNOWN_DTOR;
  }

  void initialize(Steinberg::Vst::ParamID id)
  {
    ASSERT(!mInitialized);//misuse

    mInitialized = true;
    mId = id;
  }

  bool isInitialized() const noexcept
  {
    return mInitialized;
  }

  void dispose() noexcept
  {
    mInitialized = false;
    mPoints.clear();
  }


  Steinberg::Vst::ParamID PLUGIN_API getParameterId() override
  {
    ASSERT(mInitialized);
    return mId;
  }

  Steinberg::int32 PLUGIN_API getPointCount() override
  {
    ASSERT(mInitialized);
    return static_cast<int>(mPoints.size());
  }

  Steinberg::tresult PLUGIN_API getPoint(Steinberg::int32 index, Steinberg::int32& sampleOffset, Steinberg::Vst::ParamValue& value) override
  {
    ASSERT(mInitialized);
    if (index >= 0 && index < mPoints.size())
    {
      sampleOffset = mPoints[index].sampleOffset;
      value = mPoints[index].paramValue;
      return Steinberg::kResultOk;
    }
    return Steinberg::kInvalidArgument;
  }

  Steinberg::tresult PLUGIN_API addPoint(Steinberg::int32 sampleOffset, Steinberg::Vst::ParamValue value, Steinberg::int32& index) override
  {
    //store points sorted by time value

    ASSERT(mInitialized);

    auto newPoint = Point{ sampleOffset, value };
    const auto it =
      std::lower_bound(mPoints.begin(), mPoints.end(), newPoint);

    if (it == mPoints.end())
    {
      index = i32(mPoints.size());
      mPoints.push_back(std::move(newPoint));
    }
    else
    {
      if (it->sampleOffset == sampleOffset)
        it->paramValue = value;
      else
        mPoints.insert(it, std::move(newPoint));
      index = std::distance(mPoints.begin(), it);
    }

    return Steinberg::kResultOk;
  }

  DECLARE_FUNKNOWN_METHODS
};
IMPLEMENT_FUNKNOWN_METHODS(ParameterValueQueue, Steinberg::Vst::IParamValueQueue, Steinberg::Vst::IParamValueQueue::iid)

//! Helper object. Global instance provides ParameterChanges
//! with ParameterValueQueue objects
class ParameterQueuePool final
{
  std::vector<Steinberg::IPtr<ParameterValueQueue>> mPool;
public:

  class QueueCleanup {
  public:
    void operator()(ParameterValueQueue* queue)
    {
      queue->dispose();
      queue->release();
    }
  };

  using ParameterValueQueuePtr = std::unique_ptr<
    ParameterValueQueue,
    QueueCleanup
  >;

  static ParameterQueuePool& Instance()
  {
    static ParameterQueuePool instance;
    return instance;
  }

  ParameterValueQueuePtr Get(Steinberg::Vst::ParamID id)
  {
    Steinberg::IPtr<ParameterValueQueue> queue;
    for (auto& q : mPool)
    {
      if (!q->isInitialized())
      {
        queue = q;
        break;
      }
    }
    if (queue == nullptr)
    {
      queue = new ParameterValueQueue;
      mPool.push_back(queue);
    }
    queue->initialize(id);
    queue->addRef();
    return { queue.get(), QueueCleanup { } };
  }

  //Cleanup
  void Reset() noexcept
  {
    mPool.clear();
  }

};

class ParameterChanges final : public Steinberg::Vst::IParameterChanges
{
  std::vector<ParameterQueuePool::ParameterValueQueuePtr> mParamQueues;

public:

  ParameterChanges()
  {
    FUNKNOWN_CTOR
  }

  virtual ~ParameterChanges()
  {
    clear();
    FUNKNOWN_DTOR;
  }

  Steinberg::int32 PLUGIN_API getParameterCount() override
  {
    return static_cast<Steinberg::int32>(mParamQueues.size());
  }

  Steinberg::Vst::IParamValueQueue* PLUGIN_API getParameterData(Steinberg::int32 index) override
  {
    if (index >= 0 && index < mParamQueues.size())
      return mParamQueues[index].get();
    return nullptr;
  }

  Steinberg::Vst::IParamValueQueue* PLUGIN_API addParameterData(const Steinberg::Vst::ParamID& id, Steinberg::int32& index) override
  {
    using namespace Steinberg;
    {
      for (int32 i = 0, count = static_cast<int32>(mParamQueues.size()); i < count; ++i)
      {
        auto& queue = mParamQueues[i];
        if (queue->getParameterId() == id)
        {
          index = i;
          return queue.get();
        }
      }
    }
    auto queue = ParameterQueuePool::Instance().Get(id);
    index = static_cast<int32>(mParamQueues.size());
    mParamQueues.push_back(std::move(queue));
    return mParamQueues.back().get();
  }

  void clear()
  {
    mParamQueues.clear();
  }

  DECLARE_FUNKNOWN_METHODS
};
IMPLEMENT_FUNKNOWN_METHODS(ParameterChanges, Steinberg::Vst::IParameterChanges, Steinberg::Vst::IParameterChanges::iid)

class ComponentHandler : public Steinberg::Vst::IComponentHandler
{
  std::atomic<Steinberg::Vst::IParameterChanges*> mFirst{ nullptr };
  std::atomic<Steinberg::Vst::IParameterChanges*> mSecond{ nullptr };
  std::atomic<Steinberg::Vst::IParameterChanges*> mPendingChanges{ nullptr };

public:
  using PendingChangesPtr = std::unique_ptr<
    Steinberg::Vst::IParameterChanges,
    std::function<void(Steinberg::Vst::IParameterChanges*)>>;

  ComponentHandler()
  {
    //We use two instances of ParameterChanges to ensure
    //that read/write is lock and wait free.
    //Once parameter value change is requested, they are
    //written either to the pending outgoing parameters,
    //if they are not yet requested, or to any of those
    //two instances whichever of them is available.
    //When done writing, parameter changes become pending.

    using namespace Steinberg;

    FUNKNOWN_CTOR
      auto first = new ParameterChanges;
    auto second = new ParameterChanges;
    if (first && second)
    {
      mFirst = first;
      mSecond = second;
    }
  }
  virtual ~ComponentHandler()
  {
    Steinberg::Vst::IParameterChanges* ptr{ nullptr };
    if (ptr = mFirst.load())
      ptr->release();
    if (ptr = mSecond.load())
      ptr->release();
    if (ptr = mPendingChanges.load())
      ptr->release();

    FUNKNOWN_DTOR;
  }

  Steinberg::tresult PLUGIN_API beginEdit(Steinberg::Vst::ParamID /*id*/) override
  {
    return Steinberg::kResultOk;
  }

  Steinberg::tresult PLUGIN_API performEdit(Steinberg::Vst::ParamID id, Steinberg::Vst::ParamValue valueNormalized) override
  {
    using namespace Steinberg;

    //Try grab pending changes first (processing thread will not
    //see them until they are applied)
    auto lock = mPendingChanges.exchange(nullptr);
    if (lock == nullptr)
    {
      //There is no pending changes, then try grab
      //any of the containers. At least one of them always should be
      //available, but another one may be grabbed by the processing thread
      lock = mFirst.exchange(nullptr);
      if (lock == nullptr)
        lock = mSecond.exchange(nullptr);

      //Grabbed object may contain some old data, clean it.
      //We surely don't want to delay the processing thread with cleanup
      //routines
      static_cast<ParameterChanges*>(lock)->clear();
    }

    int32 index;
    tresult result = kInternalError;

    if (const auto queue = lock->addParameterData(id, index))
      //Since we don't yet have a support for automation,
      //sampleOffset is always 0
      result = queue->addPoint(0, valueNormalized, index);
    //else
    // for some unknown reason we have failed to
    // find/create appropriate queue for this parameter

    //now processing thread can see changes
    mPendingChanges.exchange(lock);

    return result;
  }

  Steinberg::tresult PLUGIN_API endEdit(Steinberg::Vst::ParamID /*id*/) override
  {
    return Steinberg::kResultOk;
  }

  Steinberg::tresult PLUGIN_API restartComponent(Steinberg::int32 /*flags*/) override
  {
    return Steinberg::kNotImplemented;
  }

  PendingChangesPtr getPendingChanges()
  {
    using namespace Steinberg;

    const auto pendingChanges = mPendingChanges.exchange(nullptr);
    if (pendingChanges != nullptr)
    {
      return PendingChangesPtr{ pendingChanges,[handler = IPtr { this }](Vst::IParameterChanges* ptr)
      {
          //once the processing thread done reading changes
          //pointer is moved back
          ptr = handler->mFirst.exchange(ptr);
          if (ptr != nullptr)
             handler->mSecond.exchange(ptr);
       } };
    }
    return { nullptr };
  }


  DECLARE_FUNKNOWN_METHODS
};

IMPLEMENT_FUNKNOWN_METHODS(ComponentHandler, Steinberg::Vst::IComponentHandler, Steinberg::Vst::IComponentHandler::iid)

class ConnectionProxy final : public Steinberg::Vst::IConnectionPoint
{
  std::thread::id mThreadId;

  Steinberg::Vst::IConnectionPoint* mSource{ nullptr };
  Steinberg::Vst::IConnectionPoint* mTarget{ nullptr };
public:

  DECLARE_FUNKNOWN_METHODS;

  ConnectionProxy(Steinberg::Vst::IConnectionPoint* source)
    : mSource(source)
  {
    mThreadId = std::this_thread::get_id();
    FUNKNOWN_CTOR;
  }
  virtual ~ConnectionProxy()
  {
    FUNKNOWN_DTOR;
  }

  Steinberg::tresult PLUGIN_API connect(IConnectionPoint* other) override
  {
    if (other == nullptr)
      return Steinberg::kInvalidArgument;
    else if (mTarget != nullptr)
      return Steinberg::kResultFalse;

    //Looks a bit awkward, but the source can send messages to
    //the target during connection
    mTarget = other;
    auto result = mSource->connect(this);
    if (result != Steinberg::kResultOk)
      mTarget = nullptr;
    return result;
  }


  Steinberg::tresult PLUGIN_API disconnect(IConnectionPoint* other) override
  {
    if (other == nullptr)
      return Steinberg::kInvalidArgument;
    else if (other != mTarget)
      return Steinberg::kResultFalse;

    auto result = mSource->disconnect(this);
    if (result == Steinberg::kResultOk)
      mTarget = nullptr;
    return result;
  }

  Steinberg::tresult PLUGIN_API notify(Steinberg::Vst::IMessage* message) override
  {
    if (mTarget == nullptr ||
      std::this_thread::get_id() != mThreadId)
      return Steinberg::kResultFalse;

    return mTarget->notify(message);
  }
};

IMPLEMENT_FUNKNOWN_METHODS(ConnectionProxy, Steinberg::Vst::IConnectionPoint, Steinberg::Vst::IConnectionPoint::iid);


struct Vst3Plugin
{
  std::shared_ptr<VST3::Hosting::Module> module;
  std::vector<VST3::Hosting::ClassInfo> classInfos;

  Steinberg::Vst::ProcessSetup processSetup;

  Steinberg::IPtr<Steinberg::Vst::IComponent> effectComponent;
  Steinberg::IPtr<Steinberg::Vst::IEditController> editController;
  Steinberg::IPtr<Steinberg::Vst::IAudioProcessor> audioProcessor;
  Steinberg::IPtr<ComponentHandler> componentHandler;
  Steinberg::IPtr<Steinberg::Vst::IConnectionPoint> componentConnectionProxy;
  Steinberg::IPtr<Steinberg::Vst::IConnectionPoint> controllerConnectionProxy;
  Steinberg::IPlugView* plugView{};
};

static std::vector<Vst3Plugin> vst3Plugins;

//static void loadPluginInstance(Vst3Plugin& vst3Plugin)
//{
//  ASSERT(vst3Plugin.editController);
//
//  const i32 parameterCount = vst3Plugin.editController->getParameterCount();
//  for (i32 i = 0; i < parameterCount; ++i)
//  {
//    Steinberg::Vst::ParameterInfo parameterInfo{};
//    const Steinberg::tresult result = vst3Plugin.editController->getParameterInfo(i, parameterInfo);
//    ASSERT(result == Steinberg::kResultOk);
//
//    ASSERT(!(parameterInfo.flags & Steinberg::Vst::ParameterInfo::kIsReadOnly));
//
//    //const double value = vst3Plugin.editController->getParamNormalized(parameterInfo.id);
//  }
//}

#define INT32_SWAP(val) \
   ((i32) ( \
    (((u32) (val) & (u32) 0x000000ffU) << 24) | \
    (((u32) (val) & (u32) 0x0000ff00U) <<  8) | \
    (((u32) (val) & (u32) 0x00ff0000U) >>  8) | \
    (((u32) (val) & (u32) 0xff000000U) >> 24)))

static Steinberg::Vst::HostApplication context;

static void activateBuses(Steinberg::Vst::IComponent* effectComponent)
{
  {
    const i32 inputCount = effectComponent->getBusCount(Steinberg::Vst::kAudio, Steinberg::Vst::kInput);
    for (i32 i = 0; i < inputCount; ++i)
    {
      Steinberg::Vst::BusInfo busInfo;
      const Steinberg::tresult result = effectComponent->getBusInfo(Steinberg::Vst::kAudio, Steinberg::Vst::kInput, i, busInfo);
      ASSERT(result == Steinberg::kResultOk);
      if (busInfo.flags & Steinberg::Vst::BusInfo::kDefaultActive)
        effectComponent->activateBus(Steinberg::Vst::kAudio, Steinberg::Vst::kInput, i, true);
    }
  }

  {
    const i32 outputCount = effectComponent->getBusCount(Steinberg::Vst::kAudio, Steinberg::Vst::kOutput);
    for (i32 i = 0; i < outputCount; ++i)
    {
      Steinberg::Vst::BusInfo busInfo;
      const Steinberg::tresult result = effectComponent->getBusInfo(Steinberg::Vst::kAudio, Steinberg::Vst::kOutput, i, busInfo);
      ASSERT(result == Steinberg::kResultOk);
      if (busInfo.flags & Steinberg::Vst::BusInfo::kDefaultActive)
        effectComponent->activateBus(Steinberg::Vst::kAudio, Steinberg::Vst::kOutput, i, true);
    }
  }
}

void SfxPluginVst3::init(std::vector<std::string>& sfxNames)
{
  const std::vector<std::string> paths = String::split(Settings::pathVst3, '|');
  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(".vst3"))
        continue;

#ifndef NDEBUG
      if (file.ends_with("Tonocracy.vst3")) // Error: This software cannot run in the presence of debuggers.
        continue;
#endif // NDEBUG

      Vst3Plugin& vst3Plugin = vst3Plugins.emplace_back();
      vst3Plugin.processSetup.processMode = Steinberg::Vst::kRealtime;
      vst3Plugin.processSetup.symbolicSampleSize = Steinberg::Vst::kSample32;
      vst3Plugin.processSetup.maxSamplesPerBlock = Const::audioMaximumPossibleBlockSize;
      vst3Plugin.processSetup.sampleRate = f64(sampleRate());

      {
        std::string error;
        vst3Plugin.module = VST3::Hosting::Module::create(std::string(reinterpret_cast<const char*>(file.c_str()), file.size()), error);
        ASSERT(error.size() == 0);
      }

      const VST3::Hosting::PluginFactory& pluginFactory = vst3Plugin.module->getFactory();
      vst3Plugin.classInfos = pluginFactory.classInfos();

#ifdef SHR3D_SFX_PLUGIN_VST3_BENCHMARK
      const auto start = std::chrono::high_resolution_clock::now();
      const HANDLE process = GetCurrentProcess();
      PROCESS_MEMORY_COUNTERS pmcBefore;
      GetProcessMemoryInfo(process, &pmcBefore, sizeof(pmcBefore));
#endif // SHR3D_SFX_PLUGIN_VST3_BENCHMARK

      for (u64 i = 0; i < vst3Plugin.classInfos.size(); ++i)
      {
        const VST3::Hosting::ClassInfo& classInfo = vst3Plugin.classInfos[i];
        auto a = classInfo.ID().toString();
        if (i != 0)
          continue;

        sfxNames.push_back(classInfo.name());

        vst3Plugin.effectComponent = pluginFactory.createInstance<Steinberg::Vst::IComponent>(classInfo.ID());
        ASSERT(vst3Plugin.effectComponent != nullptr);

        const Steinberg::tresult result = vst3Plugin.effectComponent->initialize(&context);
        ASSERT(result == Steinberg::kResultOk);

        vst3Plugin.audioProcessor = Steinberg::FUnknownPtr<Steinberg::Vst::IAudioProcessor>(vst3Plugin.effectComponent);
        ASSERT(vst3Plugin.audioProcessor != nullptr);

        activateBuses(vst3Plugin.effectComponent);

        vst3Plugin.editController = Steinberg::FUnknownPtr<Steinberg::Vst::IEditController>(vst3Plugin.effectComponent);

        if (!vst3Plugin.editController)
        {
          Steinberg::TUID controllerCID;
          {
            const Steinberg::tresult result = vst3Plugin.effectComponent->getControllerClassId(controllerCID);
            ASSERT(result == Steinberg::kResultOk);
          }
          {
            vst3Plugin.editController = pluginFactory.createInstance<Steinberg::Vst::IEditController>(VST3::UID(controllerCID));
            ASSERT(vst3Plugin.editController);
          }
          {
            const Steinberg::tresult result = vst3Plugin.editController->initialize(&context);
            ASSERT(result == Steinberg::kResultOk);
          }
        }

        vst3Plugin.componentHandler = new ComponentHandler;
        vst3Plugin.editController->setComponentHandler(vst3Plugin.componentHandler);

        auto componentConnectionPoint = Steinberg::FUnknownPtr<Steinberg::Vst::IConnectionPoint>{ vst3Plugin.effectComponent };
        auto controllerConnectionPoint = Steinberg::FUnknownPtr<Steinberg::Vst::IConnectionPoint>{ vst3Plugin.editController };

        if (componentConnectionPoint && controllerConnectionPoint)
        {
          vst3Plugin.componentConnectionProxy = new ConnectionProxy(componentConnectionPoint);
          vst3Plugin.controllerConnectionProxy = new ConnectionProxy(controllerConnectionPoint);

          vst3Plugin.componentConnectionProxy->connect(controllerConnectionPoint);
          vst3Plugin.controllerConnectionProxy->connect(componentConnectionPoint);
        }



        ASSERT(vst3Plugin.audioProcessor->canProcessSampleSize(vst3Plugin.processSetup.symbolicSampleSize) == Steinberg::kResultOk);
        {
          const Steinberg::tresult result = vst3Plugin.audioProcessor->setupProcessing(vst3Plugin.processSetup);
          ASSERT(result == Steinberg::kResultOk);
        }
        {
          const Steinberg::tresult result = vst3Plugin.effectComponent->setActive(true);
          ASSERT(result == Steinberg::kResultOk);
        }
        {
          /*const Steinberg::tresult result =*/ vst3Plugin.audioProcessor->setProcessing(true);
          //ASSERT(result == Steinberg::kResultOk);
        }
        {
          //const u32 latency = vst3Plugin.audioProcessor->getLatencySamples();
          //ASSERT(latency < 512);
        }
      }

#ifdef SHR3D_SFX_PLUGIN_VST3_BENCHMARK
      SfxBenchmarkResult& benchmarkResult = Global::sfxPluginVst3BenchmarkResults.emplace_back();
      benchmarkResult.duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count();
      PROCESS_MEMORY_COUNTERS pmcAfter;
      GetProcessMemoryInfo(process, &pmcAfter, sizeof(pmcAfter));
      benchmarkResult.memoryUsageInBytes = pmcAfter.WorkingSetSize - pmcBefore.WorkingSetSize;
#endif // SHR3D_SFX_PLUGIN_VST3_BENCHMARK
    }
  }
}

//class PlugFrame final : public Steinberg::IPlugFrame
//{
//  HWND parent;
//  PlugFrame(HWND parent) : parent(parent)
//  {
//    //FUNKNOWN_CTOR
//  }
//
//  Steinberg::tresult PLUGIN_API resizeView(Steinberg::IPlugView* view, Steinberg::ViewRect* newSize) override
//  {
//    return Steinberg::kResultOk;
//  }
//}

bool SfxPluginVst3::hasSfxPluginWindow(const SfxIndex index)
{
  return true;
}

void SfxPluginVst3::openWindow(const SfxIndex index, const i32 instance, Shr3DWindow parentWindow)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());

  Vst3Plugin& vst3Plugin = vst3Plugins[index];
  ASSERT(vst3Plugin.plugView != nullptr);

  //PlugFrame* plugFrame = new PlugFrame(parentWindow);
  //vst3Plugin.plugView->setFrame(plugFrame);

  {
    const Steinberg::tresult result = vst3Plugin.plugView->attached(parentWindow, Steinberg::kPlatformTypeHWND);
    ASSERT(result == Steinberg::kResultOk);
  }
}

Size SfxPluginVst3::getSfxPluginWindowSize(const SfxIndex index, const i32 instance)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());

  Vst3Plugin& vst3Plugin = vst3Plugins[index];
  if (vst3Plugin.plugView == nullptr)
  {
    ASSERT(vst3Plugin.editController != nullptr);
    vst3Plugin.plugView = vst3Plugin.editController->createView(Steinberg::Vst::ViewType::kEditor);
    ASSERT(vst3Plugin.plugView != nullptr);
  }

  Steinberg::ViewRect rect;
  const Steinberg::tresult result = vst3Plugin.plugView->getSize(&rect);
  ASSERT(result == Steinberg::kResultOk);

  const Size size = {
    .width = u32(rect.right - rect.left),
    .height = u32(rect.bottom - rect.top)
  };

  return size;
}

void SfxPluginVst3::closeWindow(const SfxIndex index, const i32 instance)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());

  Vst3Plugin& vst3Plugin = vst3Plugins[index];

  //CloseWindow(vst3Plugin.hwnd);

  vst3Plugin.plugView->removed();
  vst3Plugin.plugView = nullptr;

  //DestroyWindow(vst3Plugin.hwnd);
}

i32 SfxPluginVst3::numParams(const SfxIndex index, const i32 instance)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());
  Vst3Plugin& vst3Plugin = vst3Plugins[index];
  ASSERT(vst3Plugin.editController != nullptr);
  const i32 parameterCount = vst3Plugin.editController->getParameterCount();
  return parameterCount;
}

void SfxPluginVst3::getParameterProperties(const SfxIndex index, const i32 instance, const i32 param, SfxParameterProperties& parameterProperties)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());
  Vst3Plugin& vst3Plugin = vst3Plugins[index];
  ASSERT(vst3Plugin.editController != nullptr);
  Steinberg::Vst::ParameterInfo parameterInfo{};
  vst3Plugin.editController->getParameterInfo(param, parameterInfo);
  const std::string title = String::ws2s((wchar_t*)parameterInfo.title, ARRAYSIZE(parameterInfo.title));
  const std::string units = String::ws2s((wchar_t*)parameterInfo.units, ARRAYSIZE(parameterInfo.units));
  strcpy(parameterProperties.name, reinterpret_cast<const char*>(title.c_str()));
  strcpy(parameterProperties.label, reinterpret_cast<const char*>(units.c_str()));

  const Steinberg::Vst::ParamValue paramValue = vst3Plugin.editController->getParamNormalized(param);
  Steinberg::Vst::String128 valueString{};
  const Steinberg::tresult result = vst3Plugin.editController->getParamStringByValue(param, paramValue, valueString);
  if (result == Steinberg::kResultOk /*&& paramValue != 0.0 && valueString[0] != L'0' && valueString[1] != L'.' && valueString[2] != L'0'*/)
  {
    const std::string valueStringU8 = String::ws2s((wchar_t*) valueString, ARRAYSIZE(valueString));
    strcpy(parameterProperties.display, reinterpret_cast<const char*>(valueStringU8.c_str()));
  }
  else
  {
    const Steinberg::Vst::ParamValue paramValue = vst3Plugin.editController->getParamNormalized(param);
    sprintf(parameterProperties.display, "%.4lf", paramValue);
  }
}

f32 SfxPluginVst3::getParameter(SfxIndex index, i32 instance, i32 param)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());
  Vst3Plugin& vst3Plugin = vst3Plugins[index];
  ASSERT(vst3Plugin.editController != nullptr);
  const Steinberg::Vst::ParamValue paramValue = vst3Plugin.editController->getParamNormalized(param);
  return paramValue;
}

void SfxPluginVst3::setParameter(SfxIndex index, i32 instance, i32 param, f32 value)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());
  Vst3Plugin& vst3Plugin = vst3Plugins[index];
  ASSERT(vst3Plugin.editController != nullptr);
  const Steinberg::tresult result = vst3Plugin.editController->setParamNormalized(param, value);
  ASSERT(result == Steinberg::kResultOk);
}

void SfxPluginVst3::processBlock(SfxIndex index, i32 instance, f32** inBlock, f32** outBlock, i32 blockSize)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());

  Vst3Plugin& vst3Plugin = vst3Plugins[index];

  using namespace Steinberg;
  if (auto audioProcessor = FUnknownPtr<Vst::IAudioProcessor>(vst3Plugin.effectComponent))
  {
    Vst::ProcessData data;
    data.processMode = vst3Plugin.processSetup.processMode;
    data.symbolicSampleSize = vst3Plugin.processSetup.symbolicSampleSize;
    data.numSamples = blockSize;
    data.numInputs = vst3Plugin.effectComponent->getBusCount(Vst::kAudio, Vst::kInput);
    data.numOutputs = vst3Plugin.effectComponent->getBusCount(Vst::kAudio, Vst::kOutput);

    if (data.numInputs > 0)
    {
      int inputBlocksOffset{ 0 };

      data.inputs = static_cast<Vst::AudioBusBuffers*>(alloca(sizeof(Vst::AudioBusBuffers) * data.numInputs));

      for (int busIndex = 0; busIndex < data.numInputs; ++busIndex)
      {
        Vst::BusInfo busInfo{ };
        if (vst3Plugin.effectComponent->getBusInfo(Vst::kAudio, Vst::kInput, busIndex, busInfo) != kResultOk)
        {
          return;
        }
        if (busInfo.busType == Vst::kMain)
        {
          data.inputs[busIndex].numChannels = busInfo.channelCount;
          data.inputs[busIndex].channelBuffers32 = const_cast<f32**>(inBlock + inputBlocksOffset);
          inputBlocksOffset += busInfo.channelCount;
        }
        else
        {
          //aux is not yet supported
          data.inputs[busIndex].numChannels = 0;
          data.inputs[busIndex].channelBuffers32 = nullptr;
        }
        data.inputs[busIndex].silenceFlags = 0UL;
      }
    }
    if (data.numOutputs > 0)
    {
      int outputBlocksOffset{ 0 };

      data.outputs = static_cast<Vst::AudioBusBuffers*>(alloca(sizeof(Vst::AudioBusBuffers) * data.numOutputs));
      for (int busIndex = 0; busIndex < data.numOutputs; ++busIndex)
      {
        Vst::BusInfo busInfo{ };
        if (vst3Plugin.effectComponent->getBusInfo(Vst::kAudio, Vst::kOutput, busIndex, busInfo) != kResultOk)
        {
          return;
        }
        if (busInfo.busType == Vst::kMain)
        {
          data.outputs[busIndex].numChannels = busInfo.channelCount;
          data.outputs[busIndex].channelBuffers32 = const_cast<float**>(outBlock + outputBlocksOffset);
          outputBlocksOffset += busInfo.channelCount;
        }
        else
        {
          //aux is not yet supported
          data.outputs[busIndex].numChannels = 0;
          data.outputs[busIndex].channelBuffers32 = nullptr;
        }
        data.outputs[busIndex].silenceFlags = 0UL;
      }
    }

    const auto processResult = audioProcessor->process(data);

    ASSERT(processResult == kResultOk);
  }
}

class ParameterBuffer : public Steinberg::Vst::BufferStream
{
public:
  Steinberg::Buffer& get()
  {
    return mBuffer;
  }
  u8* data()
  {
    return static_cast<u8*>(static_cast<void*>(mBuffer));
  }
};

std::string SfxPluginVst3::saveParameters(SfxIndex index, i32 instance)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());

  Vst3Plugin& vst3Plugin = vst3Plugins[index];

  ParameterBuffer parameterBuffer;
  const Steinberg::tresult result = vst3Plugin.effectComponent->getState(&parameterBuffer);
  ASSERT(result == Steinberg::kResultOk);
  ASSERT(parameterBuffer.get().getFillSize() > 0);

  const u8* para = reinterpret_cast<u8*>(parameterBuffer.data());

  const std::string parameter(para, para + parameterBuffer.get().getFillSize());

  return parameter;
}

void SfxPluginVst3::loadParameters(SfxIndex index, i32 instance, const std::string& parameters)
{
  ASSERT(index >= 0);
  ASSERT(index < vst3Plugins.size());

  Vst3Plugin& vst3Plugin = vst3Plugins[index];

  ParameterBuffer parameterBuffer;
  {
    parameterBuffer.get().setSize(u32(parameters.size()));
    memcpy(parameterBuffer.data(), parameters.data(), parameters.size());
  }

  ASSERT(parameterBuffer.get().getFillSize() == 0); // does not work when fill size is set

  // TODO handle multiple instances like in VST
  //if (instance == vstPlugins[index].aEffect.size())
  //  loadPluginInstance(vstPlugins[index]); // plugin is loaded multiple times, create another instance

  const Steinberg::tresult result = vst3Plugin.effectComponent->setState(&parameterBuffer);
  ASSERT(parameters.size() == 0 || result == Steinberg::kResultOk);
}

#endif // SHR3D_SFX_PLUGIN_VST3
