/*************************************************************************************
 *  Copyright 2014-2015 Sebastian Kügler <sebas@kde.org>                             *
 *  Copyright 2013 Martin Gräßlin <mgraesslin@kde.org>                               *
 *                                                                                   *
 *  This library is free software; you can redistribute it and/or                    *
 *  modify it under the terms of the GNU Lesser General Public                       *
 *  License as published by the Free Software Foundation; either                     *
 *  version 2.1 of the License, or (at your option) any later version.               *
 *                                                                                   *
 *  This library 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                *
 *  Lesser General Public License for more details.                                  *
 *                                                                                   *
 *  You should have received a copy of the GNU Lesser General Public                 *
 *  License along with this library; if not, write to the Free Software              *
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA       *
 *************************************************************************************/
#include "waylandconfig.h"

#include "waylandbackend.h"
#include "waylandoutput.h"
#include "waylandscreen.h"

#include "tabletmodemanager_interface.h"

#include <configmonitor.h>
#include <mode.h>

#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/event_queue.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/outputconfiguration.h>
#include <KWayland/Client/outputmanagement.h>

#include <QTimer>

using namespace KScreen;

WaylandConfig::WaylandConfig(QObject *parent)
    : QObject(parent)
    , m_outputManagement(nullptr)
    , m_registryInitialized(false)
    , m_blockSignals(true)
    , m_kscreenConfig(new Config)
    , m_kscreenPendingConfig(nullptr)
    , m_screen(new WaylandScreen(this))
    , m_tabletModeAvailable(false)
    , m_tabletModeEngaged(false)
{
    initKWinTabletMode();

    connect(this, &WaylandConfig::initialized, &m_syncLoop, &QEventLoop::quit);
    QTimer::singleShot(3000, this, [this] {
        if (m_syncLoop.isRunning()) {
            qCWarning(KSCREEN_WAYLAND) << "Connection to Wayland server at socket:"
                                       << m_connection->socketName() << "timed out.";
            m_syncLoop.quit();
            m_thread->quit();
            m_thread->wait();
        }
    });

    initConnection();
    m_syncLoop.exec();
}

WaylandConfig::~WaylandConfig()
{
    m_thread->quit();
    m_thread->wait();
    m_syncLoop.quit();
}

void WaylandConfig::initKWinTabletMode()
{
    auto *interface = new OrgKdeKWinTabletModeManagerInterface(QStringLiteral("org.kde.KWin"),
                                                           QStringLiteral("/org/kde/KWin"),
                                                           QDBusConnection::sessionBus(), this);
    if (!interface->isValid()) {
        m_tabletModeAvailable = false;
        m_tabletModeEngaged = false;
        return;
    }

    m_tabletModeAvailable = interface->tabletModeAvailable();
    m_tabletModeEngaged = interface->tabletMode();

    connect(interface, &OrgKdeKWinTabletModeManagerInterface::tabletModeChanged,
            this, [this](bool tabletMode) {
                if (m_tabletModeEngaged == tabletMode) {
                    return;
                }
                m_tabletModeEngaged = tabletMode;
                if (!m_blockSignals && m_initializingOutputs.empty()) {
                    Q_EMIT configChanged();
                }
            }
    );
    connect(interface, &OrgKdeKWinTabletModeManagerInterface::tabletModeAvailableChanged,
            this, [this](bool available) {
                if (m_tabletModeAvailable == available) {
                    return;
                }
                m_tabletModeAvailable = available;
                if (!m_blockSignals && m_initializingOutputs.empty()) {
                    Q_EMIT configChanged();
                }
    });
}

