#include "Date.h" 
#include <string.h>
#include <stdlib.h>
#include <algorithm>

%%{
    machine date_parser;
    
    action digit {
        acc *= 10;
        acc += fc - '0';
    }

    action sec   { NSAVE(_date.sec); }
    action min   { NSAVE(_date.min); }
    action hour  { NSAVE(_date.hour); }
    action day   { NSAVE(_date.mday); }
    action month { _date.mon = acc - 1; acc = 0; }
    action year  { NSAVE(_date.year); }
    
    action yr {
        if (acc <= 50) _date.year = 2000 + acc;
        else           _date.year = 1900 + acc;
        acc = 0;
    }

    action mks_start {
        mksec_ptr = p;
    }
    
    action mks {
        switch (p - mksec_ptr) {
            case 1:  _mksec = acc * 100000; break;
            case 2:  _mksec = acc *  10000; break;
            case 3:  _mksec = acc *   1000; break;
            case 4:  _mksec = acc *    100; break;
            case 5:  _mksec = acc *     10; break;
            case 6:  _mksec = acc;          break;
            default: abort();
        }
        acc = 0;
    }
    
    ### TZ rule syntax for arbitrary offset
    ###         < + 0 1 : 3 0 > - 0 1  :  3  0
    ### indexes 0 1 2 3 4 5 6 7 8 9 10 11 12 13
    
    action tzsign {
        tzi.rule[0] = '<';
        tzi.rule[1] = *p;
        tzi.rule[4] = ':';
        tzi.rule[7] = '>';
        tzi.rule[8] = *p ^ 6; // swap '+'<->'-': yes, it is reversed
        tzi.rule[11] = ':';
        tzi.rule[5] = tzi.rule[6] = tzi.rule[12] = tzi.rule[13] = '0'; // in case there will be no minutes
        tzi.len = 14;
    }
    
    action tzhour {
        tzi.rule[2] = tzi.rule[9]  = *(p-2);
        tzi.rule[3] = tzi.rule[10] = *(p-1);
    }
    
    action tzmin {
        tzi.rule[5] = tzi.rule[12] = *(p-2);
        tzi.rule[6] = tzi.rule[13] = *(p-1);
    }
    
    action tzgmt {
        if (!gmt_zone) gmt_zone = panda::time::tzget("GMT");
        _zone = gmt_zone;
    }
    
    action week { NSAVE(week); }
    action wday { _date.wday = *p - '0'; }
    
    nn = digit{2} $digit;

    sec      = nn %sec;
    min      = nn %min;
    hour     = nn %hour;
    day      = nn %day;
    day_void = day | (" " digit $digit) %day;
    month    = nn %month;
    year     = digit{4} $digit %year;
    yr       = nn %yr;
    mks      = digit{1,6} >mks_start $digit %mks;
    smks     = sec ([.,] mks)?;

    tzoff_sign = [+\-] $tzsign;
    tzoff_hour = nn %tzhour;
    tzoff_min  = nn %tzmin;
    tzoff      = tzoff_sign tzoff_hour (":" tzoff_min)?;
    tzgmt      = "Z" %tzgmt;
    tzd        = tzoff | tzgmt;
    tzclf      = tzoff_sign tzoff_hour tzoff_min;
    
    mon_name = "Jan" %{ _date.mon = 0; } |
               "Feb" %{ _date.mon = 1; } |
               "Mar" %{ _date.mon = 2; } |
               "Apr" %{ _date.mon = 3; } |
               "May" %{ _date.mon = 4; } |
               "Jun" %{ _date.mon = 5; } |
               "Jul" %{ _date.mon = 6; } |
               "Aug" %{ _date.mon = 7; } |
               "Sep" %{ _date.mon = 8; } |
               "Oct" %{ _date.mon = 9; } |
               "Nov" %{ _date.mon = 10;} |
               "Dec" %{ _date.mon = 11;};
    
    wday_name = "Mon" %{ _date.wday = 1; } |
                "Tue" %{ _date.wday = 2; } |
                "Wed" %{ _date.wday = 3; } |
                "Thu" %{ _date.wday = 4; } |
                "Fri" %{ _date.wday = 5; } |
                "Sat" %{ _date.wday = 6; } |
                "Sun" %{ _date.wday = 0; };
                
    weekday_name = "Monday"    %{ _date.wday = 1; } |
                   "Tuesday"   %{ _date.wday = 2; } |
                   "Wednesday" %{ _date.wday = 3; } |
                   "Thursday"  %{ _date.wday = 4; } |
                   "Friday"    %{ _date.wday = 5; } |
                   "Saturday"  %{ _date.wday = 6; } |
                   "Sunday"    %{ _date.wday = 0; };

    iso = (
        ((year "/" month "/" day) | (year "-" month "-" day)) (" " hour ":" min (":" smks)? tzd?)?
    ) %{ format |= InputFormat::iso; };

    iso8601_tzd  = tzd | ((tzoff_sign tzoff_hour tzoff_min?) | tzgmt);
    iso8601_void = year     month      day ( "T" hour     (min      smks?)?  iso8601_tzd? )?;
    iso8601_std  = year "-" month ("-" day ( "T" hour (":" min (":" smks)?)? iso8601_tzd? )?)?;
    iso8601_week = year "-W" nn %week ("-" digit $wday)?;
    iso8601      = (iso8601_std | iso8601_void | iso8601_week) %{ format |= InputFormat::iso8601; };
    
    rfc1123_zone = ("Z" | "UT" | "GMT") %tzgmt |
                   ("EST" | "EDT")      %{ TZRULE("EST5EDT"); } |
                   ("CST" | "CDT")      %{ TZRULE("CST6CDT"); } |
                   ("MST" | "MDT")      %{ TZRULE("MST7MDT"); } |
                   ("PST" | "PDT")      %{ TZRULE("PST8PDT"); } |
                   "A"                  %{ TZRULE("<-01:00>+01:00"); } |
                   "M"                  %{ TZRULE("<-12:00>+12:00"); } |
                   "N"                  %{ TZRULE("<+01:00>-01:00"); } |
                   "Y"                  %{ TZRULE("<+12:00>-12:00"); } |
                   (tzoff_sign tzoff_hour tzoff_min);
    rfc1123 = (
        (wday_name ", ")? day " " mon_name " " (year | yr) " " hour ":" min (":" sec)? " " rfc1123_zone
    ) %{ format |= InputFormat::rfc1123; };
    
    rfc850 = (
        weekday_name ", " day "-" mon_name "-" yr " " hour ":" min ":" sec " " rfc1123_zone
    ) %{ format |= InputFormat::rfc850; };
    
    ansi_c = (
        wday_name " " mon_name " " day_void " " hour ":" min ":" sec " " year
    ) %{ format |= InputFormat::ansi_c; };

    dot = (day "." month "." year) %{ format |= InputFormat::dot; };
    
    clf_raw = day "/" mon_name "/" year ":" hour ":" min ":" sec " " tzclf;
    clfb = ( "[" clf_raw "]" );
    clf = (clf_raw | clfb) %{ format |= InputFormat::clf; };


    all := iso | iso8601 | dot | rfc1123 | rfc850 | ansi_c | clf;
}%%

namespace panda { namespace date {

%% write data;

static TimezoneSP gmt_zone;

#define NSAVE(dest) { dest = acc; acc = 0; }
        
#define TZRULE(str) do {                    \
    memcpy(tzi.rule, str, sizeof(str) - 1); \
    tzi.len = sizeof(str) - 1;              \
} while(0)

void Date::parse (string_view str, int allowed_formats) {
    memset(&_date, 0, sizeof(_date)); // reset all values
    _date.mday = 1;
    _error = errc::ok;
    _mksec = 0;

    enum class TZType { LOCAL, OFFSET };
    
    const char* p      = str.data();
    const char* pe     = p + str.length();
    const char* eof    = pe;
    int         cs     = date_parser_en_all;
    uint64_t    acc    = 0;
    const char* mksec_ptr;
    int         format = 0;
    
    struct {
        char rule[14];
        int  len = 0;
    } tzi;
    
    unsigned week = 0;

    %% write exec;

    if (cs < date_parser_first_final || !(allowed_formats & format)) {
        _error = errc::parser_error;
        return;
    }
    
    if (tzi.len) _zone = panda::time::tzget(string_view(tzi.rule, tzi.len));
    _post_parse_week(week);
}

}}