/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil -*-
 *
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * The Original Code is the Grendel mail/news client.
 *
 * The Initial Developer of the Original Code is Netscape Communications
 * Corporation.  Portions created by Netscape are
 * Copyright (C) 1997 Netscape Communications Corporation. All
 * Rights Reserved.
 *
 * Contributor(s): 
 *
 * Created: Jamie Zawinski <jwz@netscape.com>,  3 Aug 1995.
 * Ported from C on 19 Aug 1997.
 *
 * Contributors: Edwin Woudt <edwin@woudt.nl>
 */

package calypso.util;

import java.util.Date;
import java.util.TimeZone;
import calypso.util.ByteBuf;

/** Parses a date out of a string of bytes.  Call it like this:

    <P><UL>
        <TT>ByteBuf bytes = </TT>...<TT> ;</TT><BR>
        <TT>Date date = NetworkDate.parseDate(bytes);</TT>
    </UL>

    <P> Note that it operates on bytes, not chars, since network
    dates are always ASCII.

    <P> Why would you want to use this instead of
    <TT>java.text.DateFormat.parse()</TT>?  Because this algorithm has been
    tested in the field against real-world message headers for several
    years (the C code hadn't changed substantively since Netscape 2.0.)
    (There had been DST-related problems, but the tokenizer/parser was
    always sound.)

    @see Date
    @see Calendar
    @see java.text.DateFormat
 */

public class NetworkDate {

  private NetworkDate() { }

  static private final int UNKNOWN = 0;     // random unknown token

  static private final int SUN =    101;    // days of the week
  static private final int MON =    102;
  static private final int TUE =    103;
  static private final int WED =    104;
  static private final int THU =    105;
  static private final int FRI =    107;
  static private final int SAT =    108;

  static private final int JAN =    201;    // months
  static private final int FEB =    202;
  static private final int MAR =    203;
  static private final int APR =    204;
  static private final int MAY =    205;
  static private final int JUN =    206;
  static private final int JUL =    207;
  static private final int AUG =    208;
  static private final int SEP =    209;
  static private final int OCT =    210;
  static private final int NOV =    211;
  static private final int DEC =    212;

  static private final int PST =    301;    // a smattering of timezones
  static private final int PDT =    302;
  static private final int MST =    303;
  static private final int MDT =    304;
  static private final int CST =    305;
  static private final int CDT =    306;
  static private final int EST =    307;
  static private final int EDT =    308;
  static private final int AST =    309;
  static private final int NST =    310;
  static private final int GMT =    311;
  static private final int BST =    312;
  static private final int MET =    313;
  static private final int EET =    314;
  static private final int JST =    315;

  /** This parses a time/date string into a Time object.
   If it can't be parsed, -1 is returned.

   <P>Many formats are handled, including:

   <P><UL>
   <LI>  <TT> 14 Apr 89 03:20:12                </TT>
   <LI>  <TT> 14 Apr 89 03:20 GMT               </TT>
   <LI>  <TT> Fri, 17 Mar 89 4:01:33            </TT>
   <LI>  <TT> Fri, 17 Mar 89 4:01 GMT           </TT>
   <LI>  <TT> Mon Jan 16 16:12 PDT 1989         </TT>
   <LI>  <TT> Mon Jan 16 16:12 +0130 1989       </TT>
   <LI>  <TT> 6 May 1992 16:41-JST (Wednesday)  </TT>
   <LI>  <TT> 22-AUG-1993 10:59:12.82           </TT>
   <LI>  <TT> 22-AUG-1993 10:59pm               </TT>
   <LI>  <TT> 22-AUG-1993 12:59am               </TT>
   <LI>  <TT> 22-AUG-1993 12:59 PM              </TT>
   <LI>  <TT> Friday, August 04, 1995 3:54 PM   </TT>
   <LI>  <TT> 06/21/95 04:24:34 PM              </TT>
   <LI>  <TT> 20/06/95 21:07                    </TT>
   <LI>  <TT> 95-06-08 19:32:48 EDT             </TT>
   </UL>

  <P> But note that <TT>6/5/95</TT> is ambiguous, since it's not obvious
  which is the day and which is the month.  (<TT>6/13/95</TT> is not
  ambiguous, however.)

  @param buf                The bytes to parse.  This assumes the input to be
                            a sequence of 8-bit ASCII characters.  High-bit
                            characters are handled; 16-bit Unicode characters
                            are not.

  @param default_to_gmt     If the input string doesn't contain a description
    of the timezone, then this argument determines whether the string is
    interpreted relative to the local time zone (false) or to GMT (true).
    The correct value to pass in for this argument depends on what standard
    specified the time string which you are parsing; for RFC822 dates, this
    argument should be true.

  @return                   Microseconds since Jan 1, 1900, GMT.
                            You can pass this to <TT>new&nbsp;Date(long)</TT>.
                            Returns -1 if the string is unparsable
                            (no other negative value will ever be returned;
                            dates before the Epoch are not handled.)
  */

