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

#define NSAVE(dest) { dest = acc; acc = 0; }

enum class WeekInterpretation { none = 2, iso = 1, monday = 0, sunday = -7 };

namespace panda { namespace date {

struct MetaConsume {
    int cs;
    int consumed;
};

struct TZInfo {
    char rule[14];
    int  len = 0;
};


%%{
    machine parser;

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

    action cent    { _date.year += acc * 100; acc = 0; }
    action year    { NSAVE(_date.year);        }
    action sec     { NSAVE(_date.sec);         }
    action min     { NSAVE(_date.min);         }
    action hour    { NSAVE(_date.hour);        }
    action hour_pm { _date.hour += 12;         }
    action day     { NSAVE(_date.mday);        }
    action wday    { NSAVE(_date.wday);        }
    action wday_s  { --acc; NSAVE(_date.wday); }
    action yday    { NSAVE(_date.mday);        }
    action week    { NSAVE(week);              }
    action epoch   { NSAVE(epoch_);            }
    action month   { _date.mon = acc - 1; acc = 0; }
    action done    { fbreak; }

    action yr {
        if (acc <= 50) _date.year = 2000 + acc;
        else           _date.year = 1900 + acc;
        acc = 0;
    }

    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 tz_h1 { tzi.rule[2] = tzi.rule[9]  = *p; }
    action tz_h2 { tzi.rule[3] = tzi.rule[10] = *p; }
    action tz_m1 { tzi.rule[5] = tzi.rule[12] = *p; }
    action tz_m2 { tzi.rule[6] = tzi.rule[13] = *p; }

    nn         = digit{2} $digit;
    P_day_nn   = nn @day @done;
    P_AMPM     =  ('AM') | ('PM' @hour_pm);
    P_ampm     =  ('am') | ('pm' @hour_pm);
    P_wname    = ("Mon" | "Monday")    @{ _date.wday = 1; } |
                 ("Tue" | "Tuesday")   @{ _date.wday = 2; } |
                 ("Wed" | "Wednesday") @{ _date.wday = 3; } |
                 ("Thu" | "Thursday")  @{ _date.wday = 4; } |
                 ("Fri" | "Friday")    @{ _date.wday = 5; } |
                 ("Sat" | "Saturday")  @{ _date.wday = 6; } |
                 ("Sun" | "Sunday")    @{ _date.wday = 0; } ;

    P_mname  = ("Jan" | "January")   @{ _date.mon = 0; } |
               ("Feb" | "February")  @{ _date.mon = 1; } |
               ("Mar" | "March")     @{ _date.mon = 2; } |
               ("Apr" | "April")     @{ _date.mon = 3; } |
                "May"                @{ _date.mon = 4; } |
               ("Jun" | "June" )     @{ _date.mon = 5; } |
               ("Jul" | "July" )     @{ _date.mon = 6; } |
               ("Aug" | "August" )   @{ _date.mon = 7; } |
               ("Sep" | "September") @{ _date.mon = 8; } |
               ("Oct" | "October")   @{ _date.mon = 9; } |
               ("Nov" | "November")  @{ _date.mon = 10;} |
               ("Dec" | "December")  @{ _date.mon = 11;} ;


    p_AMPM     := P_AMPM @done;
    p_ampm     := P_ampm @done;
    p_sec      := nn @sec @done;
    p_min      := nn @min @done;
    p_hour     := nn @hour @done;
    p_hour_s   := (' ' | digit{1} $digit) digit{1} $digit @hour @done;
    p_hour_min := nn @hour ':' nn @min @done;
    p_hms      := nn @hour ':' nn @min ':' nn @sec @done;
    p_hmsAMPM  := nn @hour ':' nn @min ':' nn @sec ' '+ P_AMPM @done;
    p_mdy      := nn @month '/' nn @day '/' nn @yr @done;
    p_ymd      := digit{4} $digit @year '-' nn @month '-' nn @day @done;
    p_mdyhms   := nn @month '/' nn @day '/' nn @yr ' '+ nn @hour ':' nn @min ':' nn @sec @done;
    p_day      := nn @day @done;
    p_day3     := digit{3} $digit @yday @done;
    p_day_s    := P_day_nn | (" " digit $digit) @day @done;
    p_wday     := digit{1} $digit @wday @done;
    p_wday_s   := digit{1} $digit @wday_s @done;
    p_wname    := P_wname %done;
    p_wnum     := nn >{ week = 0;} @week @done;
    p_month    := nn @month @done;
    p_mname    := P_mname %done;
    p_year     := digit{4} $digit @year @done;
    p_yr       := nn @yr @done;
    p_cent     := nn @cent @done;
    p_epoch    := digit+ $digit %epoch;
    p_tz_num   := [+\-] $tzsign (digit $tz_h1 digit $tz_h2) (digit $tz_m1 digit $tz_m2) @done;
    p_tz_name  := [a-zA-Z+-/]+ >{tz_b = p;} %{tz_e = p;} %done;
    p_perc     := '%'  @done;
    p_space    := ' '*  %done;
}%%

%% write data;

static inline int _parse_str(int cs, const char* p, const char* pe, int& week, datetime& _date, ptime_t& epoch_, TZInfo& tzi, const char*& tz_b, const char*& tz_e)  {
    // printf("_parse_str cs=%d\n", cs);
    const char* pb  = p;
    const char* eof = pe;
    uint64_t    acc = 0;

    %% write exec;


    // printf("_parse_str %s -> cs=%d, consumed=%d\n", pb, cs, p - pb);
    return p - pb;
}

%%{
    machine meta_parser;
    m_yr        = '%y' @{ p_cs = parser_en_p_yr;                                                     fbreak; };
    m_AMPM      = '%p' @{ p_cs = parser_en_p_AMPM;                                                   fbreak; };
    m_ampm      = '%P' @{ p_cs = parser_en_p_ampm;                                                   fbreak; };
    m_year      = '%Y' @{ p_cs = parser_en_p_year;                                                   fbreak; };
    m_cent      = '%C' @{ p_cs = parser_en_p_cent;                                                   fbreak; };
    m_day       = '%d' @{ p_cs = parser_en_p_day;                                                    fbreak; };
    m_day3      = '%j' @{ p_cs = parser_en_p_day3;                                                   fbreak; };
    m_day_s     = '%e' @{ p_cs = parser_en_p_day_s;                                                  fbreak; };
    m_wday      = '%w' @{ p_cs = parser_en_p_wday;                                                   fbreak; };
    m_wday_s    = '%u' @{ p_cs = parser_en_p_wday_s;                                                 fbreak; };
    m_wname     = ('%a' | '%A') @{ p_cs = parser_en_p_wname;                                         fbreak; };
    m_wnum_iso  = '%V' @{ p_cs = parser_en_p_wnum; week_interptetation = WeekInterpretation::iso;    fbreak; };
    m_wnum_mon  = '%W' @{ p_cs = parser_en_p_wnum; week_interptetation = WeekInterpretation::monday; fbreak; };
    m_wnum_sun  = '%U' @{ p_cs = parser_en_p_wnum; week_interptetation = WeekInterpretation::sunday; fbreak; };
    m_hour      = ('%H' | '%I') @{ p_cs = parser_en_p_hour;                                          fbreak; };
    m_hour_s    = ('%k' | '%l') @{ p_cs = parser_en_p_hour_s;                                        fbreak; };
    m_month     = '%m' @{ p_cs = parser_en_p_month;                                                  fbreak; };
    m_mname     = ('%b' | '%B' | '%h')  @{ p_cs = parser_en_p_mname;                                 fbreak; };
    m_min       = '%M' @{ p_cs = parser_en_p_min;                                                    fbreak; };
    m_sec       = '%S' @{ p_cs = parser_en_p_sec;                                                    fbreak; };
    m_hour_min  = '%R' @{ p_cs = parser_en_p_hour_min;                                               fbreak; };
    m_mdyhms    = '%c' @{ p_cs = parser_en_p_mdyhms;                                                 fbreak; };
    m_hmsAMPM   = '%r'  @{ p_cs = parser_en_p_hmsAMPM;                                               fbreak; };
    m_ymd       = '%F' @{ p_cs = parser_en_p_ymd;                                                    fbreak; };
    m_hms       = ('%T' | '%X')  @{ p_cs = parser_en_p_hms;                                          fbreak; };
    m_mdy       = ('%D' | '%x')  @{ p_cs = parser_en_p_mdy;                                          fbreak; };
    m_epoch     = '%s'  @{ p_cs = parser_en_p_epoch;                                                 fbreak; };
    m_tz_num    = '%z'  @{ p_cs = parser_en_p_tz_num;                                                fbreak; };
    m_tz_name   = '%Z'  @{ p_cs = parser_en_p_tz_name;                                               fbreak; };
    m_perc      = '%%' @{ p_cs = parser_en_p_perc;                                                   fbreak; };
    m_space_enc = ('%t' | '%n') @{ p_cs = parser_en_p_space;                                         fbreak; };
    m_space     = (' ' | '\t')+  @{ p_cs = parser_en_p_space;                                        fbreak; };

    m_main := m_space | m_space_enc | m_perc | m_epoch | m_yr | m_AMPM | m_ampm | m_cent | m_day3 | m_mname |
              m_wnum_iso | m_wnum_mon | m_wnum_sun | m_tz_num | m_tz_name |
              m_year | m_month | m_day | m_day_s | m_wday | m_wday_s | m_wname | m_hour | m_hour_s | m_min | m_sec |
              m_hour_min | m_hms | m_mdy | m_mdyhms | m_hmsAMPM | m_ymd
           ;
}%%

%% write data;

static inline MetaConsume _parse_meta(const char* p, const char* pe, WeekInterpretation& week_interptetation)  {
    const char* pb     = p;
    int         cs     = meta_parser_en_m_main;
    int         p_cs   = 0;

    %% write exec;

    auto consumed = p - pb;
    // printf("_parse_meta '%s' p_cs=%d, c=%d, cs=%d\n", pb, p_cs, consumed, cs);
    return MetaConsume { p_cs, (int)consumed };
}

void Date::_strptime (string_view str, string_view format) {
    memset(&_date, 0, sizeof(_date)); // reset all values
    _date.mday = 1;
    _error = errc::ok;
    _mksec = 0;
    _has_date = true;

    ptime_t epoch_ = 0;
    int week       = -1;
    WeekInterpretation week_interptetation = WeekInterpretation::none;
    TZInfo tzi;

    const char* m_p = format.data();
    const char* m_e = m_p + format.length();
    const char* s_p = str.data();
    const char* s_e = s_p + str.length();
    const char* tz_b = nullptr;
    const char* tz_e = nullptr;

    while((m_p != m_e) && (s_p != s_e)) {
        // printf("cycle, meta='%s', str='%s'\n", m_p, s_p);
        auto meta_result = _parse_meta(m_p, m_e, week_interptetation);
        if (meta_result.cs) {
            int consumed = _parse_str(meta_result.cs, s_p, s_e, week, _date, epoch_, tzi, tz_b, tz_e);
            if (consumed >= 0) {
                s_p += consumed;
            } else {
                _error = errc::parser_error;
                break;
            }
        } else {
            meta_result.consumed = 0;
            if (*m_p++ != *s_p++) {
                // printf("char mismatch\n");
                _error = errc::parser_error;
                break;
            }
        }
        m_p += meta_result.consumed;
    }

    if ((m_p < m_e) || (s_p < s_e)) {
        _error = errc::parser_error;
        return;
    }

    if (epoch_ != 0) {
        epoch(epoch_);
    } else {
        _has_date = true;
    }

    switch (week_interptetation) {
        case WeekInterpretation::none: break;
        case WeekInterpretation::iso: _post_parse_week((unsigned)week); break;
        case WeekInterpretation::monday: ; /* fallthrough */
        case WeekInterpretation::sunday:
        if (!_date.wday) _date.wday = 1;
            auto days_since_christ = panda::time::christ_days(_date.year);
            int32_t beginning_weekday = days_since_christ % 7;

            //static constexpr const int32_t WEEK_DELTA[] = {6, 0, 1, 2, 3, 4, 5};
            //static constexpr const int32_t WEEK_DELTA[] = {-1, 0, 1, 2, 3, 4, 5};
            //auto delta = WEEK_DELTA[beginning_weekday];
            if (!beginning_weekday) beginning_weekday = (int)week_interptetation;   // for %U
            auto delta = ((beginning_weekday - 1) + 7) % 7;

            //printf("y = %d, wday = %d, delta = %d, beg = %d\n", _date.year, _date.wday, delta, beginning_weekday);
            _date.mday = week * 7  + (_date.wday - 1) - delta;
    }

    if (tzi.len) _zone = panda::time::tzget(string_view(tzi.rule, tzi.len));
    if (tz_e) {
        auto zkey = string_view(tz_b, tz_e - tz_b);
        _zone = panda::time::tzget(zkey);
        if (_zone->name == panda::time::GMT_FALLBACK) {
            _zone = panda::time::tzget_abbr(zkey);
        }
    }
}

}}