// Written in the D programming language.

/**
 * dateparse module.
 *
 * Copyright: Copyright Digital Mars 2000 - 2009.
 * License:   <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
 * Authors:   $(WEB digitalmars.com, Walter Bright)
 *
 *          Copyright Digital Mars 2000 - 2009.
 * Distributed under the Boost Software License, Version 1.0.
 *    (See accompanying file LICENSE_1_0.txt or copy at
 *          http://www.boost.org/LICENSE_1_0.txt)
 */
module std.dateparse;

private
{
    import std.string;
    import std.c.stdlib;
    import std.date;
}

//debug=dateparse;

class DateParseError : Error
{
    this(string s)
    {
        super("Invalid date string: " ~ s);
    }
}

struct DateParse
{
    void parse(string s, out Date date)
    {
        this = DateParse.init;

        //version (Win32)
            buffer = (cast(char *)alloca(s.length))[0 .. s.length];
        //else
            //buffer = new char[s.length];

        debug(dateparse) printf("DateParse.parse('%.*s')\n", s);
        if (!parseString(s))
        {
            goto Lerror;
        }

    /+
        if (year == year.init)
            year = 0;
        else
    +/
        debug(dateparse)
            printf("year = %d, month = %d, day = %d\n%02d:%02d:%02d.%03d\nweekday = %d, tzcorrection = %d\n",
                year, month, day,
                hours, minutes, seconds, ms,
                weekday, tzcorrection);
        if (
            year == year.init ||
            (month < 1 || month > 12) ||
            (day < 1 || day > 31) ||
            (hours < 0 || hours > 23) ||
            (minutes < 0 || minutes > 59) ||
            (seconds < 0 || seconds > 59) ||
            (tzcorrection != int.min &&
             ((tzcorrection < -2300 || tzcorrection > 2300) ||
              (tzcorrection % 10)))
            )
        {
         Lerror:
            throw new DateParseError(s);
        }

        if (ampm)
        {   if (hours > 12)
                goto Lerror;
            if (hours < 12)
            {
                if (ampm == 2)  // if P.M.
                    hours += 12;
            }
            else if (ampm == 1) // if 12am
            {
                hours = 0;              // which is midnight
            }
        }

//      if (tzcorrection != tzcorrection.init)
//          tzcorrection /= 100;

        if (year >= 0 && year <= 99)
            year += 1900;

        date.year = year;
        date.month = month;
        date.day = day;
        date.hour = hours;
        date.minute = minutes;
        date.second = seconds;
        date.ms = ms;
        date.weekday = weekday;
        date.tzcorrection = tzcorrection;
    }


private:
    int year = int.min; // our "nan" Date value
    int month;          // 1..12
    int day;            // 1..31
    int hours;          // 0..23
    int minutes;        // 0..59
    int seconds;        // 0..59
    int ms;             // 0..999
    int weekday;        // 1..7
    int ampm;           // 0: not specified
                        // 1: AM
                        // 2: PM
    int tzcorrection = int.min; // -1200..1200 correction in hours

    string s;
    int si;
    int number;
    char[] buffer;

    enum DP : byte
    {
        err,
        weekday,
        month,
        number,
        end,
        colon,
        minus,
        slash,
        ampm,
        plus,
        tz,
        dst,
        dsttz,
    }

