/** @file
*
*  Copyright (c) 2016, Hisilicon Limited. All rights reserved.
*  Copyright (c) 2016-2019, Linaro Limited. All rights reserved.
*  Copyright (c) 2021, Ampere Computing LLC. All rights reserved.
*
*  SPDX-License-Identifier: BSD-2-Clause-Patent
*
**/

#include <Uefi/UefiBaseType.h>
#include <Uefi/UefiSpec.h>
#include <Library/DebugLib.h>
#include <Library/TimeBaseLib.h>

/**
  Converts Epoch seconds (elapsed since 1970 JANUARY 01, 00:00:00 UTC) to EFI_TIME.

  @param  EpochSeconds   Epoch seconds.
  @param  Time           The time converted to UEFI format.

**/
VOID
EFIAPI
EpochToEfiTime (
  IN  UINTN     EpochSeconds,
  OUT EFI_TIME  *Time
  )
{
  UINTN  a;
  UINTN  b;
  UINTN  c;
  UINTN  d;
  UINTN  g;
  UINTN  j;
  UINTN  m;
  UINTN  y;
  UINTN  da;
  UINTN  db;
  UINTN  dc;
  UINTN  dg;
  UINTN  hh;
  UINTN  mm;
  UINTN  ss;
  UINTN  J;

  J  = (EpochSeconds / 86400) + 2440588;
  j  = J + 32044;
  g  = j / 146097;
  dg = j % 146097;
  c  = (((dg / 36524) + 1) * 3) / 4;
  dc = dg - (c * 36524);
  b  = dc / 1461;
  db = dc % 1461;
  a  = (((db / 365) + 1) * 3) / 4;
  da = db - (a * 365);
  y  = (g * 400) + (c * 100) + (b * 4) + a;
  m  = (((da * 5) + 308) / 153) - 2;
  d  = da - (((m + 4) * 153) / 5) + 122;

  Time->Year  = (UINT16)(y - 4800 + ((m + 2) / 12));
  Time->Month = ((m + 2) % 12) + 1;
  Time->Day   = (UINT8)(d + 1);

  ss = EpochSeconds % 60;
  a  = (EpochSeconds - ss) / 60;
  mm = a % 60;
  b  = (a - mm) / 60;
  hh = b % 24;

  Time->Hour       = (UINT8)hh;
  Time->Minute     = (UINT8)mm;
  Time->Second     = (UINT8)ss;
  Time->Nanosecond = 0;
}

/**
  Calculate Epoch days.

  @param    Time  The UEFI time to be calculated.

  @return   Number of days.

**/
UINTN
EFIAPI
EfiGetEpochDays (
  IN  EFI_TIME  *Time
  )
{
  UINTN  a;
  UINTN  y;
  UINTN  m;
  UINTN  JulianDate; // Absolute Julian Date representation of the supplied Time
  UINTN  EpochDays;  // Number of days elapsed since EPOCH_JULIAN_DAY

  a = (14 - Time->Month) / 12;
  y = Time->Year + 4800 - a;
  m = Time->Month + (12*a) - 3;

  JulianDate = Time->Day + ((153*m + 2)/5) + (365*y) + (y/4) - (y/100) + (y/400) - 32045;

  ASSERT (JulianDate >= EPOCH_JULIAN_DATE);
  EpochDays = JulianDate - EPOCH_JULIAN_DATE;

  return EpochDays;
}

/**
  Converts EFI_TIME to Epoch seconds (elapsed since 1970 JANUARY 01, 00:00:00 UTC).

  @param    Time  The UEFI time to be converted.

  @return   Number of seconds.

**/
UINTN
EFIAPI
EfiTimeToEpoch (
  IN  EFI_TIME  *Time
  )
{
  UINTN  EpochDays;  // Number of days elapsed since EPOCH_JULIAN_DAY
  UINTN  EpochSeconds;

  EpochDays = EfiGetEpochDays (Time);

  EpochSeconds = (EpochDays * SEC_PER_DAY) + ((UINTN)Time->Hour * SEC_PER_HOUR) + (Time->Minute * SEC_PER_MIN) + Time->Second;

  return EpochSeconds;
}

