#include <xs/date.h>
#include <xs/export.h>
#include "private.h"

using namespace xs;
using namespace xs::date;
using panda::string;
using panda::string_view;

#ifdef _WIN32
    const auto LT_FORMAT = string_view("%a %b %d %H:%M:%S %Y");
#else
    const auto LT_FORMAT = string_view("%a %b %e %H:%M:%S %Y");
#endif
    
// arguments overloading for new_ymd(), date_ymd(), ->set_ymd()
static inline Date xs_date_ymd (SV** args, I32 items) {
    ptime_t vals[8] = {1970, 1, 1, 0, 0, 0, 0, -1};
    auto tz = list2vals(args, items, vals);
    auto ret = Date(vals[0], vals[1], vals[2], vals[3], vals[4], vals[5], vals[6], vals[7], tz);
    if (ret.error() && is_strict_mode()) throw xs::out(ret.error());
    return ret;
}

MODULE = Date::Date                PACKAGE = Date
PROTOTYPES: DISABLE

BOOT {
    Stash stash(__PACKAGE__);
    
    xs::exp::create_constants(stash, {
        {"FORMAT_ISO",          (int)Date::Format::iso},
        {"FORMAT_ISO_TZ",       (int)Date::Format::iso_tz},
        {"FORMAT_ISO_DATE",     (int)Date::Format::iso_date},
        {"FORMAT_ISO8601",      (int)Date::Format::iso8601},
        {"FORMAT_ISO8601_NOTZ", (int)Date::Format::iso8601_notz},
        {"FORMAT_RFC1123",      (int)Date::Format::rfc1123},
        {"FORMAT_COOKIE",       (int)Date::Format::cookie},
        {"FORMAT_RFC850",       (int)Date::Format::rfc850},
        {"FORMAT_ANSI_C",       (int)Date::Format::ansi_c},
        {"FORMAT_YMD",          (int)Date::Format::ymd},
        {"FORMAT_DOT",          (int)Date::Format::dot},
        {"FORMAT_HMS",          (int)Date::Format::hms},
        {"FORMAT_CLF",          (int)Date::Format::clf},
        {"FORMAT_CLF_BRACKETS", (int)Date::Format::clfb},
        
        {"INPUT_FORMAT_ALL",     Date::InputFormat::all},
        {"INPUT_FORMAT_ISO",     Date::InputFormat::iso},
        {"INPUT_FORMAT_ISO8601", Date::InputFormat::iso8601},
        {"INPUT_FORMAT_RFC1123", Date::InputFormat::rfc1123},
        {"INPUT_FORMAT_RFC850",  Date::InputFormat::rfc850},
        {"INPUT_FORMAT_ANSI_C",  Date::InputFormat::ansi_c},
        {"INPUT_FORMAT_DOT",     Date::InputFormat::dot},
        {"INPUT_FORMAT_CLF",     Date::InputFormat::clf},
    });
    
    Stash ecstash("Date::Error", GV_ADD);
    xs::exp::create_constants(ecstash, {
        {"parser_error", xs::out(make_error_code(errc::parser_error))},
        {"out_of_range", xs::out(make_error_code(errc::out_of_range))},
    });
    
    stash.add_const_sub("error_category", xs::out<const std::error_category*>(&error_category));
}

#///////////////////////////// STATIC FUNCTIONS ///////////////////////////////////

const Timezone* tzget (string_view zonename = {})

void tzset (TimezoneSP newzone = {})

string tzdir (SV* newdir = NULL) {
    if (newdir) {
        tzdir(xs::in<string>(newdir));
        XSRETURN_UNDEF;
    }
    RETVAL = tzdir();
}

string tzsysdir ()

string tzembededdir(SV* newdir = NULL) {
    if (newdir) {
        tzembededdir(xs::in<string>(newdir));
        XSRETURN_UNDEF;
    }
    RETVAL = tzembededdir();
}

