//
// Copyright (C) 2017 James Turner  zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

#include "applicationcontroller.h"

#include <QNetworkDiskCache>
#include <QStandardPaths>
#include <QNetworkRequest>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QDebug>
#include <QFile>
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <QDataStream>
#include <QWindow>
#include <QTimer>
#include <QGuiApplication>
#include <QSettings>
#include <QQuickView>
#include <QQmlContext>

#include "jsonutils.h"
#include "canvasconnection.h"
#include "WindowData.h"

ApplicationController::ApplicationController(QObject *parent)
    : QObject(parent)
    , m_status(Idle)
{
    m_netAccess = new QNetworkAccessManager;

    QSettings settings;
    m_host = settings.value("last-host", "localhost").toString();
    m_port = settings.value("last-port", 8080).toUInt();

    QNetworkDiskCache* cache = new QNetworkDiskCache;
    cache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
    m_netAccess->setCache(cache); // takes ownership

    setStatus(Idle);
    rebuildConfigData();
    rebuildSnapshotData();

    m_uiIdleTimer = new QTimer(this);
    m_uiIdleTimer->setInterval(10 * 1000);
    connect(m_uiIdleTimer, &QTimer::timeout, this,
            &ApplicationController::onUIIdleTimeout);
    m_uiIdleTimer->start();

    qApp->installEventFilter(this);
}

ApplicationController::~ApplicationController()
{
    delete m_netAccess;
}

void ApplicationController::loadFromFile(QString path)
{
    if (!QFile::exists(path)) {
        qWarning() << Q_FUNC_INFO << "no such file:" << path;
    }

    QFile f(path);
    if (!f.open(QIODevice::ReadOnly)) {
        qWarning() << Q_FUNC_INFO << "failed to open" << path;
        return;
    }

    restoreState(f.readAll());
}

void ApplicationController::setDaemonMode()
{
    m_daemonMode = true;
}

void ApplicationController::createWindows()
{
    if (m_windowList.empty()) {
        defineDefaultWindow();
    }

    for (int index = 0; index < m_windowList.size(); ++index) {
        auto wd = m_windowList.at(index);
        QQuickView* qqv = new QQuickView;
        qqv->rootContext()->setContextProperty("_application", this);
        qqv->rootContext()->setContextProperty("_windowNumber", index);
        qqv->setResizeMode(QQuickView::SizeRootObjectToView);
        qqv->setSource(QUrl{"qrc:///qml/Window.qml"});
        qqv->setTitle(wd->title());

        if (m_daemonMode) {
            qqv->setScreen(wd->screen());
            qqv->setGeometry(wd->windowRect());
            qqv->setWindowState(wd->windowState());
        } else {
            // interactive mode, restore window size etc

        }

        qqv->show();
    }
}

void ApplicationController::defineDefaultWindow()
{
    auto w = new WindowData(this);
    w->setWindowRect(QRect{0, 0, 1024, 768});
    m_windowList.append(w);
    emit windowListChanged();
}

void ApplicationController::save(QString configName)
{
    QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
    if (!d.exists()) {
        d.mkpath(".");
    }

    // convert spaces to underscores
    QString filesystemCleanName = configName.replace(QRegularExpression("[\\s-\\\"/]"), "_");

    QFile f(d.filePath(filesystemCleanName + ".json"));
    if (f.exists()) {
        qWarning() << "not over-writing" << f.fileName();
        return;
    }

    f.open(QIODevice::WriteOnly | QIODevice::Truncate);
    f.write(saveState(configName));

    QVariantMap m;
    m["path"] = f.fileName();
    m["name"] = configName;
    m_configs.append(m);
    emit configListChanged(m_configs);
}

