/* strptime - Convert a string representation of time to a time value.
Copyright (C) 1996 Free Software Foundation, Inc.
This file is part of the GNU C Library.
Contributed by Ulrich Drepper <drepper@cygnus.com>, 1996.

The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.

The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Library General Public License for more details.

You should have received a copy of the GNU Library General Public
License along with the GNU C Library; see the file COPYING.LIB.  If
not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA.  */

#include <ctype.h>
#include <langinfo.h>
#include <limits.h>
#include <string.h>
#include <time.h>

#include "../locale/localeinfo.h"


#define match_char(ch1, ch2) if (ch1 != ch2) return NULL
#define match_string(cs1, s2)						      \
  ({ size_t len = strlen (cs1);						      \
     int result = strncasecmp (cs1, s2, len) == 0;			      \
     if (result) s2 += len;						      \
     result; })
/* We intentionally do not use isdigit() for testing because this will
   lead to problems with the wide character version.  */
#define get_number(from, to)						      \
  do {									      \
    val = 0;								      \
    if (*rp < '0' || *rp > '9')						      \
      return NULL;							      \
    do {								      \
      val *= 10;							      \
      val += *rp++ - '0';						      \
    } while (val * 10 <= to && *rp >= '0' && *rp <= '9');		      \
    if (val < from || val > to)						      \
      return NULL;							      \
  } while (0)
#define get_alt_number(from, to)					      \
  do {									      \
    const char *alts = _NL_CURRENT (LC_TIME, ALT_DIGITS);		      \
    val = 0;								      \
    while (*alts != '\0')						      \
      {									      \
	size_t len = strlen (alts);					      \
	if (strncasecmp (alts, rp, len) == 0)				      \
	  break;							      \
	alts = strchr (alts, '\0') + 1;					      \
	++val;								      \
      }									      \
    if (*alts == '\0')							      \
      return NULL;							      \
  } while (0)
#define recursive(new_fmt)						      \
  do {									      \
    if (*new_fmt == '\0')						      \
      return NULL;							      \
    rp = strptime (rp, new_fmt, tm);					      \
    if (rp == NULL)							      \
      return NULL;							      \
  } while (0)
  

