// jj_date.h — date accessors for jalapenojson.
//
// Dates are stored as ASCII in an ISO 8601 / RFC 3339 profile, one tag per
// shape:
//
//   d  local date              2026-01-15
//   t  time of day             09:30:00[.ffffff]
//   n  naive datetime          2026-01-15T09:30:00[.ffffff]
//   z  datetime with offset    2026-01-15T09:30:00[.ffffff](Z|(+|-)HH:MM)
//
// Fractional seconds carry 1 to 6 digits. Second 60 is not accepted.
//
// Nothing here runs during jj_parse: like jj_int and jj_float, these validate
// and convert only when called, so the parser still reads a length and jumps
// the value without looking inside it.
//
// Usage:
//   jj_date d;
//   int rc = jj_date_z(jj_field(&doc, r, c), &d);
//   if (rc != JJ_DATE_OK) { fprintf(stderr, "%s\n", jj_date_strerror(rc)); ... }
//   long long t = jj_date_epoch_sec(&d);     // needs has_date && has_offset
//
// Values are read through v->len only; the buffer is never assumed to be
// NUL-terminated and is never read past v->ptr + v->len.
#ifndef JJ_DATE_H
#define JJ_DATE_H
#include "jalapenojson.h"

enum {
    JJ_DATE_OK = 0,
    JJ_DATE_ERR_ARGS,     // NULL value or output
    JJ_DATE_ERR_TAG,      // not a date-bearing type tag
    JJ_DATE_ERR_FORMAT,   // wrong shape: bad separator, missing or extra bytes
    JJ_DATE_ERR_RANGE     // right shape, impossible value (month 13, 2026-02-29)
};

typedef struct {
    int  year, month, day;   // 0 when the value carries no date (tag 't')
    int  hour, min, sec;     // 0 when the value carries no time (tag 'd')
    long nanos;              // fractional seconds, 0 when absent; the input
                             // carries at most 6 digits, so this is always a
                             // whole number of thousands
    int  offset_min;         // minutes east of UTC, 0 unless has_offset
    char has_date, has_time, has_offset;
} jj_date;

static inline const char *jj_date_strerror(int rc) {
    switch (rc) {
        case JJ_DATE_OK:         return "ok";
        case JJ_DATE_ERR_ARGS:   return "invalid arguments";
        case JJ_DATE_ERR_TAG:    return "not a date type tag";
        case JJ_DATE_ERR_FORMAT: return "malformed date";
        case JJ_DATE_ERR_RANGE:  return "date field out of range";
        default:                 return "unknown error";
    }
}

// Two ASCII digits -> 0..99, or -1.
static inline int jj__d2(const char *p) {
    if (p[0] < '0' || p[0] > '9' || p[1] < '0' || p[1] > '9') return -1;
    return (p[0] - '0') * 10 + (p[1] - '0');
}

static inline int jj__days_in_month(int y, int m) {
    static const int dm[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
    if (m < 1 || m > 12) return 0;
    if (m == 2 && ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0)) return 29;
    return dm[m];
}

// YYYY-MM-DD at p, which the caller has checked holds at least 10 bytes.
static inline int jj__read_date(const char *p, jj_date *d) {
    int hi = jj__d2(p), lo = jj__d2(p + 2);
    if (p[4] != '-' || p[7] != '-') return JJ_DATE_ERR_FORMAT;
    if (hi < 0 || lo < 0) return JJ_DATE_ERR_FORMAT;
    d->month = jj__d2(p + 5);
    d->day   = jj__d2(p + 8);
    if (d->month < 0 || d->day < 0) return JJ_DATE_ERR_FORMAT;
    d->year = hi * 100 + lo;
    if (d->month < 1 || d->month > 12) return JJ_DATE_ERR_RANGE;
    if (d->day < 1 || d->day > jj__days_in_month(d->year, d->month))
        return JJ_DATE_ERR_RANGE;
    d->has_date = 1;
    return JJ_DATE_OK;
}

// HH:MM:SS at p, which the caller has checked holds at least 8 bytes.
static inline int jj__read_time(const char *p, jj_date *d) {
    if (p[2] != ':' || p[5] != ':') return JJ_DATE_ERR_FORMAT;
    d->hour = jj__d2(p);
    d->min  = jj__d2(p + 3);
    d->sec  = jj__d2(p + 6);
    if (d->hour < 0 || d->min < 0 || d->sec < 0) return JJ_DATE_ERR_FORMAT;
    // Second 60 (a leap second) is rejected: neither Python's `time` nor a JS
    // Date can represent it, and a value only one implementation can read is
    // worse than one nobody can write.
    if (d->hour > 23 || d->min > 59 || d->sec > 59) return JJ_DATE_ERR_RANGE;
    d->has_time = 1;
    return JJ_DATE_OK;
}

// ".<1-6 digits>" at *pp if present. Advances *pp past it.
//
// Six digits is the cap because Python's datetime and JavaScript both stop at
// microseconds. Allowing nanoseconds here would let C write values the other
// two implementations could only read by silently rounding them.
#define JJ_DATE_MAX_FRAC 6

