/*
 * datetimepage.cc - Implementation of the UI.SysMeta.DateTime page
 * horizon-qt5, the Qt 5 user interface for
 * Project Horizon
 *
 * Copyright (c) 2019 Adélie Linux and contributors.  All rights reserved.
 * This code is licensed under the AGPL 3.0 license, as noted in the
 * LICENSE-code file in the root directory of this repository.
 *
 * SPDX-License-Identifier: AGPL-3.0-only
 */

#include "datetimepage.hh"

#include <QCheckBox>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QTimeZone>
#include <QVBoxLayout>

#ifdef HAS_INSTALL_ENV
#   include <sys/capability.h>
#   include <time.h>
#endif  /* HAS_INSTALL_ENV */


TimeZone::TimeZone() : ianaName(), friendlyName(), offset(0) {}

TimeZone::TimeZone(const QByteArray &iana) : ianaName(iana) {
    const QTimeZone zone(iana);
    QString offset_str, longName, comment;

    offset_str = zone.displayName(QTimeZone::GenericTime, QTimeZone::OffsetName);
    this->friendlyName = "(" + offset_str + ") " + iana;
    longName = zone.displayName(QTimeZone::GenericTime, QTimeZone::LongName);
    if(longName != offset_str) {
        this->friendlyName += " - " + longName;
    }

    comment = zone.comment();
    if(comment.size() > 0 && comment != offset) {
        this->friendlyName += " (" + comment + ")";
    }

    this->offset = zone.standardTimeOffset(QDateTime::currentDateTimeUtc());
}


TimeZoneModel::TimeZoneModel(QWidget *parent) : QAbstractListModel(parent) {
    for(auto &iana : QTimeZone::availableTimeZoneIds()) {
        /* we don't support raw timezones because tzdata doesn't */
        if(iana.startsWith("UTC") && iana.size() > 3) continue;
        const TimeZone tzObj(iana);
        zones.push_back(tzObj);
    }

    std::sort(zones.begin(), zones.end(), [](const TimeZone &tz1, const TimeZone &tz2) {
        if(tz1.offset < tz2.offset) return true;
        if(tz2.offset < tz1.offset) return false;
        return tz1.friendlyName < tz2.friendlyName;
    });
}

int TimeZoneModel::rowCount(const QModelIndex &) const {
    return zones.size();
}

QVariant TimeZoneModel::data(const QModelIndex &index, int role) const {
    if(!index.isValid()) {
        return {};
    }

    if(index.row() > zones.size()) {
        return {};
    }

    const TimeZone &zone = zones.at(index.row());

    switch(role)
    {
    case Qt::DisplayRole:
        return zone.friendlyName;
    case Qt::ToolTipRole:
        return QString(zone.ianaName);
    default:
        return {};
    }
}

QVariant TimeZoneModel::headerData(int, Qt::Orientation, int role) const {
    if(role != Qt::DisplayRole) {
        return {};
    }

    return QString("Time Zone");
}


/*! Try to gain CAP_SYS_TIME capability.
 * If we do, then enable the date/time edit boxes.
 * This does nothing in the Runtime Environment.
 */
void DateTimePage::maybeRaiseCap() {
#ifdef HAS_INSTALL_ENV
    cap_t captain;
    cap_value_t time_cap = CAP_SYS_TIME;

    if(!CAP_IS_SUPPORTED(CAP_SYS_TIME))
        return;

    captain = cap_get_proc();
    if(captain == nullptr)
        return;

    if(cap_set_flag(captain, CAP_EFFECTIVE, 1, &time_cap, CAP_SET) == -1) {
        cap_free(captain);
        return;
    }

    if(cap_set_proc(captain)) {
        cap_free(captain);
        return;
    }

    cap_free(captain);

    dateEdit->setEnabled(true);
    timeEdit->setEnabled(true);
#endif  /* HAS_INSTALL_ENV */
}