void ApplicationController::rebuildConfigData()
{
    m_configs.clear();
    QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
    if (!d.exists()) {
        emit configListChanged(m_configs);
        return;
    }

    // this requires parsing each config in its entirety just to extract
    // the name, which is horrible.
    Q_FOREACH (auto entry, d.entryList(QStringList() << "*.json")) {
        QString path = d.filePath(entry);
        QFile f(path);
        f.open(QIODevice::ReadOnly);
        QJsonDocument doc = QJsonDocument::fromJson(f.readAll());

        QVariantMap m;
        m["path"] = path;
        m["name"] = doc.object().value("configName").toString();
        m_configs.append(m);
    }

    emit configListChanged(m_configs);
}

void ApplicationController::saveSnapshot(QString snapshotName)
{
    QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
    d.cd("Snapshots");
    if (!d.exists()) {
        d.mkpath(".");
    }

    // convert spaces to underscores
    QString filesystemCleanName = snapshotName.replace(QRegularExpression("[\\s-\\\"/]"), "_");
    QFile f(d.filePath(filesystemCleanName + ".fgcanvassnapshot"));
    if (f.exists()) {
        qWarning() << "not over-writing" << f.fileName();
        return;
    }

    f.open(QIODevice::WriteOnly | QIODevice::Truncate);
    f.write(createSnapshot(snapshotName));

    QVariantMap m;
    m["path"] = f.fileName();
    m["name"] = snapshotName;
    m_snapshots.append(m);
    emit snapshotListChanged();
}

void ApplicationController::restoreSnapshot(int index)
{
    QString path = m_snapshots.at(index).toMap().value("path").toString();
    QFile f(path);
    if (!f.open(QIODevice::ReadOnly)) {
        qWarning() << Q_FUNC_INFO << "failed to open the file";
        return;
    }

    clearConnections();

    {
        QDataStream ds(&f);
        int version, canvasCount;
        QString name;
        ds >> version >> name >> canvasCount;

        for (int i=0; i < canvasCount; ++i) {
            CanvasConnection* cc = new CanvasConnection(this);
            cc->restoreSnapshot(ds);
            m_activeCanvases.append(cc);
        }
    }

    emit activeCanvasesChanged();
}

void ApplicationController::rebuildSnapshotData()
{
    m_snapshots.clear();
    QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
    d.cd("Snapshots");
    if (!d.exists()) {
        emit snapshotListChanged();
        return;
    }

    Q_FOREACH (auto entry, d.entryList(QStringList() << "*.fgcanvassnapshot")) {
        QFile f(d.filePath(entry));
        f.open(QIODevice::ReadOnly);
        {
            QDataStream ds(&f);
            int version;
            QString name;
            ds >> version;

            QVariantMap m;
            m["path"] = f.fileName();
            ds >>name;
            m["name"] = name;
            m_snapshots.append(m);
        }
    }

    emit snapshotListChanged();
}

void ApplicationController::query()
{
    if (m_query) {
        cancelQuery();
    }

    if (m_host.isEmpty() || (m_port == 0))
        return;

    QSettings settings;
    settings.setValue("last-host", m_host);
    settings.setValue("last-port", m_port);

    QUrl queryUrl;
    queryUrl.setScheme("http");
    queryUrl.setHost(m_host);
    queryUrl.setPort(static_cast<int>(m_port));
    queryUrl.setPath("/json/canvas/by-index");
    queryUrl.setQuery("d=2");

    m_query = m_netAccess->get(QNetworkRequest(queryUrl));
    connect(m_query, &QNetworkReply::finished,
            this, &ApplicationController::onFinishedGetCanvasList);

    setStatus(Querying);
}

void ApplicationController::cancelQuery()
{
    setStatus(Idle);
    if (m_query) {
        m_query->abort();
        m_query->deleteLater();
    }

    m_query = nullptr;
    m_canvases.clear();
    emit canvasListChanged();
}

void ApplicationController::clearQuery()
{
    cancelQuery();
}

void ApplicationController::restoreConfig(int index)
{
    QString path = m_configs.at(index).toMap().value("path").toString();
    QFile f(path);
    if (!f.open(QIODevice::ReadOnly)) {
        qWarning() << Q_FUNC_INFO << "failed to open the file";
        return;
    }

    restoreState(f.readAll());
}

