#pragma once

#include <util/datetime/base.h>

#include <contrib/libs/cctz/include/cctz/civil_time.h>
#include <contrib/libs/cctz/include/cctz/time_zone.h>

#include <util/generic/strbuf.h>
#include <util/generic/string.h>
#include <util/generic/yexception.h>

#if __clang__ && __cpp_constexpr >= 201304
#define CONSTEXPR_M constexpr
#else
#define CONSTEXPR_M inline
#endif

namespace NDatetime {
    /** Exception class which throws when time zone is not valid
     */
    class TInvalidTimezone: public yexception {
    };

    using TSystemClock = std::chrono::system_clock;
    using TTimePoint = std::chrono::time_point<TSystemClock>;

    /*
     * An opaque class representing past, present, and future rules of
     * mapping between absolute and civil times in a given region.
     * It is very lightweight and may be passed by value.
     */
    using TTimeZone = cctz::time_zone;

    using TCivilYear = cctz::civil_year;
    using TCivilMonth = cctz::civil_month;
    using TCivilDay = cctz::civil_day;
    using TCivilHour = cctz::civil_hour;
    using TCivilMinute = cctz::civil_minute;
    using TCivilSecond = cctz::civil_second;
    using TWeekday = cctz::weekday;
    using TDiff = cctz::diff_t;

    using TYear = cctz::year_t;
    using TMonth = cctz::detail::month_t;

    enum class ECivilUnit : int {
        Second = 0,
        Minute = 1,
        Hour = 2,
        Day = 3,
        Month = 4,
        Year = 5
    };

    namespace NDetail {
        template <typename T>
        struct TGetCivilUnit;

        template <ECivilUnit Unit>
        struct TGetCivilTime;
    }

    template <typename T>
    CONSTEXPR_M ECivilUnit GetCivilUnit(const T& = {}) {
        return NDetail::TGetCivilUnit<T>::Value;
    }

    template <ECivilUnit Unit>
    using TCivilTime = typename NDetail::TGetCivilTime<Unit>::TResult;

    /**
     * Class with variable unit diff.
     */
    struct TCivilDiff {
        TDiff Value = 0;
        ECivilUnit Unit = ECivilUnit::Second;

        TCivilDiff() = default;
        TCivilDiff(TDiff value, ECivilUnit unit)
            : Value(value)
            , Unit(unit)
        {
        }

        /**
         * Straightfoward implementation of operators <, == and unit conversions
         * can be potentially misleading (e.g. 1 month == 30 days?);
         * we leave it to user to implement it properly for each application.
         */
    };

    /**
     * Gets the time zone by an IANA name.
     * @param name  A name in IANA format (e.g., "Europe/Moscow").
     * @note        After you request a time zone for the first time, it is cached
     *              in a (thread-safe) cache, so subsequent requests to the
     *              same time zone should be extremely fast.
     * @throw       TInvalidTimezone if the name is invalid.
     * @see         http://www.iana.org/time-zones
     */
    TTimeZone GetTimeZone(TStringBuf name);

    /**
     * Returns a time zone that is a fixed offset (seconds east) from UTC.
     * Note: If the absolute value of the offset is greater than 24 hours
     * you'll get UTC (i.e., zero offset) instead.
     */
    TTimeZone GetFixedTimeZone(const long offset);

    /** Convert civil time from one timezone to another
     * @param[in] src is source time with 'from' timezone 
     * @param[in] from is a initial timezone
     * @param[in] from is a destination timezone
     * @return a civil time
     */
    template <typename T>
    T Convert(const T& src, const TTimeZone& from, const TTimeZone& to) {
        return cctz::convert(cctz::convert(src, from), to);
    }

    /** Convert absolute time to civil time by rules from timezone.
     * @param[in] absTime is an absolute time which is used to convert
     * @param[in] tz is a loaded timezone
     * @return a civil time
     *
     * Note: This function doesn't work properly for dates before 1 Jan 1970!
     */
    TCivilSecond Convert(const TInstant& absTime, const TTimeZone& tz);

    /** Convert absolute time to civil time by rules from timezone which will be loaded.
     * @throw InvalidTimezone if the name is invalid.
     * @param[in] absTime is an absolute time which is used to convert
     * @param[in] tzName is a timezone name which will be loaded
     * @return a civil time
     *
     * Note: This function doesn't work properly for dates before 1 Jan 1970!
     */
    TCivilSecond Convert(const TInstant& absTime, TStringBuf tzName);