  public static long parseLong(ByteBuf buf, boolean default_to_gmt) {
    return parseLong(buf.toBytes(), 0, buf.length(), default_to_gmt);
  }

  /** The same, but takes a byte array and a region within it, instead
      of a ByteBuf. */
  public static long parseLong(byte string[], int start, int end,
                               boolean default_to_gmt) {

    int dotw  = UNKNOWN;
    int month = UNKNOWN;
    int zone  = UNKNOWN;

    int zone_offset = -1;
    int date = -1;
    int year = -1;
    int hour = -1;
    int min  = -1;
    int sec  = -1;

    int i = start;
    while (i < end) {

      byte c = string[i];
      byte c2 = ((i+1 < end) ? string[i+1] : 0);
      byte c3 = ((i+2 < end) ? string[i+2] : 0);

      switch (c) {
      case (byte)'a': case (byte)'A':
        if (month == UNKNOWN &&
            (c2 == 'p' || c2 == 'P') &&
            (c3 == 'r' || c3 == 'R'))
          month = APR;
        else if (zone == UNKNOWN &&
                 (c2 == 's' || c2 == 's') &&
                 (c3 == 't' || c3 == 'T'))
          zone = AST;
        else if (month == UNKNOWN &&
                 (c2 == 'u' || c2 == 'U') &&
                 (c3 == 'g' || c3 == 'G'))
          month = AUG;
        break;
      case (byte)'b': case (byte)'B':
        if (zone == UNKNOWN &&
            (c2 == 's' || c2 == 'S') &&
            (c3 == 't' || c3 == 'T'))
          zone = BST;
        break;
      case (byte)'c': case (byte)'C':
        if (zone == UNKNOWN &&
            (c2 == 'd' || c2 == 'D') &&
            (c3 == 't' || c3 == 'T'))
          zone = CDT;
        else if (zone == UNKNOWN &&
                 (c2 == 's' || c2 == 'S') &&
                 (c3 == 't' || c3 == 'T'))
          zone = CST;
        break;
      case (byte)'d': case (byte)'D':
        if (month == UNKNOWN &&
            (c2 == 'e' || c2 == 'E') &&
            (c3 == 'c' || c3 == 'C'))
          month = DEC;
        break;
      case (byte)'e': case (byte)'E':
        if (zone == UNKNOWN &&
            (c2 == 'd' || c2 == 'D') &&
            (c3 == 't' || c3 == 'T'))
          zone = EDT;
        else if (zone == UNKNOWN &&
                 (c2 == 'e' || c2 == 'E') &&
                 (c3 == 't' || c3 == 'T'))
          zone = EET;
        else if (zone == UNKNOWN &&
                 (c2 == 's' || c2 == 'S') &&
                 (c3 == 't' || c3 == 'T'))
          zone = EST;
        break;
      case (byte)'f': case (byte)'F':
        if (month == UNKNOWN &&
            (c2 == 'e' || c2 == 'E') &&
            (c3 == 'b' || c3 == 'B'))
          month = FEB;
        else if (dotw == UNKNOWN &&
                 (c2 == 'r' || c2 == 'R') &&
                 (c3 == 'i' || c3 == 'I'))
          dotw = FRI;
        break;
      case (byte)'g': case (byte)'G':
        if (zone == UNKNOWN &&
            (c2 == 'm' || c2 == 'M') &&
            (c3 == 't' || c3 == 'T'))
          zone = GMT;
        break;
      case (byte)'j': case (byte)'J':
        if (month == UNKNOWN &&
            (c2 == 'a' || c2 == 'A') &&
            (c3 == 'n' || c3 == 'N'))
          month = JAN;
        else if (zone == UNKNOWN &&
                 (c2 == 's' || c2 == 'S') &&
                 (c3 == 't' || c3 == 'T'))
          zone = JST;
        else if (month == UNKNOWN &&
                 (c2 == 'u' || c2 == 'U') &&
                 (c3 == 'l' || c3 == 'L'))
          month = JUL;
        else if (month == UNKNOWN &&
                 (c2 == 'u' || c2 == 'U') &&
                 (c3 == 'n' || c3 == 'N'))
          month = JUN;
        break;
      case (byte)'m': case (byte)'M':
        if (month == UNKNOWN &&
            (c2 == 'a' || c2 == 'A') &&
            (c3 == 'r' || c3 == 'R'))
          month = MAR;
        else if (month == UNKNOWN &&
                 (c2 == 'a' || c2 == 'A') &&
                 (c3 == 'y' || c3 == 'Y'))
          month = MAY;
        else if (zone == UNKNOWN &&
                 (c2 == 'd' || c2 == 'D') &&
                 (c3 == 't' || c3 == 'T'))
          zone = MDT;
        else if (zone == UNKNOWN &&
                 (c2 == 'e' || c2 == 'E') &&
                 (c3 == 't' || c3 == 'T'))
          zone = MET;
        else if (dotw == UNKNOWN &&
                 (c2 == 'o' || c2 == 'O') &&
                 (c3 == 'n' || c3 == 'N'))
          dotw = MON;
        else if (zone == UNKNOWN &&
                 (c2 == 's' || c2 == 'S') &&
                 (c3 == 't' || c3 == 'T'))
          zone = MST;
        break;
      case (byte)'n': case (byte)'N':
        if (month == UNKNOWN &&
            (c2 == 'o' || c2 == 'O') &&
            (c3 == 'v' || c3 == 'V'))
          month = NOV;
        else if (zone == UNKNOWN &&
                 (c2 == 's' || c2 == 'S') &&
                 (c3 == 't' || c3 == 'T'))
          zone = NST;
        break;
      case (byte)'o': case (byte)'O':
        if (month == UNKNOWN &&
            (c2 == 'c' || c2 == 'C') &&
            (c3 == 't' || c3 == 'T'))
          month = OCT;
        break;
      case (byte)'p': case (byte)'P':
        if (zone == UNKNOWN &&
            (c2 == 'd' || c2 == 'D') &&
            (c3 == 't' || c3 == 'T'))
          zone = PDT;
        else if (zone == UNKNOWN &&
                 (c2 == 's' || c2 == 'S') &&
                 (c3 == 't' || c3 == 'T'))
          zone = PST;
        break;
      case (byte)'s': case (byte)'S':
        if (dotw == UNKNOWN &&
            (c2 == 'a' || c2 == 'A') &&
            (c3 == 't' || c3 == 'T'))
          dotw = SAT;
        else if (month == UNKNOWN &&
                 (c2 == 'e' || c2 == 'E') &&
                 (c3 == 'p' || c3 == 'P'))
          month = SEP;
        else if (dotw == UNKNOWN &&
                 (c2 == 'u' || c2 == 'U') &&
                 (c3 == 'n' || c3 == 'N'))
          dotw = SUN;
        break;
      case (byte)'t': case (byte)'T':
        if (dotw == UNKNOWN &&
            (c2 == 'h' || c2 == 'H') &&
            (c3 == 'u' || c3 == 'U'))
          dotw = THU;
        else if (dotw == UNKNOWN &&
                 (c2 == 'u' || c2 == 'U') &&
                 (c3 == 'e' || c3 == 'E'))
          dotw = TUE;
        break;
      case (byte)'u': case (byte)'U':
        if (zone == UNKNOWN &&
            (c2 == 't' || c2 == 'T') &&
            !(c3 >= 'A' && c3 <= 'Z') &&
            !(c3 >= 'a' && c3 <= 'z'))
          // UT is the same as GMT but UTx is not.
          zone = GMT;
        break;
      case (byte)'w': case (byte)'W':
        if (dotw == UNKNOWN &&
            (c2 == 'e' || c2 == 'E') &&
            (c3 == 'd' || c3 == 'D'))
          dotw = WED;
        break;



        // parsing timezone offsets

      case (byte)'+': case (byte)'-': {

        if (zone_offset >= 0) {
          // already got one...
          i++;
          break;
        }

        if (zone != UNKNOWN && zone != GMT) {
          // GMT+0300 is legal, but PST+0300 is not.
          i++;
          break;
        }

        int sign = ((c == '+') ? 1 : -1);
        i++; /* move over sign */

        int token_start = i;
        int token_end;

        for (token_end = token_start; token_end < end; token_end++) {
          c = string[token_end];
          if (c < '0' || c > '9')
            break;
        }

        if ((token_end - token_start) == 4)
          // GMT+0000: offset in HHMM
          zone_offset = (((((string[token_start]-'0') * 10) +
                           (string[token_start+1]-'0'))
                          * 60) +
                         (((string[token_start+2]-'0') * 10) +
                          (string[token_start+3]-'0')));
        else if ((token_end - token_start) == 2)
          // GMT+00: offset in hours
          zone_offset = (((string[token_start]-'0') * 10) +
                         (string[token_start+1]-'0')) * 60;
        else if ((token_end - token_start) == 1)
          // GMT+0: offset in hours
          zone_offset = (string[token_start]-'0') * 60;
        else
          /* three digits, or more than 4: unrecognised. */
          break;

        zone_offset *= sign;
        zone = GMT;
        break;
      }


      // parsing numeric tokens

      case (byte)'0': case (byte)'1': case (byte)'2': case (byte)'3': 
      case (byte)'4': case (byte)'5': case (byte)'6': case (byte)'7': 
      case (byte)'8': case (byte)'9': {

        int tmp_hour = -1;
        int tmp_min = -1;
        int tmp_sec = -1;

        int token_start = i;
        int token_end;

        // move token_end to the first non-digit, or end of string.
        for (token_end = token_start; token_end < end; token_end++) {
          c = string[token_end];
          if (c < '0' || c > '9')
            break;
        }

        if (token_end < end && c == ':') {
          // If there's a colon, this is an hour/minute specification.
          // If we've already set hour or min, ignore this one.
          if (hour > 0 && min > 0)
            break;

          // We have seen "[0-9]+:", so this is probably HH:MM[:SS]

          if ((token_end - token_start) == 2)
            // two digits then a colon -- it is the hour.
            tmp_hour = ((string[token_start]-'0') * 10 +
                        (string[token_start+1]-'0'));
          else if ((token_end - token_start) == 1)
            // one digit then a colon -- it is the hour.
            tmp_hour = (string[token_start]-'0');
          else
            break;

          // Got the hour; move over the colon, and parse the minutes.

          // move token_start just past the colon.
          token_start = token_end+1;
          if (token_start >= end)
            break;

          // move token_end to the first non-digit, or end of string.
          for(token_end = token_start; token_end < end; token_end++) {
            c = string[token_end];
            if (c < '0' || c > '9')
              break;
          }

          if ((token_end - token_start) == 2)
            // two digits followed by a colon or EOS -- it is the minutes.
            tmp_min = ((string[token_start]-'0') * 10 +
                       (string[token_start+1]-'0'));
          else if ((token_end - token_start) == 1)
            // one digit followed by a colon or EOS -- it is the minutes.
            tmp_min = (string[token_start]-'0');
          else
            break;

          // Got the hour and minutes; move over the colon, and try to
          // parse the seconds.

          token_start = token_end;
          if (token_end < end && string[token_end] == ':') {
            // move token_start just past the colon.
            token_start++;

            // move token_end to the first non-digit, or end of string.
            for(token_end = token_start; token_end < end; token_end++) {
              c = string[token_end];
              if (c < '0' || c > '9')
                break;
            }

            if ((token_end - token_start) == 2)
              // two digits followed by EOT -- it is the seconds.
              tmp_sec = ((string[token_start]-'0') * 10 +
                         (string[token_start+1]-'0'));
            else if ((token_end - token_start) == 1)
              // one digit followed by EOT -- it is the seconds.
              tmp_sec = (string[token_start]-'0');
          }

          // If we made it here, we've parsed hour and min,
          // and possibly sec, so it worked as a unit.

          // skip over whitespace and see if there's an AM or PM
          // directly following the time.
          if (token_end < end && tmp_hour <= 12) {

            for(; token_start < end; token_start++) {
              c = string[token_start];
              if (c < '0' || c > '9')
                break;
            }

            while (token_start < end &&
                   string[token_start] <= ' ')
              token_start++;

            if (token_start+1 < end) {
              if ((string[token_start] == 'p' ||
                   string[token_start] == 'P') &&
                  (string[token_start+1] == 'm' ||
                   string[token_start+1] == 'M'))
                // 10:05pm == 22:05, and 12:05pm == 12:05
                tmp_hour = (tmp_hour == 12 ? 12 : tmp_hour + 12);
              else if (tmp_hour == 12 &&
                       (string[token_start] == 'a' ||
                        string[token_start] == 'A') &&
                       (string[token_start+1] == 'm' ||
                        string[token_start+1] == 'M'))
                // 12:05am == 00:05
                tmp_hour = 0;
            }
          }

          // if we made it all the way to here, we can keep these values.
          // (We might have bugged out after parsing hour and before
          // parsing min, and it would be bad to take one and not the
          // other.)
          hour = tmp_hour;
          min = tmp_min;
          sec = tmp_sec;
          i = token_end-1;
          break;
        }

        else if (token_end+1 < end &&
                 (c == '/' || c == '-') &&
                 (string[token_end+1] >= '0' &&
                  string[token_end+1] <= '9')) {
          // Perhaps this is 6/16/95, 16/6/95, 6-16-95, or 16-6-95
          // or even 95-06-05...
          //   #### But it doesn't handle 1995-06-22.
          //
          int n1, n2, n3;

          if (month != UNKNOWN)
            // if we saw a month name, this can't be.
            break;

          token_start = i;

          // get the first one or two digits

          n1 = (string[token_start] - '0');
          token_start++;
          if (token_start+1 < end &&
              string[token_start] >= '0' &&
              string[token_start] <= '9') {
            n1 = (n1*10) + (string[token_start] - '0');
            token_start++;
          }

          // demand that the next char be / or -.

          if (token_start >= end-1 ||
              (string[token_start] != '/' &&
               string[token_start] != '-'))
            break;
          token_start++;

          // demand that the next char be a digit.

          if (string[token_start] < '0' ||
              string[token_start] > '9')
            break;

          // get the second group of one or two digits

          n2 = (string[token_start] - '0');
          token_start++;
          if (token_start+1 < end &&
              string[token_start] >= '0' &&
              string[token_start] <= '9') {
            n2 = (n2*10) + (string[token_start] - '0');
            token_start++;
          }

          // demand that the next char be / or -.

          if (token_start >= end-1 ||
              (string[token_start] != '/' &&
               string[token_start] != '-'))
            break;
          token_start++;

          // demand that the next char be a digit.

          if (string[token_start] < '0' ||
              string[token_start] > '9')
            break;

          // get the third group of one, two, or four digits

          n3 = (string[token_start] - '0');
          token_start++;

          if (token_start+1 <= end &&
              string[token_start] >= '0' &&
              string[token_start] <= '9') {
            // digit two
            n3 = (n3*10) + (string[token_start] - '0');
            token_start++;

            // digit three
            if (token_start+1 < end &&
                string[token_start] >= '0' &&
                string[token_start] <= '9') {

              // if we have digit three but not digit four, give up on it:
              // there are no three-digit-numbers in this context.
              if (token_start+2 > end ||
                  (string[token_start+1] < '0' &&
                   string[token_start+1] > '9'))
                break;

              n3 = (n3*10) + (string[token_start++] - '0');
              n3 = (n3*10) + (string[token_start++] - '0');
            }
          }

          // if the digits are followed by an alphanumeric, give up on it.
          if (token_start < end &&
              ((string[token_start] >= '0' &&
                string[token_start] <= '9') ||
               (string[token_start] >= 'a' &&
                string[token_start] <= 'z') ||
               (string[token_start] >= 'A' &&
                string[token_start] <= 'Z')))
            break;


          // At this point, we've parsed three 1-2 digit numbers, with / or -
          // between them.  Now decide what the hell they are (DD/MM/YY or
          // MM/DD/YY or YY/MM/DD.)

          if (n1 > 70) {    // must be YY/MM/DD
            if (n2 > 12) break;
            if (n3 > 31) break;
            year = n1;
            if (year < 1900) year += 1900;
            month = (n2 + JAN - 1);
            date = n3;
            i = token_start;
            break;
          }

          if (n3 < 70 ||                // before epoch - can't represent it.
              (n1 > 12 && n2 > 12)) {   // illegal
            i = token_start;
            break;
          }

          if (n3 < 1900) n3 += 1900;

          if (n1 > 12) {        // must be DD/MM/YY
            date = n1;
            month = (n2 + JAN - 1);
            year = n3;
          } else {          // assume MM/DD/YY
            // #### In the ambiguous case, should we consult the locale to
            // find out the local default?
            month = (n1 + JAN - 1);
            date = n2;
            year = n3;
          }

          i = token_start;
        }

        else if (token_end+1 < end &&
                 (string[token_end] >= 'A' ||
                  string[token_end] <= 'Z') &&
                 (string[token_end+1] >= 'a' &&
                  string[token_end+1] <= 'z')) {
          // Digits followed by non-punctuation - what's that?
        }

        else if ((token_end - token_start) == 4) {
          // four digits in a row must be a year.
          if (year < 0)
            year = ((string[i]-'0')   * 1000 +
                    (string[i+1]-'0') * 100 +
                    (string[i+2]-'0') * 10 +
                    (string[i+3]-'0'));
        }

        else if ((token_end - token_start) == 2) {
          // two digits in a row might be a date, or a year

          int n = ((string[i]-'0') * 10 +
                   (string[i+1]-'0'));
          // If we don't have a date (day of the month) and we see a number
          // less than 32, then assume that is the date.
          //
          // Otherwise, if we have a date and not a year, assume this is the
          // year.  If it is less than 70, then assume it refers to the 21st
          // century.  If it is two digits (>= 70), assume it refers to the
          // 20th century.  Otherwise, assume it refers to an unambiguous year.
          //
          // The world will surely end soon.
          //
          if (date < 0 && n < 32)
            date = n;
          else if (year < 0) {
            if (n < 70)
              year = 2000 + n;
            else if (n < 100)
              year = 1900 + n;
            else
              year = n;
          }
        }

        else if ((token_end - token_start) == 1) {
          // one digit all alone -- it must be a date.
          if (date < 0)
            date = string[i]-'0';
        }

        else {
          // else, three or more than four digits - what's that?
          break;
        }
      }
      } // closes switch

      // Skip to the end of this token, whether we parsed it or not.
      // Tokens are delimited by whitespace, or ,;-/
      // But explicitly not :+-.

      for (; i < end; i++) {
        c = string[i];
        if (c <= ' ' || c == ',' || c == ';' || c == '-' || c == '+' ||
            c == '/' || c == '(' || c == ')' || c == '[' || c == ']')
          break;
      }

      boolean done = false;
      while (!done) {
        done = true;
        // skip over uninteresting chars before the next token.
        for (; i < end; i++) {
          c = string[i];
          if (! (c <= ' ' || c == ',' || c == ';' || c == '/' ||
                 c == '(' || c == ')' || c == '[' || c == ']'))
            break;
        }

        // "-" is ignored at the beginning of a token if we have not yet
        // parsed a year, or if the character after the dash is not a digit.
        if (i < end &&
            c == '-' &&
            (year < 0 ||
             string[i+1] < '0' ||
             string[i+1] > '9')) {
          i++;
          done = false;
        }
      }
    }

    // done parsing string -- turn symbolic zones into numeric offsets.

    if (zone != UNKNOWN && zone_offset == -1) {
      switch (zone) {
      case PST: zone_offset = -8 * 60; break;
      case PDT: zone_offset = -7 * 60; break;
      case MST: zone_offset = -7 * 60; break;
      case MDT: zone_offset = -6 * 60; break;
      case CST: zone_offset = -6 * 60; break;
      case CDT: zone_offset = -5 * 60; break;
      case EST: zone_offset = -5 * 60; break;
      case EDT: zone_offset = -4 * 60; break;
      case AST: zone_offset = -4 * 60; break;
      case NST: zone_offset = -3 * 60 - 30; break;
      case GMT: zone_offset =  0 * 60; break;
      case BST: zone_offset =  1 * 60; break;
      case MET: zone_offset =  1 * 60; break;
      case EET: zone_offset =  2 * 60; break;
      case JST: zone_offset =  9 * 60; break;
      }
    }

    // If we didn't find a year, month, or day-of-the-month, we can't
    // possibly parse this, so just give up.  (In fact, in the Unix C version
    // of this code, I'd seen mktime() do something random -- it always
    // returned "Tue Feb  5 06:28:16 2036", which is no doubt a
    // numerologically significant date...
    //
    if (month == UNKNOWN || date == -1 || year == -1)
      return -1;

    if (zone_offset == -1) {
      if (default_to_gmt)
        zone_offset = 0;
      else
        zone_offset = localZoneOffset();
    }

    return UTC((year - 1900),
               (month - JAN),
               date,
               (hour == -1 ? 0 : hour),
               (min  == -1 ? 0 : min) - zone_offset,
               (sec  == -1 ? 0 : sec));
  }