    DP nextToken()
    {   int nest;
        uint c;
        int bi;
        DP result = DP.err;

        //printf("DateParse::nextToken()\n");
        for (;;)
        {
            assert(si <= s.length);
            if (si == s.length)
            {   result = DP.end;
                goto Lret;
            }
            //printf("\ts[%d] = '%c'\n", si, s[si]);
            switch (s[si])
            {
                case ':':       result = DP.colon; goto ret_inc;
                case '+':       result = DP.plus;  goto ret_inc;
                case '-':       result = DP.minus; goto ret_inc;
                case '/':       result = DP.slash; goto ret_inc;
                case '.':
                    version(DATE_DOT_DELIM)
                    {
                        result = DP.slash;
                        goto ret_inc;
                    }
                    else
                    {
                        si++;
                        break;
                    }

                ret_inc:
                    si++;
                    goto Lret;

                case ' ':
                case '\n':
                case '\r':
                case '\t':
                case ',':
                    si++;
                    break;

                case '(':               // comment
                    nest = 1;
                    for (;;)
                    {
                        si++;
                        if (si == s.length)
                            goto Lret;          // error
                        switch (s[si])
                        {
                            case '(':
                                nest++;
                                break;

                            case ')':
                                if (--nest == 0)
                                    goto Lendofcomment;
                                break;

                            default:
                                break;
                        }
                    }
                Lendofcomment:
                    si++;
                    break;

                default:
                    number = 0;
                    for (;;)
                    {
                        if (si == s.length)
                            // c cannot be undefined here
                            break;
                        c = s[si];
                        if (!(c >= '0' && c <= '9'))
                            break;
                        result = DP.number;
                        number = number * 10 + (c - '0');
                        si++;
                    }
                    if (result == DP.number)
                        goto Lret;

                    bi = 0;
                bufloop:
                    while (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')
                    {
                        if (c < 'a')            // if upper case
                            c += cast(uint)'a' - cast(uint)'A'; // to lower case
                        buffer[bi] = cast(char)c;
                        bi++;
                        do
                        {
                            si++;
                            if (si == s.length)
                                break bufloop;
                            c = s[si];
                        } while (c == '.');     // ignore embedded '.'s
                    }
                    result = classify(buffer[0 .. bi].idup);
                    goto Lret;
            }
        }
    Lret:
        //printf("-DateParse::nextToken()\n");
        return result;
    }

    DP classify(string buf)
    {
        struct DateID
        {
            string name;
            DP tok;
            short value;
        }

        static immutable DateID dateidtab[] =
        [
            {   "january",      DP.month,       1},
            {   "february",     DP.month,       2},
            {   "march",        DP.month,       3},
            {   "april",        DP.month,       4},
            {   "may",          DP.month,       5},
            {   "june",         DP.month,       6},
            {   "july",         DP.month,       7},
            {   "august",       DP.month,       8},
            {   "september",    DP.month,       9},
            {   "october",      DP.month,       10},
            {   "november",     DP.month,       11},
            {   "december",     DP.month,       12},
            {   "jan",          DP.month,       1},
            {   "feb",          DP.month,       2},
            {   "mar",          DP.month,       3},
            {   "apr",          DP.month,       4},
            {   "jun",          DP.month,       6},
            {   "jul",          DP.month,       7},
            {   "aug",          DP.month,       8},
            {   "sep",          DP.month,       9},
            {   "sept",         DP.month,       9},
            {   "oct",          DP.month,       10},
            {   "nov",          DP.month,       11},
            {   "dec",          DP.month,       12},

            {   "sunday",       DP.weekday,     1},
            {   "monday",       DP.weekday,     2},
            {   "tuesday",      DP.weekday,     3},
            {   "tues",         DP.weekday,     3},
            {   "wednesday",    DP.weekday,     4},
            {   "wednes",       DP.weekday,     4},
            {   "thursday",     DP.weekday,     5},
            {   "thur",         DP.weekday,     5},
            {   "thurs",        DP.weekday,     5},
            {   "friday",       DP.weekday,     6},
            {   "saturday",     DP.weekday,     7},

            {   "sun",          DP.weekday,     1},
            {   "mon",          DP.weekday,     2},
            {   "tue",          DP.weekday,     3},
            {   "wed",          DP.weekday,     4},
            {   "thu",          DP.weekday,     5},
            {   "fri",          DP.weekday,     6},
            {   "sat",          DP.weekday,     7},

            {   "am",           DP.ampm,                1},
            {   "pm",           DP.ampm,                2},

            {   "gmt",          DP.tz,          +000},
            {   "ut",           DP.tz,          +000},
            {   "utc",          DP.tz,          +000},
            {   "wet",          DP.tz,          +000},
            {   "z",            DP.tz,          +000},
            {   "wat",          DP.tz,          +100},
            {   "a",            DP.tz,          +100},
            {   "at",           DP.tz,          +200},
            {   "b",            DP.tz,          +200},
            {   "c",            DP.tz,          +300},
            {   "ast",          DP.tz,          +400},
            {   "d",            DP.tz,          +400},
            {   "est",          DP.tz,          +500},
            {   "e",            DP.tz,          +500},
            {   "cst",          DP.tz,          +600},
            {   "f",            DP.tz,          +600},
            {   "mst",          DP.tz,          +700},
            {   "g",            DP.tz,          +700},
            {   "pst",          DP.tz,          +800},
            {   "h",            DP.tz,          +800},
            {   "yst",          DP.tz,          +900},
            {   "i",            DP.tz,          +900},
            {   "ahst",         DP.tz,          +1000},
            {   "cat",          DP.tz,          +1000},
            {   "hst",          DP.tz,          +1000},
            {   "k",            DP.tz,          +1000},
            {   "nt",           DP.tz,          +1100},
            {   "l",            DP.tz,          +1100},
            {   "idlw",         DP.tz,          +1200},
            {   "m",            DP.tz,          +1200},

            {   "cet",          DP.tz,          -100},
            {   "fwt",          DP.tz,          -100},
            {   "met",          DP.tz,          -100},
            {   "mewt",         DP.tz,          -100},
            {   "swt",          DP.tz,          -100},
            {   "n",            DP.tz,          -100},
            {   "eet",          DP.tz,          -200},
            {   "o",            DP.tz,          -200},
            {   "bt",           DP.tz,          -300},
            {   "p",            DP.tz,          -300},
            {   "zp4",          DP.tz,          -400},
            {   "q",            DP.tz,          -400},
            {   "zp5",          DP.tz,          -500},
            {   "r",            DP.tz,          -500},
            {   "zp6",          DP.tz,          -600},
            {   "s",            DP.tz,          -600},
            {   "wast",         DP.tz,          -700},
            {   "t",            DP.tz,          -700},
            {   "cct",          DP.tz,          -800},
            {   "u",            DP.tz,          -800},
            {   "jst",          DP.tz,          -900},
            {   "v",            DP.tz,          -900},
            {   "east",         DP.tz,          -1000},
            {   "gst",          DP.tz,          -1000},
            {   "w",            DP.tz,          -1000},
            {   "x",            DP.tz,          -1100},
            {   "idle",         DP.tz,          -1200},
            {   "nzst",         DP.tz,          -1200},
            {   "nzt",          DP.tz,          -1200},
            {   "y",            DP.tz,          -1200},

            {   "bst",          DP.dsttz,       000},
            {   "adt",          DP.dsttz,       +400},
            {   "edt",          DP.dsttz,       +500},
            {   "cdt",          DP.dsttz,       +600},
            {   "mdt",          DP.dsttz,       +700},
            {   "pdt",          DP.dsttz,       +800},
            {   "ydt",          DP.dsttz,       +900},
            {   "hdt",          DP.dsttz,       +1000},
            {   "mest",         DP.dsttz,       -100},
            {   "mesz",         DP.dsttz,       -100},
            {   "sst",          DP.dsttz,       -100},
            {   "fst",          DP.dsttz,       -100},
            {   "wadt",         DP.dsttz,       -700},
            {   "eadt",         DP.dsttz,       -1000},
            {   "nzdt",         DP.dsttz,       -1200},

            {   "dst",          DP.dst,         0},
        ];

        //message(DTEXT("DateParse::classify('%s')\n"), buf);

        // Do a linear search. Yes, it would be faster with a binary
        // one.
        for (uint i = 0; i < dateidtab.length; i++)
        {
            if (std.string.cmp(dateidtab[i].name, buf) == 0)
            {
                number = dateidtab[i].value;
                return dateidtab[i].tok;
            }
        }
        return DP.err;
    }

    int parseString(string s)
    {
        int n1;
        int dp;
        int sisave;
        int result;

        //message(DTEXT("DateParse::parseString('%ls')\n"), s);
        this.s = s;
        si = 0;
        dp = nextToken();
        for (;;)
        {
            //message(DTEXT("\tdp = %d\n"), dp);
            switch (dp)
            {
            case DP.end:
                result = 1;
            Lret:
                return result;

            case DP.err:
            case_error:
                //message(DTEXT("\terror\n"));
            default:
                result = 0;
                goto Lret;

            case DP.minus:
                break;                  // ignore spurious '-'

            case DP.weekday:
                weekday = number;
                break;

            case DP.month:              // month day, [year]
                month = number;
                dp = nextToken();
                if (dp == DP.number)
                {
                    day = number;
                    sisave = si;
                    dp = nextToken();
                    if (dp == DP.number)
                    {
                        n1 = number;
                        dp = nextToken();
                        if (dp == DP.colon)
                        {   // back up, not a year
                            si = sisave;
                        }
                        else
                        {   year = n1;
                            continue;
                        }
                        break;
                    }
                }
                continue;

            case DP.number:
                n1 = number;
                dp = nextToken();
                switch (dp)
                {
                case DP.end:
                    year = n1;
                    break;

                case DP.minus:
                case DP.slash:  // n1/ ? ? ?
                    dp = parseCalendarDate(n1);
                    if (dp == DP.err)
                        goto case_error;
                    break;

                case DP.colon:  // hh:mm [:ss] [am | pm]
                    dp = parseTimeOfDay(n1);
                    if (dp == DP.err)
                        goto case_error;
                    break;

                case DP.ampm:
                    hours = n1;
                    minutes = 0;
                    seconds = 0;
                    ampm = number;
                    break;

                case DP.month:
                    day = n1;
                    month = number;
                    dp = nextToken();
                    if (dp == DP.number)
                    {   // day month year
                        year = number;
                        dp = nextToken();
                    }
                    break;

                default:
                    year = n1;
                    break;
                }
                continue;
            }
            dp = nextToken();
        }
        // @@@ bug in the compiler: this is never reachable
        assert(0);
    }

    int parseCalendarDate(int n1)
    {
        int n2;
        int n3;
        int dp;

        debug(dateparse) printf("DateParse.parseCalendarDate(%d)\n", n1);
        dp = nextToken();
        if (dp == DP.month)     // day/month
        {
            day = n1;
            month = number;
            dp = nextToken();
            if (dp == DP.number)
            {   // day/month year
                year = number;
                dp = nextToken();
            }
            else if (dp == DP.minus || dp == DP.slash)
            {   // day/month/year
                dp = nextToken();
                if (dp != DP.number)
                    goto case_error;
                year = number;
                dp = nextToken();
            }
            return dp;
        }
        if (dp != DP.number)
            goto case_error;
        n2 = number;
        //message(DTEXT("\tn2 = %d\n"), n2);
        dp = nextToken();
        if (dp == DP.minus || dp == DP.slash)
        {
            dp = nextToken();
            if (dp != DP.number)
                goto case_error;
            n3 = number;
            //message(DTEXT("\tn3 = %d\n"), n3);
            dp = nextToken();

            // case1: year/month/day
            // case2: month/day/year
            int case1, case2;

            case1 = (n1 > 12 ||
                     (n2 >= 1 && n2 <= 12) &&
                     (n3 >= 1 && n3 <= 31));
            case2 = ((n1 >= 1 && n1 <= 12) &&
                     (n2 >= 1 && n2 <= 31) ||
                     n3 > 31);
            if (case1 == case2)
                goto case_error;
            if (case1)
            {
                year = n1;
                month = n2;
                day = n3;
            }
            else
            {
                month = n1;
                day = n2;
                year = n3;
            }
        }
        else
        {   // must be month/day
            month = n1;
            day = n2;
        }
        return dp;

    case_error:
        return DP.err;
    }

    int parseTimeOfDay(int n1)
    {
        int dp;
        int sign;

        // 12am is midnight
        // 12pm is noon

        //message(DTEXT("DateParse::parseTimeOfDay(%d)\n"), n1);
        hours = n1;
        dp = nextToken();
        if (dp != DP.number)
            goto case_error;
        minutes = number;
        dp = nextToken();
        if (dp == DP.colon)
        {
            dp = nextToken();
            if (dp != DP.number)
                goto case_error;
            seconds = number;
            dp = nextToken();
        }
        else
            seconds = 0;

        if (dp == DP.ampm)
        {
            ampm = number;
            dp = nextToken();
        }
        else if (dp == DP.plus || dp == DP.minus)
        {
        Loffset:
            sign = (dp == DP.minus) ? -1 : 1;
            dp = nextToken();
            if (dp != DP.number)
                goto case_error;
            tzcorrection = -sign * number;
            dp = nextToken();
        }
        else if (dp == DP.tz)
        {
            tzcorrection = number;
            dp = nextToken();
            if (number == 0 && (dp == DP.plus || dp == DP.minus))
                goto Loffset;
            if (dp == DP.dst)
            {   tzcorrection += 100;
                dp = nextToken();
            }
        }
        else if (dp == DP.dsttz)
        {
            tzcorrection = number;
            dp = nextToken();
        }

        return dp;

    case_error:
        return DP.err;
    }

}

unittest
{
    DateParse dp;
    Date d;

    dp.parse("March 10, 1959 12:00 -800", d);
    assert(d.year         == 1959);
    assert(d.month        == 3);
    assert(d.day          == 10);
    assert(d.hour         == 12);
    assert(d.minute       == 0);
    assert(d.second       == 0);
    assert(d.ms           == 0);
    assert(d.weekday      == 0);
    assert(d.tzcorrection == 800);

    dp.parse("Tue Apr 02 02:04:57 GMT-0800 1996", d);
    assert(d.year         == 1996);
    assert(d.month        == 4);
    assert(d.day          == 2);
    assert(d.hour         == 2);
    assert(d.minute       == 4);
    assert(d.second       == 57);
    assert(d.ms           == 0);
    assert(d.weekday      == 3);
    assert(d.tzcorrection == 800);

    dp.parse("March 14, -1980 21:14:50", d);
    assert(d.year         == 1980);
    assert(d.month        == 3);
    assert(d.day          == 14);
    assert(d.hour         == 21);
    assert(d.minute       == 14);
    assert(d.second       == 50);
    assert(d.ms           == 0);
    assert(d.weekday      == 0);
    assert(d.tzcorrection == int.min);

    dp.parse("Tue Apr 02 02:04:57 1996", d);
    assert(d.year         == 1996);
    assert(d.month        == 4);
    assert(d.day          == 2);
    assert(d.hour         == 2);
    assert(d.minute       == 4);
    assert(d.second       == 57);
    assert(d.ms           == 0);
    assert(d.weekday      == 3);
    assert(d.tzcorrection == int.min);

    dp.parse("Tue, 02 Apr 1996 02:04:57 G.M.T.", d);
    assert(d.year         == 1996);
    assert(d.month        == 4);
    assert(d.day          == 2);
    assert(d.hour         == 2);
    assert(d.minute       == 4);
    assert(d.second       == 57);
    assert(d.ms           == 0);
    assert(d.weekday      == 3);
    assert(d.tzcorrection == 0);

    dp.parse("December 31, 3000", d);
    assert(d.year         == 3000);
    assert(d.month        == 12);
    assert(d.day          == 31);
    assert(d.hour         == 0);
    assert(d.minute       == 0);
    assert(d.second       == 0);
    assert(d.ms           == 0);
    assert(d.weekday      == 0);
    assert(d.tzcorrection == int.min);

    dp.parse("Wed, 31 Dec 1969 16:00:00 GMT", d);
    assert(d.year         == 1969);
    assert(d.month        == 12);
    assert(d.day          == 31);
    assert(d.hour         == 16);
    assert(d.minute       == 0);
    assert(d.second       == 0);
    assert(d.ms           == 0);
    assert(d.weekday      == 4);
    assert(d.tzcorrection == 0);

    dp.parse("1/1/1999 12:30 AM", d);
    assert(d.year         == 1999);
    assert(d.month        == 1);
    assert(d.day          == 1);
    assert(d.hour         == 0);
    assert(d.minute       == 30);
    assert(d.second       == 0);
    assert(d.ms           == 0);
    assert(d.weekday      == 0);
    assert(d.tzcorrection == int.min);

    dp.parse("Tue, 20 May 2003 15:38:58 +0530", d);
    assert(d.year         == 2003);
    assert(d.month        == 5);
    assert(d.day          == 20);
    assert(d.hour         == 15);
    assert(d.minute       == 38);
    assert(d.second       == 58);
    assert(d.ms           == 0);
    assert(d.weekday      == 3);
    assert(d.tzcorrection == -530);

    debug(dateparse) printf("year = %d, month = %d, day = %d\n%02d:%02d:%02d.%03d\nweekday = %d, tzcorrection = %d\n",
        d.year, d.month, d.day,
        d.hour, d.minute, d.second, d.ms,
        d.weekday, d.tzcorrection);
}
