#include "PDate.h"

PDate::PDate (time_t from) : _error(E_OK)  { epoch(from); }
PDate::PDate (const char* str, size_t len) { setFrom(str, len); }
PDate::PDate (PDate* from)                 { setFrom(from); }
PDate::PDate (SV* arg)                     { setFrom(arg); }

void PDate::eCheck () { if (!_hasEpoch && _hasData) eSync(); }
void PDate::eSync  () { // this function is heavy
    _hasEpoch = true;
    _hasFullData = true;
    _epoch = timelocal(&_data);
}

void PDate::dNorm () { dNorm(0); }
void PDate::dNorm (uint8_t flags) {
    struct tm old;

    if (flags & NORM_YDAY) {
        _data.tm_mon = 0;
        _data.tm_mday = 0;
    } else {
        _data.tm_yday = -1;
    }
    
    _data.tm_wday = 7; // wday is only set when its out of range, yday is always set
    
    if (flags & (NORM_RCHECK|NORM_MBA)) old = _data;
    
    normalize(&_data);
    _hasFullData = false;
    
    if (flags & NORM_RCHECK) {
        if (old.tm_sec > 60 || old.tm_min >= 60 || old.tm_hour >= 24) {
            error(E_RANGE);
            return;
        }
       
        if (flags & NORM_YDAY) {
            if (old.tm_year != _data.tm_year) {
                error(E_RANGE);
                return;
            }
        } else {
            if (old.tm_mday != _data.tm_mday || old.tm_mon != _data.tm_mon || old.tm_year != _data.tm_year) {
                error(E_RANGE);
                return;
            }
        }
    }
    
    // dNorm should be called with NORM_MBA flag ONLY WHEN month or year changed ONLY. ONLY MONTH OR YEAR, nothing else.
    // if you need add more properties and still use NORM_MBA you should add months and years and call dNorm(NORM_MBA);
    // and then add rest, then call dNorm()
    if ((flags & NORM_MBA) && old.tm_mday > _data.tm_mday) {
        // December cannot cause overflow because it has 31 - max possible days.
        // Therefore we can safely get previous month by 'mon--'. Also set the last day of month.
        // Should tune yday,wday either.
        _data.tm_mon--;
        _data.tm_yday -= _data.tm_mday;
        _data.tm_wday -= _data.tm_mday;
        if (_data.tm_wday < 0) _data.tm_wday += 7;
        _data.tm_mday = days_in_month(_data.tm_year+1900, _data.tm_mon+1);
    }
}

void PDate::dCheck () { if (!_hasData && _hasEpoch) dSync(); }

void PDate::dCheckFull () {
    if (_hasData && _hasFullData) return;
    if (_hasEpoch) dSync(); // use 'from epoch sync' if possible (lite)
    else if (_hasData) eSync(); // otherwise use 'from data sync' (heavy)
}

void PDate::dSync  () { // this function is relatively lite
    _hasData = true;
    _hasFullData = true;
    localtime_r(&_epoch, &this->_data);
}

struct tm * PDate::data        ()            { dCheck(); return &_data; }
bool        PDate::hasEpoch    ()            { return _hasEpoch; }
bool        PDate::hasData     ()            { return _hasData; }
bool        PDate::hasFullData ()            { return _hasFullData; }
uint8_t     PDate::error       ()            { return _error; }
void        PDate::error       (uint8_t val) { _error = val; epoch(0); }

time_t PDate::epoch ()           { eCheck(); return _epoch; }
void   PDate::epoch (time_t val) { _epoch = val; _hasEpoch = true; _hasData = false; _hasFullData = false; }

int32_t PDate::year  ()            { dCheck(); return _data.tm_year + 1900; }
void    PDate::year  (int32_t val) { dCheck(); _data.tm_year = val - 1900; _hasEpoch = false; dNorm(monthBorderAdjust ? NORM_MBA : 0); }
int32_t PDate::_year ()            { return year()-1900; }
void    PDate::_year (int32_t val) { year(val+1900); }
int8_t  PDate::yr    ()            { dCheck(); return _data.tm_year % 100; }
void    PDate::yr    (int8_t val)  { year( year() - yr() + val ); }

uint8_t PDate::month ()             { dCheck(); return _data.tm_mon + 1; }
void    PDate::month (int32_t val)  { dCheck(); _data.tm_mon = val - 1; _hasEpoch = false; dNorm(monthBorderAdjust ? NORM_MBA : 0); }
uint8_t PDate::_month ()            { return month() - 1; }
void    PDate::_month (int32_t val) { month(val+1); }

