
/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) NGINX, Inc.
 */


#include <njs_main.h>


#define NJS_DATE_TIME_LEN                                                     \
    sizeof("Mon Sep 28 1970 12:00:00 GMT+0600 (XXXXX)")

#define NJS_DATE_MAX_FIELDS             8
#define NJS_DATE_WDAY                   0
#define NJS_DATE_YR                     1
#define NJS_DATE_MON                    2
#define NJS_DATE_DAY                    3
#define NJS_DATE_HR                     4
#define NJS_DATE_MIN                    5
#define NJS_DATE_SEC                    6
#define NJS_DATE_MSEC                   7


#define njs_date_magic(field, local)                                          \
    ((local << 6) + field)


#define njs_date_magic2(since, len, local)                                    \
    ((local << 6) + ((len & 7) << 3) + since)


typedef enum {
    NJS_DATE_FMT_TO_TIME_STRING,
    NJS_DATE_FMT_TO_DATE_STRING,
    NJS_DATE_FMT_TO_STRING,
    NJS_DATE_FMT_TO_UTC_STRING,
    NJS_DATE_FMT_TO_ISO_STRING,
} njs_date_fmt_t;


static double njs_date_string_parse(njs_value_t *date);
static double njs_date_rfc2822_string_parse(int64_t tm[], const u_char *p,
    const u_char *end);
static double njs_date_js_string_parse(int64_t tm[], const u_char *p,
    const u_char *end);
static const u_char *njs_date_skip_week_day(const u_char *p, const u_char *end);
static const u_char *njs_date_skip_spaces(const u_char *p, const u_char *end);
static njs_int_t njs_date_month_parse(const u_char *p, const u_char *end);
static const u_char *njs_date_time_parse(int64_t tm[], const u_char *p,
    const u_char *end);
static int64_t njs_date_gmtoff_parse(const u_char *start, const u_char *end);
static const u_char *njs_date_number_parse(int64_t *value, const u_char *p,
    const u_char *end, size_t size);
static njs_int_t njs_date_string(njs_vm_t *vm, njs_value_t *retval,
    njs_date_fmt_t fmt, double time);


static const njs_value_t  njs_string_invalid_date = njs_string("Invalid Date");


njs_inline int64_t
njs_mod(int64_t a, int64_t b)
{
    int64_t  m;

    m = a % b;

    return m + (m < 0) * b;
}


njs_inline int64_t
njs_floor_div(int64_t a, int64_t b)
{
    int64_t  m;

    m = a % b;

    return (a - (m + (m < 0) * b)) / b;
}


njs_inline uint64_t
njs_gettime(void)
{
    struct timeval  tv;

    gettimeofday(&tv, NULL);

    return (uint64_t) tv.tv_sec * 1000 + tv.tv_usec / 1000;
}


njs_inline double
njs_timeclip(double time)
{
    if (time < -8.64e15 || time > 8.64e15) {
        return NAN;
    }

    return trunc(time) + 0.0;
}


njs_inline int64_t
njs_days_in_year(int64_t y)
{
    return 365 + !(y % 4) - !(y % 100) + !(y % 400);
}


njs_inline int64_t
njs_days_from_year(int64_t y)
{
    return 365 * (y - 1970) + njs_floor_div(y - 1969, 4)
           - njs_floor_div(y - 1901, 100) + njs_floor_div(y - 1601, 400);
}


njs_inline double
njs_make_day(int64_t yr, int64_t month, int64_t date)
{
    double   days;
    int64_t  i, ym, mn, md;

    static const int min_year = -271821;
    static const int max_year = 275760;
    static const int month_days[] = { 31, 28, 31, 30, 31, 30,
                                      31, 31, 30, 31, 30, 31 };

    if (yr < min_year || yr > max_year
        || month < (min_year * 12) || month > (max_year * 12)
        || date < (min_year * 12 * 366) || date > (max_year * 12 * 366))
    {
        return NAN;
    }

    mn = njs_mod(month, 12);
    ym = yr + (month - mn) / 12;

    days = njs_days_from_year(ym);

    for (i = 0; i < mn; i++) {
        md = month_days[i];

        if (i == 1) {
            /* Leap day. */
            md += njs_days_in_year(ym) - 365;
        }

        days += md;
    }

    return days + date - 1;
}


