#ifndef KYTY_COMMON_DATETIME_H_
#define KYTY_COMMON_DATETIME_H_

#include "common/common.h"

#include <string>

namespace Common {

using jd_t = int32_t;

constexpr jd_t DATE_JD_INVALID = (INT32_MIN);
constexpr int  TIME_MS_INVALID = (-1);
constexpr int  TIME_MS_IN_DAY  = (24 * 3600 * 1000);

constexpr int MONTH_JANUARY   = 1;
constexpr int MONTH_FEBRUARY  = 2;
constexpr int MONTH_MARCH     = 3;
constexpr int MONTH_APRIL     = 4;
constexpr int MONTH_MAY       = 5;
constexpr int MONTH_JUNE      = 6;
constexpr int MONTH_JULY      = 7;
constexpr int MONTH_AUGUST    = 8;
constexpr int MONTH_SEPTEMBER = 9;
constexpr int MONTH_OCTOBER   = 10;
constexpr int MONTH_NOVEMBER  = 11;
constexpr int MONTH_DECEMBER  = 12;

class Date final {
public:
	Date() = default;
	explicit Date(jd_t d): m_jd(d) {}
	Date(const Date& d)     = default;
	Date(Date&& d) noexcept = default;
	explicit Date(int year, int month, int day); // month in [1...12], day in [1...31]
	~Date() = default;

	static Date FromMacros(const std::string& date);

	void Set(int year, int month, int day);
	void Set(jd_t jd) { this->m_jd = jd; }
	void Get(int* year, int* month, int* day) const;

	[[nodiscard]] bool IsInvalid() const { return m_jd == DATE_JD_INVALID; }
	[[nodiscard]] int  DaysInMonth() const;
	[[nodiscard]] int  DaysInYear() const;
	[[nodiscard]] bool IsLeapYear() const;
	[[nodiscard]] int  Year() const;
	[[nodiscard]] int  Month() const;
	[[nodiscard]] int  Day() const;
	[[nodiscard]] jd_t JulianDay() const { return m_jd; }
	[[nodiscard]] int  DayOfWeek() const;
	[[nodiscard]] int  DayOfYear() const;
	[[nodiscard]] int  QuarterOfYear() const;

	std::string ToString(const char* format = "YYYY.MM.DD") const;

	static bool IsValid(int year, int month, int day);
	static int  DaysInMonth(int month);
	static int  DaysInYear(int year);
	static bool IsLeapYear(int year);

	bool operator==(const Date& other) const { return m_jd == other.m_jd; }
	bool operator!=(const Date& other) const { return m_jd != other.m_jd; }
	bool operator<(const Date& other) const { return m_jd < other.m_jd; }
	bool operator<=(const Date& other) const { return m_jd <= other.m_jd; }
	bool operator>(const Date& other) const { return m_jd > other.m_jd; }
	bool operator>=(const Date& other) const { return m_jd >= other.m_jd; }

	Date& operator=(const Date& other)     = default;
	Date& operator=(Date&& other) noexcept = default;
	Date  operator+(int days) const { return Date(m_jd + days); }
	Date  operator-(int days) const { return Date(m_jd - days); }
	Date  operator+=(int days) {
		m_jd += days;
		return *this;
	}
	Date operator-=(int days) {
		m_jd -= days;
		return *this;
	}

private:
	jd_t m_jd {DATE_JD_INVALID};
};

class Time final {
public:
	Time() = default;
	explicit Time(int msec): m_ms(msec) {
		if (m_ms < 0 || m_ms >= TIME_MS_IN_DAY) {
			m_ms = TIME_MS_INVALID;
		}
	}
	Time(const Time& t)     = default;
	Time(Time&& t) noexcept = default;
	explicit Time(int hour24, int minute, int second, int msec = 0);
	~Time() = default;

	void Set(int hour24, int minute, int second, int msec = 0);
	void Set(int msec) { m_ms = msec; }
	void Get(int* hour24, int* minute, int* second, int* msec = nullptr) const;

	[[nodiscard]] bool IsInvalid() const { return m_ms < 0 || m_ms >= TIME_MS_IN_DAY; }
	[[nodiscard]] int  Hour12() const;
	[[nodiscard]] int  Hour24() const;
	[[nodiscard]] bool IsAM() const;
	[[nodiscard]] bool IsPM() const;
	[[nodiscard]] int  Minute() const;
	[[nodiscard]] int  Second() const;
	[[nodiscard]] int  Msec() const;
	[[nodiscard]] int  MsecTotal() const { return m_ms; }

	std::string ToString(const char* format = "HH24:MI:SS") const;

	static bool IsValid(int hour, int minute, int second, int msec = 0);

	bool operator==(const Time& other) const { return m_ms == other.m_ms; }
	bool operator!=(const Time& other) const { return m_ms != other.m_ms; }
	bool operator<(const Time& other) const { return m_ms < other.m_ms; }
	bool operator<=(const Time& other) const { return m_ms <= other.m_ms; }
	bool operator>(const Time& other) const { return m_ms > other.m_ms; }
	bool operator>=(const Time& other) const { return m_ms >= other.m_ms; }

	Time& operator=(const Time& other)     = default;
	Time& operator=(Time&& other) noexcept = default;
	Time  operator+(int secs) const;
	Time  operator-(int secs) const;
	Time  operator+=(int secs);
	Time  operator-=(int secs);

private:
	int m_ms {TIME_MS_INVALID};
};

class DateTime final {
public:
	DateTime() = default;
	explicit DateTime(const Date& d): m_date(d) {}
	explicit DateTime(const Time& t): m_time(t) {}
	explicit DateTime(const Date& d, const Time& t): m_date(d), m_time(t) {}
	DateTime(const DateTime& dt)     = default;
	DateTime(DateTime&& dt) noexcept = default;
	~DateTime()                      = default;

	static DateTime FromSystem();
	static DateTime FromSystemUTC();

	[[nodiscard]] double ToUnix() const;

	[[nodiscard]] bool IsInvalid() const { return m_date.IsInvalid() || m_time.IsInvalid(); }

	void                      SetDate(const Date& d) { m_date = d; }
	void                      SetTime(const Time& t) { m_time = t; }
	Date&                     GetDate() { return m_date; }
	Time&                     GetTime() { return m_time; }
	[[nodiscard]] const Date& GetDate() const { return m_date; }
	[[nodiscard]] const Time& GetTime() const { return m_time; }

	std::string ToString(const char* format = "YYYY.MM.DD HH24:MI:SS") const;

	DateTime& operator=(const DateTime& other)     = default;
	DateTime& operator=(DateTime&& other) noexcept = default;

	bool operator==(const DateTime& other) const {
		return m_date == other.m_date && m_time == other.m_time;
	}
	bool operator!=(const DateTime& other) const { return !(*this == other); }
	bool operator<(const DateTime& other) const {
		return m_date < other.m_date || (m_date == other.m_date && m_time < other.m_time);
	}
	bool operator<=(const DateTime& other) const { return !(other < *this); }
	bool operator>(const DateTime& other) const { return other < *this; }
	bool operator>=(const DateTime& other) const { return !(*this < other); }

private:
	Date m_date;
	Time m_time;
};

} // namespace Common

#endif /* KYTY_COMMON_DATETIME_H_ */