DateTimePage::DateTimePage(QWidget *parent) : HorizonWizardPage(parent) {
    setTitle(tr("Date and Time Settings"));
    loadWatermark("intro");

    dateEdit = new QDateEdit(QDate::currentDate());
    dateEdit->setDisplayFormat("dd MMMM yyyy");
    dateEdit->setEnabled(false);
    dateEdit->setWhatsThis(tr("Allows you to input the current date."));
    timeEdit = new QTimeEdit(QTime::currentTime());
    timeEdit->setDisplayFormat("HH:mm:ss");
    timeEdit->setEnabled(false);
    timeEdit->setWhatsThis(tr("Allows you to input the current time."));

#ifdef HAS_INSTALL_ENV
    /* explanations:
     *
     * isEnabled check:
     *     to prevent wasting time in the case where we don't have perms
     *
     * Qt::UTC:
     *     CLOCK_REALTIME is always UTC, so we need the UTC form of the
     *     current date/time.
     */
    connect(dateEdit, &QDateEdit::dateChanged, [=](const QDate &date) {
        if(dateEdit->isEnabled() && date != QDate::currentDate()) {
            QDateTime *newDT = new QDateTime(date, QTime::currentTime(),
                                             Qt::UTC);
            struct timespec ts = {newDT->toSecsSinceEpoch(), 0};
            clock_settime(CLOCK_REALTIME, &ts);
        }
    });
    connect(timeEdit, &QTimeEdit::timeChanged, [=](const QTime &qtime) {
        if(timeEdit->isEnabled() && qtime != QTime::currentTime()) {
            QDateTime *newDT = new QDateTime(QDate::currentDate(), qtime);
            struct timespec ts = {newDT->toSecsSinceEpoch(), 0};
            clock_settime(CLOCK_REALTIME, &ts);
        }
    });
#endif  /* HAS_INSTALL_ENV */

    updateTimer = new QTimer(this);
    updateTimer->setInterval(1000);
    connect(updateTimer, &QTimer::timeout, [this]{
        if(!dateEdit->hasFocus()) dateEdit->setDate(QDate::currentDate());
        if(!timeEdit->hasFocus()) timeEdit->setTime(QTime::currentTime());
    });

    auto dateTimeLayout = new QHBoxLayout;
    dateTimeLayout->addWidget(dateEdit);
    dateTimeLayout->addWidget(timeEdit);
    auto dateTimeGroup = new QGroupBox(tr("Date and Time"));
    dateTimeGroup->setLayout(dateTimeLayout);

    auto timeZoneSearch = new QLineEdit;
    timeZoneSearch->addAction(QIcon::fromTheme("edit-find"),
                              QLineEdit::LeadingPosition);
    timeZoneSearch->setPlaceholderText(tr("Search for a time zone"));
    timeZoneSearch->setWhatsThis(tr("Type here to search for a time zone."));
    sortModel = new QSortFilterProxyModel(this);
    sortModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
    sortModel->setSourceModel(&zoneModel);
    connect(timeZoneSearch, &QLineEdit::textChanged, [this](const QString &text) {
        sortModel->setFilterFixedString(text);
    });
    timeZoneList = new QListView;
    timeZoneList->setModel(sortModel);
    timeZoneList->setSelectionMode(QAbstractItemView::SingleSelection);
    timeZoneList->setWhatsThis(tr("This list contains all time zones known to the system.  Select the one you wish to use for representing time on this computer."));
    connect(timeZoneList->selectionModel(), &QItemSelectionModel::currentChanged,
            [this]() {
        emit timezoneChanged();
    });

    registerField("timezone*", this, "selectedTimeZone", SIGNAL(timezoneChanged()));

    auto timeZoneLayout = new QVBoxLayout;
    timeZoneLayout->addWidget(timeZoneSearch);
    timeZoneLayout->addWidget(timeZoneList);
    auto timeZoneGroup = new QGroupBox(tr("Time Zone"));
    timeZoneGroup->setLayout(timeZoneLayout);

    auto mainLayout = new QVBoxLayout;
    mainLayout->addStretch();
#ifdef HAS_INSTALL_ENV
    mainLayout->addWidget(dateTimeGroup);
    mainLayout->addStretch();
#endif  /* HAS_INSTALL_ENV */
    mainLayout->addWidget(timeZoneGroup);
    mainLayout->addStretch();
    setLayout(mainLayout);

    maybeRaiseCap();
}

QString DateTimePage::selectedTimeZone() {
    const QModelIndex curr = timeZoneList->selectionModel()->currentIndex();
    return sortModel->data(curr, Qt::ToolTipRole).toString();
}

void DateTimePage::initializePage() {
    updateTimer->start();
}

void DateTimePage::cleanupPage() {
    updateTimer->stop();
}