njs_inline int64_t
njs_tz_offset(int64_t time)
{
    time_t     ti;
    struct tm  tm;

    time /= 1000;

#if (NJS_TIME_T_SIZE < 8)

    /* Smart truncation. */

    if ((time_t) -1 < 0) {
        if (time < INT32_MIN) {
            time = INT32_MIN;

        } else if (time > INT32_MAX) {
            time = INT32_MAX;
        }

    } else {
        if (time < 0) {
            time = 0;

        } else if (time > UINT32_MAX) {
            time = UINT32_MAX;
        }
    }

#endif

    ti = time;
    localtime_r(&ti, &tm);

    /*
     * As njs_timezone(&tm) may return value which is not a multiple of 60
     * secs (see "zdump -v /etc/localtime" for MSK zone) rounding it to
     * minutes precision here to ensure:
     * var date = new Date(<args>)
     * date.valueOf() - date.getTimezoneOffset() * 60000 == Date.UTC(<args>)
     * which is expected by test262.
     */

    return -njs_timezone(&tm) / 60;
}


njs_inline int64_t
njs_year_from_days(int64_t *days)
{
    int64_t  y, d1, nd, d;

    d = *days;

    y = njs_floor_div(d * 10000, 3652425) + 1970;

    for ( ;; ) {
        d1 = d - njs_days_from_year(y);

        if (d1 < 0) {
            y--;

        } else {
            nd = njs_days_in_year(y);

            if (d1 < nd) {
                break;
            }

            y++;
        }
    }

    *days = d1;

    return y;
}


njs_inline double
njs_make_date(int64_t tm[], njs_bool_t local)
{
    double  time, days;

    days = njs_make_day(tm[NJS_DATE_YR], tm[NJS_DATE_MON],
                        tm[NJS_DATE_DAY]);

    time = ((tm[NJS_DATE_HR] * 60.0 + tm[NJS_DATE_MIN]) * 60.0
            + tm[NJS_DATE_SEC]) * 1000.0 + tm[NJS_DATE_MSEC];

    time += days * 86400000.0;

    if (local) {
        time += njs_tz_offset(time) * 60000;
    }

    return njs_timeclip(time);
}


njs_inline int64_t
njs_destruct_date(double time, int64_t tm[], int index, njs_bool_t local)
{
    int64_t  days, wd, y, i, md, h, m, s, ms;

    static const int month_days[] = { 31, 28, 31, 30, 31, 30,
                                      31, 31, 30, 31, 30, 31 };

    if (njs_slow_path(isnan(time))) {
        time = 0;

    } else if (local) {
        time -= njs_tz_offset(time) * 60000;
    }

    h = njs_mod(time, 86400000);
    days = (time - h) / 86400000;
    ms = h % 1000;
    h = (h - ms) / 1000;
    s = h % 60;
    h = (h - s) / 60;
    m = h % 60;
    h = (h - m) / 60;
    wd = njs_mod(days + 4, 7);
    y = njs_year_from_days(&days);

    for (i = 0; i < 11; i++) {
        md = month_days[i];

        if (i == 1) {
            /* Leap day. */
            md += njs_days_in_year(y) - 365;
        }

        if (days < md) {
            break;
        }

        days -= md;
    }

    tm[NJS_DATE_YR] = y;
    tm[NJS_DATE_MON] = i;
    tm[NJS_DATE_DAY] = days + 1;
    tm[NJS_DATE_HR] = h;
    tm[NJS_DATE_MIN] = m;
    tm[NJS_DATE_SEC] = s;
    tm[NJS_DATE_MSEC] = ms;
    tm[NJS_DATE_WDAY] = wd;

    return tm[index];
}


static njs_int_t
njs_date_args(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    int64_t tm[])
{
    double      num;
    njs_int_t   ret;
    njs_uint_t  i, n;

    njs_memzero(tm, NJS_DATE_MAX_FIELDS * sizeof(int64_t));

    tm[NJS_DATE_DAY] = 1;

    n = njs_min(8, nargs);

    for (i = 1; i < n; i++) {
        ret = njs_value_to_number(vm, &args[i], &num);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }

        if (!isfinite(num)) {
            tm[NJS_DATE_YR] = INT64_MIN;
            continue;
        }

        tm[i] = njs_number_to_integer(num);
    }

    if (tm[NJS_DATE_YR] >= 0 && tm[NJS_DATE_YR] < 100) {
        tm[NJS_DATE_YR] += 1900;
    }

    return NJS_OK;
}


