/*
 *
 *    Copyright (c) 2020-2021 Project CHIP Authors
 *    Copyright (c) 2013-2017 Nest Labs, Inc.
 *    All rights reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

/**
 *    @file
 *      Various utility functions for dealing with time and dates.
 *
 */

#include <limits>
#include <stdint.h>
#include <type_traits>

#include <lib/core/CHIPCore.h>
#include <lib/support/SafeInt.h>

#include "TimeUtils.h"

namespace chip {

enum
{
    // Number of days during the invariant part of the year (after the leap day).
    kDaysFromMarch1ToDecember31 = 306,

    // Number of years in a Gregorian "cycle", where a cycle is the 400-year period
    // over which the Gregorian calendar repeats.
    kYearsPerCycle = 400,

    // Total number of days within cycle.
    kDaysPerCycle = 146097,

    // Total number of days between 0000/03/01 and 1970/01/01.
    kEpochOffsetDays = 719468
};

/* Returns the number of days between January 1st and March 1st for a given year.
 */
static inline uint8_t DaysToMarch1(uint16_t year)
{
    if (IsLeapYear(year))
        return 60;
    return 59;
}

/* Converts a March-based month number (0=March, 1=April, etc.) to a March-1st based day of year (0=March 1st, 1=March 2nd, etc.).
 *
 * NOTE: This is based on the math described in http://howardhinnant.github.io/date_algorithms.html.
 */
static uint16_t MarchBasedMonthToDayOfYear(uint8_t month)
{
    return static_cast<uint16_t>((153 * month + 2) / 5);
}

/* Converts a March-1st based day of year (0=March 1st, 1=March 2nd, etc.) to a March-based month number (0=March, 1=April, etc.).
 */
static uint8_t MarchBasedDayOfYearToMonth(uint16_t dayOfYear)
{
    // This assumes dayOfYear is not using the full uint16_t range, so the cast
    // to uint8_t doesn't overflow.
    return static_cast<uint8_t>((5 * dayOfYear + 2) / 153);
}

/**
 *  @def IsLeapYear
 *
 *  @brief
 *    Returns true if the given year is a leap year according to the Gregorian calendar.
 *
 *  @param year
 *    Gregorian calendar year.
 *
 */
bool IsLeapYear(uint16_t year)
{
    return (year % kLeapYearInterval) == 0 && ((year % kYearsPerCentury) != 0 || (year % kYearsPerCycle) == 0);
}

/**
 *  @def DaysInMonth
 *
 *  @brief
 *    Returns the number of days in the given month/year.
 *
 *  @param year
 *    Gregorian calendar year.
 *
 *  @param month
 *    Month in standard form (1=January ... 12=December).
 *
 *  @return
 *    Number of days in the given month.
 */
uint8_t DaysInMonth(uint16_t year, uint8_t month)
{
    static const uint8_t daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    if (month == kFebruary && IsLeapYear(year))
        return 29;
    if (month >= kJanuary && month <= kDecember)
        return daysInMonth[month - 1];
    return 0;
}

/**
 *  @def FirstWeekdayOfYear
 *
 *  @brief
 *    Returns the day of the week for January 1st of the given year.
 *
 *  @param year
 *    Gregorian calendar year.
 *
 *  @return
 *    The day-of-week (0=Sunday...6=Saturday).
 */
uint8_t FirstWeekdayOfYear(uint16_t year)
{
    // Compute the day of the week for the first day of the given year using Gauss' algorithm.
    return static_cast<uint8_t>(
        (1 + 5 * ((year - 1) % kLeapYearInterval) + 4 * ((year - 1) % kYearsPerCentury) + 6 * ((year - 1) % kYearsPerCycle)) %
        kDaysPerWeek);
}

/**
 *  @def OrdinalDateToCalendarDate
 *
 *  @brief
 *    Convert an ordinal date (year/day-of-year) to a calendar date.
 *
 *  @param year
 *    Gregorian calendar year.
 *
 *  @param dayOfYear
 *    Ordinal day of year, base 1 (1=January 1st, 2=January 2nd, etc.).
 *
 *  @param month
 *    [OUTPUT] Corresponding month in standard form (1=January ... 12=December).
 *
 *  @param dayOfMonth
 *    [OUTPUT] Corresponding day-of-month in standard form (1=1st, 2=2nd, etc.).
 *
 */
void OrdinalDateToCalendarDate(uint16_t year, uint16_t dayOfYear, uint8_t & month, uint8_t & dayOfMonth)
{
    uint8_t daysToMarch1 = DaysToMarch1(year);

    // Make dayOfYear base 0.
    dayOfYear = static_cast<uint16_t>(dayOfYear - 1);

    // Adjust dayOfYear to a March 1st base (i.e. 0 = March 1, 1 = March 2, etc.).  This numbers January
    // and February at the end of the range, with the benefit that day numbering is identical between
    // standard and leap years with the exception of the leap day itself.
    if (dayOfYear < daysToMarch1)
        dayOfYear = static_cast<uint16_t>(dayOfYear + kDaysFromMarch1ToDecember31);
    else
        dayOfYear = static_cast<uint16_t>(dayOfYear - daysToMarch1);

    // Compute a March-based month number (i.e. 0=March...11=February) from the day of year.  This is based
    // on the logic in http://howardhinnant.github.io/date_algorithms.html.
    month = MarchBasedDayOfYearToMonth(dayOfYear);

    // Compute the days from March 1st to the start of the corresponding month.
    uint16_t daysFromMarch1ToStartOfMonth = MarchBasedMonthToDayOfYear(month);

    // Compute the day of month in standard form (1=1st, 2=2nd, etc.).
    dayOfMonth = static_cast<uint8_t>(dayOfYear - daysFromMarch1ToStartOfMonth + 1);

    // Convert the month number to standard form (1=January...12=December).
    month = static_cast<uint8_t>(month + (month < 10 ? 3 : -9));
}

/**
 *  @def CalendarDateToOrdinalDate
 *
 *  @brief
 *    Convert an calendar date to ordinal form (year/day-of-year).
 *
 *  @param year
 *    Gregorian calendar year.
 *
 *  @param month
 *    Month in standard form (1=January ... 12=December).
 *
 *  @param dayOfMonth
 *    Day-of-month in standard form (1=1st, 2=2nd, etc.).
 *
 *  @param dayOfYear
 *    [OUTPUT] Ordinal day of year, base 1 (1=January 1st, 2=January 2nd, etc.).
 *
 */
void CalendarDateToOrdinalDate(uint16_t year, uint8_t month, uint8_t dayOfMonth, uint16_t & dayOfYear)
{
    // Convert month to a March-based month number (i.e. 0=March, 1=April, ...11=February).
    month = static_cast<uint8_t>(month + (month > kFebruary ? -3 : 9));

    // Compute the days from March 1st to the start of the corresponding month.
    dayOfYear = MarchBasedMonthToDayOfYear(month);

    // Adjust dayOfYear to be January-based (0=January 1st, 1=January 2nd...).
    if (dayOfYear < kDaysFromMarch1ToDecember31)
        dayOfYear = static_cast<uint16_t>(dayOfYear + DaysToMarch1(year));
    else
        dayOfYear = static_cast<uint16_t>(dayOfYear - kDaysFromMarch1ToDecember31);

    // Add in day of month, converting to base 1 in the process.
    dayOfYear = static_cast<uint16_t>(dayOfYear + dayOfMonth);
}

/**
 *  @def CalendarDateToDaysSinceUnixEpoch
 *
 *  @brief
 *    Convert a calendar date to the number of days since 1970-01-01.
 *
 *  @param year
 *    Gregorian calendar year in the range 1970 to 28276.
 *
 *  @param month
 *    Month in standard form (1=January ... 12=December).
 *
 *  @param dayOfMonth
 *    Day-of-month in standard form (1=1st, 2=2nd, etc.).
 *
 *  @param daysSinceEpoch
 *    [OUTPUT] Number of days since 1970-01-01.
 *
 *  @return
 *    True if the date was converted successfully.  False if the given year falls outside the
 *    representable range.
 *
 *  @note
 *    This function makes no attempt to verify the correct range of any arguments other than year.
 *    Therefore callers must make sure the supplied values are valid prior to calling the function.
 */
bool CalendarDateToDaysSinceUnixEpoch(uint16_t year, uint8_t month, uint8_t dayOfMonth, uint32_t & daysSinceEpoch)
{
    // NOTE: This algorithm is based on the logic described in http://howardhinnant.github.io/date_algorithms.html.

    // Return immediately if the year is out of range.
    if (year < kUnixEpochYear || year > kMaxYearInDaysSinceUnixEpoch32)
    {
        daysSinceEpoch = UINT32_MAX;
        return false;
    }

    // Adjust the year and month to be March-based (i.e. 0=March, 1=April, ...11=February).
    if (month <= kFebruary)
    {
        year--;
        month = static_cast<uint8_t>(month + 9);
    }
    else
        month = static_cast<uint8_t>(month - 3);

    // Compute the days from March 1st to the start of the specified day.
    uint16_t dayOfYear = static_cast<uint16_t>(MarchBasedMonthToDayOfYear(month) + (dayOfMonth - 1));

    // Compute the 400-year Gregorian "cycle" within which the given year falls.
    uint16_t cycle = static_cast<uint16_t>(year / kYearsPerCycle);

    // Compute the relative year within the cycle.
    uint32_t yearOfCycle = year - (cycle * kYearsPerCycle);

    // Compute the relative day within the cycle, accounting for leap-years.
    uint32_t dayOfCycle =
        (yearOfCycle * kDaysPerStandardYear) + dayOfYear - (yearOfCycle / kYearsPerCentury) + (yearOfCycle / kLeapYearInterval);

    // Compute the total number of days since the start of the logical calendar (0000-03-01).
    uint32_t daysSinceCalendarStart = (cycle * kDaysPerCycle) + dayOfCycle;

    // Adjust the days value to be days since 1970-01-01.
    daysSinceEpoch = daysSinceCalendarStart - kEpochOffsetDays;

    return true;
}

/**
 *  @def DaysSinceUnixEpochToCalendarDate
 *
 *  @brief
 *    Convert the number of days since 1970-01-01 to a calendar date.
 *
 *  @param daysSinceEpoch
 *    Number of days since 1970-01-01.
 *
 *  @param year
 *    [OUTPUT] Gregorian calendar year.
 *
 *  @param month
 *    [OUTPUT] Month in standard form (1=January ... 12=December).
 *
 *  @param dayOfMonth
 *    [OUTPUT] Day-of-month in standard form (1=1st, 2=2nd, etc.).
 *
 *  @return
 *     True if the conversion was successful.  False if the year would not fit
 *     in uint16_t.
 */
bool DaysSinceUnixEpochToCalendarDate(uint32_t daysSinceEpoch, uint16_t & year, uint8_t & month, uint8_t & dayOfMonth)
{
    // NOTE: This algorithm is based on the logic described in http://howardhinnant.github.io/date_algorithms.html.
    if (daysSinceEpoch / kDaysPerStandardYear + 1 > std::numeric_limits<std::remove_reference<decltype(year)>::type>::max())
    {
        // Our year calculation will likely overflow.
        return false;
    }

    // Adjust days value to be relative to 0000-03-01.
    daysSinceEpoch += kEpochOffsetDays;

    // Compute the 400-year Gregorian cycle in which the given day resides.
    uint32_t cycle = daysSinceEpoch / kDaysPerCycle;

    // Compute the relative day within the cycle.
    uint32_t dayOfCycle = daysSinceEpoch - (cycle * kDaysPerCycle);

    // Compute the relative year within the cycle, adjusting for leap-years.
    uint16_t yearOfCycle =
        static_cast<uint16_t>((dayOfCycle - dayOfCycle / 1460 + dayOfCycle / 36524 - dayOfCycle / 146096) / kDaysPerStandardYear);

    // Compute the relative day with the year.
    uint16_t dayOfYear = static_cast<uint16_t>(
        dayOfCycle - (yearOfCycle * kDaysPerStandardYear + yearOfCycle / kLeapYearInterval - yearOfCycle / kYearsPerCentury));

    // Compute a March-based month number (i.e. 0=March...11=February) from the day of year.
    month = MarchBasedDayOfYearToMonth(dayOfYear);

    // Compute the days from March 1st to the start of the corresponding month.
    uint16_t daysFromMarch1ToStartOfMonth = MarchBasedMonthToDayOfYear(month);

    // Compute the day of month in standard form (1=1st, 2=2nd, etc.).
    dayOfMonth = static_cast<uint8_t>(dayOfYear - daysFromMarch1ToStartOfMonth + 1);

    // Convert the month number to standard form (1=January...12=December).
    month = static_cast<uint8_t>(month + (month < 10 ? 3 : -9));

    // Compute the year, adjusting for the standard start of year (January).
    year = static_cast<uint16_t>(yearOfCycle + cycle * kYearsPerCycle);
    if (month <= kFebruary)
        year++;
    return true;
}

/**
 *  @def AdjustCalendarDate
 *
 *  @brief
 *    Adjust a calendar date by a given number of days (positive or negative).
 *
 *  @param year
 *    [INPUT/OUTPUT] Gregorian calendar year.
 *
 *  @param month
 *    [INPUT/OUTPUT] Month in standard form (1=January ... 12=December).
 *
 *  @param dayOfMonth
 *    [INPUT/OUTPUT] Day-of-month in standard form (1=1st, 2=2nd, etc.).
 *
 *  @param relativeDays
 *    Number of days to add/subtract from given calendar date.
 *
 *  @return
 *    True if the adjustment succeeded.  False if the adjustment would put us
 *    outside the representable date range.
 *
 *  @note
 *    Given date must be equal to or greater than 1970-01-01.
 */
bool AdjustCalendarDate(uint16_t & year, uint8_t & month, uint8_t & dayOfMonth, int32_t relativeDays)
{
    uint32_t daysSinceEpoch;
    if (!CalendarDateToDaysSinceUnixEpoch(year, month, dayOfMonth, daysSinceEpoch))
    {
        return false;
    }

    // Make sure we can do our additions without overflowing.
    int64_t adjustedDays = static_cast<int64_t>(daysSinceEpoch) + relativeDays;
    if (!CanCastTo<uint32_t>(adjustedDays))
    {
        return false;
    }

    return DaysSinceUnixEpochToCalendarDate(static_cast<uint32_t>(adjustedDays), year, month, dayOfMonth);
}

/**
 *  @def CalendarTimeToSecondsSinceUnixEpoch
 *
 *  @brief
 *    Convert a calendar date and time to the number of seconds since 1970-01-01 00:00:00 UTC.
 *
 *  @details
 *    This function is roughly equivalent to the POSIX gmtime() function with the exception
 *    that the output time value is limited to positive values up to 2^32-1.  This limits the
 *    representable date range to the year 2105.
 *
 *  @note
 *    This function makes no attempt to verify the correct range of any arguments other than year.
 *    Therefore callers must make sure the supplied values are valid prior to invocation.
 *
 *  @param secondsSinceEpoch
 *    Number of seconds since 1970-01-01 00:00:00 UTC.  Note: this value is compatible with
 *    *positive* values of the POSIX time_t value up to the year 2105.
 *
 *  @param year
 *    Gregorian calendar year in the range 1970 to 2105.
 *
 *  @param month
 *    Month in standard form (1=January ... 12=December).
 *
 *  @param dayOfMonth
 *    Day-of-month in standard form (1=1st, 2=2nd, etc.).
 *
 *  @param hour
 *    Hour (0-23).
 *
 *  @param minute
 *    Minute (0-59).
 *
 *  @param second
 *    Second (0-59).
 *
 *  @return
 *    True if the date/time was converted successfully.  False if the given year falls outside the
 *    representable range.
 */
bool CalendarTimeToSecondsSinceUnixEpoch(uint16_t year, uint8_t month, uint8_t dayOfMonth, uint8_t hour, uint8_t minute,
                                         uint8_t second, uint32_t & secondsSinceEpoch)
{
    uint32_t daysSinceEpoch;

    // Return immediately if the year is out of range.
    if (year < kUnixEpochYear || year > kMaxYearInSecondsSinceUnixEpoch32)
    {
        secondsSinceEpoch = UINT32_MAX;
        return false;
    }

    CalendarDateToDaysSinceUnixEpoch(year, month, dayOfMonth, daysSinceEpoch);

    secondsSinceEpoch = (daysSinceEpoch * kSecondsPerDay) + (hour * kSecondsPerHour) + (minute * kSecondsPerMinute) + second;

    return true;
}
/**
 *  @brief
 *    Convert the number of seconds since 1970-01-01 00:00:00 UTC to a calendar date and time.
 *
 *  @note
 *    If secondsSinceEpoch is large enough this function will generate bad result. The way it is
 *    used in this file the generated result should be valid. Specifically, the largest
 *    possible value of secondsSinceEpoch input is (UINT32_MAX + kChipEpochSecondsSinceUnixEpoch),
 *    when it is called from ChipEpochToCalendarTime().
 */
static void SecondsSinceUnixEpochToCalendarTime(uint64_t secondsSinceEpoch, uint16_t & year, uint8_t & month, uint8_t & dayOfMonth,
                                                uint8_t & hour, uint8_t & minute, uint8_t & second)
{
    uint32_t daysSinceEpoch = static_cast<uint32_t>(secondsSinceEpoch / kSecondsPerDay);
    static_assert((static_cast<uint64_t>(UINT32_MAX) + kChipEpochSecondsSinceUnixEpoch) / kSecondsPerDay <=
                      std::numeric_limits<decltype(daysSinceEpoch)>::max(),
                  "daysSinceEpoch would overflow");
    uint32_t timeOfDay = static_cast<uint32_t>(secondsSinceEpoch - (daysSinceEpoch * kSecondsPerDay));

    // Note: This call to DaysSinceUnixEpochToCalendarDate can't fail, because we
    // can't overflow a uint16_t year with a muximum possible value of the
    // secondsSinceEpoch input.
    static_assert((static_cast<uint64_t>(UINT32_MAX) + kChipEpochSecondsSinceUnixEpoch) / (kDaysPerStandardYear * kSecondsPerDay) +
                          1 <=
                      std::numeric_limits<std::remove_reference<decltype(year)>::type>::max(),
                  "What happened to our year or day lengths?");
    DaysSinceUnixEpochToCalendarDate(daysSinceEpoch, year, month, dayOfMonth);

    hour = static_cast<uint8_t>(timeOfDay / kSecondsPerHour);
    timeOfDay -= (hour * kSecondsPerHour);
    minute = static_cast<uint8_t>(timeOfDay / kSecondsPerMinute);
    timeOfDay -= (minute * kSecondsPerMinute);
    second = static_cast<uint8_t>(timeOfDay);
}

/**
 *  @def SecondsSinceUnixEpochToCalendarTime
 *
 *  @brief
 *    Convert the number of seconds since 1970-01-01 00:00:00 UTC to a calendar date and time.
 *
 *  @details
 *    This function is roughly equivalent to the POSIX mktime() function, with the following
 *    exceptions:
 *
 *    - Input time values are limited to positive values up to 2^32-1.  This limits the
 *    representable date range to the year 2105.
 *
 *    - The output time is always UTC (unlike mktime() which outputs time in the process's
 *    configured timezone).
 *
 *  @param secondsSinceEpoch
 *    Number of seconds since 1970-01-01 00:00:00 UTC.  Note: this value is compatible with
 *    *positive* values of the POSIX time_t value up to the year 2105.
 *
 *  @param year
 *    [OUTPUT] Gregorian calendar year.
 *
 *  @param month
 *    [OUTPUT] Month in standard form (1=January ... 12=December).
 *
 *  @param dayOfMonth
 *    [OUTPUT] Day-of-month in standard form (1=1st, 2=2nd, etc.).
 *
 *  @param hour
 *    [OUTPUT] Hour (0-23).
 *
 *  @param minute
 *    [OUTPUT] Minute (0-59).
 *
 *  @param second
 *    [OUTPUT] Second (0-59).
 */
void SecondsSinceUnixEpochToCalendarTime(uint32_t secondsSinceEpoch, uint16_t & year, uint8_t & month, uint8_t & dayOfMonth,
                                         uint8_t & hour, uint8_t & minute, uint8_t & second)
{
    SecondsSinceUnixEpochToCalendarTime(static_cast<uint64_t>(secondsSinceEpoch), year, month, dayOfMonth, hour, minute, second);
}

bool CalendarToChipEpochTime(uint16_t year, uint8_t month, uint8_t dayOfMonth, uint8_t hour, uint8_t minute, uint8_t second,
                             uint32_t & chipEpochTime)
{
    VerifyOrReturnError(year >= kChipEpochBaseYear && year <= kChipEpochMaxYear, false);

    uint32_t daysSinceUnixEpoch;
    CalendarDateToDaysSinceUnixEpoch(year, month, dayOfMonth, daysSinceUnixEpoch);

    chipEpochTime = ((daysSinceUnixEpoch - kChipEpochDaysSinceUnixEpoch) * kSecondsPerDay) + (hour * kSecondsPerHour) +
        (minute * kSecondsPerMinute) + second;

    return true;
}

void ChipEpochToCalendarTime(uint32_t chipEpochTime, uint16_t & year, uint8_t & month, uint8_t & dayOfMonth, uint8_t & hour,
                             uint8_t & minute, uint8_t & second)
{
    SecondsSinceUnixEpochToCalendarTime(static_cast<uint64_t>(chipEpochTime) + kChipEpochSecondsSinceUnixEpoch, year, month,
                                        dayOfMonth, hour, minute, second);
}

bool ChipEpochToUnixEpochMicros(uint64_t chipEpochTimeMicros, uint64_t & outUnixEpochTimeMicros)
{
    if ((chipEpochTimeMicros + kChipEpochUsSinceUnixEpoch) < kChipEpochUsSinceUnixEpoch)
    {
        return false;
    }

    outUnixEpochTimeMicros = chipEpochTimeMicros + kChipEpochUsSinceUnixEpoch;
    return true;
}

bool UnixEpochToChipEpochMicros(uint64_t unixEpochTimeMicros, uint64_t & outChipEpochTimeMicros)
{
    VerifyOrReturnValue(unixEpochTimeMicros >= kChipEpochUsSinceUnixEpoch, false);
    outChipEpochTimeMicros = unixEpochTimeMicros - kChipEpochUsSinceUnixEpoch;

    return true;
}

bool UnixEpochToChipEpochTime(uint32_t unixEpochTimeSeconds, uint32_t & outChipEpochTimeSeconds)
{
    VerifyOrReturnError(unixEpochTimeSeconds >= kChipEpochSecondsSinceUnixEpoch, false);

    outChipEpochTimeSeconds = unixEpochTimeSeconds - kChipEpochSecondsSinceUnixEpoch;

    return true;
}

} // namespace chip
