//===-- Implementation of mktime function ---------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "include/errno.h"

#include "src/__support/common.h"
#include "src/errno/llvmlibc_errno.h"
#include "src/time/mktime.h"

namespace __llvm_libc {

constexpr int SecondsPerMin = 60;
constexpr int MinutesPerHour = 60;
constexpr int HoursPerDay = 24;
constexpr int DaysPerWeek = 7;
constexpr int MonthsPerYear = 12;
constexpr int DaysPerNonLeapYear = 365;
constexpr int TimeYearBase = 1900;
constexpr int EpochYear = 1970;
constexpr int EpochWeekDay = 4;
// The latest time that can be represented in this form is 03:14:07 UTC on
// Tuesday, 19 January 2038 (corresponding to 2,147,483,647 seconds since the
// start of the epoch). This means that systems using a 32-bit time_t type are
// susceptible to the Year 2038 problem.
constexpr int EndOf32BitEpochYear = 2038;

constexpr int NonLeapYearDaysInMonth[] = {31 /* Jan */, 28, 31, 30, 31, 30,
                                          31,           31, 30, 31, 30, 31};

constexpr bool isLeapYear(const time_t year) {
  return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
}

// POSIX.1-2017 requires this.
static inline time_t outOfRange() {
  llvmlibc_errno = EOVERFLOW;
  return static_cast<time_t>(-1);
}

LLVM_LIBC_FUNCTION(time_t, mktime, (struct tm * t1)) {
  // Unlike most C Library functions, mktime doesn't just die on bad input.
  // TODO(rtenneti); Handle leap seconds. Handle out of range time and date
  // values that don't overflow or underflow.
  // TODO (rtenneti): Implement the following suggestion Siva: "As we start
  // accumulating the seconds, we should be able to check if the next amount of
  // seconds to be added can lead to an overflow. If it does, return the
  // overflow value. If not keep accumulating. The benefit is that, we don't
  // have to validate every input, and also do not need the special cases for
  // sizeof(time_t) == 4".
  if (t1->tm_sec < 0 || t1->tm_sec > (SecondsPerMin - 1))
    return outOfRange();
  if (t1->tm_min < 0 || t1->tm_min > (MinutesPerHour - 1))
    return outOfRange();
  if (t1->tm_hour < 0 || t1->tm_hour > (HoursPerDay - 1))
    return outOfRange();
  time_t tmYearFromBase = t1->tm_year + TimeYearBase;

  if (tmYearFromBase < EpochYear)
    return outOfRange();

  // 32-bit end-of-the-world is 03:14:07 UTC on 19 January 2038.
  if (sizeof(time_t) == 4 && tmYearFromBase >= EndOf32BitEpochYear) {
    if (tmYearFromBase > EndOf32BitEpochYear)
      return outOfRange();
    if (t1->tm_mon > 0)
      return outOfRange();
    if (t1->tm_mday > 19)
      return outOfRange();
    if (t1->tm_hour > 3)
      return outOfRange();
    if (t1->tm_min > 14)
      return outOfRange();
    if (t1->tm_sec > 7)
      return outOfRange();
  }

  // Years are ints.  A 32-bit year will fit into a 64-bit time_t.
  // A 64-bit year will not.
  static_assert(sizeof(int) == 4,
                "ILP64 is unimplemented.  This implementation requires "
                "32-bit integers.");

  if (t1->tm_mon < 0 || t1->tm_mon > (MonthsPerYear - 1))
    return outOfRange();
  bool tmYearIsLeap = isLeapYear(tmYearFromBase);
  time_t daysInMonth = NonLeapYearDaysInMonth[t1->tm_mon];
  // Add one day if it is a leap year and the month is February.
  if (tmYearIsLeap && t1->tm_mon == 1)
    ++daysInMonth;
  if (t1->tm_mday < 1 || t1->tm_mday > daysInMonth)
    return outOfRange();

  time_t totalDays = t1->tm_mday - 1;
  for (int i = 0; i < t1->tm_mon; ++i)
    totalDays += NonLeapYearDaysInMonth[i];
  // Add one day if it is a leap year and the month is after February.
  if (tmYearIsLeap && t1->tm_mon > 1)
    totalDays++;
  t1->tm_yday = totalDays;
  totalDays += (tmYearFromBase - EpochYear) * DaysPerNonLeapYear;

  // Add an extra day for each leap year, starting with 1972
  for (time_t year = EpochYear + 2; year < tmYearFromBase;) {
    if (isLeapYear(year)) {
      totalDays += 1;
      year += 4;
    } else {
      year++;
    }
  }

  t1->tm_wday = (EpochWeekDay + totalDays) % DaysPerWeek;
  if (t1->tm_wday < 0)
    t1->tm_wday += DaysPerWeek;
  // TODO(rtenneti): Need to handle timezone and update of tm_isdst.
  return t1->tm_sec + t1->tm_min * SecondsPerMin +
         t1->tm_hour * MinutesPerHour * SecondsPerMin +
         totalDays * HoursPerDay * MinutesPerHour * SecondsPerMin;
}

} // namespace __llvm_libc