char *
strptime (const char *buf, const char *format, struct tm *tm)
{
  const char *rp;
  const char *fmt;
  int cnt;
  size_t val;
  int have_I, is_pm;

  rp = buf;
  fmt = format;
  have_I = is_pm = 0;

  while (*fmt != '\0')
    {
      /* A white space in the format string matches 0 more or white
	 space in the input string.  */
      if (isspace (*fmt))
	{
	  while (isspace (*rp))
	    ++rp;
	  ++fmt;
	  continue;
	}

      /* Any character but `%' must be matched by the same character
	 in the iput string.  */
      if (*fmt != '%')
	{
	  match_char (*fmt++, *rp++);
	  continue;
	}

      ++fmt;
      switch (*fmt++)
	{
	case '%':
	  /* Match the `%' character itself.  */
	  match_char ('%', *rp++);
	  break;
	case 'a':
	case 'A':
	  /* Match day of week.  */
	  for (cnt = 0; cnt < 7; ++cnt)
	    {
	      if (match_string (_NL_CURRENT (LC_TIME, ABDAY_1 + cnt), rp))
		break;
	      if (match_string (_NL_CURRENT (LC_TIME, DAY_1 + cnt), rp))
		break;
	    }
	  if (cnt == 7)
	    /* Does not match a weekday name.  */
	    return NULL;
	  tm->tm_wday = cnt;
	  break;
	case 'b':
	case 'B':
	case 'h':
	  /* Match month name.  */
	  for (cnt = 0; cnt < 12; ++cnt)
	    {
	      if (match_string (_NL_CURRENT (LC_TIME, ABMON_1 + cnt), rp))
		break;
	      if (match_string (_NL_CURRENT (LC_TIME, MON_1 + cnt), rp))
		break;
	    }
	  if (cnt == 12)
	    /* Does not match a month name.  */
	    return NULL;
	  tm->tm_mon = cnt;
	  break;
	case 'c':
	  /* Match locale's date and time format.  */
	  recursive (_NL_CURRENT (LC_TIME, D_T_FMT));
	  break;
	case 'C':
	  /* Match century number.  */
	  get_number (0, 99);
	  /* We don't need the number.  */
	  break;
	case 'd':
	case 'e':
	  /* Match day of month.  */
	  get_number (1, 31);
	  tm->tm_mday = val;
	  break;
	case 'D':
	  /* Match standard day format.  */
	  recursive ("%m/%d/%y");
	  break;
	case 'H':
	  /* Match hour in 24-hour clock.  */
	  get_number (0, 23);
	  tm->tm_hour = val;
	  have_I = 0;
	  break;
	case 'I':
	  /* Match hour in 12-hour clock.  */
	  get_number (1, 12);
	  tm->tm_hour = val - 1;
	  have_I = 1;
	  break;
	case 'j':
	  /* Match day number of year.  */
	  get_number (1, 366);
	  tm->tm_yday = val - 1;
	  break;
	case 'm':
	  /* Match number of month.  */
	  get_number (1, 12);
	  tm->tm_mon = val - 1;
	  break;
	case 'M':
	  /* Match minute.  */
	  get_number (0, 59);
	  tm->tm_min = val;
	  break;
	case 'n':
	case 't':
	  /* Match any white space.  */
	  while (isspace (*rp))
	    ++rp;
	  break;
	case 'p':
	  /* Match locale's equivalent of AM/PM.  */
	  if (match_string (_NL_CURRENT (LC_TIME, AM_STR), rp))
	    break;
	  if (match_string (_NL_CURRENT (LC_TIME, PM_STR), rp))
	    {
	      is_pm = 1;
	      break;
	    }
	  return NULL;
	case 'r':
	  recursive (_NL_CURRENT (LC_TIME, T_FMT_AMPM));
	  break;
	case 'R':
	  recursive ("%H:%M");
	  break;
	case 'S':
	  get_number (0, 61);
	  tm->tm_sec = val;
	  break;
	case 'T':
	  recursive ("%H:%M:%S");
	  break;
	case 'U':
	case 'V':
	case 'W':
	  get_number (0, 53);
	  /* XXX This cannot determine any field in TM.  */
	  break;
	case 'w':
	  /* Match number of weekday.  */
	  get_number (0, 6);
	  tm->tm_wday = val;
	  break;
	case 'x':
	  recursive (_NL_CURRENT (LC_TIME, D_FMT));
	  break;
	case 'X':
	  recursive (_NL_CURRENT (LC_TIME, T_FMT));
	  break;
	case 'y':
	  /* Match year within century.  */
	  get_number (0, 99);
	  tm->tm_year = val;
	  break;
	case 'Y':
	  /* Match year including century number.  */
	  get_number (0, INT_MAX);
	  tm->tm_year = val - (val >= 2000 ? 2000 : 1900);
	  break;
	case 'Z':
	  /* XXX How to handle this?  */
	  break;
	case 'E':
	  switch (*fmt++)
	    {
	    case 'c':
	      /* Match locale's alternate date and time format.  */
	      recursive (_NL_CURRENT (LC_TIME, ERA_D_T_FMT));
	      break;
	    case 'C':
	    case 'y':
	    case 'Y':
	      /* Match name of base year in locale's alternate
		 representation.  */
	      /* XXX This is currently not implemented.  It should
		 use the value _NL_CURRENT (LC_TIME, ERA) but POSIX
		 leaves this implementation defined and we haven't
		 figured out how to do it yet.  */
	      break;
	    case 'x':
	      recursive (_NL_CURRENT (LC_TIME, ERA_D_FMT));
	      break;
	    case 'X':
	      recursive (_NL_CURRENT (LC_TIME, ERA_T_FMT));
	      break;
	    default:
	      return NULL;
	    }
	  break;
	case 'O':
	  switch (*fmt++)
	    {
	    case 'd':
	    case 'e':
	      /* Match day of month using alternate numeric symbols.  */
	      get_alt_number (1, 31);
	      tm->tm_mday = val;
	      break;
	    case 'H':
	      /* Match hour in 24-hour clock using alternate numeric
		 symbols.  */
	      get_alt_number (0, 23);
	      tm->tm_hour = val;
	      have_I = 0;
	      break;
	    case 'I':
	      /* Match hour in 12-hour clock using alternate numeric
		 symbols.  */
	      get_alt_number (1, 12);
	      tm->tm_hour = val - 1;
	      have_I = 1;
	      break;
	    case 'm':
	      /* Match month using alternate numeric symbols.  */
	      get_alt_number (1, 12);
	      tm->tm_mon = val - 1;
	      break;
	    case 'M':
	      /* Match minutes using alternate numeric symbols.  */
	      get_alt_number (0, 59);
	      tm->tm_min = val;
	      break;
	    case 'S':
	      /* Match seconds using alternate numeric symbols.  */
	      get_alt_number (0, 61);
	      tm->tm_sec = val;
	      break;
	    case 'U':
	    case 'V':
	    case 'W':
	      get_alt_number (0, 53);
	      /* XXX This cannot determine any field in TM.  */
	      break;
	    case 'w':
	      /* Match number of weekday using alternate numeric symbols.  */
	      get_alt_number (0, 6);
	      tm->tm_wday = val;
	      break;
	    case 'y':
	      /* Match year within century using alternate numeric symbols.  */
	      get_alt_number (0, 99);
	      break;
	    default:
	      return NULL;
	    }
	  break;
	default:
	  return NULL;
	}
    }

  if (have_I && is_pm)
    tm->tm_hour += 12;
  
  return (char *) rp;
}