#include "batchprocessing.h"
#include "ui_batchprocessing.h"
#include <functional>
#include <QDir>
#include <QFileDialog>
#include <QStandardPaths>
#include <QProcess>
#include <QSettings>
#include <QCloseEvent>
#include <QMessageBox>
#include <QDesktopServices>
#include <QInputDialog>
#include <QChart>
#include <QChartView>
#include <QLineSeries>
#include <QCompleter>
#include "scriptengine.h"
#include "chartgraph.h"

#ifdef Q_OS_LINUX
#include <QDBusConnection>
#include <QDBusMessage>
#endif

QList<QPair<QString, QString>> scanDirectories(const QStringList &paths)
{
    QList<QPair<QString, QString>> files;
    QStringList scannedDirs;

    std::function<void(const QString &root, const QString &path)> scanDirectory = [&](const QString &root, const QString &path)
    {
        QFileInfo info(path);
        if(info.isDir() && !scannedDirs.contains(info.canonicalFilePath()))
        {
            scannedDirs.append(info.canonicalFilePath());
            QDir dir(path);
            QStringList entries = dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
            for(QString &entry : entries)
                scanDirectory(root, dir.absoluteFilePath(entry));
        }
        else if(info.isFile())
        {
            if(path == root)
                files.append({path, info.absolutePath()});
            else
                files.append({path, root});
        }
    };

    for(const QString &path : paths)
        scanDirectory(path, path);

    return files;
}

void BatchProcessing::scanScriptDir()
{
    QString current;
    if(_ui->scriptsList->currentItem())
        current = _ui->scriptsList->currentItem()->text();

    _ui->scriptsList->clear();
    QDir dir(_scriptBasePath);
    QDir embededDir(":/scripts");
    QStringList scripts = dir.entryList(QDir::Files | QDir::Readable);
    scripts.append(embededDir.entryList(QDir::Files));
    scripts.removeDuplicates();
    _ui->scriptsList->addItems(scripts);

    int idx = scripts.indexOf(current);
    if(idx>=0)_ui->scriptsList->setCurrentRow(idx);
}