  /** The same, but assumes GMT is the default timezone. */
  public static long parseLong(ByteBuf string) {
    return parseLong(string, true);
  }

  /** Like parseLong(), but returns a new Date object instead. */
  public static Date parseDate(ByteBuf string, boolean default_to_gmt) {
    long date = parseLong(string, default_to_gmt);
    if (date == -1) return null;
    return new Date(date);
  }

  /** The same, but assumes GMT is the default timezone. */
  public static Date parseDate(ByteBuf string) {
    return parseDate(string, true);
  }


  /** Composes the given date into a number suitable for passing to
      <TT>new&nbsp;Date(n)</TT>.  This is the number of milliseconds
      past the Epoch.
   */
  public static long UTC(int year, int month, int date,
                         int hour, int min, int sec) {
    long day = (date
                + monthOffset[month]
                + ((((year & 3) != 0)
                    || ((year % 100 == 0) && ((year + 300) % 400 != 0))
                    || (month < 2))
                   ? -1 : 0)  /* convert day-of-month to 0 based range,
                               * except following February in a leap year,
                               * in which case we skip the conversion to
                               * account for the extra day in February */
                + ((year - 70) * 365L)      // days per year
                + ((year - 69) / 4)         // plus leap days
                - ((year - 1) / 100)        // no leap on century years
                + ((year + 299) / 400));    // except %400 years
    return (1000 *
            ((sec + (60 * (min + (60 * hour))))
             + (60 * 60 * 24) * day));
    }

