/*
	PluginFramework
	by John Ryland
	Copyright (c) 2023
*/

////////////////////////////////////////////////////////////////////////////////////
//	Plugin Manager

#include "PluginManager.h"
#include <thread>
#include "FileWatch.hpp"
#include "Library.h"
#include "Utilities.h"
#include <string>
#include <regex>
#include <iostream>
#include <filesystem>
#include <unordered_set>

namespace details {

#ifdef _NDEBUG
static const char* buildTypeSuffix = "";
#else
static const char* buildTypeSuffix = "_d";
#endif

#ifdef _WIN32
static const char* pluginPrefix = "";
static const char* pluginExtension = ".dll";
#elif __APPLE__
static const char* pluginPrefix = "lib";
static const char* pluginExtension = ".dylib";
#else // UNIX / Linux
static const char* pluginPrefix = "lib";
static const char* pluginExtension = ".so";
#endif

std::string PluginNameFromFileName(std::string fileName)
{
    static const std::string prefix = pluginPrefix;
    static const std::string suffix = std::string(buildTypeSuffix) + pluginExtension;

    std::string baseName = Utilities::base_name(fileName);
    std::string filePrefix = baseName.substr(0, prefix.length());
    std::string fileSuffix = baseName.substr(std::max<ssize_t>(baseName.length() - suffix.length(), 0));

    // Only load plugins of matching build type and file pattern of operating system
    if (filePrefix == prefix && fileSuffix == suffix)
        return baseName.substr(prefix.length(), baseName.length() - (prefix.length() + suffix.length()));
    return "";
}

}

namespace PluginFramework {

PluginManager::PluginManager()
    : mNeedReload(false)
    , mPendingAdd(false)
{
}

PluginManager::~PluginManager()
{
}

void PluginManager::Initialize()
{
    SetPluginDirectory("bin/plugins/");
    LoadPlugins();
    RegisterPlugins();
}

void PluginManager::Shutdown()
{
}

void PluginManager::Update()
{
    if (mNeedReload)
    {
        mNeedReload = false; // This needs to be before Loaded/Reload
        mPendingAdd = false;
        printf("plugin reload needed\n");
        Reload();
    }
}

void PluginManager::SetPluginDirectory(const char* pluginDirectoryName)
{
    mPluginDirectoryName = pluginDirectoryName;
    /*
    mFileWatcher = std::make_unique<FileWatcher>(pluginDirectoryName, std::regex(".*"), [this](const std::string& file, const filewatch::Event eventType)
    {
        std::string pluginName = details::PluginNameFromFileName(file);
        if (!pluginName.empty() && eventType == filewatch::Event::added)
        {
            mPendingAdd = true;
        }
        if (!pluginName.empty() && eventType == filewatch::Event::modified && mPendingAdd != true)
        {
            printf("=======================================================================================\n");
            printf("WARNING: don't overwrite plugins to update them.\n");
            printf("Please remove and re-add plugins to hot reload them (plugin: %s)\n", pluginName.c_str());
            printf("eg: rm %s ; cp .build/pluginlib %s\n", file.c_str(), file.c_str());
            printf("Sometimes this warning happens if remove and copy too quickly, and can be ignored, but otherwise it will crash\n");
            printf("=======================================================================================\n");
        }
        mNeedReload = true;
    });
    */
}

void PluginManager::Reload()
{
    // Reload plugins
    LoadPlugins();
    RegisterPlugins();
}

void PluginManager::UnloadPlugin(Plugin& plugin)
{
    //std::cout << "unloading plugin " << plugin.Name() << " from file " << plugin.FileName() << std::endl;
}

void PluginManager::LoadPlugins()
{
    // Get the set of plugin file names now currently on disk
    std::unordered_map<std::string, std::string> pluginFileNames;
    /*
    for (const auto& entry : std::filesystem::directory_iterator(mPluginDirectoryName))
    {
        std::string fileName = entry.path().u8string();
        std::string pluginName = details::PluginNameFromFileName(fileName);
        if (!pluginName.empty())
            pluginFileNames[pluginName] = fileName;
    }
    */

    std::unordered_set<std::string> currentPlugins;
    Utilities::GetKeys(currentPlugins, pluginFileNames);

    std::unordered_set<std::string> loadedPlugins;
    Utilities::GetKeys(loadedPlugins, mLoadedPlugins);

    std::unordered_set<std::string> newPlugins;
    Utilities::Difference(newPlugins, currentPlugins, loadedPlugins);

    std::unordered_set<std::string> removedPlugins;
    Utilities::Difference(removedPlugins, loadedPlugins, currentPlugins);

    // Attempt to unload plugins that the library file has been removed
    for (const auto &pluginName : removedPlugins)
    {
        //std::cout << "unloading plugin " << pluginName << std::endl;
        mLoadedPlugins.at(pluginName).HotEject();
        mLoadedPlugins.erase(pluginName);
    }

    // Attempt to load plugins that are not currently loaded
    size_t loadedCount = mLoadedPlugins.size();
    bool haveUnloadablePlugins = false;
    for (const auto &pluginName : newPlugins)
    {
        std::string fileName = pluginFileNames[pluginName];
        //std::cout << "loading plugin " << pluginName << " from file " << fileName << std::endl;
        Plugin plugin(pluginName, fileName.c_str());
        if (plugin.Loaded())
            mLoadedPlugins.emplace(std::make_pair(pluginName, std::move(plugin)));
        else
            haveUnloadablePlugins = true;
    }

    // Iteratively retry as some plugins may depend on other plugins being loaded
    // Idea is to keep iterating the unloaded plugins until no new plugins are loaded
    // If a plugin was loaded while iterating, then need to iterate again to see if the
    // newly loaded plugin then allows others to load. Circular dependancies between
    // plugins are not supported.
    // Idea here is that it is doing this over a number of frames/updates.
    if (loadedCount != mLoadedPlugins.size() && haveUnloadablePlugins)
        mNeedReload = true;
}

void PluginManager::RegisterPlugins()
{
    for (auto& plugin : mLoadedPlugins)
        plugin.second.Register(*this);
}

} // PluginFramework namespace