uint8_t PDate::day ()            { dCheck(); return _data.tm_mday; }
void    PDate::day (int32_t val) { dCheck(); _data.tm_mday = val; _hasEpoch = false; dNorm(); }

uint8_t PDate::hour ()            { dCheck(); return _data.tm_hour; }
void    PDate::hour (int32_t val) { dCheck(); _data.tm_hour = val; _hasEpoch = false; dNorm(); }

uint8_t PDate::min ()            { dCheck(); return _data.tm_min; }
void    PDate::min (int32_t val) { dCheck(); _data.tm_min = val; _hasEpoch = false; dNorm(); }

uint8_t PDate::sec ()            { dCheck(); return _data.tm_sec; }
void    PDate::sec (int32_t val) { dCheck(); _data.tm_sec = val; _hasEpoch = false; dNorm(); }

uint8_t PDate::wday ()              { dCheck(); return _data.tm_wday + 1; }
void    PDate::wday (uint8_t val)   { dCheck(); _data.tm_mday += val - (_data.tm_wday + 1); _hasEpoch = false; dNorm(); }
uint8_t PDate::_wday ()             { return wday() - 1; }
void    PDate::_wday (uint8_t val)  { wday(val+1); }
uint8_t PDate::ewday ()             { dCheck(); return _data.tm_wday == 0 ? 7 : _data.tm_wday; }
void    PDate::ewday (uint8_t val)  { _data.tm_mday += val - ewday(); _hasEpoch = false; dNorm(); }

uint16_t PDate::yday ()              { dCheck(); return _data.tm_yday + 1; }
void     PDate::yday (uint32_t val)  { dCheck(); _data.tm_yday = val - 1; _hasEpoch = false; dNorm(NORM_YDAY); }
uint16_t PDate::_yday ()             { return yday() - 1; }
void     PDate::_yday (uint32_t val) { yday(val+1); }

bool        PDate::isdst    () { dCheckFull(); return _data.tm_isdst == 0 ? false : true; }
int32_t     PDate::tzoffset () { dCheckFull(); return _data.tm_gmtoff; }
const char* PDate::tz       () { return tzname[0]; }
const char* PDate::tzdst    () { dCheckFull(); return _data.tm_zone; }

uint8_t PDate::daysInMonth () {
    dCheck();
    return days_in_month(_data.tm_year+1900, _data.tm_mon+1);
}

void PDate::setFrom (SV* arg) {
    _error = E_OK; // reset possible error;
    if (SvOK(arg)) {
        if (SvROK(arg)) {
            if (sv_isobject(arg) && sv_isa(arg, PDATE_CLASS)) setFrom((PDate *) SvIV(SvRV(arg)));
            else {
                SV* rarg = SvRV(arg);
                if (SvTYPE(rarg) == SVt_PVHV) setFrom((HV*) rarg, false);
                else if (SvTYPE(rarg) == SVt_PVAV) setFrom((AV*) rarg);
                else croak("Panda::Date - cannot create object - unknown argument passed");
            }
        }
        else if (looks_like_number(arg)) {
            epoch(SvIV(arg));
        }
        else {
            STRLEN len;
            const char* str = SvPV(arg, len);
            setFrom(str, len);
        }
    }
    else epoch(0);
}

void PDate::setFrom (AV* from) {
    int32_t year = 2000;
    int32_t month = 1;
    int32_t day = 1;
    int32_t hour = 0;
    int32_t min = 0;
    int32_t sec = 0;
    I32 len = av_len(from)+1;
    SV** ref;
    if (len > 0) { ref = av_fetch(from, 0, 0); if (ref != NULL) year  = SvIV(*ref); }
    if (len > 1) { ref = av_fetch(from, 1, 0); if (ref != NULL) month = SvIV(*ref); }
    if (len > 2) { ref = av_fetch(from, 2, 0); if (ref != NULL) day   = SvIV(*ref); }
    if (len > 3) { ref = av_fetch(from, 3, 0); if (ref != NULL) hour  = SvIV(*ref); }
    if (len > 4) { ref = av_fetch(from, 4, 0); if (ref != NULL) min   = SvIV(*ref); }
    if (len > 5) { ref = av_fetch(from, 5, 0); if (ref != NULL) sec   = SvIV(*ref); }
    setFrom(year, month, day, hour, min, sec);
}