BatchProcessing::BatchProcessing(Database *database, QWidget *parent) : QDialog(parent)
    , _database(database)
{
    _ui = new Ui::BatchProcessing;
    _ui->setupUi(this);

    QStringList scriptsPath = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
    if(scriptsPath.size())
    {
        QDir dir(scriptsPath.first());
        if(!dir.exists("scripts"))
        {
            if(!dir.mkpath("scripts"))
                qWarning() << "Failed to create scripts directory";
        }
        dir.cd("scripts");

        _scriptBasePath = dir.absolutePath() + "/";
        scanScriptDir();
        _fileWatcher.addPath(_scriptBasePath);
        connect(&_fileWatcher, &QFileSystemWatcher::directoryChanged, this, &BatchProcessing::scanScriptDir);
    }
    else
    {
        qWarning() << "Failed to get app data location";
    }

    connect(_ui->addFilesButton, &QPushButton::released, this, static_cast<void (BatchProcessing::*)()>(&BatchProcessing::addFiles));
    connect(_ui->addDirButton, &QPushButton::released, this, static_cast<void (BatchProcessing::*)()>(&BatchProcessing::addDir));
    connect(_ui->addMarkedButton, &QPushButton::released, this, &BatchProcessing::addMarked);
    connect(_ui->removeButton, &QPushButton::released, this, &BatchProcessing::removePath);
    connect(_ui->removeAllButton, &QPushButton::released, this, &BatchProcessing::removeAllPaths);
    connect(_ui->startButton, &QPushButton::released, this, static_cast<void (BatchProcessing::*)()>(&BatchProcessing::runScript));
    connect(_ui->stopButton, &QPushButton::released, this, &BatchProcessing::stopScript);
    connect(_ui->browseButton, &QPushButton::released, this, &BatchProcessing::browse);
    connect(_ui->openScriptsButton, &QPushButton::released, this, &BatchProcessing::openScriptDir);

    _textColor = _ui->log->palette().text().color();

    _engine = new Script::ScriptEngine(_database, this);
    connect(_engine, &Script::ScriptEngine::newMessage, this, &BatchProcessing::newMessage);

    _completerModel = new QStringListModel(this);
    _completer = new QCompleter(_completerModel, this);
    _ui->consoleLineEdit->setCompleter(_completer);
    connect(_ui->executeButton, &QPushButton::clicked, _ui->consoleLineEdit, &QLineEdit::returnPressed);
    connect(_ui->consoleLineEdit, &QLineEdit::returnPressed, [this](){
        if(!_completer->popup()->isVisible())
        {
            QString program = _ui->consoleLineEdit->text();
            QJSValue val = _engine->eval(program);
            _ui->consoleLineEdit->addLine();
            //qDebug() << val.toString();
        }
    });

    connect(_ui->consoleLineEdit, &QLineEdit::textEdited, [this](const QString &text){
        QStringList comp = _engine->complete(text);
        //qDebug() << comp;
        _completerModel->setStringList(comp);
    });

    _ui->addFilesButton->setAutoDefault(false);

    QSettings settings;
    _ui->outputPath->setText(settings.value("batchprocessing/outputpath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString());
}

BatchProcessing::~BatchProcessing()
{
    delete _engineThread;
    QSettings settings;
    settings.setValue("batchprocessing/outputpath", _ui->outputPath->text());
    delete _ui;
}

void BatchProcessing::setOutputDir(const QString &output)
{
    _ui->outputPath->setText(output);
}

void BatchProcessing::setPaths(const QStringList &paths)
{
    _ui->pathsList->addItems(paths);
    refreshPaths();
}

void BatchProcessing::closeEvent(QCloseEvent *event)
{
    if(_engineThread)
    {
        QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Interrupt running script?"), tr("Interrupt running script?"));
        if(ret == QMessageBox::StandardButton::Yes)
        {
            _engineThread->interrupt();
            event->accept();
        }
        else
        {
            event->ignore();
        }
    }
    else
    {
        event->accept();
    }
}

void BatchProcessing::refreshPaths()
{
    QStringList paths;
    for(int i=0; i<_ui->pathsList->count(); i++)
        paths.append(_ui->pathsList->item(i)->text());
    _paths = scanDirectories(paths);
    _engine->setParams("", _paths, _ui->outputPath->text(), QString());
}

void BatchProcessing::addFiles()
{
    QSettings settings;
    QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files"), settings.value("batchprocessing/inputpath", QDir::homePath()).toString());
    if(!files.isEmpty())
    {
        _ui->pathsList->addItems(files);
        settings.setValue("batchprocessing/inputpath", QFileInfo(files.first()).absolutePath());
    }
    refreshPaths();
}

void BatchProcessing::addDir()
{
    QSettings settings;
    QString dir = QFileDialog::getExistingDirectory(this, tr("Select directory"), settings.value("batchprocessing/inputpath", QDir::homePath()).toString());
    if(!dir.isEmpty())
    {
        _ui->pathsList->addItem(dir);
        settings.setValue("batchprocessing/inputpath", dir);
    }
    refreshPaths();
}

void BatchProcessing::addMarked()
{
    QStringList files = _database->getMarkedFiles();
    for(const QString &file : files)
    {
        QFileInfo info(file);
        if(info.exists() && info.isReadable())
            _ui->pathsList->addItem(file);
    };
    refreshPaths();
}

void BatchProcessing::removePath()
{
    for(auto &item : _ui->pathsList->selectedItems())
        delete item;
    refreshPaths();
}

void BatchProcessing::removeAllPaths()
{
    _ui->pathsList->clear();
    refreshPaths();
}

void BatchProcessing::browse()
{
    QString output = QFileDialog::getExistingDirectory(this, tr("Select output directory"), _ui->outputPath->text());
    if(!output.isEmpty())
        _ui->outputPath->setText(output);
}

void BatchProcessing::openScriptDir()
{
    openDir(_scriptBasePath);
}

void BatchProcessing::runScript()
{
    _ui->log->clear();
    auto selectedItems = _ui->scriptsList->selectedItems();
    if(selectedItems.size())
    {
        _engineThread = new Script::ScriptEngineThread(_database, this);
        connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessage);
        connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::scriptFinished);

        QFileInfo outDir(_ui->outputPath->text());
        if(outDir.exists() && outDir.isWritable())
        {
            QString script = selectedItems.first()->text();
            if(QDir(_scriptBasePath).exists(script))
                script = _scriptBasePath + script;
            else
                script = ":/scripts/" + script;

            _engineThread->setParams(script, _paths, _ui->outputPath->text(), QString());
            _engineThread->start();
            _ui->startButton->setEnabled(false);
            _ui->stopButton->setEnabled(true);
        }
        else
        {
            QMessageBox::warning(this, tr("Invalid output directory"), tr("Output directory path doesn't exist or is not writable"));
            delete _engineThread;
            _engineThread = nullptr;
        }
    }
}