/**
  Get the day of the week from the UEFI time.

  @param    Time  The UEFI time to be calculated.

  @return   The day of the week: Sunday=0, Monday=1, ... Saturday=6

**/
UINTN
EfiTimeToWday (
  IN  EFI_TIME  *Time
  )
{
  UINTN  EpochDays;  // Number of days elapsed since EPOCH_JULIAN_DAY

  EpochDays = EfiGetEpochDays (Time);

  // 4=1/1/1970 was a Thursday

  return (EpochDays + 4) % 7;
}

/**
  Check if it is a leap year.

  @param    Time  The UEFI time to be checked.

  @retval   TRUE  It is a leap year.
  @retval   FALSE It is NOT a leap year.

**/
BOOLEAN
EFIAPI
IsLeapYear (
  IN EFI_TIME  *Time
  )
{
  if (Time->Year % 4 == 0) {
    if (Time->Year % 100 == 0) {
      if (Time->Year % 400 == 0) {
        return TRUE;
      } else {
        return FALSE;
      }
    } else {
      return TRUE;
    }
  } else {
    return FALSE;
  }
}

/**
  Check if the day in the UEFI time is valid.

  @param    Time    The UEFI time to be checked.

  @retval   TRUE    Valid.
  @retval   FALSE   Invalid.

**/
BOOLEAN
EFIAPI
IsDayValid (
  IN  EFI_TIME  *Time
  )
{
  STATIC CONST INTN  DayOfMonth[12] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

  if ((Time->Day < 1) ||
      (Time->Day > DayOfMonth[Time->Month - 1]) ||
      ((Time->Month == 2) && (!IsLeapYear (Time) && (Time->Day > 28)))
      )
  {
    return FALSE;
  }

  return TRUE;
}

/**
  Check if the time zone is valid.
  Valid values are between -1440 and 1440 or 2047 (EFI_UNSPECIFIED_TIMEZONE).

  @param    TimeZone    The time zone to be checked.

  @retval   TRUE    Valid.
  @retval   FALSE   Invalid.

**/
BOOLEAN
EFIAPI
IsValidTimeZone (
  IN  INT16  TimeZone
  )
{
  return TimeZone == EFI_UNSPECIFIED_TIMEZONE ||
         (TimeZone >= -1440 && TimeZone <= 1440);
}

/**
  Check if the daylight is valid.
  Valid values are:
    0 : Time is not affected.
    1 : Time is affected, and has not been adjusted for daylight savings.
    3 : Time is affected, and has been adjusted for daylight savings.
  All other values are invalid.

  @param    Daylight    The daylight to be checked.

  @retval   TRUE    Valid.
  @retval   FALSE   Invalid.

**/
BOOLEAN
EFIAPI
IsValidDaylight (
  IN  INT8  Daylight
  )
{
  return Daylight == 0 ||
         Daylight == EFI_TIME_ADJUST_DAYLIGHT ||
         Daylight == (EFI_TIME_ADJUST_DAYLIGHT | EFI_TIME_IN_DAYLIGHT);
}

/**
  Check if the UEFI time is valid.

  @param    Time    The UEFI time to be checked.

  @retval   TRUE    Valid.
  @retval   FALSE   Invalid.

**/
BOOLEAN
EFIAPI
IsTimeValid (
  IN EFI_TIME  *Time
  )
{
  // Check the input parameters are within the range specified by UEFI
  if ((Time->Year  < 1900)              ||
      (Time->Year   > 9999)              ||
      (Time->Month  < 1)              ||
      (Time->Month  > 12)              ||
      (!IsDayValid (Time))              ||
      (Time->Hour   > 23)              ||
      (Time->Minute > 59)              ||
      (Time->Second > 59)              ||
      (Time->Nanosecond > 999999999)     ||
      (!IsValidTimeZone (Time->TimeZone)) ||
      (!IsValidDaylight (Time->Daylight)))
  {
    return FALSE;
  }

  return TRUE;
}