static njs_int_t
njs_date_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    double      time;
    njs_int_t   ret;
    njs_date_t  *date;
    int64_t     tm[NJS_DATE_MAX_FIELDS];

    if (!vm->top_frame->ctor) {
        return njs_date_string(vm, &vm->retval, NJS_DATE_FMT_TO_STRING,
                               njs_gettime());
    }

    if (nargs == 1) {
        time = njs_gettime();

    } else if (nargs == 2) {
        if (njs_is_object(&args[1])) {
            if (!njs_is_date(&args[1])) {
                ret = njs_value_to_primitive(vm, &args[1], &args[1], 0);
                if (njs_slow_path(ret != NJS_OK)) {
                    return ret;
                }
            }
        }

        if (njs_is_date(&args[1])) {
            time = njs_date(&args[1])->time;

        } else if (njs_is_string(&args[1])) {
            time = njs_date_string_parse(&args[1]);

        } else {
            time = njs_timeclip(njs_number(&args[1]));
        }

    } else {

        ret = njs_date_args(vm, args, nargs, tm);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }

        time = njs_make_date(tm, 1);
    }

    date = njs_mp_alloc(vm->mem_pool, sizeof(njs_date_t));
    if (njs_slow_path(date == NULL)) {
        njs_memory_error(vm);
        return NJS_ERROR;
    }

    njs_lvlhsh_init(&date->object.hash);
    njs_lvlhsh_init(&date->object.shared_hash);
    date->object.type = NJS_DATE;
    date->object.shared = 0;
    date->object.extensible = 1;
    date->object.error_data = 0;
    date->object.fast_array = 0;
    date->object.__proto__ = &vm->prototypes[NJS_OBJ_TYPE_DATE].object;
    date->object.slots = NULL;

    date->time = time;

    njs_set_date(&vm->retval, date);

    return NJS_OK;
}


static njs_int_t
njs_date_utc(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    double      time;
    njs_int_t   ret;
    int64_t     tm[NJS_DATE_MAX_FIELDS];

    time = NAN;

    if (nargs > 1) {
        ret = njs_date_args(vm, args, nargs, tm);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }

        time = njs_make_date(tm, 0);
    }

    njs_set_number(&vm->retval, time);

    return NJS_OK;
}


static njs_int_t
njs_date_now(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    njs_set_number(&vm->retval, njs_gettime());

    return NJS_OK;
}


static njs_int_t
njs_date_parse(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    double     time;
    njs_int_t  ret;

    if (nargs > 1) {
        if (njs_slow_path(!njs_is_string(&args[1]))) {
            ret = njs_value_to_string(vm, &args[1], &args[1]);
            if (njs_slow_path(ret != NJS_OK)) {
                return ret;
            }
        }

        time = njs_date_string_parse(&args[1]);

    } else {
        time = NAN;
    }

    njs_set_number(&vm->retval, time);

    return NJS_OK;
}


