#pragma once

#include <maps/libs/introspection/include/comparison.h>
#include <maps/libs/tagged/include/tagged.h>
#include <maps/libs/common/include/exception.h>

#include <array>
#include <iostream>
#include <string>
#include <tuple>

namespace maps::wiki::common {

class YearTag;
class MonthTag;
class DayTag;
class HourTag;
class MinuteTag;

using Year = tagged::Tagged<YearTag, int>;
using Month = tagged::Tagged<MonthTag, int>;
using Day = tagged::Tagged<DayTag, int>;
using Hour = tagged::Tagged<HourTag, int>;
using Minute = tagged::Tagged<MinuteTag, int>;

constexpr int SECONDS_PER_MINUTE = 60;
constexpr int MINUTES_PER_HOUR = 60;
constexpr int HOURS_PER_DAY = 24;
constexpr int DAYS_PER_WEEK = 7;
constexpr int MAX_DAYS_PER_YEAR = 366;

const std::array<int, 12> MAX_DAY_BY_MONTH =
    {{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
constexpr int MONTHS_PER_YEAR = static_cast<int>(MAX_DAY_BY_MONTH.size());

constexpr Year YEAR_ANY = static_cast<Year>(0);

enum class WeekdayFlags
{
    Monday = (1 << 0),
    Tuesday = (1 << 1),
    Wednesday = (1 << 2),
    Thursday = (1 << 3),
    Friday = (1 << 4),
    Saturday = (1 << 5),
    Sunday = (1 << 6),

    Workdays = Monday | Tuesday | Wednesday | Thursday | Friday,
    Holidays = Saturday | Sunday,

    None = 0,
    All = Workdays | Holidays,

    Min = Monday,
    Max = All
};

constexpr WeekdayFlags operator&(
    WeekdayFlags lhs,
    WeekdayFlags rhs)
{
    return static_cast<WeekdayFlags>(
        static_cast<int>(lhs) & static_cast<int>(rhs));
}

constexpr WeekdayFlags operator|(
    WeekdayFlags lhs,
    WeekdayFlags rhs)
{
    return static_cast<WeekdayFlags>(
        static_cast<int>(lhs) | static_cast<int>(rhs));
}

constexpr WeekdayFlags operator~(WeekdayFlags f)
{
    return static_cast<WeekdayFlags>(
        ~static_cast<int>(f));
}

constexpr bool operator!(WeekdayFlags f)
{ return f == WeekdayFlags::None; }

inline WeekdayFlags weekdayFromOrdinal(int dayOfWeek)
{
    REQUIRE(dayOfWeek > 0 && dayOfWeek <= DAYS_PER_WEEK,
        "Invalid day of week: " << dayOfWeek);
    return static_cast<WeekdayFlags>(1 << (dayOfWeek - 1));
}

class Date
{
public:
    Date(const Year& year, const Month& month, const Day& day);
    explicit Date(const std::string& str);
    explicit Date(int dayOfYear);
    Date(int dayOfYear, const Year& year);
    Date(const std::string& str, const Date& defaultValue);

    const Year& year() const
    { return year_; }

    const Month& month() const
    { return month_; }

    const Day& day() const
    { return day_; }

    void setYear(const Year& year)
    { year_ = year; }

    void setMonth(const Month& month)
    { month_ = month; }

    void setDay(const Day& day)
    { day_ = day; }

    bool isValid() const;

    WeekdayFlags weekday() const;
    int dayOfYear() const;

    Date next() const;
    Date addDays(int days) const;

    bool operator==(const Date& d) const;

    auto introspect() const
    { return std::tie(year_, month_, day_); }

private:
    Year year_;
    Month month_;
    Day day_;
};

class Time
{
public:
    Time(const Hour& hour, const Minute& minute);
    explicit Time(const std::string& str);

    const Hour& hour() const
    { return hour_; }

    const Minute& minute() const
    { return minute_; }

    void setHour(const Hour& hour)
    { hour_ = hour; }

    void setMinute(const Minute& minute)
    { minute_ = minute; }

    bool isValid() const;

    auto introspect() const
    { return std::tie(hour_, minute_); }

private:
    Hour hour_;
    Minute minute_;
};

class DateTime
{
public:
    DateTime(const Date& date, const Time& time);
    explicit DateTime(std::time_t timestamp);

    const Date& date() const
    { return date_; }

    const Time& time() const
    { return time_; }

    std::time_t timestamp() const;

    static DateTime now();

private:
    Date date_;
    Time time_;
};

const Date YEAR_START_DATE = {YEAR_ANY, Month(1), Day(1)};
const Date YEAR_END_DATE = {YEAR_ANY, Month(12), Day(31)};

const Time DAY_START_TIME = {Hour(0), Minute(0)};
const Time DAY_END_TIME = {Hour(24), Minute(0)};

std::ostream& operator<<(std::ostream& os, const Date& date);
std::ostream& operator<<(std::ostream& os, const Time& time);

using introspection::operator!=;
using introspection::operator<;
using introspection::operator<=;

enum class WithTimeZone : bool
{
    No = false,
    Yes = true
};

std::string
canonicalDateTimeString(const std::string& str, WithTimeZone withTimeZone);

} // namespace maps::wiki::common