void PDate::setFrom (HV* from, bool cloning) {
    if (cloning) dCheck();
    SV** ref;
    
    ref = hv_fetch(from, "year", 4, 0);
    if (ref != NULL) _data.tm_year = SvIV(*ref) - 1900;
    else if (!cloning) _data.tm_year = 100;
    
    ref = hv_fetch(from, "month", 5, 0);
    if (ref != NULL) _data.tm_mon = SvIV(*ref) - 1;
    else if (!cloning) _data.tm_mon = 0;

    ref = hv_fetch(from, "day", 3, 0);
    if (ref != NULL) _data.tm_mday = SvIV(*ref);
    else if (!cloning) _data.tm_mday = 1;

    ref = hv_fetch(from, "hour", 4, 0);
    if (ref != NULL) _data.tm_hour = SvIV(*ref);
    else if (!cloning) _data.tm_hour = 0;

    ref = hv_fetch(from, "min", 3, 0);
    if (ref != NULL) _data.tm_min = SvIV(*ref);
    else if (!cloning) _data.tm_min = 0;

    ref = hv_fetch(from, "sec", 3, 0);
    if (ref != NULL) _data.tm_sec = SvIV(*ref);
    else if (!cloning) _data.tm_sec = 0;
    
    if (cloning) {
        ref = hv_fetch(from, "_year", 5, 0);
        if (ref != NULL) _data.tm_year = SvIV(*ref);
        
        ref = hv_fetch(from, "_month", 6, 0);
        if (ref != NULL) _data.tm_mon = SvIV(*ref);
    }
    
    dNorm(rangeCheck ? NORM_RCHECK : 0);
    _hasData = true;
    _hasFullData = false;
    _hasEpoch = false;
}

void PDate::setFrom (int32_t year, int32_t month, int32_t day, int32_t hour, int32_t min, int32_t sec) {
    _data.tm_year = year - 1900;
    _data.tm_mon  = month - 1;
    _data.tm_mday = day;
    _data.tm_hour = hour;
    _data.tm_min  = min;
    _data.tm_sec  = sec;
    dNorm(rangeCheck ? NORM_RCHECK : 0);
    _hasData = true;
    _hasFullData = false;
    _hasEpoch = false;
}

void PDate::setFrom (const char* str, size_t len) {
    _error = parse_sql(str, len, _data);
    
    if (_error != E_OK) {
        error(_error);
        return;
    }
    
    dNorm(rangeCheck ? NORM_RCHECK : 0);
    _hasData = true;
    _hasFullData = false;
    _hasEpoch = false;
}

void PDate::setFrom (PDate* from) {
    _hasEpoch    = from->hasEpoch();
    _hasData     = from->hasData();
    _hasFullData = from->hasFullData();
    if (_hasEpoch) _epoch = from->epoch();
    if (_hasData) _data = *(from->data());
    _error = from->error();
}

const char* PDate::toString () {
    if (_error > E_OK) return NULL;
    return strfmt == NULL ? sql() : strFtime(strfmt, NULL, 0);
}

const char* PDate::sql () {
    TOSTR_START(50);
    TOSTR_YEAR; TOSTR_DEL('-'); TOSTR_MONTH; TOSTR_DEL('-'); TOSTR_DAY; TOSTR_DEL(' ');
    TOSTR_HOUR; TOSTR_DEL(':'); TOSTR_MIN; TOSTR_DEL(':'); TOSTR_SEC;
    TOSTR_END;
}

const char* PDate::hms () {
    TOSTR_START(8); TOSTR_HOUR; TOSTR_DEL(':'); TOSTR_MIN; TOSTR_DEL(':'); TOSTR_SEC; TOSTR_END;
}

const char* PDate::ymd () {
    TOSTR_START(41); TOSTR_YEAR; TOSTR_DEL('/'); TOSTR_MONTH; TOSTR_DEL('/'); TOSTR_DAY; TOSTR_END;
}

const char* PDate::mdy () {
    TOSTR_START(41); TOSTR_MONTH; TOSTR_DEL('/'); TOSTR_DAY; TOSTR_DEL('/'); TOSTR_YEAR; TOSTR_END;
}

const char* PDate::dmy () {
    TOSTR_START(41); TOSTR_DAY; TOSTR_DEL('/'); TOSTR_MONTH; TOSTR_DEL('/'); TOSTR_YEAR; TOSTR_END;
}

const char* PDate::meridiam () {
    TOSTR_START(8);
    uint8_t hour = _data.tm_hour % 12;
    if (hour == 0) hour = 12;
    TOSTR_VAL2(hour); TOSTR_DEL(':'); TOSTR_MIN; TOSTR_DEL(' '); TOSTR_AMPM;
    TOSTR_END;
}