  static private final short monthOffset[] = {
    0,     // 31   January
    31,    // 28   February
    59,    // 31   March
    90,    // 30   April
    120,   // 31   May
    151,   // 30   June
    181,   // 31   July
    212,   // 31   August
    243,   // 30   September
    273,   // 31   October
    304,   // 30   November
    334    // 31   December
           // 365
  };

  static private int localZoneOffset() {
    TimeZone z = TimeZone.getDefault();
    int off = z.getRawOffset() / (1000 * 60);
    if (z.inDaylightTime(new Date()))           // Consing!!
      off += 60;
    return off;
  }


/*
  private static void debug_print(int dotw, int month, int zone, int offset,
                                  int date, int year, int hour, int min,
                                  int sec) {
    System.out.println("dotw  = " + debug_token_to_string(dotw)
                       + " (" + dotw + ")");
    System.out.println("date  = " + date);
    System.out.println("month = " + debug_token_to_string(month)
                       + " (" + month + ")");
    System.out.println("year  = " + year);
    System.out.println("hour  = " + hour);
    System.out.println("min   = " + min);
    System.out.println("sec   = " + sec);
    System.out.println("zone  = " + debug_token_to_string(zone)
                       + " (" + zone + ")");
    System.out.println("off   = " + offset);
    System.out.println("");
  }

  private static String debug_token_to_string(int t) {
    switch (t) {
    case UNKNOWN: return "UNKNOWN"; case SUN: return "SUN";
    case MON: return "MON"; case TUE: return "TUE"; case WED: return "WED";
    case THU: return "THU"; case FRI: return "FRI"; case SAT: return "SAT";
    case JAN: return "JAN"; case FEB: return "FEB"; case MAR: return "MAR";
    case APR: return "APR"; case MAY: return "MAY"; case JUN: return "JUN";
    case JUL: return "JUL"; case AUG: return "AUG"; case SEP: return "SEP";
    case OCT: return "OCT"; case NOV: return "NOV"; case DEC: return "DEC";
    case PST: return "PST"; case PDT: return "PDT"; case MST: return "MST";
    case MDT: return "MDT"; case CST: return "CST"; case CDT: return "CDT";
    case EST: return "EST"; case EDT: return "EDT"; case AST: return "AST";
    case NST: return "NST"; case GMT: return "GMT"; case BST: return "BST";
    case MET: return "MET"; case EET: return "EET"; case JST: return "JST";
    default: return "???";
    }
  }
 */
}


  /*
    We only handle a few time zones, but in case someone is feeling ambitious,
    here are a bunch of other timezone names and numbers.

    It's generally frowned upon to put timezone names in date headers instead
    of GMT offsets, so the small set of zone names that we already handle is
    probably sufficient.

    ---------------------------------------------------------------------
    I lifted this list of time zone abbreviations out of a UNIX computers
    setup file (specifically, from an AT&T StarServer running UNIX System V
    Release 4, in the /usr/lib/local/TZ directory).

    The list is by no means complete or comprehensive, as much of it comes
    out of scripts designed to adjust the time on computers when Daylight
    Savings Time (DST) rolls around. Also, I would consider it at least a
    little suspect. First, because it was compiled by Americans, and you know
    how us Americans are with geography :). Second, the data looks to be a
    little old, circa 1991 (note the reference to the "Soviet Union").

    The first column is an approximate name for the time zone described, the
    second column gives the time relative to GMT, the third column takes a
    stab at listing the country that the time zone is in, and the final
    column gives one or more abbreviations that apply to that time zone (note
    that abbreviations that end with "DST" or with "S" as the middle letter
    indicate Daylight Savings Time is in effect).

    I've also tried to roughly divide the listings into geographical
    groupings.

    Hope this helps,

    Raymond McCauley
    Texas A&M University
    scooter@tamu.edu

    <List follows...>
    =================

    Europe

    Great Britain 0:00 GB-Eire GMT, BST
    Western European nations +0:00 W-Eur WET, WET DST
    Iceland +0:00 - WET
    Middle European nations +1:00 M-Eur MET, MET DST
    Poland +1:00 W-Eur MET, MET DST
    Eastern European nations +2:00 E-Eur EET, EET DST
    Turkey +3:00 Turkey EET, EET DST
    Warsaw Pact/Soviet Union +3:00 M-Eur ????

    North America

    Canada/Newfoundland -3:30 Canada NST, NDT
    Canada/Atlantic -4:00 Canada AST, ADT
    Canada/Eastern -5:00 Canada EST, EDT
    Canada/Central -6:00 Canada CST, CDT
    Canada/East-Saskatchewan -6:00 Canada CST
    Canada/Mountain -7:00 Canada MST, MDT
    Canada/Pacific -8:00 Canada PST, PDT
    Canada/Yukon -9:00 Canada YST, YDT

    US/Eastern -5:00 US EST, EDT
    US/Central -6:00 US CST, CDT
    US/Mountain -7:00 US MST, MDT
    US/Pacific -8:00 US PST, PDT
    US/Yukon -9:00 US YST, YDT
    US/Hawaii -10:00 US HST, PST, PDT, PPET

    Mexico/BajaNorte -8:00 Mexico PST, PDT
    Mexico/BajaSur -7:00 Mexico MST
    Mexico/General -6:00 Mexico CST

    South America

    Brazil/East -3:00 Brazil EST, EDT
    Brazil/West -4:00 Brazil WST, WDT
    Brazil/Acre -5:00 Brazil AST, ADT
    Brazil/DeNoronha -2:00 Brazil FST, FDT

    Chile/Continental -4:00 Chile CST, CDT
    Chile/EasterIsland -6:00 Chile EST, EDT


    Asia

    People's Repub. of China +8:00 PRC CST, CDT
    (Yes, they really have only one time zone.)

    Republic of Korea +9:00 ROK KST, KDT

    Japan +9:00 Japan JST
    Singapore +8:00 Singapore SST
    Hongkong +8:00 U.K. HKT
    ROC +8:00 - CST

    Middle East

    Israel +3:00 Israel IST, IDT
    (This was the only listing I found)

    Australia

    Australia/Tasmania +10:00 Oz EST
    Australia/Queensland +10:00 Oz EST
    Australia/North +9:30 Oz CST
    Australia/West +8:00 Oz WST
    Australia/South +9:30 Oz CST


    Hour    TZN     DZN     Zone    Example
    0       GMT             Greenwich Mean Time             GMT0
    0       UTC             Universal Coordinated Time      UTC0
    2       FST     FDT     Fernando De Noronha Std         FST2FDT
    3       BST             Brazil Standard Time            BST3
    3       EST     EDT     Eastern Standard (Brazil)       EST3EDT
    3       GST             Greenland Standard Time         GST3
    3:30    NST     NDT     Newfoundland Standard Time      NST3:30NDT
    4       AST     ADT     Atlantic Standard Time          AST4ADT
    4       WST     WDT     Western Standard (Brazil)       WST4WDT
    5       EST     EDT     Eastern Standard Time           EST5EDT
    5       CST     CDT     Chile Standard Time             CST5CDT

    Hour    TZN     DZN     Zone    Example
    5       AST     ADT     Acre Standard Time              AST5ADT
    5       CST     CDT     Cuba Standard Time              CST5CDT
    6       CST     CDT     Central Standard Time           CST6CDT
    6       EST     EDT     Easter Island Standard          EST6EDT
    7       MST     MDT     Mountain Standard Time          MST7MDT
    8       PST     PDT     Pacific Standard Time           PST8PDT
    9       AKS     AKD     Alaska Standard Time            AKS9AKD
    9       YST     YDT     Yukon Standard Time             YST9YST
    10      HST     HDT     Hawaii Standard Time            HST10HDT
    11      SST             Somoa Standard Time             SST11
    -12     NZS     NZD     New Zealand Standard Time       NZS-12NZD
    -10     GST             Guam Standard Time              GST-10
    -10     EAS     EAD     Eastern Australian Standard     EAS-10EAD
    -9:30   CAS     CAD     Central Australian Standard     CAS-9:30CAD
    -9      JST             Japan Standard Time             JST-9
    -9      KST     KDT     Korean Standard Time            KST-9KDT
    -8      CCT             China Coast Time                CCT-8
    -8      HKT             Hong Kong Time                  HKT-8
    -8      SST             Singapore Standard Time         SST-8
    -8      WAS     WAD     Western Australian Standard     WAS-8WAD
    -7:30   JT              Java Standard Time              JST-7:30
    -7      NST             North Sumatra Time              NST-7
    -5:30   IST             Indian Standard Time            IST-5:30
    -3:30   IST     IDT     Iran Standard Time              IST-3:30IDT
    -3      MSK     MSD     Moscow Winter Time              MSK-3MSD
    -2      EET             Eastern Europe Time             EET-2
    -2      IST     IDT     Israel Standard Time            IST-2IDT
    -1      MEZ     MES     Middle European Time            MEZ-1MES
    -1      SWT     SST     Swedish Winter Time             SWT-1SST
    -1      FWT     FST     French Winter Time              FWT-1FST
    -1      CET     CES     Central European Time           CET-1CES
    -1 WAT West African Time WAT-1
  */
