#include "PluginLoader.h"
#include <QDebug>
#include <vector>
#include <filesystem>
#include <sstream>

#ifdef _WIN32
#include <stdio.h>
#include <tchar.h>
#include <string.h>
#include <atlbase.h>
#include <QDebug>
#include <QFileInfo>
#define MAX_KEY_LENGTH 255
#define MAX_VALUE_NAME 16383

std::vector<std::string> QueryKey(HKEY hKey, std::string path) 
{
	//See https://docs.microsoft.com/en-us/windows/desktop/sysinfo/enumerating-registry-subkeys
	std::vector<std::string> list;
    TCHAR    achClass[MAX_PATH] = TEXT("");  // buffer for class name 
    DWORD    cchClassName = MAX_PATH;  // size of class string 
    DWORD    cSubKeys=0;               // number of subkeys 
    DWORD    cbMaxSubKey;              // longest subkey size 
    DWORD    cchMaxClass;              // longest class string 
    DWORD    cValues;              // number of values for key 
    DWORD    cchMaxValue;          // longest value name 
    DWORD    cbMaxValueData;       // longest value data 
    DWORD    cbSecurityDescriptor; // size of security descriptor 
    FILETIME ftLastWriteTime;      // last write time 

    DWORD i, retCode;

    TCHAR  achValue[MAX_VALUE_NAME]; 
    DWORD cchValue = MAX_VALUE_NAME; 

    // Get the class name and the value count. 
    retCode = RegQueryInfoKey(
        hKey,                    // key handle 
        achClass,                // buffer for class name 
        &cchClassName,           // size of class string 
        NULL,                    // reserved 
        &cSubKeys,               // number of subkeys 
        &cbMaxSubKey,            // longest subkey size 
        &cchMaxClass,            // longest class string 
        &cValues,                // number of values for this key 
        &cchMaxValue,            // longest value name 
        &cbMaxValueData,         // longest value data 
        &cbSecurityDescriptor,   // security descriptor 
        &ftLastWriteTime);       // last write time 
 
    // Enumerate the key values. 
    if (cValues) 
    {
       	//printf( "\nNumber of values: %d\n", cValues);

        for (i=0, retCode=ERROR_SUCCESS; i<cValues; i++) 
        { 
            cchValue = MAX_VALUE_NAME; 
            achValue[0] = '\0'; 
            retCode = RegEnumValue(hKey, i, 
                achValue, 
                &cchValue, 
                NULL, 
                NULL,
                NULL,
                NULL);
 
            if (retCode == ERROR_SUCCESS ) 
            { 
				CRegKey regKey;
				CHAR szBuffer[512];
				ULONG dwBufferSize = sizeof(szBuffer);

				if(ERROR_SUCCESS != regKey.Open(HKEY_LOCAL_MACHINE, path.c_str()))
				{
					qWarning() << "Error opening registry path " << path.c_str(); 
					regKey.Close();
				}
				if( ERROR_SUCCESS != regKey.QueryStringValue(achValue,szBuffer,&dwBufferSize))
				{
					qWarning() << "Error opening registry value " << achValue;
					regKey.Close();
				}

				std::string fp = szBuffer;
				std::replace( fp.begin(), fp.end(), '\\', '/');
				list.push_back(fp);
            } 
        }
    }
	return list;
}
#endif

std::vector<std::string> PluginLoader::queryRegistryBehaviors(std::string path)
{
	std::vector<std::string> list;
	
	#ifdef _WIN32
	HKEY hTestKey;

	if( RegOpenKeyEx( HKEY_LOCAL_MACHINE,
		TEXT(path.c_str()),
		0,
		KEY_READ,
		&hTestKey) == ERROR_SUCCESS)
	{
		list = QueryKey(hTestKey, path);
	}

	RegCloseKey(hTestKey);
	#endif

	return list;
}

#ifdef _WIN32
const char * WinGetEnv(const char * name)
{
    const DWORD buffSize = 65535;
    static char buffer[buffSize];
    if (GetEnvironmentVariableA(name, buffer, buffSize))
    {
        return buffer;
    }
    else
    {
        return 0;
    }
}
bool WinSetEnv(const char* name, const char* toWhat){
	return SetEnvironmentVariableA(name, toWhat);
}
#endif

const char* PluginLoader::addDllPath(std::string f)
{
	//Get the directory of the DLL/*.so and add it to the PATH env variable.
	//This way dependencies can be shipped in the same directory
	#ifdef _WIN32
		QFileInfo finf(f.c_str());
		//rather than the buggy _getenv: https://docs.microsoft.com/de-de/windows/desktop/api/winbase/nf-winbase-getenvironmentvariable
		auto old_path = WinGetEnv("PATH");
		auto path = std::ostringstream();
		if(old_path){
			path << old_path << ";" << finf.absolutePath().toStdString().c_str();
			WinSetEnv("PATH", path.str().c_str());
		}else{
			qWarning() << "Failed to get and modify PATH enviromental variable.";
		}
		return old_path;
	#endif
	
	return "";
}

