/*
 * Copyright 2020-2022 UBports foundation
 *
 * This program 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; version 3.
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Marius Gripsgard <marius@ubports.com>
 */

#include "credentialscache.h"
#include "dbusAdaptor.h"
#include "vibrator.h"
#include "leds.h"
#include "usersettings.h"
#include "utils.h"
#include "vibrator-controller.h"

#include "hfdadaptor.h"

#include <climits>
#include <functional>
#include <memory>
#include <iostream>

#include <QCoreApplication>
#include <QDBusError>

class DbusAdaptorPrivilegedService : public DbusAdaptor, protected QDBusContext {
    Q_OBJECT
public:
    DbusAdaptorPrivilegedService(std::shared_ptr<hfd::VibratorController> vibratorCtl, std::shared_ptr<hfd::Leds> leds)
        : DbusAdaptor()
        , m_vibratorCtl(vibratorCtl)
        , m_leds(leds)
    {}

public Q_SLOTS:
    void vibrate() override {
        constexpr uint defaultVibrationMs = 500;
        m_vibratorCtl->vibrate(defaultVibrationMs);
    };
    void vibrate(int durationMs) override {
        if (!verifyDBusInt("durationMs", durationMs, 1, 1000 * 60))
            return;
        m_vibratorCtl->vibrate(durationMs);
    };
    void rumble(int durationMs, int repeat) override {
        if (!verifyDBusInt("durationMs", durationMs, 1, 1000))
            return;
        if (!verifyDBusInt("repeat", repeat, 1, 60))
            return;
        m_vibratorCtl->rumble(durationMs, repeat);
    };
    void vibratePattern(const QVector<uint> &patternMs, uint repeat) override {
        for (uint patOnOffMs : patternMs) {
            if (!verifyDBusInt("patternMs", patOnOffMs, 1, 1000))
                return;
        }
        if (!verifyDBusInt("repeat", repeat, 1, 60))
            return;

        m_vibratorCtl->vibratePattern(patternMs, repeat);
    }

    void setState(int state) override { m_leds->setState(hfd::utils::toState(state)); };
    void setColor(unsigned int color) override { m_leds->setColor(color); }
    void setOnMs(int ms) override {
        if (!verifyDBusInt("onMs", ms))
            return;
        m_leds->setOnMs(ms);
    };
    void setOffMs(int ms) override {
        if (!verifyDBusInt("offMs", ms, 0))
            return;
        m_leds->setOffMs(ms);
    };

protected:

    bool verifyDBusInt(const std::string &name, int val, int min = 1, int max = INT_MAX) {
        if (val >= min && val <= max)
            return true;

        this->sendErrorReply(QDBusError::InvalidArgs, QString::fromStdString(std::string(name) + ": parameter out of range"));
        return false;
    }

private:
    std::shared_ptr<hfd::VibratorController> m_vibratorCtl;
    std::shared_ptr<hfd::Leds> m_leds;
};

class DbusAdaptorService : public DbusAdaptor, protected QDBusContext {
    Q_OBJECT
public:
    DbusAdaptorService(
        std::shared_ptr<hfd::VibratorController> vibratorCtl,
        std::shared_ptr<hfd::Leds> leds,
        QDBusConnection const& connection)
        : DbusAdaptor()
        , m_vibratorCtl(vibratorCtl)
        , m_leds(leds)
        , m_connection(connection)
        , m_credcache(connection)
        , m_usersettings(connection)
    {}

public Q_SLOTS:
    void vibrate() override {
        callIfVibrateAllowed([this] () {
            constexpr uint defaultVibrationMs = 500;
            m_vibratorCtl->vibrate(defaultVibrationMs);
        });
    };
    void vibrate(int durationMs) override {
        if (!verifyDBusInt("durationMs", durationMs, 1, 1000 * 60))
            return;

        callIfVibrateAllowed([=] () {
            m_vibratorCtl->vibrate(durationMs);
        });
    };
    void rumble(int durationMs, int repeat) override {
        if (!verifyDBusInt("durationMs", durationMs, 1, 1000))
            return;
        if (!verifyDBusInt("repeat", repeat, 1, 60))
            return;
        callIfVibrateAllowed([=] () {
            m_vibratorCtl->rumble(durationMs, repeat);
        });
    };
    void vibratePattern(const QVector<uint> &patternMs, uint repeat) override {
        for (uint patOnOffMs : patternMs) {
            if (!verifyDBusInt("patternMs", patOnOffMs, 1, 1000))
                return;
        }
        if (!verifyDBusInt("repeat", repeat, 1, 60))
            return;

        callIfVibrateAllowed([=] () {
            m_vibratorCtl->vibratePattern(patternMs, repeat);
        });
    }