void available_timezones () {
    auto list = available_timezones();
    if (list.size()) EXTEND(SP, (int)list.size());
    for (auto& name : list) {
        mPUSHs(xs::out(name).detach());
    }
    XSRETURN(list.size());
}

void use_system_timezones ()

void use_embed_timezones ()

void gmtime (SV* epochSV = {}, TimezoneSP tz = {}) : ALIAS(localtime=1, anytime=2) {
    ptime_t epoch;
    if (epochSV) epoch = xs::in<ptime_t>(epochSV);
    else epoch = (ptime_t) ::time(NULL);

    datetime date;
    bool success = false;
    switch (ix) {
        case 0: success = gmtime(epoch, &date);                       break;
        case 1: success = localtime(epoch, &date);                    break;
        case 2: success = anytime(epoch, &date, tz ? tz : tzlocal()); break;
    }

    if (GIMME_V == G_ARRAY) {
        if (!success) XSRETURN_EMPTY;
        EXTEND(SP, 9);
        EXTEND_MORTAL(9);
        mPUSHu(date.sec);
        mPUSHu(date.min);
        mPUSHu(date.hour);
        mPUSHu(date.mday);
        mPUSHu(date.mon);
        mPUSHi(date.year);
        mPUSHu(date.wday);
        mPUSHu(date.yday);
        mPUSHu(date.isdst);
        XSRETURN(9);
    } else {
        EXTEND(SP, 1);
        if (!success) XSRETURN_UNDEF;
        mPUSHs(xs::out(strftime(LT_FORMAT, date)).detach());
        XSRETURN(1);
    }
}

ptime_t timegm (SV* sec, SV* min, SV* hour, SV* mday, SV* mon, SV* year, SV* isdst = {}, TimezoneSP tz = {}) : ALIAS(timelocal=1, timeany=2, timegmn=3, timelocaln=4, timeanyn=5) {
    datetime date;
    date.sec  = xs::in<ptime_t>(sec);
    date.min  = xs::in<ptime_t>(min);
    date.hour = xs::in<ptime_t>(hour);
    date.mday = xs::in<ptime_t>(mday);
    date.mon  = xs::in<ptime_t>(mon);
    date.year = xs::in<ptime_t>(year);

    if (isdst) date.isdst = SvIV(isdst);
    else date.isdst = -1;

    switch (ix) {
        case 0: RETVAL = timegml(&date);                       break;
        case 1: RETVAL = timelocall(&date);                    break;
        case 2: RETVAL = timeanyl(&date, tz ? tz : tzlocal()); break;
        case 3: RETVAL = timegm(&date);                        break;
        case 4: RETVAL = timelocal(&date);                     break;
        case 5: RETVAL = timeany(&date, tz ? tz : tzlocal());  break;
        default: croak("not reached");
    }

    if (ix >= 3) {
        sv_setiv(sec, date.sec);
        sv_setiv(min, date.min);
        sv_setiv(hour, date.hour);
        sv_setiv(mday, date.mday);
        sv_setiv(mon, date.mon);
        sv_setiv(year, date.year);
        if (isdst) sv_setiv(isdst, date.isdst);
    }
}

Date* now () {
    RETVAL = new Date(Date::now());
}

Date* now_hires () {
    RETVAL = new Date(Date::now_hires());
}

Date* today () {
    RETVAL = new Date(Date::today());
}

ptime_t today_epoch () {
    RETVAL = Date::today_epoch();
}

Date* date (SV* val = {}, TimezoneSP tz = {}, int fmt = Date::InputFormat::all) {
    RETVAL = new Date(sv2date(val, tz, fmt));
}

Date* date_ymd (...) {
    RETVAL = new Date(xs_date_ymd(&ST(0), items));
}

bool range_check (Sv newval = {}) {
    if (newval) Date::range_check(newval.is_true());
    RETVAL = Date::range_check();
}

#///////////////////////////// OBJECT METHODS ///////////////////////////////////