void BatchProcessing::runScript(const QString &script, const QString &arg, bool exit)
{
    _ui->log->clear();
    {
        _engineThread = new Script::ScriptEngineThread(_database, this);
        connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessage);
        connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessageCli);
        connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::scriptFinished);
        if(exit)connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::accept);

        QFileInfo outDir(_ui->outputPath->text());
        if(outDir.exists() && outDir.isWritable())
        {
            _engineThread->setParams(script, _paths, _ui->outputPath->text(), arg);
            _engineThread->start();
            _ui->startButton->setEnabled(false);
            _ui->stopButton->setEnabled(true);
        }
        else
        {
            QMessageBox::warning(this, tr("Invalid output directory"), tr("Output directory path doesn't exist or is not writable"));
            delete _engineThread;
            _engineThread = nullptr;
        }
    }
}

void BatchProcessing::stopScript()
{
    qDebug() << "Stop script";
    if(_engineThread)
        _engineThread->interrupt();
}

void BatchProcessing::scriptFinished()
{
    _ui->startButton->setEnabled(true);
    _ui->stopButton->setEnabled(false);
    qDebug() << "script finished";
    _engineThread->deleteLater();
    _engineThread = nullptr;
}

void BatchProcessing::newMessage(const QString &message, bool error)
{
    if(error)_ui->log->setTextColor(Qt::red);
    else _ui->log->setTextColor(_textColor);
    _ui->log->append(message);
}

void BatchProcessing::newMessageCli(const QString &message, bool error)
{
    if(error)
        qWarning() << message;
    else
        qInfo() << message;
}

QJSValue BatchProcessing::getString(const QString &label, const QString &text)
{
    bool ok = false;
    QString ret = QInputDialog::getText(this, tr("Enter text"), label, QLineEdit::Normal, text, &ok);
    return ok ? ret : QJSValue();
}

QJSValue BatchProcessing::getInt(const QString &label, int value)
{
    bool ok = false;
    int ret = QInputDialog::getInt(this, tr("Enter integer number"), label, value, INT_MIN, INT_MAX, 1, &ok);
    return ok ? ret : QJSValue();
}

QJSValue BatchProcessing::getFloat(const QString &label, double value, int decimals)
{
    bool ok = false;
    double ret = QInputDialog::getDouble(this, tr("Enter float number"), label, value, -INFINITY, INFINITY, decimals, &ok);
    return ok ? ret : QJSValue();
}

QJSValue BatchProcessing::getItem(const QStringList &items, const QString &label, int current)
{
    bool ok = false;
    QString ret = QInputDialog::getItem(this, tr("Select item"), label, items, current, false, &ok);
    return ok ? ret : QJSValue();
}