void WaylandConfig::initConnection()
{
    m_thread = new QThread(this);
    m_connection = new KWayland::Client::ConnectionThread;

    connect(m_connection, &KWayland::Client::ConnectionThread::connected,
            this, &WaylandConfig::setupRegistry, Qt::QueuedConnection);

    connect(m_connection, &KWayland::Client::ConnectionThread::connectionDied,
            this, &WaylandConfig::disconnected, Qt::QueuedConnection);

    connect(m_connection, &KWayland::Client::ConnectionThread::failed, this, [this] {
        qCWarning(KSCREEN_WAYLAND) << "Failed to connect to Wayland server at socket:"
                                   << m_connection->socketName();
        m_syncLoop.quit();
        m_thread->quit();
        m_thread->wait();
    });

    m_thread->start();
    m_connection->moveToThread(m_thread);
    m_connection->initConnection();

}

void WaylandConfig::blockSignals()
{
    Q_ASSERT(m_blockSignals == false);
    m_blockSignals = true;
}

void WaylandConfig::unblockSignals()
{
    Q_ASSERT(m_blockSignals == true);
    m_blockSignals = false;
}

void WaylandConfig::disconnected()
{
    qCWarning(KSCREEN_WAYLAND) << "Wayland disconnected, cleaning up.";
    qDeleteAll(m_outputMap);
    m_outputMap.clear();

    // Clean up
    if (m_queue) {
        delete m_queue;
        m_queue = nullptr;
    }

    m_connection->deleteLater();
    m_connection = nullptr;

    if (m_thread) {
        m_thread->quit();
        if (!m_thread->wait(3000)) {
            m_thread->terminate();
            m_thread->wait();
        }
        delete m_thread;
        m_thread = nullptr;
    }

    Q_EMIT configChanged();
}

void WaylandConfig::setupRegistry()
{
    m_queue = new KWayland::Client::EventQueue(this);
    m_queue->setup(m_connection);

    m_registry = new KWayland::Client::Registry(this);

    connect(m_registry, &KWayland::Client::Registry::outputDeviceAnnounced,
            this, &WaylandConfig::addOutput);

    connect(m_registry, &KWayland::Client::Registry::outputManagementAnnounced,
            this, [this](quint32 name, quint32 version) {
                m_outputManagement = m_registry->createOutputManagement(name, version, m_registry);
                checkInitialized();
            }
    );

    connect(m_registry, &KWayland::Client::Registry::interfacesAnnounced,
            this, [this] {
                m_registryInitialized = true;
                unblockSignals();
                checkInitialized();
            }
    );

    m_registry->create(m_connection);
    m_registry->setEventQueue(m_queue);
    m_registry->setup();
}

int s_outputId = 0;

void WaylandConfig::addOutput(quint32 name, quint32 version)
{
    WaylandOutput *waylandoutput = new WaylandOutput(++s_outputId, this);
    m_initializingOutputs << waylandoutput;

    connect(waylandoutput, &WaylandOutput::deviceRemoved, this, [this, waylandoutput]() {
        removeOutput(waylandoutput);
    });
    waylandoutput->createOutputDevice(m_registry, name, version);

    // finalize: when the output is done, we put it in the known outputs map,
    // remove if from the list of initializing outputs, and emit configChanged()
    connect(waylandoutput, &WaylandOutput::complete, this, [this, waylandoutput]{
        m_outputMap.insert(waylandoutput->id(), waylandoutput);
        m_initializingOutputs.removeOne(waylandoutput);
        checkInitialized();

        if (!m_blockSignals && m_initializingOutputs.empty()) {
            m_screen->setOutputs(m_outputMap.values());
            Q_EMIT configChanged();
        }

        connect(waylandoutput, &WaylandOutput::changed, this, [this]() {
            if (!m_blockSignals) {
                Q_EMIT configChanged();
            }
        });
    });
}

void WaylandConfig::removeOutput(WaylandOutput *output)
{
    if (m_initializingOutputs.removeOne(output)) {
        // output was not yet fully initialized, just remove here and return
        delete output;
        return;
    }

    // remove the output from output mapping
    const auto removedOutput = m_outputMap.take(output->id());
    Q_ASSERT(removedOutput == output); Q_UNUSED(removedOutput);
    m_screen->setOutputs(m_outputMap.values());
    delete output;

    if (!m_blockSignals) {
        Q_EMIT configChanged();
    }
}