Date* new (SV*, SV* val = {}, TimezoneSP tz = {}, int fmt = Date::InputFormat::all) {
    RETVAL = new Date(sv2date(val, tz, fmt));
}

Date* new_ymd (...) {
    RETVAL = new Date(xs_date_ymd(&ST(1), items - 1));
}

Date* strptime (string date, string format) {
    RETVAL = new Date(Date::strptime(date, format));
}

void Date::set (SV* val = {}, TimezoneSP tz = {}, int fmt = Date::InputFormat::all) {
    THIS->set(sv2date(val, tz, fmt));
}

void Date::set_ymd (...) {
    THIS->set(xs_date_ymd(&ST(1), items - 1));
}

void Date::epoch (SV* newval = NULL) {
    if (newval) {
        if (SvNOK(newval)) THIS->epoch((double)SvNV(newval));
        else THIS->epoch(xs::in<ptime_t>(newval));
        XSRETURN(1);
    }
    dXSTARG; XSprePUSH;
    if (THIS->mksec()) PUSHn(THIS->epoch_mks());
    else               PUSHi(THIS->epoch());
}

ptime_t Date::epoch_sec () {
    RETVAL = THIS->epoch();
}

int32_t Date::year (SV* newval = NULL) {
    if (newval) THIS->year(xs::in<ptime_t>(newval));
    RETVAL = THIS->year();
}

int32_t Date::c_year (SV* newval = NULL) : ALIAS(_year=1) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->c_year(xs::in<ptime_t>(newval));
    RETVAL = THIS->c_year();
}

int8_t Date::yr (SV* newval = NULL) {
    if (newval) THIS->yr(xs::in<ptime_t>(newval));
    RETVAL = THIS->yr();
}

uint8_t Date::month (SV* newval = NULL) : ALIAS(mon=1) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->month(xs::in<ptime_t>(newval));
    RETVAL = THIS->month();
}

uint8_t Date::c_month (SV* newval = NULL) : ALIAS(c_mon=1, _mon=2, _month=3) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->c_month(xs::in<ptime_t>(newval));
    RETVAL = THIS->c_month();
}

uint8_t Date::day (SV* newval = NULL) : ALIAS(mday=1, day_of_month=2) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->day(xs::in<ptime_t>(newval));
    RETVAL = THIS->day();
}

uint8_t Date::hour (SV* newval = NULL) {
    if (newval) THIS->hour(xs::in<ptime_t>(newval));
    RETVAL = THIS->hour();
}

uint8_t Date::min (SV* newval = NULL) : ALIAS(minute=1) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->min(xs::in<ptime_t>(newval));
    RETVAL = THIS->min();
}

uint8_t Date::sec (SV* newval = NULL) : ALIAS(second=1) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->sec(xs::in<ptime_t>(newval));
    RETVAL = THIS->sec();
}

uint32_t Date::mksec (SV* newval = NULL) {
    if (newval) THIS->mksec(xs::in<ptime_t>(newval));
    RETVAL = THIS->mksec();
}

uint8_t Date::wday (SV* newval = NULL) : ALIAS(day_of_week=1) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->wday(xs::in<ptime_t>(newval));
    RETVAL = THIS->wday();
}

uint8_t Date::c_wday (SV* newval = NULL) : ALIAS(_wday=1) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->c_wday(xs::in<ptime_t>(newval));
    RETVAL = THIS->c_wday();
}

uint8_t Date::ewday (SV* newval = NULL) {
    if (newval) THIS->ewday(xs::in<ptime_t>(newval));
    RETVAL = THIS->ewday();
}

uint16_t Date::yday (SV* newval = NULL) : ALIAS(day_of_year=1) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->yday(xs::in<ptime_t>(newval));
    RETVAL = THIS->yday();
}

uint16_t Date::c_yday (SV* newval = NULL) : ALIAS(_yday=1) {
    PERL_UNUSED_VAR(ix);
    if (newval) THIS->c_yday(xs::in<ptime_t>(newval));
    RETVAL = THIS->c_yday();
}

