/*=========================================================================
 *
 *  Copyright NumFOCUS
 *
 *  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
 *
 *         https://www.apache.org/licenses/LICENSE-2.0.txt
 *
 *  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.
 *
 *=========================================================================*/
#include "itkRealTimeStamp.h"
#include "itkNumericTraits.h"

// This macro makes sure that if the number of microseconds is ever enough to
// complete a second, then the number of seconds will be incremented, and the
// remainder of microseconds will be kept. It also ensures that if the number
// of microseconds is negative, then we decrement the number of seconds and
// assign to the microseconds variable the complement that is a positive number.
#define CARRY_UNITS_OVER_UNSIGNED(seconds, micro_seconds) \
  if (micro_seconds > 1000000L)                           \
  {                                                       \
    seconds += 1;                                         \
    micro_seconds -= 1000000L;                            \
  }                                                       \
  ITK_MACROEND_NOOP_STATEMENT

#define CARRY_UNITS_OVER_SIGNED(seconds, micro_seconds) \
  if (micro_seconds > 1000000L)                         \
  {                                                     \
    seconds += 1;                                       \
    micro_seconds -= 1000000L;                          \
  }                                                     \
  if (micro_seconds < 0)                                \
  {                                                     \
    seconds -= 1;                                       \
    micro_seconds += 1000000L;                          \
  }                                                     \
  ITK_MACROEND_NOOP_STATEMENT

// This macro ensures that the sign of the seconds is the same as the sign of
// the microseconds. In other words, both of them are measured toward the same
// direction of time.
#define ALIGN_THE_ARROW_OF_TIME(seconds, micro_seconds) \
  if (seconds > 0 && micro_seconds < 0)                 \
  {                                                     \
    seconds -= 1;                                       \
    micro_seconds = 1000000L - micro_seconds;           \
  }                                                     \
  if (seconds < 0 && micro_seconds > 0)                 \
  {                                                     \
    seconds += 1;                                       \
    micro_seconds = 1000000L + micro_seconds;           \
  }                                                     \
  ITK_MACROEND_NOOP_STATEMENT