static double
njs_date_string_parse(njs_value_t *date)
{
    size_t         ms_length;
    int64_t        ext, skipped;
    njs_str_t      string;
    njs_bool_t     sign, week, utc;
    const u_char   *p, *next, *end;
    int64_t        tm[NJS_DATE_MAX_FIELDS];

    njs_string_get(date, &string);

    p = string.start;
    end = p + string.length;

    if (njs_slow_path(p >= end)) {
        return NAN;
    }

    if (*p == '+' || *p == '-') {
        p++;
        sign = 1;

    } else {
        sign = 0;
    }

    tm[NJS_DATE_MON] = 0;
    tm[NJS_DATE_DAY] = 1;
    tm[NJS_DATE_HR] = 0;
    tm[NJS_DATE_MIN] = 0;
    tm[NJS_DATE_SEC] = 0;
    tm[NJS_DATE_MSEC] = 0;

    next = njs_date_number_parse(&tm[NJS_DATE_YR], p, end, 4);

    if (next != NULL) {
        /* ISO-8601 format: "1970-09-28T06:00:00.000Z" */

        if (next == end) {
            goto done;
        }

        if (*next != '-') {
            /* Extended ISO-8601 format: "+001970-09-28T06:00:00.000Z" */

            next = njs_date_number_parse(&ext, next, end, 2);
            if (njs_slow_path(next == NULL)) {
                return NAN;
            }

            tm[NJS_DATE_YR] *= 100;
            tm[NJS_DATE_YR] += ext;

            if (string.start[0] == '-') {
                if (tm[NJS_DATE_YR] == 0) {
                    return NAN;
                }

                tm[NJS_DATE_YR] = -tm[NJS_DATE_YR];
            }

            if (next == end) {
                goto done;
            }

            if (*next != '-') {
                return NAN;
            }
        }

        p = njs_date_number_parse(&tm[NJS_DATE_MON], next + 1, end, 2);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        tm[NJS_DATE_MON]--;

        if (p == end) {
            goto done;
        }

        if (njs_slow_path(*p != '-')) {
            return NAN;
        }

        p = njs_date_number_parse(&tm[NJS_DATE_DAY], p + 1, end, 2);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        if (p == end) {
            goto done;
        }

        if (njs_slow_path(*p != 'T')) {
            return NAN;
        }

        utc = 1;
        end--;

        if (*end != 'Z') {
           utc = 0;
           end++;
        }

        p = njs_date_time_parse(tm, p + 1, end);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        if (p == end) {
            goto done;
        }

        if (njs_slow_path(p > end || *p != '.')) {
            return NAN;
        }

        p++;

        ms_length = (end - p < 3) ? end - p : 3;

        p = njs_date_number_parse(&tm[NJS_DATE_MSEC], p, end, ms_length);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        if (end > p) {
            p = njs_date_number_parse(&skipped, p, end, end - p);
            if (njs_slow_path(p == NULL)) {
                return NAN;
            }
        }

        if (ms_length == 1) {
            tm[NJS_DATE_MSEC] *= 100;

        } else if (ms_length == 2) {
            tm[NJS_DATE_MSEC] *= 10;
        }

        return njs_make_date(tm, !utc);
    }

    if (sign) {
        return NAN;
    }

    week = 1;

    for ( ;; ) {
        next = njs_date_number_parse(&tm[NJS_DATE_DAY], p, end, 2);

        if (next != NULL) {
            /*
             * RFC 2822 format:
             *   "Mon, 28 Sep 1970 06:00:00 GMT",
             *   "Mon, 28 Sep 1970 06:00:00 UTC",
             *   "Mon, 28 Sep 1970 12:00:00 +0600".
             */
            return njs_date_rfc2822_string_parse(tm, next, end);
        }

        tm[NJS_DATE_MON] = njs_date_month_parse(p, end);

        if (tm[NJS_DATE_MON] >= 0) {
            /* Date.toString() format: "Mon Sep 28 1970 12:00:00 GMT+0600". */

            return njs_date_js_string_parse(tm, p + 3, end);
        }

        if (!week) {
            return NAN;
        }

        p = njs_date_skip_week_day(p, end);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        p = njs_date_skip_spaces(p, end);
        if (njs_slow_path(p == NULL)) {
            return NAN;
        }

        week = 0;
    }

done:

    return njs_make_date(tm, 0);
}


