#include <MainWindow.hpp>

#include <iostream>
#include <fstream>

#include <alib/exception>

#include <QtCore/QTextStream>
#include <QFileDialog>
#include <QGraphicsItem>
#include <QtWidgets/QMessageBox>

#include <json/json.h>

#include <Algorithm/Registry.hpp>
#include <Execution/ParallelExecutor.hpp>
#include <Converter.hpp>
#include <Graphics/Dialogs/InputDialog.hpp>
#include <Graphics/GraphicsBox.hpp>
#include <Graphics/InputGraphicsBox.hpp>
#include <Models/InputModelBox.hpp>
#include <ui_MainWindow.h>
#include <Models/AlgorithmModelBox.hpp>
#include <Models/OutputModelBox.hpp>
#include <Graphics/Connection/Connection.hpp>
#include <Graphics/Connection/InputConnectionBox.hpp>
#include <Graphics/Connection/OutputConnectionBox.hpp>
#include <Graphics/Dialogs/OutputDialog.hpp>

MainWindow* MainWindow::instance = nullptr;
const std::string MainWindow::ADD_INPUT_CURSOR_DATA = "__ADD_INPUT__";

MainWindow::MainWindow()
    : ui(new Ui::MainWindow)
    , scene(new GraphicsScene(this))
{
    Q_ASSERT(MainWindow::instance == nullptr);
    MainWindow::instance = this;

    ui->setupUi(this);
    ui->graphicsView->setScene(this->scene.get());

    Registry::initialize();

    this->setupMenu();

    this->clearScene();
}

MainWindow::~MainWindow ( ) = default;

void MainWindow::displayError(const QString& text) const {
    QMessageBox::critical(ui->graphicsView, "Error", text, QMessageBox::Close);
}

void MainWindow::on_RunBtn_clicked()
{
    std::shared_ptr<abstraction::Value> result;
    try {
#if 1
        ModelBox::clearCachedResults();
        result = this->outputBox->getModelBox()->evaluate();
#else
        result = ParallelExecutor::execute(dynamic_cast<OutputModelBox*>(this->outputBox->getModelBox()));
#endif
    } catch ( ... ) {
		std::stringstream ss;
		alib::ExceptionHandler::handle ( ss );
		this->displayError ( QString::fromStdString ( ss.str ( ) ) );
        return;
    }
    if (!result) {
        this->displayError("Nothing was outputted.");
    }
    else {
        auto dialog = std::make_unique<OutputDialog>(result);
        dialog->exec();
    }
}

void MainWindow::setupMenu() {
    connect(ui->actionAdd_Input, &QAction::triggered, [&]{ this->setCursorData(MainWindow::ADD_INPUT_CURSOR_DATA); });

    for (const auto& algo: Registry::getAlgorithms()) {
        QMenu* menu = ui->menuInsert;
        for (const auto& group: algo.second->getGroups()) {
            QMenu* nextMenu = nullptr;
            for (auto* subMenu: menu->findChildren<QMenu*>()) {
                if (subMenu->title().toStdString() == group) {
                    nextMenu = subMenu;
                    break;
                }
            }

            if (!nextMenu) {
                nextMenu = menu->addMenu(QString::fromStdString(group));
            }

            menu = nextMenu;
        }

        auto name = QString::fromStdString(algo.second->getPrettyName());
        const auto& fullName = algo.second->getFullName();
        auto* action = menu->addAction(name, [this, fullName]{ this->setCursorData(fullName); });
        action->setStatusTip(QString::fromStdString(fullName));
        action->setToolTip(QString::fromStdString(fullName));
    }
}

void MainWindow::setCursorData(const std::string& p_data) {
    this->cursorData = p_data;
    static QCursor cursor(Qt::CrossCursor);
    if (!p_data.empty())
        this->setCursor(cursor);
    else
        this->unsetCursor();
}

MainWindow* MainWindow::getInstance() {
    return instance;
}