namespace itk
{

/**
 * Constructor to initialize a time stamp
 */
RealTimeStamp::RealTimeStamp()
{
  this->m_Seconds = itk::NumericTraits<SecondsCounterType>::ZeroValue();
  this->m_MicroSeconds = itk::NumericTraits<MicroSecondsCounterType>::ZeroValue();
}

/**
 * Constructor with values to initialize a time stamp.
 * This constructor should only be called from the RealTimeClock,
 * or from a file reading class.
 */
RealTimeStamp::RealTimeStamp(SecondsCounterType seconds, MicroSecondsCounterType micro_seconds)
{
  this->m_Seconds = seconds;
  this->m_MicroSeconds = micro_seconds;
}

/**
 * Destructor.
 */
RealTimeStamp::~RealTimeStamp() = default;

/**
 * Return time in microseconds.
 */
RealTimeStamp::TimeRepresentationType
RealTimeStamp::GetTimeInMicroSeconds() const
{
  auto result = static_cast<TimeRepresentationType>(this->m_Seconds);
  result *= 1e6;
  result += static_cast<TimeRepresentationType>(this->m_MicroSeconds);

  return result;
}

/**
 * Return time in milliseconds.
 */
RealTimeStamp::TimeRepresentationType
RealTimeStamp::GetTimeInMilliSeconds() const
{
  auto result = static_cast<TimeRepresentationType>(this->m_Seconds);
  result *= 1e3;
  result += static_cast<TimeRepresentationType>(this->m_MicroSeconds) / 1e3;

  return result;
}

/**
 * Return time in second.
 */
RealTimeStamp::TimeRepresentationType
RealTimeStamp::GetTimeInSeconds() const
{
  auto result = static_cast<TimeRepresentationType>(this->m_MicroSeconds);
  result /= 1e6;
  result += static_cast<TimeRepresentationType>(this->m_Seconds);

  return result;
}

/**
 * Return time in minutes.
 */
RealTimeStamp::TimeRepresentationType
RealTimeStamp::GetTimeInMinutes() const
{
  const TimeRepresentationType result = this->GetTimeInSeconds() / 60.00;
  return result;
}

/**
 * Return time in hours.
 */
RealTimeStamp::TimeRepresentationType
RealTimeStamp::GetTimeInHours() const
{
  const TimeRepresentationType result = this->GetTimeInSeconds() / 3600.00;
  return result;
}


/**
 * Return time in days.
 */
RealTimeStamp::TimeRepresentationType
RealTimeStamp::GetTimeInDays() const
{
  const TimeRepresentationType result = this->GetTimeInSeconds() / 86400.00;
  return result;
}


/**
 * Compute the time interval between two time stamps.
 */
RealTimeInterval
RealTimeStamp::operator-(const Self & other) const
{
  SecondsDifferenceType      seconds = this->m_Seconds - other.m_Seconds;
  MicroSecondsDifferenceType micro_seconds = this->m_MicroSeconds - other.m_MicroSeconds;

  ALIGN_THE_ARROW_OF_TIME(seconds, micro_seconds);

  RealTimeInterval difference;
  difference.m_Seconds = seconds;
  difference.m_MicroSeconds = micro_seconds;

  return difference;
}


/**
 * Add a time interval to this time stamp to compute a new time stamp.
 */
RealTimeStamp
RealTimeStamp::operator+(const RealTimeInterval & difference) const
{
  SecondsCounterType      seconds = this->m_Seconds + difference.m_Seconds;
  MicroSecondsCounterType micro_seconds = this->m_MicroSeconds + difference.m_MicroSeconds;
  CARRY_UNITS_OVER_UNSIGNED(seconds, micro_seconds);

  Self result;
  result.m_Seconds = seconds;
  result.m_MicroSeconds = micro_seconds;

  return result;
}


/**
 * Add a time interval to this time stamp to compute a new time stamp.
 */
RealTimeStamp
RealTimeStamp::operator-(const RealTimeInterval & difference) const
{
  SecondsDifferenceType seconds = this->m_Seconds - difference.m_Seconds;

  if (seconds < 0)
  {
    itkGenericExceptionMacro("RealTimeStamp can't go before the origin of time");
  }

  MicroSecondsDifferenceType micro_seconds = this->m_MicroSeconds - difference.m_MicroSeconds;

  CARRY_UNITS_OVER_SIGNED(seconds, micro_seconds);

  Self result;

  result.m_Seconds = static_cast<SecondsCounterType>(seconds);
  result.m_MicroSeconds = static_cast<MicroSecondsCounterType>(micro_seconds);

  return result;
}

/**
 * Add a time interval to this time stamp and update it.
 */
const RealTimeStamp &
RealTimeStamp::operator+=(const RealTimeInterval & difference)
{
  SecondsDifferenceType seconds = this->m_Seconds + difference.m_Seconds;

  if (seconds < 0)
  {
    itkGenericExceptionMacro("RealTimeStamp can't go before the origin of time");
  }

  MicroSecondsCounterType micro_seconds = this->m_MicroSeconds + difference.m_MicroSeconds;

  CARRY_UNITS_OVER_UNSIGNED(seconds, micro_seconds);

  this->m_Seconds = static_cast<SecondsCounterType>(seconds);
  this->m_MicroSeconds = static_cast<MicroSecondsCounterType>(micro_seconds);

  return *this;
}

/**
 * Subtract a time interval from this time stamp and update it.
 */
const RealTimeStamp &
RealTimeStamp::operator-=(const RealTimeInterval & difference)
{
  SecondsDifferenceType seconds = this->m_Seconds - difference.m_Seconds;

  if (seconds < 0)
  {
    itkGenericExceptionMacro("RealTimeStamp can't go before the origin of time");
  }

  MicroSecondsDifferenceType micro_seconds = this->m_MicroSeconds - difference.m_MicroSeconds;

  CARRY_UNITS_OVER_SIGNED(seconds, micro_seconds);

  this->m_Seconds = static_cast<SecondsCounterType>(seconds);
  this->m_MicroSeconds = static_cast<MicroSecondsCounterType>(micro_seconds);

  return *this;
}

/**
 * Compare two time Stamps.
 */
bool
RealTimeStamp::operator>(const Self & other) const
{
  if (this->m_Seconds > other.m_Seconds)
  {
    return true;
  }

  if (this->m_Seconds < other.m_Seconds)
  {
    return false;
  }

  return (this->m_MicroSeconds > other.m_MicroSeconds);
}

/**
 * Compare two time Stamps.
 */
bool
RealTimeStamp::operator<(const Self & other) const
{
  if (this->m_Seconds < other.m_Seconds)
  {
    return true;
  }

  if (this->m_Seconds > other.m_Seconds)
  {
    return false;
  }

  return (this->m_MicroSeconds < other.m_MicroSeconds);
}

/**
 * Compare two time Stamps.
 */
bool
RealTimeStamp::operator>=(const Self & other) const
{
  if (this->m_Seconds > other.m_Seconds)
  {
    return true;
  }

  if (this->m_Seconds < other.m_Seconds)
  {
    return false;
  }

  return (this->m_MicroSeconds >= other.m_MicroSeconds);
}

/**
 * Compare two time Stamps.
 */
bool
RealTimeStamp::operator<=(const Self & other) const
{
  if (this->m_Seconds < other.m_Seconds)
  {
    return true;
  }

  if (this->m_Seconds > other.m_Seconds)
  {
    return false;
  }

  return (this->m_MicroSeconds <= other.m_MicroSeconds);
}


/**
 * Compare two time Stamps.
 */
bool
RealTimeStamp::operator==(const Self & other) const
{
  return ((this->m_MicroSeconds == other.m_MicroSeconds) && (this->m_Seconds == other.m_Seconds));
}

/** Default print out of a RealTimeStamp */
std::ostream &
operator<<(std::ostream & os, const RealTimeStamp & v)
{
  os << v.GetTimeInSeconds() << " seconds ";
  return os;
}

} // end of namespace itk