void ApplicationController::deleteConfig(int index)
{
    QString path = m_configs.at(index).toMap().value("path").toString();
    QFile f(path);
    if (!f.remove()) {
        qWarning() << "failed to remove file";
        return;
    }

    m_configs.removeAt(index);
    emit configListChanged(m_configs);
}

void ApplicationController::saveConfigChanges(int index)
{
    QString path = m_configs.at(index).toMap().value("path").toString();
    QString name = m_configs.at(index).toMap().value("name").toString();
    doSaveToFile(path, name);
}

void ApplicationController::doSaveToFile(QString path, QString configName)
{
    QFile f(path);
    f.open(QIODevice::WriteOnly | QIODevice::Truncate);
    f.write(saveState(configName));
}

void ApplicationController::openCanvas(QString path)
{
    CanvasConnection* cc = new CanvasConnection(this);

    cc->setNetworkAccess(m_netAccess);
    m_activeCanvases.append(cc);

    cc->setRootPropertyPath(path.toUtf8());
    cc->connectWebSocket(m_host.toUtf8(), m_port);

    emit activeCanvasesChanged();
}

void ApplicationController::closeCanvas(CanvasConnection *canvas)
{
    Q_ASSERT(m_activeCanvases.indexOf(canvas) >= 0);
    m_activeCanvases.removeOne(canvas);
    canvas->deleteLater();
    emit activeCanvasesChanged();
}

QString ApplicationController::host() const
{
    return m_host;
}

unsigned int ApplicationController::port() const
{
    return m_port;
}

QVariantList ApplicationController::canvases() const
{
    return m_canvases;
}

QQmlListProperty<CanvasConnection> ApplicationController::activeCanvases()
{
    return QQmlListProperty<CanvasConnection>(this, m_activeCanvases);
}

QQmlListProperty<WindowData> ApplicationController::windowList()
{
    return QQmlListProperty<WindowData>(this, m_windowList);
}

QNetworkAccessManager *ApplicationController::netAccess() const
{
    return m_netAccess;
}

bool ApplicationController::showUI() const
{
    if (m_daemonMode)
        return false;

    if (m_blockUIIdle)
        return true;

    return m_showUI;
}

QString ApplicationController::gettingStartedText() const
{
    QFile f(":/doc/gettingStarted.html");
    f.open(QIODevice::ReadOnly);
    return QString::fromUtf8(f.readAll());
}

bool ApplicationController::showGettingStarted() const
{
    if (m_daemonMode) return false;
    QSettings settings;
    return settings.value("show-getting-started", true).toBool();
}

void ApplicationController::setHost(QString host)
{
    if (m_host == host)
        return;

    m_host = host;
    emit hostChanged(m_host);
    setStatus(Idle);
}

void ApplicationController::setPort(unsigned int port)
{
    if (m_port == port)
        return;

    m_port = port;
    emit portChanged(m_port);
    setStatus(Idle);
}

void ApplicationController::setShowGettingStarted(bool show)
{
    QSettings settings;
    if (settings.value("show-getting-started", true).toBool() == show)
        return;

    settings.setValue("show-getting-started", show);
    emit showGettingStartedChanged(show);
}

QJsonObject jsonPropNodeFindChild(QJsonObject obj, QByteArray name)
{
    Q_FOREACH (QJsonValue v, obj.value("children").toArray()) {
        QJsonObject vo = v.toObject();
        if (vo.value("name").toString() == name) {
            return vo;
        }
    }

    return QJsonObject();
}