bool Date::isdst () : ALIAS(daylight_savings=1) {
    PERL_UNUSED_VAR(ix);
    RETVAL = THIS->isdst();
}

string Date::to_string (int format = (int)Date::Format::iso) {
    if (THIS->error()) XSRETURN_UNDEF;
    RETVAL = THIS->to_string((Date::Format)format);
}

string Date::_op_str (...) {
    if (THIS->error()) XSRETURN_UNDEF;
    RETVAL = THIS->to_string();
}

#// $date->strftime($format)
#// Date::strftime($format, $epoch, [$timezone])
#// Date::strftime($format, $sec, $min, $hour, $mday, $mon, $year, [$isdst], [$timezone])
string strftime (Sv arg0, SV* arg1, ...) {
    if (items == 2 && arg0.is_object_ref()) {
        auto THIS = xs::in<Date*>(arg0);
        RETVAL = THIS->strftime(xs::in<string_view>(arg1));
    }
    else {
        string_view format = xs::in<string_view>(arg0);
        TimezoneSP tz;
        datetime date;
        date.isdst = -1;
        
        switch (items) {
            case 9: tz = xs::in<TimezoneSP>(ST(8));     // fall through
            case 8: date.isdst = SvIV(ST(7));           // fall through
            case 7:
                date.sec   = xs::in<ptime_t>(ST(1));
                date.min   = xs::in<ptime_t>(ST(2));
                date.hour  = xs::in<ptime_t>(ST(3));
                date.mday  = xs::in<ptime_t>(ST(4));
                date.mon   = xs::in<ptime_t>(ST(5));
                date.year  = xs::in<ptime_t>(ST(6));
                timeany(&date, tz ? tz : tzlocal());
                break;
            case 3: tz = xs::in<TimezoneSP>(ST(2));     // fall through
            case 2: {
                auto epoch  = xs::in<ptime_t>(arg1);
                if (!anytime(epoch, &date, tz ? tz : tzlocal())) XSRETURN_UNDEF;
                break;
            }
            default:
                throw "wrong number of arguments";
        }
        RETVAL = strftime(format, date);
    }    
}

bool Date::to_bool (...) {
    RETVAL = THIS->error() ? false : true;
}

ptime_t Date::to_number (...) {
    RETVAL = THIS->error() ? 0 : THIS->epoch();
}

string_view Date::month_name () : ALIAS(monname=1, monthname=2) {
    PERL_UNUSED_VAR(ix);
    RETVAL = THIS->month_name();
}

string_view Date::month_sname ()

string_view Date::wday_name () : ALIAS(day_of_weekname=1, wdayname=2) {
    PERL_UNUSED_VAR(ix);
    RETVAL = THIS->wday_name();
}

string_view Date::wday_sname ()

int Date::gmtoff ()

string_view Date::tzabbr ()

#// Date::tzname()
#// $date->tzname()
string tzname (Date* date = nullptr) {
    auto& zone = date ? date->timezone() : tzlocal();
    RETVAL = zone->name;
}

bool Date::tzlocal () {
    RETVAL = THIS->timezone()->is_local;
}

TimezoneSP Date::timezone (TimezoneSP newzone = {}) : ALIAS(tz=1, zone=2) {
    if (newzone) {
        THIS->timezone(newzone);
        XSRETURN_UNDEF;
    }
    RETVAL = THIS->timezone();
    PERL_UNUSED_VAR(ix);
}

void Date::to_timezone (TimezoneSP newzone) : ALIAS(to_tz=1, to_zone=2) {
    THIS->to_timezone(newzone);
    PERL_UNUSED_VAR(ix);
}

void Date::array () {
    auto cnt = THIS->mksec() ? 7 : 6;
    EXTEND(SP, cnt);
    mPUSHi(THIS->year());
    mPUSHu(THIS->month());
    mPUSHu(THIS->day());
    mPUSHu(THIS->hour());
    mPUSHu(THIS->min());
    mPUSHu(THIS->sec());
    if (THIS->mksec()) mPUSHu(THIS->mksec());
    XSRETURN(cnt);
}