static double
njs_date_rfc2822_string_parse(int64_t tm[], const u_char *p, const u_char *end)
{
    int64_t  gmtoff;

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    tm[NJS_DATE_MON] = njs_date_month_parse(p, end);
    if (njs_slow_path(tm[NJS_DATE_MON] < 0)) {
        return NAN;
    }

    p = njs_date_skip_spaces(p + 3, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    p = njs_date_number_parse(&tm[NJS_DATE_YR], p, end, 4);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    gmtoff = 0;

    if (p == end) {
        goto done;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_time_parse(tm, p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    if (njs_slow_path(p + 2 >= end)) {
        return NAN;
    }

    if ((p[0] == 'G' && p[1] == 'M' && p[2] == 'T')
        || (p[0] == 'U' && p[1] == 'T' && p[2] == 'C'))
    {
        gmtoff = 0;

    } else {
        gmtoff = njs_date_gmtoff_parse(p, end);

        if (njs_slow_path(gmtoff == -1)) {
            return NAN;
        }
    }

done:

    tm[NJS_DATE_MSEC] = -gmtoff * 60000;

    return njs_make_date(tm, 0);
}


static double
njs_date_js_string_parse(int64_t tm[], const u_char *p, const u_char *end)
{
    int64_t  gmtoff;

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    p = njs_date_number_parse(&tm[NJS_DATE_DAY], p, end, 2);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    p = njs_date_number_parse(&tm[NJS_DATE_YR], p, end, 4);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_time_parse(tm, p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    p = njs_date_skip_spaces(p, end);
    if (njs_slow_path(p == NULL)) {
        return NAN;
    }

    if (p == end) {
        goto done;
    }

    if (p + 2 < end && p[0] == 'G' && p[1] == 'M' && p[2] == 'T') {
        gmtoff = njs_date_gmtoff_parse(&p[3], end);

        if (njs_fast_path(gmtoff != -1)) {
            tm[NJS_DATE_MSEC] = -gmtoff * 60000;
            return njs_make_date(tm, 0);
        }
    }

    return NAN;

done:

    return njs_make_date(tm, 0);
}


static const u_char *
njs_date_skip_week_day(const u_char *p, const u_char *end)
{
    while (p < end) {
        if (*p == ' ') {
            return p;
        }

        p++;
    }

    return NULL;
}


static const u_char *
njs_date_skip_spaces(const u_char *p, const u_char *end)
{
    if (p < end && *p++ == ' ') {

        while (p < end) {
            if (*p != ' ') {
                return p;
            }

            p++;
        }

        return p;
    }

    return NULL;
}


static njs_int_t
njs_date_month_parse(const u_char *p, const u_char *end)
{
    if (p + 2 < end) {
        switch (p[0]) {

        case 'J':
            if (p[1] == 'a' && p[2] == 'n') {
                return 0;
            }

            if (p[1] == 'u') {
                if (p[2] == 'n') {
                    return 5;
                }

                if (p[2] == 'l') {
                    return 6;
                }
            }

            break;

        case 'F':
            if (p[1] == 'e' && p[2] == 'b') {
                return 1;
            }

            break;

        case 'M':
            if (p[1] == 'a') {
                if (p[2] == 'r') {
                    return 2;
                }

                if (p[2] == 'y') {
                    return 4;
                }
            }

            break;

        case 'A':
            if (p[1] == 'p' && p[2] == 'r') {
                return 3;
            }

            if (p[1] == 'u' && p[2] == 'g') {
                return 7;
            }

            break;

        case 'S':
            if (p[1] == 'e' && p[2] == 'p') {
                return 8;
            }

            break;

        case 'O':
            if (p[1] == 'c' && p[2] == 't') {
                return 9;
            }

            break;

        case 'N':
            if (p[1] == 'o' && p[2] == 'v') {
                return 10;
            }

            break;

        case 'D':
            if (p[1] == 'e' && p[2] == 'c') {
                return 11;
            }

            break;
        }
    }

    return -1;
}


static const u_char *
njs_date_time_parse(int64_t tm[], const u_char *p, const u_char *end)
{
    p = njs_date_number_parse(&tm[NJS_DATE_HR], p, end, 2);
    if (njs_slow_path(p == NULL)) {
        return p;
    }

    if (njs_slow_path(p >= end || *p != ':')) {
        return NULL;
    }

    p = njs_date_number_parse(&tm[NJS_DATE_MIN], p + 1, end, 2);
    if (njs_slow_path(p == NULL)) {
        return p;
    }

    if (p == end) {
        return p;
    }

    if (njs_slow_path(*p != ':')) {
        return NULL;
    }

    return njs_date_number_parse(&tm[NJS_DATE_SEC], p + 1, end, 2);
}


static int64_t
njs_date_gmtoff_parse(const u_char *start, const u_char *end)
{
    int64_t       gmtoff, hour, min;
    const u_char  *p;

    if (njs_fast_path(start + 4 < end && (*start == '+' || *start == '-'))) {

        p = njs_date_number_parse(&hour, start + 1, end, 2);
        if (njs_slow_path(p == NULL)) {
            return -1;
        }

        p = njs_date_number_parse(&min, p, end, 2);
        if (njs_slow_path(p == NULL)) {
            return -1;
        }

        gmtoff = hour * 60 + min;

        if (*start == '-') {
            gmtoff = -gmtoff;
        }

        return gmtoff;
    }

    return -1;
}


static const u_char *
njs_date_number_parse(int64_t *value, const u_char *p, const u_char *end,
    size_t size)
{
    u_char     c;
    njs_int_t  n;

    n = 0;

    do {
        if (njs_slow_path(p >= end)) {
            return NULL;
        }

        c = *p++;

        /* Values below '0' become >= 208. */
        c = c - '0';

        if (njs_slow_path(c > 9)) {
            return NULL;
        }

        n = n * 10 + c;

        size--;

    } while (size != 0);

    *value = n;

    return p;
}


static const njs_object_prop_t  njs_date_constructor_properties[] =
{
    {
        .type = NJS_PROPERTY,
        .name = njs_string("name"),
        .value = njs_string("Date"),
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("length"),
        .value = njs_value(NJS_NUMBER, 1, 7.0),
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY_HANDLER,
        .name = njs_string("prototype"),
        .value = njs_prop_handler(njs_object_prototype_create),
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("UTC"),
        .value = njs_native_function(njs_date_utc, 7),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("now"),
        .value = njs_native_function(njs_date_now, 0),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("parse"),
        .value = njs_native_function(njs_date_parse, 1),
        .writable = 1,
        .configurable = 1,
    },
};


const njs_object_init_t  njs_date_constructor_init = {
    njs_date_constructor_properties,
    njs_nitems(njs_date_constructor_properties),
};


static njs_int_t
njs_date_prototype_value_of(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    njs_set_number(&vm->retval, njs_date(&args[0])->time);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_to_string(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t fmt)
{
    double  time;

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    time = njs_date(&args[0])->time;

    if (fmt == NJS_DATE_FMT_TO_ISO_STRING && isnan(time)) {
        njs_range_error(vm, "Invalid time value");
        return NJS_ERROR;
    }

    return njs_date_string(vm, &vm->retval, fmt, time);
}


static njs_int_t
njs_date_string(njs_vm_t *vm, njs_value_t *retval, njs_date_fmt_t fmt,
    double time)
{
    int      year, tz;
    u_char   *p, sign;
    u_char   buf[NJS_DATE_TIME_LEN];
    int64_t  tm[NJS_DATE_MAX_FIELDS];

    static const char  *week[] = { "Sun", "Mon", "Tue", "Wed",
                                   "Thu", "Fri", "Sat" };

    static const char  *month[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

    if (njs_slow_path(isnan(time))) {
        *retval = njs_string_invalid_date;
        return NJS_OK;
    }

    p = buf;

    switch (fmt) {
    case NJS_DATE_FMT_TO_ISO_STRING:
    case NJS_DATE_FMT_TO_UTC_STRING:
        njs_destruct_date(time, tm, 0, 0);
        year = tm[NJS_DATE_YR];

        if (fmt == NJS_DATE_FMT_TO_UTC_STRING) {
            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN,
                            "%s, %02L %s %04d %02L:%02L:%02L GMT",
                            week[tm[NJS_DATE_WDAY]], tm[NJS_DATE_DAY],
                            month[tm[NJS_DATE_MON]], year, tm[NJS_DATE_HR],
                            tm[NJS_DATE_MIN], tm[NJS_DATE_SEC]);

            break;
        }

        if (year >= 0 && year <= 9999) {
            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN, "%04d", year);

        } else {
            if (year > 0) {
                *p++ = '+';
            }

            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN, "%06d", year);
        }

        p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN,
                        "-%02L-%02LT%02L:%02L:%02L.%03LZ",
                        tm[NJS_DATE_MON] + 1, tm[NJS_DATE_DAY], tm[NJS_DATE_HR],
                        tm[NJS_DATE_MIN], tm[NJS_DATE_SEC], tm[NJS_DATE_MSEC]);

        break;

    case NJS_DATE_FMT_TO_TIME_STRING:
    case NJS_DATE_FMT_TO_DATE_STRING:
    case NJS_DATE_FMT_TO_STRING:
    default:
        njs_destruct_date(time, tm, 0, 1);

        if (fmt != NJS_DATE_FMT_TO_TIME_STRING) {
            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN,
                            "%s %s %02L %04L",
                            week[tm[NJS_DATE_WDAY]], month[tm[NJS_DATE_MON]],
                            tm[NJS_DATE_DAY], tm[NJS_DATE_YR]);
        }

        if (fmt != NJS_DATE_FMT_TO_DATE_STRING) {
            tz = -njs_tz_offset(time);
            sign = (tz < 0) ? '-' : '+';

            if (tz < 0) {
                tz = -tz;
            }

            if (p != buf) {
                *p++ = ' ';
            }

            p = njs_sprintf(p, buf + NJS_DATE_TIME_LEN,
                            "%02L:%02L:%02L GMT%c%02d%02d",
                            tm[NJS_DATE_HR], tm[NJS_DATE_MIN], tm[NJS_DATE_SEC],
                            sign, tz / 60, tz % 60);
        }
    }

    return njs_string_new(vm, retval, buf, p - buf, p - buf);
}


njs_int_t
njs_date_to_string(njs_vm_t *vm, njs_value_t *retval, const njs_value_t *date)
{
    if (njs_slow_path(!njs_is_date(date))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(date->type));

        return NJS_ERROR;
    }

    return njs_date_string(vm, retval, NJS_DATE_FMT_TO_ISO_STRING,
                           njs_date(date)->time);
}