QJSValue BatchProcessing::question(const QString &question, const QStringList &buttons, const QString &title)
{
    QMessageBox::StandardButtons standardButtons = QMessageBox::NoButton;
    if(buttons.contains("ok"))standardButtons |= QMessageBox::Ok;
    if(buttons.contains("yes"))standardButtons |= QMessageBox::Yes;
    if(buttons.contains("no"))standardButtons |= QMessageBox::No;
    if(buttons.contains("yesall"))standardButtons |= QMessageBox::YesToAll;
    if(buttons.contains("noall"))standardButtons |= QMessageBox::NoToAll;
    if(buttons.contains("abort"))standardButtons |= QMessageBox::Abort;
    if(buttons.contains("retry"))standardButtons |= QMessageBox::Retry;
    if(buttons.contains("ignore"))standardButtons |= QMessageBox::Ignore;
    if(buttons.contains("cancel"))standardButtons |= QMessageBox::Cancel;
    if(buttons.contains("discard"))standardButtons |= QMessageBox::Discard;
    if(buttons.contains("apply"))standardButtons |= QMessageBox::Apply;
    if(buttons.contains("reset"))standardButtons |= QMessageBox::Reset;
    if(standardButtons == QMessageBox::NoButton)standardButtons = QMessageBox::Ok;

    QMessageBox::StandardButton button = QMessageBox::question(this, title, question, standardButtons);
    QJSValue ret;
    switch(button)
    {
    default:
    case QMessageBox::Ok: ret = "ok"; break;
    case QMessageBox::Yes: ret = "yes";  break;
    case QMessageBox::No: ret = "no"; break;
    case QMessageBox::YesToAll: ret = "yesall"; break;
    case QMessageBox::NoToAll: ret = "noall"; break;
    case QMessageBox::Abort: ret = "abort"; break;
    case QMessageBox::Retry: ret = "retry"; break;
    case QMessageBox::Ignore: ret = "ignore"; break;
    case QMessageBox::Cancel: ret = "cancel"; break;
    case QMessageBox::Discard: ret = "discard"; break;
    case QMessageBox::Apply: ret = "apply"; break;
    case QMessageBox::Reset: ret = "reset"; break;
    }

    return ret;
}

void BatchProcessing::plot(const QVariant &graph)
{
    ChartGraph *chart = new ChartGraph(this);
    chart->plot(graph);
}

ConsoleLine::ConsoleLine(QWidget *parent) : QLineEdit(parent)
{
}

void ConsoleLine::addLine()
{
    QString line = text();
    clear();
    if(_history.size() && _history.last() == line)return;

    _history.append(line);
    if(_history.size() > 100)_history.removeFirst();
    _currentLine = _history.size();
}

void ConsoleLine::keyReleaseEvent(QKeyEvent *event)
{
    if(event->key() == Qt::Key_Up)
    {
        _currentLine--;
        if(_currentLine < 0)
        {
            _currentLine = -1;
            clear();
            return;
        }
        setText(_history.at(_currentLine));
    }
    else if(event->key() == Qt::Key_Down)
    {
        _currentLine++;
        if(_currentLine >= _history.size())
        {
            _currentLine = _history.size();
            clear();
            return;
        }
        setText(_history.at(_currentLine));
    }
    else QLineEdit::keyReleaseEvent(event);
}

void openDir(const QString &path)
{
#ifdef Q_OS_LINUX
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.FileManager1", "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1", "ShowFolders");
    QList<QVariant> args = {QStringList(QUrl::fromLocalFile(path).toString()), QString()};
    message.setArguments(args);
    con.call(message);
#endif
#ifdef Q_OS_WINDOWS
    QProcess::startDetached("explorer.exe", {QDir::toNativeSeparators(path)});
#endif
#ifdef Q_OS_MACOS
    QDesktopServices::openUrl(QUrl::fromLocalFile(path));
#endif
}
