#pragma once
#include <limits>
#include <time.h>
#include <vector>
#include <stdint.h>
#include <stddef.h>
#include <panda/refcnt.h>
#include <panda/string.h>
#include <panda/string_view.h>

namespace panda { namespace time {

using ptime_t = int64_t;

constexpr const size_t ZONE_ABBR_MAX = 7;   // max length of local type abbrev name (MSK, EST, EDT, ...)
constexpr const size_t ZONE_ABBR_MIN = 3;

static constexpr const ptime_t EPOCH_MAX    =  67767976233446399l; // calculated for gmtime(s=59,m=59,h=23,mday=31,y=2**31-1) - 24 hours for possible tz offsets
static constexpr const ptime_t EPOCH_MIN    = -67768100567884800l; // calculated for gmtime(s=00,m=00,h=00,mday=01,y=-2**31)  + 24 hours for possible tz offsets
static constexpr const ptime_t EPOCH_NEGINF = std::numeric_limits<ptime_t>::min();

extern const int         DAYS_IN_MONTH[2][12];
extern const int         MON2YDAY[2][12];
extern const string_view MONTH_NAMES[12];
extern const string_view WDAY_NAMES[7];

static constexpr const char GMT_FALLBACK[] = "GMT0";

struct datetime {
    ptime_t sec;
    ptime_t min;
    ptime_t hour;
    ptime_t mday;
    ptime_t mon;
    int32_t yday;
    int32_t wday;
    int32_t year;
    int32_t isdst;
    int32_t gmtoff;
    union {
        char    zone[ZONE_ABBR_MAX+1];
        int64_t n_zone;
    };
};

struct Timezone;
using TimezoneSP = panda::iptr<const Timezone>;

TimezoneSP        tzget      (const string_view& zonename);
TimezoneSP        tzget_abbr (const string_view& zoneabbr);
const TimezoneSP& tzlocal    ();

void tzset (const string_view& zonename);
void tzset (const TimezoneSP& = {});

const string& tzdir    ();
void          tzdir    (const string&);
const string& tzsysdir ();

const string& tzembededdir();
void          tzembededdir(const string&);

void use_system_timezones ();
void use_embed_timezones();
std::vector<string> available_timezones ();

bool     gmtime   (ptime_t epoch, datetime* result);
datetime gmtime   (ptime_t epoch);
ptime_t  timegm   (datetime* date);
ptime_t  timegml  (datetime* date);
ptime_t  timegmll (const datetime* date);

bool     anytime  (ptime_t epoch, datetime* result, const TimezoneSP& zone);
datetime anytime  (ptime_t epoch, const TimezoneSP& zone);
ptime_t  timeany  (datetime* date, const TimezoneSP& zone);
ptime_t  timeanyl (datetime* date, const TimezoneSP& zone);

inline bool     localtime  (ptime_t epoch, datetime* result) { return anytime(epoch, result, tzlocal()); }
inline datetime localtime  (ptime_t epoch)                   { return anytime(epoch, tzlocal()); }
inline ptime_t  timelocal  (datetime* date)                  { return timeany(date, tzlocal()); }
inline ptime_t  timelocall (datetime* date)                  { return timeanyl(date, tzlocal()); }

string strftime (string_view format, const datetime&);

inline int is_leap_year  (int32_t year)                { return (year % 4) == 0 && ((year % 25) != 0 || (year % 16) == 0); }
inline int days_in_month (int32_t year, uint8_t month) { return DAYS_IN_MONTH[is_leap_year(year)][month]; }

inline string_view month_name  (int mon)  { return MONTH_NAMES[mon]; }
inline string_view month_sname (int mon)  { return MONTH_NAMES[mon].substr(0,3); }
inline string_view wday_name   (int wday) { return WDAY_NAMES[wday]; }
inline string_view wday_sname  (int wday) { return WDAY_NAMES[wday].substr(0,3); }

// DAYS PASSED SINCE 1 Jan 0001 00:00:00 TILL 1 Jan <year> 00:00:00
inline ptime_t christ_days (int32_t year) {
    ptime_t yearpos = (ptime_t)year + 2147483999U;
    ptime_t ret = yearpos*365;
    yearpos >>= 2;
    ret += yearpos;
    yearpos /= 25;
    ret -= yearpos - (yearpos >> 2) + (ptime_t)146097*5368710;
    return ret;
}

// DAYS PASSED SINCE 1 Jan 0001 00:00:00 TILL supplied date
inline ptime_t christ_days (int32_t year, uint8_t month, uint8_t mday) {
    return christ_days(year) + MON2YDAY[is_leap_year(year)][month] + mday - 1;
}

// returns week day number for supplied date (0=Sun, 6=Sat), only for dates later than 0000y
inline uint8_t wday (int32_t year, uint8_t month, uint8_t mday) {
    return (1 + christ_days(year, month, mday)) % 7; // "1" because 1 Jan 0001 was monday :)
}

struct Timezone : panda::AtomicRefcnt {
    struct Transition {
        ptime_t start;        // time of transition
        ptime_t local_start;  // local time of transition (epoch+offset).
        ptime_t local_end;    // local time of transition's end (next transition epoch + MY offset).
        ptime_t local_lower;  // local_start or prev transition's local_end
        ptime_t local_upper;  // local_start or prev transition's local_end
        int32_t offset;       // offset from non-leap GMT
        int32_t gmt_offset;   // offset from leap GMT
        int32_t delta;        // offset minus previous transition's offset
        int32_t isdst;        // is DST in effect after this transition
        int32_t leap_corr;    // summary leap seconds correction at the moment
        int32_t leap_delta;   // delta leap seconds correction (0 if it's just a transition, != 0 if it's a leap correction)
        ptime_t leap_end;     // end of leap period (not including last second) = start + leap_delta
        ptime_t leap_lend;    // local_start + 2*leap_delta
        union {
            char    abbrev[ZONE_ABBR_MAX+1]; // transition (zone) abbreviation
            int64_t n_abbrev;                // abbrev as int64_t
        };
    };