    // TODO: maybe disallow LEDs on the unprivileged object path.
    void setState(int state) override { m_leds->setState(hfd::utils::toState(state)); };
    void setColor(unsigned int color) override { m_leds->setColor(color); }
    void setOnMs(int ms) override {
        if (!verifyDBusInt("onMs", ms))
            return;
        m_leds->setOnMs(ms);
    };
    void setOffMs(int ms) override {
        if (!verifyDBusInt("offMs", ms, 0))
            return;
        m_leds->setOffMs(ms);
    };

protected:
    bool verifyDBusInt(const std::string &name, int val, int min = 1, int max = INT_MAX) {
        if (val >= min && val <= max)
            return true;

        this->sendErrorReply(QDBusError::InvalidArgs, QString::fromStdString(std::string(name) + ": parameter out of range"));
        return false;
    }

private:
    void callIfVibrateAllowed(std::function<void(void)> callback)
    {
        using namespace std::placeholders;

        auto msg = message();

        setDelayedReply(true);

        m_credcache.get(msg.service(),
            std::bind(&DbusAdaptorService::onCredentialReceived,
                this, callback, msg, _1));
    }

    void onCredentialReceived(
        std::function<void(void)> callback,
        QDBusMessage msg,
        CredentialsCache::Credentials const& cred)
    {
        using namespace std::placeholders;

        if (!cred.valid) {
            // Fallback to previous behavior and allows vibration.
            // TODO: is this a sane thing to do?
            callback();
            m_connection.send(msg.createReply());
            return;
        }

        m_usersettings.getSettingsForUid(cred.user,
            std::bind(&DbusAdaptorService::onUserSettingsReceived,
                this, callback, msg, _1));
    }

    void onUserSettingsReceived(
        std::function<void(void)> callback,
        QDBusMessage msg,
        bool allowed)
    {
        if (allowed) {
            callback();
            m_connection.send(msg.createReply());
        } else {
            m_connection.send(msg.createErrorReply(
                QStringLiteral("org.freedesktop.DBus.Error.AccessDenied"),
                QStringLiteral("User disallow general vibration")
            ));
        }
    }

    std::shared_ptr<hfd::VibratorController> m_vibratorCtl;
    std::shared_ptr<hfd::Leds> m_leds;
    QDBusConnection m_connection;

    CredentialsCache m_credcache;
    UserSettings m_usersettings;
};

#include "service.moc"


int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);

    // Environment variables to switch between implementations
    const char* vibrator_impl_env = getenv("HFD_VIBRATOR_IMPL");
    const char* leds_impl_env = getenv("HFD_LEDS_IMPL");

    std::cout << "Starting vibrator impl" << std::endl;
    std::shared_ptr<hfd::Vibrator> vibrator;
    if (vibrator_impl_env)
        vibrator = hfd::Vibrator::create(vibrator_impl_env);
    else
        vibrator = hfd::Vibrator::create();
    auto vibratorCtl = std::make_shared<hfd::VibratorController>(vibrator);

    std::cout << "Starting leds impl" << std::endl;
    std::shared_ptr<hfd::Leds> leds;
    if (leds_impl_env)
        leds = hfd::Leds::create(leds_impl_env);
    else
        leds = hfd::Leds::create();

    std::cout << "done" << std::endl;

    QDBusConnection connection = QDBusConnection::systemBus();

    auto dbusAdaptor = new DbusAdaptorService(vibratorCtl, leds, connection);
    new VibratorAdaptor(dbusAdaptor);
    new LedsAdaptor(dbusAdaptor);
    connection.registerObject("/com/lomiri/hfd", dbusAdaptor);

    auto dbusAdaptorPrivileged = new DbusAdaptorPrivilegedService(vibratorCtl, leds);
    new VibratorAdaptor(dbusAdaptorPrivileged);
    new LedsAdaptor(dbusAdaptorPrivileged);
    connection.registerObject("/com/lomiri/hfd/privileged", dbusAdaptorPrivileged);

    connection.registerService("com.lomiri.hfd");

    if (connection.lastError().isValid()) {
        std::cout << "Not connected to DBus!!!" << std::endl;
        qWarning() << connection.lastError();
        return 1;
    }

    std::cout << "Started dbus conn" << std::endl;

    return app.exec();

}