bool WaylandConfig::isInitialized() const
{
    return !m_blockSignals
            && m_registryInitialized
            && m_initializingOutputs.isEmpty()
            && m_outputMap.count() > 0
            && m_outputManagement != nullptr;
}

void WaylandConfig::checkInitialized()
{
    if (isInitialized()) {
        m_screen->setOutputs(m_outputMap.values());
        Q_EMIT initialized();
    }
}

KScreen::ConfigPtr WaylandConfig::currentConfig()
{
    // TODO: do this setScreen call less clunky
    m_kscreenConfig->setScreen(m_screen->toKScreenScreen(m_kscreenConfig));

    const auto features = Config::Feature::Writable | Config::Feature::PerOutputScaling
                        | Config::Feature::AutoRotation | Config::Feature::TabletMode;
    m_kscreenConfig->setSupportedFeatures(features);
    m_kscreenConfig->setValid(m_connection->display());

    KScreen::ScreenPtr screen = m_kscreenConfig->screen();
    m_screen->updateKScreenScreen(screen);

    //Removing removed outputs
    const KScreen::OutputList outputs = m_kscreenConfig->outputs();
    for (const auto &output : outputs) {
        if (!m_outputMap.contains(output->id())) {
            m_kscreenConfig->removeOutput(output->id());
        }
    }

    // Add KScreen::Outputs that aren't in the list yet, handle primaryOutput
    KScreen::OutputList kscreenOutputs = m_kscreenConfig->outputs();
    for (const auto &output : m_outputMap) {
        KScreen::OutputPtr kscreenOutput = kscreenOutputs[output->id()];
        if (!kscreenOutput) {
            kscreenOutput = output->toKScreenOutput();
            kscreenOutputs.insert(kscreenOutput->id(), kscreenOutput);
        }
        if (kscreenOutput && m_outputMap.count() == 1) {
            kscreenOutput->setPrimary(true);
        } else if (m_outputMap.count() > 1) {
            // primaryScreen concept doesn't exist in kwayland, so we don't set one
        }
        output->updateKScreenOutput(kscreenOutput);
    }
    m_kscreenConfig->setOutputs(kscreenOutputs);

    m_kscreenConfig->setTabletModeAvailable(m_tabletModeAvailable);
    m_kscreenConfig->setTabletModeEngaged(m_tabletModeEngaged);

    return m_kscreenConfig;
}

QMap<int, WaylandOutput*> WaylandConfig::outputMap() const
{
    return m_outputMap;
}

void WaylandConfig::tryPendingConfig()
{
    if (!m_kscreenPendingConfig) {
        return;
    }
    applyConfig(m_kscreenPendingConfig);
    m_kscreenPendingConfig = nullptr;
}

void WaylandConfig::applyConfig(const KScreen::ConfigPtr &newConfig)
{
    using namespace KWayland::Client;
    // Create a new configuration object
    auto wlConfig = m_outputManagement->createConfiguration();
    bool changed = false;

    if (m_blockSignals) {
        /* Last apply still pending, remember new changes and apply afterwards */
        m_kscreenPendingConfig = newConfig;
        return;
    }

    for (const auto &output : newConfig->outputs()) {
        changed |= m_outputMap[output->id()]->setWlConfig(wlConfig, output);
    }

    if (!changed) {
        return;
    }

    // We now block changes in order to compress events while the compositor is doing its thing
    // once it's done or failed, we'll trigger configChanged() only once, and not per individual
    // property change.
    connect(wlConfig, &OutputConfiguration::applied, this, [this, wlConfig] {
        wlConfig->deleteLater();
        unblockSignals();
        Q_EMIT configChanged();
        tryPendingConfig();
    });
    connect(wlConfig, &OutputConfiguration::failed, this, [this, wlConfig] {
        wlConfig->deleteLater();
        unblockSignals();
        Q_EMIT configChanged();
        tryPendingConfig();
    });

    // Now block signals and ask the compositor to apply the changes.
    blockSignals();
    wlConfig->apply();
}