    /** Convert a civil time to absolute by using rules from timezone
     *
     * Note: This function doesn't work properly for dates before 1 Jan 1970!
     */
    TInstant Convert(const TCivilSecond& s, const TTimeZone& tz);

    /** Just to simply calculations between dates/times.
     * NDatetime::Calc<TCivilDay>(TCivilSecond(2001, 1, 1, 10, 10, 10), 5); // returns TCivilDay(2001, 1, 6);
     * @param[in] tp is a timepoint with which calc will be
     * @param[in] diff is quantity of which will be added (of type T) to the tp
     * @return the calculated T type
     */
    template <typename T, typename S>
    inline T Calc(const S& tp, TDiff diff) {
        return T(tp) + diff;
    }

    /** Non-template methods for adding dates/times.
    * @param[in] tp is a timepoint with which calc will be
    * @param[in] diff is quantity of which will be added to the tp
    * @return the calculated TCivilSecond object
    */
    TCivilSecond AddYears(const TCivilSecond& tp, TDiff diff);
    TCivilSecond AddMonths(const TCivilSecond& tp, TDiff diff);
    TCivilSecond AddDays(const TCivilSecond& tp, TDiff diff);
    TCivilSecond AddHours(const TCivilSecond& tp, TDiff diff);
    TCivilSecond AddMinutes(const TCivilSecond& tp, TDiff diff);
    TCivilSecond AddSeconds(const TCivilSecond& tp, TDiff diff);

    /** Method to add TCivilDiff
     * @param[in] tp is a timepoint with which calc will be
     * @param[in] diff is quantity of which will be added to the tp
     * @return the calculated TCivilSecond object
     */
    TCivilSecond AddCivil(const TCivilSecond& tp, TCivilDiff diff);

    /** Method to subtract to civil dates/times and get TCivilDiff.
     * First casts to unit, then subtracts;
     * e.g. GetCivilDiff(2017-10-01, 2017-09-30, Month) = 1.
     *
     * @param[in] tpX is a timepoint
     * @param[in] tpY is a timepoint to subtract from tpX
     * @param[in] unit is a civil time unit to use in subtraction
     * @return the calculated diff as TCivilDiff object
     */
    TCivilDiff GetCivilDiff(const TCivilSecond& tpX, const TCivilSecond& tpY, ECivilUnit unit);

    /** Formats the given TimePoint in the given TTimeZone according to
     * the provided format string. Uses strftime()-like formatting options,
     * with the following extensions:
     *
     *   - %Ez  - RFC3339-compatible numeric time zone (+hh:mm or -hh:mm)
     *   - %E#S - Seconds with # digits of fractional precision
     *   - %E*S - Seconds with full fractional precision (a literal '*')
     *   - %E#f - Fractional seconds with # digits of precision
     *   - %E*f - Fractional seconds with full precision (a literal '*')
     *   - %E4Y - Four-character years (-999 ... -001, 0000, 0001 ... 9999)
     *
     * Note that %E0S behaves like %S, and %E0f produces no characters.  In
     * contrast %E*f always produces at least one digit, which may be '0'.
     *
     * Note that %Y produces as many characters as it takes to fully render the
     * year. A year outside of [-999:9999] when formatted with %E4Y will produce
     * more than four characters, just like %Y.
     *
     * Tip: Format strings should include the UTC offset (e.g., %z or %Ez) so that
     * the resultng string uniquely identifies an absolute time.
     *
     * Example:
     *   NDatetime::TTimeZone lax = NDatetime::GetTimeZone("America/Los_Angeles");
     *   NDatetime::TCivilSecond tp(2013, 1, 2, 3, 4, 5);
     *   TString f = NDatetime::Format("%H:%M:%S", tp, lax);      // "03:04:05"
     *   TString f = NDatetime::Format("%H:%M:%E3S", tp, lax);    //"03:04:05.000"
     */
    template <typename TP>
    TString Format(TStringBuf fmt, const TP& tp, const TTimeZone& tz) {
        return TString(cctz::format(static_cast<std::string>(fmt), TTimePoint(cctz::convert(tp, tz)), tz));
    }

    /** Returns the weekday by day.
     * @param[in] day is a given day
     * @return a weekday (enum)
     */
    CONSTEXPR_M TWeekday GetWeekday(const TCivilDay& day) noexcept {
        return cctz::get_weekday(day);
    }