static njs_int_t
njs_date_prototype_get_field(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t magic)
{
    double   value;
    int64_t  tm[NJS_DATE_MAX_FIELDS];

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    value = njs_date(&args[0])->time;

    if (njs_fast_path(!isnan(value))) {
        value = njs_destruct_date(value, tm, magic & 0xf, magic & 0x40);
    }

    njs_set_number(&vm->retval, value);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_get_timezone_offset(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused)
{
    double  value;

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    value = njs_date(&args[0])->time;

    if (njs_fast_path(!isnan(value))) {
        value = njs_tz_offset(value);
    }

    njs_set_number(&vm->retval, value);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_set_time(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    double     time;
    njs_int_t  ret;

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    if (nargs > 1) {
        if (njs_slow_path(!njs_is_number(&args[1]))) {
            ret = njs_value_to_numeric(vm, &args[1], &args[1]);
            if (njs_slow_path(ret != NJS_OK)) {
                return ret;
            }
        }

        time = njs_timeclip(njs_number(&args[1]));

    } else {
        time = NAN;
    }

    njs_date(&args[0])->time = time;
    njs_set_number(&vm->retval, time);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_set_fields(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t magic)
{
    double      time, num;
    njs_int_t   ret;
    njs_uint_t  since, left, i;
    int64_t     tm[NJS_DATE_MAX_FIELDS];

    if (njs_slow_path(!njs_is_date(&args[0]))) {
        njs_type_error(vm, "cannot convert %s to date",
                       njs_type_string(args[0].type));

        return NJS_ERROR;
    }

    time = njs_date(&args[0])->time;

    since = magic & 7;

    if (njs_slow_path(nargs < 2 || (since != NJS_DATE_YR && isnan(time)))) {
        time = NAN;
        goto done;
    }

    i = 1;
    left = njs_min(((magic >> 3) & 7), nargs - 1);

    njs_destruct_date(time, tm, 0, magic & 0x40);

    do {
        ret = njs_value_to_number(vm, &args[i++], &num);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }

        if (!isfinite(num)) {
            tm[NJS_DATE_YR] = INT64_MIN;
            continue;
        }

        tm[since++] = njs_number_to_integer(num);

    } while (--left);

    time = njs_make_date(tm, 1);

done:

    njs_date(&args[0])->time = time;
    njs_set_number(&vm->retval, time);

    return NJS_OK;
}


static njs_int_t
njs_date_prototype_to_json(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t retval)
{
    njs_int_t           ret;
    njs_value_t         value;
    njs_lvlhsh_query_t  lhq;

    static const njs_value_t  to_iso_string = njs_string("toISOString");

    if (njs_is_object(&args[0])) {
        njs_object_property_init(&lhq, &to_iso_string, NJS_TO_ISO_STRING_HASH);

        ret = njs_object_property(vm, &args[0], &lhq, &value);

        if (njs_slow_path(ret == NJS_ERROR)) {
            return ret;
        }

        if (njs_is_function(&value)) {
            return njs_function_apply(vm, njs_function(&value), args, nargs,
                                      &vm->retval);
        }
    }

    njs_type_error(vm, "\"this\" argument is not an object");

    return NJS_ERROR;
}


static const njs_object_prop_t  njs_date_prototype_properties[] =
{
    {
        .type = NJS_PROPERTY_HANDLER,
        .name = njs_string("__proto__"),
        .value = njs_prop_handler(njs_primitive_prototype_get_proto),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY_HANDLER,
        .name = njs_string("constructor"),
        .value = njs_prop_handler(njs_object_prototype_create_constructor),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("valueOf"),
        .value = njs_native_function(njs_date_prototype_value_of, 0),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("toString"),
        .value = njs_native_function2(njs_date_prototype_to_string, 0,
                                      NJS_DATE_FMT_TO_STRING),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("toDateString"),
        .value = njs_native_function2(njs_date_prototype_to_string, 0,
                                      NJS_DATE_FMT_TO_DATE_STRING),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("toTimeString"),
        .value = njs_native_function2(njs_date_prototype_to_string, 0,
                                      NJS_DATE_FMT_TO_TIME_STRING),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("toLocaleString"),
        .value = njs_native_function2(njs_date_prototype_to_string, 0,
                                      NJS_DATE_FMT_TO_STRING),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_long_string("toLocaleDateString"),
        .value = njs_native_function2(njs_date_prototype_to_string, 0,
                                      NJS_DATE_FMT_TO_DATE_STRING),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_long_string("toLocaleTimeString"),
        .value = njs_native_function2(njs_date_prototype_to_string, 0,
                                      NJS_DATE_FMT_TO_TIME_STRING),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("toUTCString"),
        .value = njs_native_function2(njs_date_prototype_to_string, 0,
                                      NJS_DATE_FMT_TO_UTC_STRING),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("toISOString"),
        .value = njs_native_function2(njs_date_prototype_to_string, 0,
                                      NJS_DATE_FMT_TO_ISO_STRING),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("toJSON"),
        .value = njs_native_function(njs_date_prototype_to_json, 1),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getTime"),
        .value = njs_native_function(njs_date_prototype_value_of, 0),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getFullYear"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_YR, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getUTCFullYear"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_YR, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getMonth"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_MON, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getUTCMonth"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_MON, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getDate"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_DAY, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getUTCDate"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_DAY, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getDay"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_WDAY, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getUTCDay"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_WDAY, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getHours"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_HR, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getUTCHours"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_HR, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getMinutes"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_MIN, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getUTCMinutes"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_MIN, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getSeconds"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_SEC, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("getUTCSeconds"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_SEC, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_long_string("getMilliseconds"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_MSEC, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_long_string("getUTCMilliseconds"),
        .value = njs_native_function2(njs_date_prototype_get_field, 0,
                                      njs_date_magic(NJS_DATE_MSEC, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_long_string("getTimezoneOffset"),
        .value = njs_native_function(njs_date_prototype_get_timezone_offset, 0),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setTime"),
        .value = njs_native_function(njs_date_prototype_set_time, 1),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_long_string("setMilliseconds"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 1,
                                      njs_date_magic2(NJS_DATE_MSEC, 1, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_long_string("setUTCMilliseconds"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 1,
                                      njs_date_magic2(NJS_DATE_MSEC, 1, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setSeconds"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 2,
                                      njs_date_magic2(NJS_DATE_SEC, 2, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setUTCSeconds"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 2,
                                      njs_date_magic2(NJS_DATE_SEC, 2, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setMinutes"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 3,
                                      njs_date_magic2(NJS_DATE_MIN, 3, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setUTCMinutes"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 3,
                                      njs_date_magic2(NJS_DATE_MIN, 3, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setHours"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 4,
                                      njs_date_magic2(NJS_DATE_HR, 4, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setUTCHours"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 4,
                                      njs_date_magic2(NJS_DATE_HR, 4, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setDate"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 1,
                                      njs_date_magic2(NJS_DATE_DAY, 1, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setUTCDate"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 1,
                                      njs_date_magic2(NJS_DATE_DAY, 1, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setMonth"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 2,
                                      njs_date_magic2(NJS_DATE_MON, 2, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setUTCMonth"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 2,
                                      njs_date_magic2(NJS_DATE_MON, 2, 0)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setFullYear"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 3,
                                      njs_date_magic2(NJS_DATE_YR, 3, 1)),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("setUTCFullYear"),
        .value = njs_native_function2(njs_date_prototype_set_fields, 3,
                                      njs_date_magic2(NJS_DATE_YR, 3, 0)),
        .writable = 1,
        .configurable = 1,
    },
};


const njs_object_init_t  njs_date_prototype_init = {
    njs_date_prototype_properties,
    njs_nitems(njs_date_prototype_properties),
};


const njs_object_type_init_t  njs_date_type_init = {
   .constructor = njs_native_ctor(njs_date_constructor, 7, 0),
   .constructor_props = &njs_date_constructor_init,
   .prototype_props = &njs_date_prototype_init,
   .prototype_value = { .object = { .type = NJS_OBJECT } },
};