void ApplicationController::onFinishedGetCanvasList()
{
    m_canvases.clear();
    QNetworkReply* reply = m_query;
    m_query = nullptr;
    reply->deleteLater();

    if (reply->error() != QNetworkReply::NoError) {
        setStatus(QueryFailed);
        emit canvasListChanged();
        return;
    }

    QJsonDocument json = QJsonDocument::fromJson(reply->readAll());

    QJsonArray canvasArray = json.object().value("children").toArray();
    Q_FOREACH (QJsonValue canvasValue, canvasArray) {
        QJsonObject canvas = canvasValue.toObject();
        QString canvasName = jsonPropNodeFindChild(canvas, "name").value("value").toString();
        QString propPath = canvas.value("path").toString();

        QVariantMap info;
        info["name"] = canvasName;
        info["path"] = propPath;
        m_canvases.append(info);
    }

    emit canvasListChanged();
    setStatus(SuccessfulQuery);
}

void ApplicationController::onUIIdleTimeout()
{
    m_showUI = false;
    emit showUIChanged();
}

void ApplicationController::setStatus(ApplicationController::Status newStatus)
{
    if (newStatus == m_status)
        return;

    m_status = newStatus;
    emit statusChanged(m_status);
}

QByteArray ApplicationController::saveState(QString name) const
{
    QJsonObject json;
    json["configName"] = name;

    QJsonArray canvases;
    Q_FOREACH (auto canvas, m_activeCanvases) {
        canvases.append(canvas->saveState());
    }

    json["canvases"] = canvases;

    QJsonArray windows;
    Q_FOREACH (auto w, m_windowList) {
        windows.append(w->saveState());
    }
    json["windows"] = windows;

    // background color?

    QJsonDocument doc;
    doc.setObject(json);
    return doc.toJson();
}

void ApplicationController::restoreState(QByteArray bytes)
{
    clearConnections();

    QJsonDocument jsonDoc = QJsonDocument::fromJson(bytes);
    QJsonObject json = jsonDoc.object();

    // clear windows
    Q_FOREACH(auto w, m_windowList) {
        w->deleteLater();
    }
    m_windowList.clear();

    for (auto w : json.value("windows").toArray()) {
        auto wd = new WindowData(this);
        m_windowList.append(wd);
        wd->restoreState(w.toObject());
    }

    if (m_windowList.isEmpty()) {
        // check for previous single-window data
        auto w = new WindowData(this);
        if (json.contains("window-rect")) {
            w->setWindowRect(jsonArrayToRect(json.value("window-rect").toArray()));
        }
        if (json.contains("window-state")) {
            w->setWindowState(static_cast<Qt::WindowState>(json.value("window-state").toInt()));
        }
        m_windowList.append(w);
    }

    for (auto c : json.value("canvases").toArray()) {
        auto cc = new CanvasConnection(this);
        if (m_daemonMode)
            cc->setAutoReconnect();

        cc->setNetworkAccess(m_netAccess);
        m_activeCanvases.append(cc);
        cc->restoreState(c.toObject());
        cc->reconnect();
    }

    emit windowListChanged();
    emit activeCanvasesChanged();
}

void ApplicationController::clearConnections()
{
    Q_FOREACH(auto c, m_activeCanvases) {
        c->deleteLater();
    }
    m_activeCanvases.clear();
    emit activeCanvasesChanged();
}

QByteArray ApplicationController::createSnapshot(QString name) const
{
    QByteArray bytes;
    const int version = 1;
    {
         QDataStream ds(&bytes, QIODevice::WriteOnly);
         ds << version << name;

         ds << m_activeCanvases.size();
         Q_FOREACH(auto c, m_activeCanvases) {
             c->saveSnapshot(ds);
         }
    }

    return bytes;
}

bool ApplicationController::eventFilter(QObject* obj, QEvent* event)
{
    Q_UNUSED(obj);
    switch (event->type()) {
    case QEvent::MouseButtonPress:
    case QEvent::TouchUpdate:
    case QEvent::MouseMove:
    case QEvent::TouchBegin:
    case QEvent::KeyPress:
    case QEvent::KeyRelease:
        if (!m_showUI) {
            m_showUI = true;
            emit showUIChanged();
        } else {
            m_uiIdleTimer->start();
        }

        break;
    default:
        break;
    }

    return false; //process as normal
}