#pragma once

#include <cassert>
#include <chrono>
#include <iostream>

//--------------------------------------------------------------
class Age
{
  public:
  Age()                              = default;     // Default constructor
  virtual ~Age()                     = default;     // Default destructor
  Age(const Age &obj)                = default;     // Copy constructor
  Age(Age &&obj) noexcept            = default;     // Move constructor
  Age &operator=(const Age &obj)     = default;     // Copy assignment operator
  Age &operator=(Age &&obj) noexcept = default;     // Move assignment operator

  explicit Age(std::chrono::nanoseconds age);     // Constructor
  void set(std::chrono::nanoseconds age);         // Convert age in microseconds to days, hours, minutes, seconds, milliseconds and microseconds

  std::chrono::microseconds getAge() const;              // Return age in microseconds
  std::chrono::days         getDays() const;             // Return days part in age
  std::chrono::hours        getHours() const;            // Return hours part in age
  std::chrono::minutes      getMinutes() const;          // Return minutes part in age
  std::chrono::seconds      getSeconds() const;          // Return seconds part in age
  std::chrono::milliseconds getMilliseconds() const;     // Return milliseconds part in age
  std::chrono::microseconds getMicroseconds() const;     // Return microseconds part in age
  std::chrono::nanoseconds  getNanoseconds() const;      // Return nanoseconds part in age

  std::string toString(bool show_ms = true, bool show_us = false, bool show_ns = false) const;     // Return age as a string in format "[d] H:m:s.ms.us.ns"

  void testAge() const;     // Test function to verify the correctness of the Age class implementation

  protected:
  std::chrono::nanoseconds m_age = std::chrono::nanoseconds(0);

  std::chrono::days         m_days         = std::chrono::days(0);
  std::chrono::hours        m_hours        = std::chrono::hours(0);
  std::chrono::minutes      m_minutes      = std::chrono::minutes(0);
  std::chrono::seconds      m_seconds      = std::chrono::seconds(0);
  std::chrono::milliseconds m_milliseconds = std::chrono::milliseconds(0);
  std::chrono::microseconds m_microseconds = std::chrono::microseconds(0);
  std::chrono::nanoseconds  m_nanoseconds  = std::chrono::nanoseconds(0);
};
//--------------------------------------------------------------
/* Constructor */
inline Age::Age(const std::chrono::nanoseconds age)
{
  set(age);
}
//--------------------------------------------------------------
/* Convert age in microseconds to days, hours, minutes, seconds, milliseconds and microseconds */
inline void Age::set(std::chrono::nanoseconds age)
{
  constexpr auto max_ns = std::chrono::nanoseconds::max().count();     // For an int64_t storage, the maximum value corresponds to more than 106752 days
  constexpr auto min_ns = std::chrono::nanoseconds::min().count();
  if (age.count() > max_ns || age.count() < min_ns)
    throw std::overflow_error("Duration exceeds the limits of std::chrono::nanoseconds");

  if (age < std::chrono::nanoseconds(0))
    throw std::invalid_argument("Age cannot be negative");

  m_age = age;

  m_days = std::chrono::duration_cast<std::chrono::days>(age);
  age -= m_days;

  m_hours = std::chrono::duration_cast<std::chrono::hours>(age);
  age -= m_hours;

  m_minutes = std::chrono::duration_cast<std::chrono::minutes>(age);
  age -= m_minutes;

  m_seconds = std::chrono::duration_cast<std::chrono::seconds>(age);
  age -= m_seconds;

  m_milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(age);
  age -= m_milliseconds;

  m_microseconds = std::chrono::duration_cast<std::chrono::microseconds>(age);
  age -= m_microseconds;

  m_nanoseconds = age;
}
//--------------------------------------------------------------
/* Return age in microseconds */
inline std::chrono::microseconds Age::getAge() const
{
  return std::chrono::duration_cast<std::chrono::microseconds>(m_age);
}
//--------------------------------------------------------------
/* Return days part in age */
inline std::chrono::days Age::getDays() const
{
  return m_days;
}
//--------------------------------------------------------------
/* Return hours part in age */
inline std::chrono::hours Age::getHours() const
{
  return m_hours;
}
//--------------------------------------------------------------
/* Return minutes part in age */
inline std::chrono::minutes Age::getMinutes() const
{
  return m_minutes;
}
//--------------------------------------------------------------
/* Return seconds part in age */
inline std::chrono::seconds Age::getSeconds() const
{
  return m_seconds;
}
//--------------------------------------------------------------
/* Return milliseconds part in age */
inline std::chrono::milliseconds Age::getMilliseconds() const
{
  return m_milliseconds;
}
//--------------------------------------------------------------
/* Return microseconds part in age */
inline std::chrono::microseconds Age::getMicroseconds() const
{
  return m_microseconds;
}
//--------------------------------------------------------------
/* Return nanoseconds part in age */
inline std::chrono::nanoseconds Age::getNanoseconds() const
{
  return m_nanoseconds;
}
//--------------------------------------------------------------
/* Return age as a string in format "[d] H:m:s.ms.us.ns".
 * The show_ms, show_us and show_ns parameters control whether
 * to include milliseconds, microseconds and nanoseconds in the
 * output string. */
inline std::string Age::toString(const bool show_ms, const bool show_us, const bool show_ns) const
{
  std::ostringstream oss;

  // Add days if greater than 0
  if (m_days.count() > 0)
    oss << std::format("[{}] ", m_days.count());

  // Add hours, minutes and seconds
  oss << std::format("{:02}:{:02}:{:02}", m_hours.count(), m_minutes.count(), m_seconds.count());

  // Add milliseconds, microseconds and nanoseconds if requested
  if (show_ms || show_us || show_ns)
    oss << std::format(".{:03}", m_milliseconds.count());
  if (show_us || show_ns)
    oss << std::format("{:03}", m_microseconds.count());
  if (show_ns)
    oss << std::format("{:03}", m_nanoseconds.count());

  return oss.str();
}
//--------------------------------------------------------------
/* Test function to verify the correctness of the Age class implementation */
inline void Age::testAge() const
{
  const Age age(std::chrono::nanoseconds(90061001001));     // 1 day, 1 hour, 1 minute, 1 second, 1 millisecond, 1 microsecond, 1 nanosecond

  assert(age.getDays().count() == 1);
  assert(age.getHours().count() == 1);
  assert(age.getMinutes().count() == 1);
  assert(age.getSeconds().count() == 1);
  assert(age.getMilliseconds().count() == 1);
  assert(age.getMicroseconds().count() == 1);
  assert(age.getNanoseconds().count() == 1);

  std::cout << "All tests passed!" << std::endl;
}
//--------------------------------------------------------------