static inline int jj__read_frac(const char **pp, const char *end, jj_date *d) {
    const char *p = *pp;
    long scale = 100000000L;
    int seen = 0;
    if (p == end || *p != '.') return JJ_DATE_OK;
    p++;
    while (p < end && *p >= '0' && *p <= '9') {
        if (seen == JJ_DATE_MAX_FRAC) return JJ_DATE_ERR_FORMAT;
        d->nanos += (*p - '0') * scale;
        scale /= 10;
        seen++;
        p++;
    }
    if (seen == 0) return JJ_DATE_ERR_FORMAT;       // a bare '.'
    *pp = p;
    return JJ_DATE_OK;
}

// "Z" or "(+|-)HH:MM" at *pp. Advances *pp past it.
static inline int jj__read_offset(const char **pp, const char *end, jj_date *d) {
    const char *p = *pp;
    int sign, oh, om;
    if (p == end) return JJ_DATE_ERR_FORMAT;
    if (*p == 'Z') { d->offset_min = 0; d->has_offset = 1; *pp = p + 1; return JJ_DATE_OK; }
    if (*p != '+' && *p != '-') return JJ_DATE_ERR_FORMAT;
    sign = (*p == '-') ? -1 : 1;
    if (end - p < 6 || p[3] != ':') return JJ_DATE_ERR_FORMAT;
    oh = jj__d2(p + 1);
    om = jj__d2(p + 4);
    if (oh < 0 || om < 0) return JJ_DATE_ERR_FORMAT;
    if (oh > 23 || om > 59) return JJ_DATE_ERR_RANGE;
    d->offset_min = sign * (oh * 60 + om);
    d->has_offset = 1;
    *pp = p + 6;
    return JJ_DATE_OK;
}

// want_date/want_time/want_offset: 1 = required, 0 = must be absent.
static inline int jj__date(const jj_val *v, jj_date *out,
                           int want_date, int want_time, int want_offset) {
    const char *p, *end;
    jj_date d;
    int rc;

    if (!v || !v->ptr || !out || v->len < 0) return JJ_DATE_ERR_ARGS;
    memset(&d, 0, sizeof d);
    p = v->ptr;
    end = p + v->len;

    if (want_date) {
        if (end - p < 10) return JJ_DATE_ERR_FORMAT;
        if ((rc = jj__read_date(p, &d)) != JJ_DATE_OK) return rc;
        p += 10;
    }
    if (want_time) {
        if (want_date) {                       // date and time are joined by 'T'
            if (p == end || *p != 'T') return JJ_DATE_ERR_FORMAT;
            p++;
        }
        if (end - p < 8) return JJ_DATE_ERR_FORMAT;
        if ((rc = jj__read_time(p, &d)) != JJ_DATE_OK) return rc;
        p += 8;
        if ((rc = jj__read_frac(&p, end, &d)) != JJ_DATE_OK) return rc;
    }
    if (want_offset && (rc = jj__read_offset(&p, end, &d)) != JJ_DATE_OK) return rc;

    if (p != end) return JJ_DATE_ERR_FORMAT;   // trailing bytes
    *out = d;
    return JJ_DATE_OK;
}

static inline int jj_date_d(const jj_val *v, jj_date *o) { return jj__date(v, o, 1, 0, 0); }
static inline int jj_date_t(const jj_val *v, jj_date *o) { return jj__date(v, o, 0, 1, 0); }
static inline int jj_date_n(const jj_val *v, jj_date *o) { return jj__date(v, o, 1, 1, 0); }
static inline int jj_date_z(const jj_val *v, jj_date *o) { return jj__date(v, o, 1, 1, 1); }

// Dispatch on a column's type tag, e.g. doc.col_types[c].
static inline int jj_date_parse(const jj_val *v, char tag, jj_date *o) {
    switch (tag) {
        case 'd': return jj_date_d(v, o);
        case 't': return jj_date_t(v, o);
        case 'n': return jj_date_n(v, o);
        case 'z': return jj_date_z(v, o);
        default:  return JJ_DATE_ERR_TAG;
    }
}

// Days since 1970-01-01 for a proleptic Gregorian date. Valid well beyond the
// range any of these tags can express. (Howard Hinnant's days_from_civil.)
static inline long jj_days_from_civil(int y, int m, int d) {
    long era;
    unsigned yoe, doy, doe;
    y -= m <= 2;
    era = (y >= 0 ? y : y - 399) / 400;
    yoe = (unsigned)(y - era * 400);                              // [0, 399]
    doy = (unsigned)((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1);
    doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;                  // [0, 146096]
    return era * 146097L + (long)doe - 719468L;
}

// Seconds since 1970-01-01T00:00:00Z. Only meaningful when the value carried
// both a date and an offset, i.e. a 'z' column; returns 0 otherwise.
static inline long long jj_date_epoch_sec(const jj_date *d) {
    long long s;
    if (!d || !d->has_date || !d->has_offset) return 0;
    s = (long long)jj_days_from_civil(d->year, d->month, d->day) * 86400LL;
    s += d->hour * 3600LL + d->min * 60LL + d->sec;
    return s - (long long)d->offset_min * 60LL;
}

#endif
