/*
 * Copyright 2020 Han Young <hanyoung@protonmail.com>
 * Copyright 2020 Devin Lin <espidev@gmail.com>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "alarm.h"

#include "alarmadaptor.h"
#include "kclockdsettings.h"

#include <KConfigGroup>
#include <KLocalizedString>
#include <KNotification>
#include <KSharedConfig>

#include <QDBusConnection>
#include <QDBusMessage>
#include <QDateTime>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTime>

// alarm created from UI
Alarm::Alarm(AlarmModel *parent, QString name, int minutes, int hours, int daysOfWeek)
    : QObject(parent)
    , m_name(name)
    , m_uuid(QUuid::createUuid())
    , m_enabled(true)
    , m_hours(hours)
    , m_minutes(minutes)
    , m_daysOfWeek(daysOfWeek)
{
    initialize(parent);
}

// alarm from json (loaded from storage)
Alarm::Alarm(QString serialized, AlarmModel *parent)
    : QObject(parent)
{
    if (serialized.isEmpty()) {
        m_uuid = QUuid::createUuid();
    } else {
        QJsonDocument doc = QJsonDocument::fromJson(serialized.toUtf8());
        QJsonObject obj = doc.object();

        m_uuid = QUuid::fromString(obj[QStringLiteral("uuid")].toString());
        m_name = obj[QStringLiteral("name")].toString();
        m_minutes = obj[QStringLiteral("minutes")].toInt();
        m_hours = obj[QStringLiteral("hours")].toInt();
        m_daysOfWeek = obj[QStringLiteral("daysOfWeek")].toInt();
        m_enabled = obj[QStringLiteral("enabled")].toBool();
        m_snooze = obj[QStringLiteral("snooze")].toInt();
        m_audioPath = QUrl::fromLocalFile(obj[QStringLiteral("audioPath")].toString());
    }
    initialize(parent);
}

void Alarm::initialize(AlarmModel *parent)
{
    connect(this, &Alarm::alarmChanged, this, &Alarm::save);
    connect(this, &Alarm::alarmChanged, this, &Alarm::calculateNextRingTime); // the slots will be called according to the order they have been connected.
                                                                              // always connect this before calling AlarmModel::scheduleAlarm

    calculateNextRingTime();

    if (parent) {
        connect(this, &Alarm::alarmChanged, parent, &AlarmModel::scheduleAlarm); // connect this last
    }

    // DBus
    new AlarmAdaptor(this);
    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Alarms/") + this->uuid().toString(QUuid::Id128), this);
    connect(this, &QObject::destroyed, [this] {
        QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/Alarms/") + this->uuid().toString(QUuid::Id128), QDBusConnection::UnregisterNode);
    });
}

// alarm to json
QString Alarm::serialize()
{
    QJsonObject obj;
    obj[QStringLiteral("uuid")] = uuid().toString();
    obj[QStringLiteral("name")] = name();
    obj[QStringLiteral("minutes")] = minutes();
    obj[QStringLiteral("hours")] = hours();
    obj[QStringLiteral("daysOfWeek")] = daysOfWeek();
    obj[QStringLiteral("enabled")] = enabled();
    obj[QStringLiteral("snooze")] = snooze();
    obj[QStringLiteral("audioPath")] = m_audioPath.toLocalFile();
    return QString(QJsonDocument(obj).toJson(QJsonDocument::Compact));
}

void Alarm::save()
{
    auto config = KSharedConfig::openConfig();
    KConfigGroup group = config->group(ALARM_CFG_GROUP);
    group.writeEntry(uuid().toString(), this->serialize());
    group.sync();
}

void Alarm::ring()
{
    // if not enabled, don't ring
    if (!this->enabled())
        return;

    qDebug() << "Ringing alarm" << m_name << "and sending notification...";

    KNotification *notif = new KNotification(QStringLiteral("alarm"));
    notif->setActions(QStringList{i18n("Dismiss"), i18n("Snooze")});
    notif->setIconName(QStringLiteral("kclock"));
    notif->setTitle(name() == "" ? i18n("Alarm") : name());
    notif->setText(QLocale::system().toString(QTime::currentTime(), QLocale::ShortFormat)); // TODO
    notif->setDefaultAction(i18n("View"));
    notif->setFlags(KNotification::NotificationFlag::Persistent);

    connect(notif, &KNotification::defaultActivated, this, &Alarm::handleDismiss);
    connect(notif, &KNotification::action1Activated, this, &Alarm::handleDismiss);
    connect(notif, &KNotification::action2Activated, this, &Alarm::handleSnooze);
    connect(notif, &KNotification::closed, this, &Alarm::handleDismiss);
    connect(notif, &KNotification::closed, [notif] {
        notif->close();
    });

    notif->sendEvent();

    QDBusMessage wakeupCall = QDBusMessage::createMethodCall(QStringLiteral("org.kde.Solid.PowerManagement"),
                                                             QStringLiteral("/org/kde/Solid/PowerManagement"),
                                                             QStringLiteral("org.kde.Solid.PowerManagement"),
                                                             QStringLiteral("wakeup"));
    QDBusConnection::sessionBus().call(wakeupCall);

    alarmNotifOpen = true;
    alarmNotifOpenTime = QTime::currentTime();

    // play sound (it will loop)
    qDebug() << "Alarm sound: " << m_audioPath;
    AlarmPlayer::instance().setSource(this->m_audioPath);
    AlarmPlayer::instance().play();
}

void Alarm::handleDismiss()
{
    alarmNotifOpen = false;

    qDebug() << "Alarm" << m_name << "dismissed";
    AlarmPlayer::instance().stop();

    // ignore if the snooze button was pressed and dismiss is still called
    if (!m_justSnoozed) {
        // disable alarm if set to run once
        if (daysOfWeek() == 0) {
            setEnabled(false);
        }
    } else {
        qDebug() << "Ignore dismiss (triggered by snooze)" << m_snooze;
    }

    m_justSnoozed = false;

    save();
    Q_EMIT alarmChanged();
}

void Alarm::handleSnooze()
{
    m_justSnoozed = true;

    alarmNotifOpen = false;
    qDebug() << "Alarm snoozed (" << KClockSettings::self()->alarmSnoozeLength() << ")";
    AlarmPlayer::instance().stop();

    setSnooze(snooze() + 60 * KClockSettings::self()->alarmSnoozeLength()); // snooze 5 minutes
    m_enabled = true; // can't use setSnooze because it resets snooze time
    save();
    Q_EMIT alarmChanged();
}

void Alarm::calculateNextRingTime()
{
    if (!this->m_enabled) { // if not enabled, means this would never ring
        m_nextRingTime = 0;
        return;
    }

    // get the time that the alarm will ring on the day
    QTime alarmTime = QTime(this->m_hours, this->m_minutes, 0).addSecs(this->m_snooze);

    QDateTime date = QDateTime::currentDateTime();

    if (this->m_daysOfWeek == 0) { // alarm does not repeat (no days of the week are specified)
        if (alarmTime >= date.time()) { // alarm occurs later today
            m_nextRingTime = QDateTime(date.date(), alarmTime).toSecsSinceEpoch();
        } else { // alarm occurs on the next day
            m_nextRingTime = QDateTime(date.date().addDays(1), alarmTime).toSecsSinceEpoch();
        }
    } else { // repeat alarm
        bool first = true;

        // keeping looping forward a single day until the day of week is accepted
        while (((this->m_daysOfWeek & (1 << (date.date().dayOfWeek() - 1))) == 0) // check day
               || (first && (alarmTime < date.time()))) // check time if the current day is accepted (keep looping forward if alarmTime has passed)
        {
            date = date.addDays(1); // go forward a day
            first = false;
        }

        m_nextRingTime = QDateTime(date.date(), alarmTime).toSecsSinceEpoch();
    }
}

quint64 Alarm::nextRingTime()
{
    // day changed, re-calculate
    if (this->m_nextRingTime <= static_cast<unsigned long long>(QDateTime::currentSecsSinceEpoch())) {
        calculateNextRingTime();
    }
    return m_nextRingTime;
}