    struct Rule {
        // rule for future (beyond transition list) dates and for abstract timezones
        // http://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
        // --------------------------------------------------------------------------------------------
        // 1 Jan   OUTER ZONE   OUTER END        INNER ZONE        INNER END     OUTER ZONE      31 Dec
        // --------------------------------------------------------------------------------------------
        struct Zone {
            enum class Switch { DATE, JDAY, DAY };
            union {
                char    abbrev[ZONE_ABBR_MAX+1]; // zone abbreviation
                int64_t n_abbrev;                // abbrev as int64_t
            };
            int32_t  offset;                     // offset from non-leap GMT
            int32_t  gmt_offset;                 // offset from leap GMT
            int32_t  isdst;                      // true if zone represents DST time
            Switch   type;                       // type of 'end' field
            datetime end;                        // dynamic date when this zone ends (only if hasdst=1)
        };

        uint32_t hasdst;       // does this rule have DST switching
        Zone     outer;        // always present
        Zone     inner;        // only present if hasdst=1
        int32_t  max_offset;   // max(outer.offset, inner.offset)
        int32_t  delta;        // inner.offset - outer.offset
    };

    struct Leap {
        ptime_t  time;
        uint32_t correction;
    };

    string      name;
    Transition* trans;
    uint32_t    trans_cnt;
    Transition  ltrans;              // trans[trans_cnt-1]
    Leap*       leaps;
    uint32_t    leaps_cnt;
    Rule        future;
    mutable bool is_local; // if timezone is set as local at the moment

    Timezone () {}

    void clear () {
        delete[] this->trans;
        if (this->leaps_cnt > 0) delete[] this->leaps;
    }

    ~Timezone () { clear(); }
};

}}