// SPDX-License-Identifier: LGPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea.
*
* nymea 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 3
* of the License, or (at your option) any later version.
*
* nymea 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 nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

/*!
    \class CalendarItem
    \brief Describes a clendar item.

    \ingroup rules
    \inmodule core

    \sa Rule, TimeDescriptor
*/

#include "calendaritem.h"
#include "loggingcategories.h"

#include <QDebug>

/*! Construct a invalid \l{CalendarItem}. */
CalendarItem::CalendarItem():
    m_duration(0)
{

}

/*! Returns the date time of this \l{CalendarItem}. */
QDateTime CalendarItem::dateTime() const
{
    return m_dateTime;
}

/*! Sets the \a dateTime of this \l{CalendarItem}. */
void CalendarItem::setDateTime(const QDateTime &dateTime)
{
    m_dateTime = dateTime;
}

/*! Returns the start time of this \l{CalendarItem}. */
QTime CalendarItem::startTime() const
{
    return m_startTime;
}

/*! Sets the \a startTime of this \l{CalendarItem}. */
void CalendarItem::setStartTime(const QTime &startTime)
{
    m_startTime = startTime;
}

/*! Returns the duratiorn of this \l{CalendarItem}. */
uint CalendarItem::duration() const
{
    return m_duration;
}

/*! Sets the \a duration of this \l{CalendarItem}. */
void CalendarItem::setDuration(const uint &duration)
{
    m_duration = duration;
}

/*! Returns the \l{RepeatingOption} of this \l{CalendarItem}. */
RepeatingOption CalendarItem::repeatingOption() const
{
    return m_repeatingOption;
}

/*! Sets the \a repeatingOption of this \l{CalendarItem}. */
void CalendarItem::setRepeatingOption(const RepeatingOption &repeatingOption)
{
    m_repeatingOption = repeatingOption;
}

/*! Returns true if this \l{CalendarItem} is valid. A \l{CalendarItem} is invalid
    if start time and datetime are set or if the duration is 0.
*/
bool CalendarItem::isValid() const
{
    // If dateTime AND a repeating option definend...
    if (m_dateTime.isValid() && !repeatingOption().isEmtpy())
        // ...only repeating mode yearly is allowed for dateTime
        if (repeatingOption().mode() != RepeatingOption::RepeatingModeYearly)
            return false;

    return (!m_startTime.isValid() != !m_dateTime.isValid()) && m_duration > 0;
}

/*! Returns true, if the given \a dateTime matches this \l{CalendarItem}. */
bool CalendarItem::evaluate(const QDateTime &dateTime) const
{
    if (m_startTime.isValid()) {

        switch (m_repeatingOption.mode()) {
        case RepeatingOption::RepeatingModeNone:
            // If there is no repeating option, we assume it is meant daily.
            return evaluateDaily(dateTime);
        case RepeatingOption::RepeatingModeHourly:
            return evaluateHourly(dateTime);
        case RepeatingOption::RepeatingModeDaily:
            return evaluateDaily(dateTime);
        case RepeatingOption::RepeatingModeWeekly:
            return evaluateWeekly(dateTime);
        case RepeatingOption::RepeatingModeMonthly:
            return evaluateMonthly(dateTime);
        case RepeatingOption::RepeatingModeYearly:
            return evaluateYearly(dateTime);
        }

    }
    if (m_repeatingOption.mode() == RepeatingOption::RepeatingModeYearly)
        return evaluateYearly(dateTime);

    return dateTime >= m_dateTime && dateTime < m_dateTime.addSecs(duration() * 60);
}

bool CalendarItem::evaluateHourly(const QDateTime &dateTime) const
{
    // If the duration is longer than a hour, this calendar item is always true
    // 1 hour has 60 minutes
    if (duration() >= 60)
        return true;

    QDateTime startDateTime = QDateTime(dateTime.date(), QTime(dateTime.time().hour(), startTime().minute()));
    QDateTime endDateTime = startDateTime.addSecs(duration() * 60);

    bool timeValid = dateTime >= startDateTime && dateTime < endDateTime;
    bool weekdayValid = repeatingOption().evaluateWeekDay(dateTime);
    bool monthdayValid = repeatingOption().evaluateMonthDay(dateTime);

    return timeValid && weekdayValid && monthdayValid;
}

bool CalendarItem::evaluateDaily(const QDateTime &dateTime) const
{
    // If the duration is longer than a day, this calendar item is always true
    // 1 day has 1440 minutes
    if (duration() >= 1440)
        return true;

    // Get todays startTime
    QDateTime startDateTime = dateTime;
    startDateTime.setTime(startTime());
    QDateTime endDateTime = startDateTime.addSecs(duration() * 60);

    // Get yesterdays startTime for day overlapping clendaritems
    QDateTime startDateTimeYesterday = dateTime.addDays(-1);
    startDateTimeYesterday.setTime(startTime());
    QDateTime endDateTimeYesterday = startDateTimeYesterday.addSecs(duration() * 60);

    bool todayValid = dateTime >= startDateTime && dateTime < endDateTime;
    bool yesterdayValid = dateTime >= startDateTimeYesterday && dateTime < endDateTimeYesterday;

    return todayValid || yesterdayValid;
}

