#include "factory.h"

#include "log.h"

#include <vtkMemoryResourceStream.h>

// clang-format off
extern f3d::plugin* init_plugin_static_native();
extern f3d::plugin* init_plugin_static_alembic();
extern f3d::plugin* init_plugin_static_assimp();
extern f3d::plugin* init_plugin_static_draco();
extern f3d::plugin* init_plugin_static_hdf();
extern f3d::plugin* init_plugin_static_occt();
extern f3d::plugin* init_plugin_static_usd();
extern f3d::plugin* init_plugin_static_vdb();
// clang-format on

namespace f3d
{

namespace
{
template<typename F>
reader* pickReader(
  const std::vector<plugin*>& plugins, std::optional<std::string> forceReader, F&& isValid)
{
  int bestScore = -1;
  reader* bestReader = nullptr;

  for (const auto* plugin : plugins)
  {
    for (const auto& reader : plugin->getReaders())
    {
      if (forceReader)
      {
        if (reader->getName() == *forceReader)
        {
          return reader.get();
        }
      }
      else if (reader->getScore() > bestScore && isValid(reader.get()))
      {
        bestScore = reader->getScore();
        bestReader = reader.get();
      }
    }
  }

  return bestReader;
}
}

//----------------------------------------------------------------------------
factory* factory::instance()
{
  static factory instance;
  return &instance;
}

//----------------------------------------------------------------------------
factory::factory()
{
  // clang-format off
  this->StaticPluginInitializers["native"] = init_plugin_static_native;
  this->StaticPluginInitializers["alembic"] = init_plugin_static_alembic;
  this->StaticPluginInitializers["assimp"] = init_plugin_static_assimp;
  this->StaticPluginInitializers["draco"] = init_plugin_static_draco;
  this->StaticPluginInitializers["hdf"] = init_plugin_static_hdf;
  this->StaticPluginInitializers["occt"] = init_plugin_static_occt;
  this->StaticPluginInitializers["usd"] = init_plugin_static_usd;
  this->StaticPluginInitializers["vdb"] = init_plugin_static_vdb;
  // clang-format on
}

//----------------------------------------------------------------------------
const std::vector<plugin*>& factory::getPlugins()
{
  return this->Plugins;
}

//----------------------------------------------------------------------------
factory::plugin_initializer_t factory::getStaticInitializer(const std::string& pluginName)
{
  auto it = this->StaticPluginInitializers.find(pluginName);
  if (it != this->StaticPluginInitializers.end())
  {
    return it->second;
  }
  return nullptr;
}

//----------------------------------------------------------------------------
reader* factory::getReader(const std::string& fileName, std::optional<std::string> forceReader)
{
  return f3d::pickReader(
    this->Plugins, forceReader, [&](const reader* reader) { return reader->canRead(fileName); });
}

//----------------------------------------------------------------------------
reader* factory::getReader(
  const std::byte* buffer, std::size_t size, std::optional<std::string> forceReader)
{
  vtkNew<vtkMemoryResourceStream> stream;
  stream->SetBuffer(buffer, size);

  return f3d::pickReader(this->Plugins, forceReader,
    [&](const reader* reader) { return reader->supportsStream() && reader->canRead(stream); });
}

//----------------------------------------------------------------------------
bool factory::setReaderOption(const std::string& name, const std::string& value)
{
  // Set the reader option on the first reader that accepts it
  return std::any_of(this->Plugins.begin(), this->Plugins.end(),
    [&](const f3d::plugin* plugin)
    {
      const auto& readers = plugin->getReaders();
      return std::any_of(readers.begin(), readers.end(),
        [&](const auto& reader) { return reader->setReaderOption(name, value); });
    });
}

//----------------------------------------------------------------------------
std::vector<std::string> factory::getAllReaderOptionNames()
{
  std::vector<std::string> names;
  for (const f3d::plugin* plugin : this->Plugins)
  {
    for (const auto& reader : plugin->getReaders())
    {
      auto readerNames = reader->getAllReaderOptionNames();
      names.insert(names.end(), readerNames.begin(), readerNames.end());
    }
  }
  return names;
}

//----------------------------------------------------------------------------
void factory::load(plugin* plug)
{
  if (!this->registerOnce(plug))
  {
    log::debug("A plugin named \"" + plug->getName() + "\" is already registered.");
  }
}

//----------------------------------------------------------------------------
void factory::autoload()
{
  for (const auto& [str, init] : this->StaticPluginInitializers)
  {
    this->registerOnce(init());
  }
}

//----------------------------------------------------------------------------
bool factory::registerOnce(plugin* plug)
{
  if (std::find(this->Plugins.begin(), this->Plugins.end(), plug) == this->Plugins.end())
  {
    this->Plugins.push_back(plug);

    log::debug("Loading plugin \"" + plug->getName() + "\"");
    log::debug("  Version: " + plug->getVersion());
    log::debug("  Description: " + plug->getDescription());
    log::debug("  Readers:");

    for (const auto& read : plug->getReaders())
    {
      log::debug("    " + read->getLongDescription());
    }

    return true;
  }
  return false;
}
}