void Date::struct () {
    EXTEND(SP, 9);
    mPUSHu(THIS->sec());
    mPUSHu(THIS->min());
    mPUSHu(THIS->hour());
    mPUSHu(THIS->day());
    mPUSHu(THIS->c_month());
    mPUSHi(THIS->c_year());
    mPUSHu(THIS->c_wday());
    mPUSHu(THIS->c_yday());
    mPUSHu(THIS->isdst() ? 1 : 0);
    XSRETURN(9);
}

Date* Date::clone (...) {
    if (items > 1) {
        ptime_t vals[] = {-1, -1, -1, -1, -1, -1, -1, -1};
        auto tz = list2vals(&ST(1), items - 1, vals);
        RETVAL = new Date(THIS->clone(vals[0], vals[1], vals[2], vals[3], vals[4], vals[5], vals[6], vals[7], tz));
    }
    else RETVAL = new Date(*THIS);
    
    if (RETVAL->error() && is_strict_mode()) {
        auto err = RETVAL->error();
        delete RETVAL;
        throw xs::out(err);
    }
    
    PROTO = Object(ST(0)).stash();
}

SV* Date::month_begin () {
    THIS->month_begin();
    XSRETURN(1);
}

Date* Date::month_begin_new () {
    RETVAL = new Date(THIS->month_begin_new());
    PROTO = Object(ST(0)).stash();
}

SV* Date::month_end () {
    THIS->month_end();
    XSRETURN(1);
}

Date* Date::month_end_new () {
    RETVAL = new Date(THIS->month_end_new());
    PROTO = Object(ST(0)).stash();
}

int Date::days_in_month () {
    RETVAL = THIS->days_in_month();
}

uint8_t Date::week_of_month ()

uint8_t Date::weeks_in_year ()

void Date::week_of_year () {
    auto info = THIS->week_of_year();
    int rcnt = 1;
    if (GIMME_V == G_ARRAY) {
        mXPUSHi(info.year);
        rcnt = 2;
    }
    mXPUSHu(info.week);
    XSRETURN(rcnt);
}

std::error_code Date::error ()

SV* Date::truncate () {
    THIS->truncate();
    XSRETURN(1);
}

Date* Date::truncated () {
    RETVAL = new Date(THIS->truncated());
    PROTO = Object(ST(0)).stash();
}

int Date::compare (Sv arg, bool reverse = false) {
    if (arg.is_ref() && !arg.is_object_ref()) XSRETURN_IV(-1); // avoid exception in typemap for wrong types
    RETVAL = THIS->compare(sv2date(arg, THIS->timezone()));
    if (reverse) RETVAL = -RETVAL;
    if      (RETVAL < 0) RETVAL = -1;
    else if (RETVAL > 0) RETVAL = 1;
}

Date* Date::sum (Sv arg, ...) {
    RETVAL = new Date(*THIS + sv2daterel(arg));
    PROTO = Object(ST(0)).stash();
}

SV* Date::add (Sv arg, ...) {
    *THIS += sv2daterel(arg);
    XSRETURN(1);
}

Sv Date::difference (Sv arg, bool reverse = false) {
    bool is_date = arg.is_object_ref() && Object(arg).stash().name() == "Date";
    if (is_date)      RETVAL = xs::out(new DateRel(*xs::in<Date*>(arg), *THIS));
    else if (reverse) throw "wrong date operation";
    else              RETVAL = xs::out(new Date(*THIS - sv2daterel(arg)), Object(ST(0)).stash());
}

SV* Date::subtract (Sv arg, ...) {
    *THIS -= sv2daterel(arg);
    XSRETURN(1);
}

void __assign_stub (...) {
    if (!items) throw "should not happen";
    XSRETURN(1);
}