#include "module_manager.hh"
#include <QXmlStreamReader>
#include <QFileDialog>
#include <QMessageBox>
#include <QMenu>
#include <QContextMenuEvent>
#include <memory>

namespace {

constexpr char topLevelElement[] = "modules";
constexpr char moduleElement[] = "file";

constexpr char nameFunction[] = "name";
constexpr char descriptionFunction[] = "description";
constexpr char getInstanceFunction[] = "getInstance";

typedef const char* (*ModuleInfo)();
typedef AbstractStrategy* (*StrategyGetter)();

QStringList parseFileList(QXmlStreamReader& xml)
{
    QStringList list;

    xml.readNextStartElement();
    if (xml.name() != topLevelElement)
        return list;

    while (xml.readNextStartElement()) {

        if (xml.name() == moduleElement)
            list.append(xml.readElementText());
        else
            xml.skipCurrentElement();
    }

    return list;
}

QString serializeFileList(const std::vector<Module>& list)
{
    QString xmlString;
    QXmlStreamWriter xml(&xmlString);
    xml.setAutoFormatting(true);

    xml.writeStartDocument();
    xml.writeStartElement(topLevelElement);

    for (auto& module : list)
        xml.writeTextElement(moduleElement, module.library->fileName());

    xml.writeEndElement();
    xml.writeEndElement();
    xml.writeEndDocument();

    return xmlString;
}

void setItem(
    QTableWidget& table, int row, int column,
    const QString& text, const QString& tooltip)
{
    std::unique_ptr<QTableWidgetItem> item(new QTableWidgetItem());
    item->setText(text);
    item->setToolTip(tooltip);
    table.setItem(row, column, item.release());
}

void addRow(
    QTableWidget& table, const QString& name,
    const QString& file, const QString& tooltip)
{
    int row = table.rowCount();
    table.insertRow(row);

    setItem(table, row, 0, name, tooltip);
    setItem(table, row, 1, file, tooltip);
}

std::vector<Module>::iterator findModule(
   std::vector<Module>& modules, const QString& name)
{
    return std::find_if(modules.begin(), modules.end(),
        [&name](const Module& module){
            return module.name == name;
        });
}

} //namespace

ModuleManager::ModuleManager(QWidget* parent) :
    QTableWidget(parent)
{}

StrategyPtr ModuleManager::getStrategy(const QString& name)
{
    auto it = findModule(modules_, name);
    if (it == modules_.end())
        return StrategyPtr();

    StrategyGetter getInstance =
        (StrategyGetter) it->library->resolve(getInstanceFunction);

    if (getInstance == nullptr)
        return StrategyPtr();

    return StrategyPtr(getInstance());
}

void ModuleManager::loadModuleList(const QString& fileName)
{
    moduleListName_ = fileName;
    QFile file(moduleListName_);

    if (!file.open(QFile::ReadOnly)){
        QMessageBox::critical(
            this, tr("Error opening module list file"), file.errorString());
        return;
    }

    QXmlStreamReader xml(&file);
    auto fileList = parseFileList(xml);

    for (auto& moduleFileName : fileList)
        loadModule(moduleFileName);
}

void ModuleManager::loadModule(const QString& fileName)
{
    QFileInfo fileInfo(fileName);
    if (!fileInfo.exists()) {
        QMessageBox::critical(
            this, tr("Error loading library"),
            tr("File \"") + fileName + tr("\" does not exist"));
        return;
    }

    Module module;
    module.library.reset(new QLibrary(this));
    module.library->setFileName(fileName);

    bool success;

    module.name = infoText(*module.library, nameFunction, success);
    if (!success) return;

    module.description = infoText(
        *module.library, descriptionFunction, success);
    if (!success) return;

    auto it = findModule(modules_, module.name);
    if (it != modules_.end()) {
        QMessageBox::warning(
            this, tr("Module already loaded"), tr("Module with name \"")
            + module.name + tr("\" already loaded"));
        return;
    }

    StrategyGetter getInstance =
        (StrategyGetter) module.library->resolve(getInstanceFunction);

    if (getInstance == nullptr) {
        QMessageBox::critical(
            this, tr("Library error"), module.library->errorString());
        return;
    }

    addRow(*this, module.name, fileName, module.description);
    modules_.push_back(std::move(module));
}

void ModuleManager::contextMenuEvent(QContextMenuEvent* event)
{
    QMenu menu(this);

    menu.addAction(
        tr("Add new module"), this, SLOT(onAddNewModule()));

    menu.addAction(
        tr("Remove module"), this, SLOT(onRemoveModule()));

    if (!selectionModel()->hasSelection())
        menu.actions()[1]->setDisabled(true);

    menu.exec(event->globalPos());
}

void ModuleManager::onAddNewModule()
{
    QString fileName = QFileDialog::getOpenFileName(
        this, tr("Add module"), QString(), tr("DLL library (*.dll)"));

    if (fileName.isEmpty())
        return;

    loadModule(fileName);
    saveModuleList(moduleListName_, serializeFileList(modules_));
}

void ModuleManager::onRemoveModule()
{
    int row = currentRow();
    removeRow(row);

    auto it = modules_.begin();
    modules_.erase(it + row);

    saveModuleList(moduleListName_, serializeFileList(modules_));
}

void ModuleManager::saveModuleList(
    const QString& fileName, const QString& content)
{
    QFile file(fileName);

    if (!file.open(QFile::WriteOnly)){
        QMessageBox::critical(
            this, QMessageBox::tr("Error opening module list file"),
            file.errorString());
    }
    else if (file.write(content.toLatin1()) == -1) {
        QMessageBox::critical(
            this, QMessageBox::tr("Error writing module list file"),
            file.errorString());
    }
}

QString ModuleManager::infoText(
    QLibrary& library, const char* symbol, bool& success)
{
    ModuleInfo function = (ModuleInfo) library.resolve(symbol);

    if (function == nullptr) {
        QMessageBox::critical(
            this, QMessageBox::tr("Library error"),
            library.errorString());
        success = false;
        return QString();
    }

    success = true;
    return QString(function());
}