void PluginLoader::delDllPath(const char* oldPath){
	//reset path. We don't want some weird cross-effects
	#ifdef _WIN32
		if(oldPath){
			WinSetEnv("PATH", oldPath);
		}
	#endif
}

bool endsWith(std::string value, std::string ending)
{
	std::transform(value.begin(), value.end(), value.begin(), ::tolower);
	std::transform(ending.begin(), ending.end(), ending.begin(), ::tolower);
    if (ending.size() > value.size()) return false;
    return std::equal(ending.rbegin(), ending.rend(), value.rbegin());
}

bool validSuffix(std::string f, std::string suffix){
	return (endsWith(f,suffix+".dll") || endsWith(f,suffix+".so"));
}

std::vector<std::string> PluginLoader::searchDirectoriesForPlugins(std::vector<std::string> list, std::string suffix){
	//Search directories
    std::vector<std::string> filesFromFolders;

    for (auto f: list) {
        std::string file = f;
        try {
            if (!file.empty() && file[file.size() - 1] == '/') {
                for (auto& p : std::filesystem::directory_iterator(file)) {
					std::string s = p.path().string();
					if(validSuffix(s, suffix))
                    	filesFromFolders.push_back(s);
                }
            }
            else {
				if(validSuffix(f, suffix))
                	filesFromFolders.push_back(f);
            }
        }
        catch (...){
            qWarning() << "Could not read file/directory: " << file.c_str();
        }
    }
	
	return filesFromFolders;
}

PluginLoader::PluginLoader(QObject *parent)
{
	m_MetaData = nullptr;
	m_PluginLoader = new QPluginLoader(this);
	m_PluginListModel = new QStringListModel();
}
PluginLoader::~PluginLoader()
{
	delete m_PluginLoader;
	delete m_PluginListModel;
}

bool PluginLoader::loadPluginFromFilename(QString const& filename)
{
	bool retval = false;
	if (m_PluginLoader->isLoaded()) {
		m_PluginLoader->unload();
	}

	bool isLib = QLibrary::isLibrary(filename);

	if (isLib) {
		
		auto oldPath = PluginLoader::addDllPath(filename.toStdString());
		m_PluginLoader->setFileName(filename);

		readMetaDataFromPlugin();

		retval = m_PluginLoader->load();
		QString s = m_PluginLoader->errorString();
		std::string ss = s.toStdString();
		addPluginnameToLists(getCurrentPluginName(), filename);

        if (!m_PluginLoader->isLoaded())
		{
		    qWarning() << ss.c_str();
			retval = false;
		}
		PluginLoader::delDllPath(oldPath);
	}
	else {
		retval = false;
	}

	return retval;
}

void PluginLoader::addPluginnameToLists(QString mstring, QString filename)
{	
	if (!m_PluginList.contains(mstring))
		m_PluginList.append(mstring);
	m_PluginListModel->setStringList(m_PluginList);
	m_PluginMap.insert(std::pair<QString, QString>(mstring, filename));
}

bool PluginLoader::loadPluginFromName(QString name) {
	QString filename = m_PluginMap.find(name)->second;
	return loadPluginFromFilename(filename);
}

int PluginLoader::addToPluginList(QString filename, QString suffix) {
	if (!validSuffix(filename.toStdString(), suffix.toStdString()))
		return 1;

	bool isLib = QLibrary::isLibrary(filename);

	if (isLib) {

		QPluginLoader loader;
		loader.setFileName(filename);
		QJsonValue pluginMeda(loader.metaData().value("MetaData"));
		QJsonObject metaObj = pluginMeda.toObject();
		QString mstring = metaObj.value("name").toString();
		addPluginnameToLists(mstring, filename);
	}
	else {
		return 2;
		qWarning() << "Error reading plugin: " << filename;
	}
	return 0;
}

QStringListModel* PluginLoader::getPluginList() {
	return m_PluginListModel;
}

QObject* PluginLoader::getPluginInstance() {
	return (m_PluginLoader->instance());
}

QJsonObject PluginLoader::getPluginMetaData() const
{
	if (m_MetaData == nullptr)
		qFatal("(getPluginMetaData) No plugin loaded");
	return *m_MetaData;
}

void PluginLoader::readMetaDataFromPlugin()
{
	m_MetaData = std::make_shared<QJsonObject>(m_PluginLoader->metaData().value("MetaData").toObject());
}

bool PluginLoader::getIsPluginLoaded() {
	return m_isPluginLoaded;
}

QString PluginLoader::getCurrentPluginName() {
	if (m_MetaData == nullptr)
		return "Error name";
	return m_MetaData->value("name").toString();
}
const std::map<QString, QString> &PluginLoader::getPluginMap() const
{
    return m_PluginMap;
}