char* PDate::strFtime (const char* format, char* buf, size_t maxsize) {
    dCheckFull();
    static char defbuf[1000];
    if (buf == NULL) {
        buf = defbuf;
        maxsize = 1000;
    }
    size_t reslen = strftime(buf, maxsize, format, &_data);
    return reslen > 0 ? buf : NULL;
}

const char* PDate::ampm () {
    dCheck();
    return _data.tm_hour < 12 ? "AM" : "PM";
}

PDate* PDate::clone      () { return new PDate(this); }
PDate* PDate::truncate   () { return clone()->truncateME(); }
PDate* PDate::monthBegin () { return clone()->monthBeginME(); }
PDate* PDate::monthEnd   () { return clone()->monthEndME(); }

PDate* PDate::monthBeginME () {
    dCheck();
    uint8_t delta = _data.tm_mday - 1;
    WDAY_CHANGE(_data.tm_wday, -delta);
    _data.tm_yday -= delta;
    _data.tm_mday = 1;
    _hasEpoch     = false;
    _hasFullData  = false;
    return this;
}

PDate* PDate::monthEndME () {
    dCheck();
    uint8_t newval = daysInMonth();
    uint8_t delta = newval - _data.tm_mday;
    WDAY_CHANGE(_data.tm_wday, delta);
    _data.tm_yday += delta;
    _data.tm_mday = newval;
    _hasEpoch     = false;
    _hasFullData  = false;
    return this;
}

PDate* PDate::truncateME () { // low-level sec-min-hour set -> great perfomance
    dCheck();
    _data.tm_sec  = 0;
    _data.tm_min  = 0;
    _data.tm_hour = 0;
    _hasEpoch     = false;
    _hasFullData  = false;
    return this;
}

void PDate::_dbg () {
    warn("hasE=%d e=%lli hasD=%d hasFD=%d y=%d m=%d d=%d, wd=%d yd=%d dst=%d",
         _hasEpoch ? 1 : 0, _epoch, _hasData ? 1 : 0, _hasFullData ? 1 : 0, _data.tm_year, _data.tm_mon,
         _data.tm_mday, _data.tm_wday, _data.tm_yday, _data.tm_isdst);
}

const char* PDate::errstr () {
    switch (_error) {
        case E_OK:
            return NULL;
        case E_UNPARSABLE:
            return "can't parse date string";
        case E_RANGE:
            return "input date is out of range";
        default:
            return "unknown error";
    }
}

int PDate::compare (PDate* operand) {
    if (_hasEpoch && operand->hasEpoch()) return num_compare(_epoch, operand->epoch());
    else return tm_compare(*(data()), *(operand->data()));
}

PDate* PDate::add   (PDateRel* operand) { return clone()->addME(operand); }
PDate* PDate::addME (PDateRel* operand) {
    dCheck();
    // process year/month separately from DHMS to allow monthBorderAdjust to work correctly
    if (operand->hasMPart()) {
        _data.tm_mon  += operand->month();
        _data.tm_year += operand->year();
        dNorm(monthBorderAdjust ? NORM_MBA : 0);
    }
    
    if (operand->hasSPart()) {
        _data.tm_sec  += operand->sec();
        _data.tm_min  += operand->min();
        _data.tm_hour += operand->hour();
        _data.tm_mday += operand->day();
        dNorm();
    }
    
    _hasEpoch = false;
    return this;
}

PDate* PDate::subtract   (PDateRel* operand) { return clone()->subtractME(operand); }
PDate* PDate::subtractME (PDateRel* operand) {
    operand->negativeME();
    addME(operand);
    operand->negativeME();
    return this;
}

PDate::~PDate () {}

/////////   STATIC   ///////////////////////////////
bool        PDate::monthBorderAdjust = false;
bool        PDate::rangeCheck        = false;
SV*         PDate::strfmtSV          = NULL;
const char* PDate::strfmt            = NULL;

SV* PDate::stringFormatSV () { return strfmtSV; }

void PDate::stringFormatSV (SV* format) {
    if (strfmtSV != NULL) {
        SvREFCNT_dec(strfmtSV);
        strfmtSV = NULL;
        strfmt = NULL;
    }
    
    if (SvOK(format) && SvTRUE(format)) {
        SvREFCNT_inc(format);
        strfmtSV = format;
        strfmt = SvPV_nolen(format);
    }
}

PDate* PDate::now   () { return new PDate(time(NULL)); }
PDate* PDate::today () { return now()->truncateME(); }