void MainWindow::on_actionOpen_triggered() {
    try {
        using namespace std::string_literals;

        auto filename = QFileDialog::getOpenFileName(this,
                                                     "Open file",
                                                     QDir::homePath(),
                                                     "All files (*.*);;JSON files (*.json)");
        if (filename.isEmpty())
            return;

        std::ifstream stream;
        stream.open ( filename.toStdString ( ), std::ifstream::in );

        if ( ! stream ) {
            QMessageBox::warning(this, "Warning", "File does not exist.");
            return;
        }

        Json::Value parsed;
        stream >> parsed;

        stream.close ( );

        if (parsed.isMember("boxes") || !parsed["boxes"].isArray() || parsed["boxes"].empty())
            throw std::runtime_error { "No boxes were defined." };

        // clear existing boxes
        this->scene->clear();
        this->outputBox = nullptr;

        std::vector<GraphicsBox*> boxes;
        boxes.reserve(parsed["boxes"].size());

        for (const auto& box: parsed["boxes"]) {
            if (box["type"] == "algorithm") {
                if (auto* algo = Registry::getAlgorithm(box["algorithm"].asString ( ))) {
                    boxes.push_back(new GraphicsBox(std::make_unique<AlgorithmModelBox>(algo), { box["x"].asDouble ( ), box["y"].asDouble ( ) }));
                    this->scene->addItem(boxes.back());
                } else {
                    throw std::runtime_error {
                            "Invalid algorithm '"s + box["algorithm"].asString ( ) + "'specified." };
                }
            } else if (box["type"] == "input") {
                auto* graphicsBox = new InputGraphicsBox(std::make_unique<InputModelBox>(), { box["x"].asDouble ( ), box["y"].asDouble ( ) });

                boxes.push_back(graphicsBox);
                this->scene->addItem(graphicsBox);

                if (box.isMember("data") && box["data"].isString()) {
                    auto* inputBox = static_cast < InputModelBox * > ( graphicsBox->getModelBox ( ) );
                    inputBox->setAutomaton(Converter::parseXML(QString::fromStdString(box["data"].asString ( ))));
                    graphicsBox->updateColor();
                }
            } else if (box["type"] == "output") {
                boxes.push_back(new GraphicsBox(std::make_unique<OutputModelBox>(), { box["x"].asDouble ( ), box["y"].asDouble ( ) }));
                this->scene->addItem(boxes.back());
                if (this->outputBox) {
                    throw std::runtime_error { "Multiple output boxes found." };
                }
                this->outputBox = boxes.back();
            }
        }

        for (const auto& conn: parsed["connections"]) {
            size_t from = conn["from"].asUInt ( );
            size_t to = conn["to"].asUInt ( );
            size_t slot = conn["slot"]. asUInt ( );
            if (from >= boxes.size() || to >= boxes.size()) {
                throw std::runtime_error { "Invalid box index specified." };
            }

            if (from == to) {
                throw std::runtime_error { "Cannot connect a box to itself. " };
            }

            auto* fromBox = boxes[from];
            auto* toBox = boxes[to];

            auto* fromConnectionBox = fromBox->getOutputConnectionBox();
            auto& toConnectionBoxes = toBox->getInputConnectionBoxes();

            if (slot >= toConnectionBoxes.size()) {
                throw std::runtime_error { "Invalid connection slot specified." };
            }

            auto* toConnectionBox = toConnectionBoxes[slot];

            new Connection(fromConnectionBox, toConnectionBox);
        }
    } catch ( ... ) {
		std::stringstream ss;
		alib::ExceptionHandler::handle ( ss );
		QMessageBox::critical ( this, "Error", ss.str ( ).c_str ( ) );
        this->clearScene();
    }
}

void MainWindow::on_actionSave_triggered() {
    auto filename = QFileDialog::getSaveFileName(this,
                                                 "Save file",
                                                 QDir::homePath(),
                                                 "JSON files (*.json);;All files (*.*)");
    if (filename.isEmpty())
        return;

    Json::Value l_data;

    const auto& allBoxes = GraphicsBox::getAllGraphicsBoxes();
    auto& boxes = l_data["boxes"];
    auto& connections = l_data["connections"];

    for (size_t i = 0; i < allBoxes.size(); ++i) {
        auto* box = allBoxes[i];
        Json::Value boxData;
        boxData ["x"] = box->pos().x();
        boxData ["y"] = box->pos().y();

        if (auto* algorithmBox = dynamic_cast<AlgorithmModelBox*>(box->getModelBox())) {
            boxData["type"] = "algorithm";
            boxData["algorithm"] = algorithmBox->getAlgorithm()->getFullName();
        }
        else if (auto* inputBox = dynamic_cast<InputModelBox*>(box->getModelBox())) {
            boxData["type"] = "input";
            if (auto automaton = inputBox->getAutomaton()) {
                if (auto xml = Converter::toXML(automaton, false)) {
                    boxData["data"] = xml->toStdString();
                }
            }
        }
        else if (dynamic_cast<OutputModelBox*>(box->getModelBox())) {
            boxData["type"] = "output";
        }
        else {
            throw std::runtime_error ( "Invalid box type." );
        }

        if (auto* outputConnBox = box->getOutputConnectionBox()) {
            for (const Connection* conn: outputConnBox->getConnections()) {
                auto targetIndex =
                        std::find(allBoxes.begin(), allBoxes.end(), conn->getTargetConnectionBox()->getParent()) -
                        allBoxes.begin();
                Json::Value connection;
                connection["from"] = Json::Value::UInt64(i); /* cast to UInt64 for debian compatibility, see https://github.com/open-source-parsers/jsoncpp/issues/403 */
                connection["to"] = Json::Value::UInt64(targetIndex); /* same */
                connection["slot"] = Json::Value::UInt64(conn->getTargetConnectionBox()->getSlot()); /* same */
                connections.append(connection);
            }
        }
        boxes.append(boxData);
    }

    std::ofstream stream;
    stream.open ( filename.toStdString ( ), std::ifstream::out );

    if ( ! stream ) {
        QMessageBox::warning(this, "Warning", "File does not exist.");
        return;
    }

    stream << l_data;
    stream.close ( );
}

void MainWindow::on_actionClear_triggered() {
    this->clearScene();
}

void MainWindow::clearScene() {
    ui->graphicsView->setScene(nullptr);
    this->scene.reset();
    this->scene = std::make_unique<GraphicsScene>(this);
    ui->graphicsView->setScene(this->scene.get());
    this->scene->addItem(new InputGraphicsBox(std::make_unique<InputModelBox>(), {0, 200}));
    this->outputBox = new GraphicsBox(std::make_unique<OutputModelBox>(), {400, 200});;
    this->scene->addItem(outputBox);
}