bool CalendarItem::evaluateWeekly(const QDateTime &dateTime) const
{
    // If the duration is longer than a week, this calendar item is always true
    // 1 week has 10080 minutes
    if (duration() >= 10080)
        return true;

    // Get the first day of this week with the correct start time
    QDateTime weekStartDateTime = dateTime.addDays(-dateTime.date().dayOfWeek());
    weekStartDateTime.setTime(startTime());

    // Check each week day in the list
    foreach (const int &weekDay, repeatingOption().weekDays()) {
        QDateTime startDateTime = weekStartDateTime.addDays(weekDay);
        QDateTime endDateTime = startDateTime.addSecs(duration() * 60);

        // Check if dateTime matches for this week
        if (dateTime >= startDateTime && dateTime < endDateTime)
            // Return true if the current time is between start
            // and end of this calendar item
            return true;

        // Check if this calendar item overlaps a week...
        if (startDateTime.date().weekNumber() != endDateTime.date().weekNumber()) {
            // ...jump one week back in to the past
            QDateTime startDateTimePreviousWeek = startDateTime.addDays(-7);
            QDateTime endDateTimePreviousWeek = startDateTimePreviousWeek.addSecs(duration() * 60);

            if (dateTime >= startDateTimePreviousWeek && dateTime < endDateTimePreviousWeek)
                // Return true if the current time is between start
                // and end of this calendar item from the previouse week
                return true;

        }
    }

    return false;
}

bool CalendarItem::evaluateMonthly(const QDateTime &dateTime) const
{
    // Get the first day of this month with the correct start time
    QDateTime monthStartDateTime = dateTime;
    monthStartDateTime.setDate(QDate(dateTime.date().year(), dateTime.date().month(), 1));
    monthStartDateTime.setTime(m_startTime);

    // Check each month day in the list
    foreach (const int &monthDay, repeatingOption().monthDays()) {
        QDateTime startDateTime = monthStartDateTime.addDays(monthDay - 1);
        QDateTime endDateTime = startDateTime.addSecs(duration() * 60);

        // Check if this calendar item starts in the future...
        if (startDateTime > dateTime) {
            //...go one month back
            startDateTime = startDateTime.addMonths(-1);
            endDateTime =  startDateTime.addSecs(duration() * 60);
        }

        // Check if dateTime already matches for this month
        if (dateTime >= startDateTime && dateTime < endDateTime)
            // Return true if the current time is between start
            // and end of this calendar item
            return true;

    }

    return false;
}

bool CalendarItem::evaluateYearly(const QDateTime &dateTime) const
{
    // check for this year
    QDateTime startDateTimeThisYear = dateTime;
    startDateTimeThisYear.setDate(QDate(dateTime.date().year(), m_dateTime.date().month(), m_dateTime.date().day()));
    startDateTimeThisYear.setTime(m_dateTime.time());
    QDateTime endDateTimeThisYear = startDateTimeThisYear.addSecs(duration() * 60);

    // check if we are in the interval of this year
    if (dateTime >= startDateTimeThisYear && dateTime < endDateTimeThisYear)
        return true;

    // check if this calendaritem overlaps a year
    if (startDateTimeThisYear.date().year() != endDateTimeThisYear.date().year()) {
        // go one year in to the past
       QDateTime startDateTimePreviousYear = startDateTimeThisYear.addYears(-1);
       QDateTime endDateTimePreviousYear = startDateTimePreviousYear.addSecs(duration() * 60);

       // check if we are in the interval of the previous year
       if (dateTime >= startDateTimePreviousYear && dateTime < endDateTimePreviousYear)
           return true;

    }

    return false;
}

/*! Print a CalendarItem to QDebug. */
QDebug operator<<(QDebug dbg, const CalendarItem &calendarItem)
{
    QDebugStateSaver saver(dbg);
    dbg.nospace() << "CalendarItem (StartTime:" << calendarItem.startTime() << ", DateTime:" << calendarItem.dateTime().toString() << ", " << calendarItem.repeatingOption() << ", Duration:" << calendarItem.duration() << ")";
    return dbg;
}



CalendarItems::CalendarItems()
{

}

CalendarItems::CalendarItems(const QList<CalendarItem> &other): QList<CalendarItem>(other)
{

}

QVariant CalendarItems::get(int index) const
{
    return QVariant::fromValue(at(index));
}

void CalendarItems::put(const QVariant &variant)
{
    append(variant.value<CalendarItem>());
}