    /** Returns the TCivilDay that strictly follows or precedes the given
      * civil_day, and that falls on the given weekday.
      * @code
        For example, given:

            August 2015
        Su Mo Tu We Th Fr Sa
                           1
         2  3  4  5  6  7  8
         9 10 11 12 13 14 15
        16 17 18 19 20 21 22
        23 24 25 26 27 28 29
        30 31

        TCivilDay a(2015, 8, 13);  // GetWeekday(a) == TWeekday::thursday
        TCivilDay b = NextWeekday(a, TWeekday::thursday);  // b = 2015-08-20
        TCivilDay c = PrevWeekday(a, TWeekday::thursday);  // c = 2015-08-06
        TCivilDay d = NearestWeekday(a, TWeekday::thursday);  // d = 2015-08-13

        TCivilDay e = ...
        // Gets the following Thursday if e is not already Thursday
        TCivilDay thurs1 = PrevWeekday(e, TWeekday::thursday) + 7;
        // Gets the previous Thursday if e is not already Thursday
        TCivilDay thurs2 = NextWeekday(e, TWeekday::thursday) - 7;
     * @endcode
     * @see PrevWeekday()
     * @see NearestWeekday()
     * @param[in] cd is a current day
     * @param[in] wd is a weekday which wanetd for find on next week
     * @return a civil day on weekday next week
     */
    CONSTEXPR_M TCivilDay NextWeekday(const TCivilDay& cd, TWeekday wd) noexcept {
        return cctz::next_weekday(cd, wd);
    }

    /** Returns prev weekday. See the description of NextWeekday().
     * @see NextWeekday()
     * @see NearestWeekday()
     * @param[in] cd is a current day
     * @param[in] wd is a weekday which is looking for (backward)
     * @return a first occurence of the given weekday (backward)
     */
    CONSTEXPR_M TCivilDay PrevWeekday(const TCivilDay& cd, TWeekday wd) noexcept {
        return cctz::prev_weekday(cd, wd);
    }

    /** Find a nearest occurence of the given weekday forward (could be current day).
     * @see NextWeekday()
     * @param[in] cd is a current day
     * @param[in] wd is a weekday which is looking for (current day or later)
     * @return first occurence (including current day) of the given weekday
     */
    CONSTEXPR_M TCivilDay NearestWeekday(const TCivilDay& cd, TWeekday wd) noexcept {
        return get_weekday(cd) != wd ? next_weekday(cd, wd) : cd;
    }

    /** Find the date of the given weekday within the given week.
     * @param[in] cd is a current day
     * @param[in] wd is a requested week day
     * @return day within a week of a given day
     */
    CONSTEXPR_M TCivilDay WeekdayOnTheWeek(const TCivilDay& cd, TWeekday wd) noexcept {
        const auto d = get_weekday(cd);
        if (d == wd)
            return cd;

        return d < wd ? NextWeekday(cd, wd) : PrevWeekday(cd, wd);
    }

    /** Returns an absolute day of year by given day.
     */
    CONSTEXPR_M int GetYearDay(const TCivilDay& cd) noexcept {
        return cctz::get_yearday(cd);
    }

    CONSTEXPR_M int DaysPerMonth(TYear year, TMonth month) noexcept {
        return cctz::detail::impl::days_per_month(year, month);
    }

    CONSTEXPR_M int DaysPerYear(TYear year, TMonth month) noexcept {
        return cctz::detail::impl::days_per_year(year, month);
    }

    /** Calculate week number for the given date
     * @param[in] cd is a current day
     * @param[in] usePreviousYear (optional) true if calculate week number from previous year
     *
     * The week number starts from 1 for the first week, where Thursday exist (see ISO8601), i.e.
     * Jan 2021
     * week#  mo tu we th fr sa su
     * 53                  1  2  3
     * 01      4  5  6  7  8  9 10
     * 02     11 ...
     * Jan 2020
     * week#  mo tu we th fr sa su
     * 01            1  2  3  4  5
     * 02      6  7  8  9 10 11...
     *
     * In case if you received zero value, you may call function again with usePreviousYear=true
     * Also you may use usePreviousYear to calculate week difference between two dates in different year 
     */
     CONSTEXPR_M int GetYearWeek(const TCivilDay& cd, bool usePreviousYear = false) noexcept {
         const auto jan1 = NDatetime::GetWeekday(NDatetime::TCivilDay{cd.year() - (usePreviousYear ? 1 : 0), 1, 1});
         const auto daysCount = GetYearDay(cd) + (usePreviousYear ? DaysPerYear(cd.year()-1, 1) : 0);

         return (daysCount + static_cast<int>(jan1) - 1) / 7 + (jan1 == cctz::weekday::monday || jan1 == cctz::weekday::tuesday || jan1 == cctz::weekday::wednesday);
     }
}

#include "civil-inl.h"

#undef CONSTEXPR_M
