/* Copyright (c) 2015 Stanislaw Halik
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 */

#pragma once

#include "plugin-api.hpp"
#include "options/options.hpp"

#include <QWidget>
#include <QDebug>
#include <QString>
#include <QLibrary>
#include <QFrame>
#include <QList>

#include <cstdio>
#include <cinttypes>
#include <iostream>
#include <algorithm>

#include <QCoreApplication>
#include <QFile>
#include <QDir>
#include <QList>
#include <QStringList>

#if defined(__APPLE__)
#   define OPENTRACK_SOLIB_EXT "dylib"
#elif defined(_WIN32)
#   define OPENTRACK_SOLIB_EXT "dll"
#else
#   define OPENTRACK_SOLIB_EXT "so"
#endif

#include <iostream>

#ifdef _MSC_VER
#   define OPENTRACK_SOLIB_PREFIX ""
#else
#   define OPENTRACK_SOLIB_PREFIX ""
#endif

extern "C" typedef void* (*OPENTRACK_CTOR_FUNPTR)(void);
extern "C" typedef Metadata* (*OPENTRACK_METADATA_FUNPTR)(void);

struct dylib final {
    enum Type : unsigned { Filter, Tracker, Protocol, Invalid = 0xcafebabeU };

    dylib(const QString& filename, Type t) :
        type(Invalid),
        filename(filename),
        Dialog(nullptr),
        Constructor(nullptr),
        Meta(nullptr),
        handle(nullptr)
    {
        // otherwise dlopen opens the calling executable
        if (filename.size() == 0)
            return;

        handle.setFileName(filename);
        handle.setLoadHints(QLibrary::DeepBindHint | QLibrary::ResolveAllSymbolsHint);

        if (check(!handle.load()))
            return;

        if (check((Dialog = (OPENTRACK_CTOR_FUNPTR) handle.resolve("GetDialog"), !Dialog)))
            return;

        if (check((Constructor = (OPENTRACK_CTOR_FUNPTR) handle.resolve("GetConstructor"), !Constructor)))
            return;

        if (check((Meta = (OPENTRACK_METADATA_FUNPTR) handle.resolve("GetMetadata"), !Meta)))
            return;

        auto m = mem<Metadata>(Meta());

        icon = m->icon();
        name = m->name();

        type = t;
    }
    ~dylib()
    {
        if (handle.isLoaded())
        {
            const bool success = handle.unload();
            if (!success)
                qDebug() << "can't unload dylib" << filename << handle.errorString();
        }
    }

    static QList<mem<dylib>> enum_libraries(const QString& library_path)
    {
        const char* filters_n[] = { OPENTRACK_SOLIB_PREFIX "opentrack-filter-*." OPENTRACK_SOLIB_EXT,
                                    OPENTRACK_SOLIB_PREFIX "opentrack-tracker-*." OPENTRACK_SOLIB_EXT,
                                    OPENTRACK_SOLIB_PREFIX "opentrack-proto-*." OPENTRACK_SOLIB_EXT,
                                  };
        const Type filters_t[] = { Filter, Tracker, Protocol };

        QDir settingsDir(library_path);

        QList<mem<dylib>> ret;

        for (int i = 0; i < 3; i++)
        {
            QString glob = filters_n[i];
            Type t = filters_t[i];
            QStringList filenames = settingsDir.entryList(QStringList { glob }, QDir::Files, QDir::Name);

            for (const QString& filename : filenames)
            {
                QIcon icon;
                QString longName;
                auto lib = std::make_shared<dylib>(library_path + filename, t);
                if (!get_metadata(lib, longName, icon))
                    continue;
                if (std::any_of(ret.cbegin(),
                                ret.cend(),
                                [&](mem<dylib> a) {return a->type == lib->type && a->name == lib->name;}))
                {
                    qDebug() << "Duplicate lib" << lib->filename;
                    continue;
                }
                ret.push_back(lib);
            }
        }

        return ret;
    }

    Type type;
    QString filename;

    QIcon icon;
    QString name;

    OPENTRACK_CTOR_FUNPTR Dialog;
    OPENTRACK_CTOR_FUNPTR Constructor;
    OPENTRACK_METADATA_FUNPTR Meta;
private:
    QLibrary handle;

    bool check(bool fail)
    {
        if (fail)
        {
            if (handle.isLoaded())
                (void) handle.unload();

            Constructor = nullptr;
            Dialog = nullptr;
            Meta = nullptr;

            type = Invalid;
        }

        return fail;
    }

    static bool get_metadata(mem<dylib> lib, QString& name, QIcon& icon)
    {
        Metadata* meta;
        if (!lib->Meta || ((meta = lib->Meta()), !meta))
            return false;
        name = meta->name();
        icon = meta->icon();
        delete meta;
        return true;
    }
};

struct Modules final
{
    Modules(const QString& library_path) :
        module_list(dylib::enum_libraries(library_path)),
        filter_modules(filter(dylib::Filter)),
        tracker_modules(filter(dylib::Tracker)),
        protocol_modules(filter(dylib::Protocol))
    {}
    QList<mem<dylib>>& filters() { return filter_modules; }
    QList<mem<dylib>>& trackers() { return tracker_modules; }
    QList<mem<dylib>>& protocols() { return protocol_modules; }
private:
    QList<mem<dylib>> module_list;
    QList<mem<dylib>> filter_modules;
    QList<mem<dylib>> tracker_modules;
    QList<mem<dylib>> protocol_modules;

    template<typename t>
    static void sort(QList<t>& xs)
    {
        std::sort(xs.begin(), xs.end(), [&](const t& a, const t& b) { return a->name.toLower() < b->name.toLower(); });
    }

    QList<mem<dylib>> filter(dylib::Type t)
    {
        QList<mem<dylib>> ret;
        for (auto x : module_list)
            if (x->type == t)
                ret.push_back(x);

        sort(ret);

        return ret;
    }
};

template<typename t>
mem<t> make_dylib_instance(mem<dylib> lib)
{
    mem<t> ret;
    if (lib != nullptr && lib->Constructor)
        ret = mem<t>(reinterpret_cast<t*>(reinterpret_cast<OPENTRACK_CTOR_FUNPTR>(lib->Constructor)()));
    return ret;
}