/****************************************************************************
 * libs/libc/time/lib_strptime.c
 *
 *  $OpenBSD: strptime.c,v 1.11 2005/08/08 08:05:38 espie Exp $
 *  $NetBSD: strptime.c,v 1.12 1998/01/20 21:39:40 mycroft Exp $
 *
 * Copyright (c) 1997, 1998 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code was contributed to The NetBSD Foundation by Klaus Klein.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *        This product includes software developed by the NetBSD
 *        Foundation, Inc. and its contributors.
 * 4. Neither the name of The NetBSD Foundation nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/
#include <nuttx/config.h>

#include <ctype.h>
#include <locale.h>
#include <strings.h>

#include <nuttx/clock.h>

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#define _ctloc(x)           (g_defaulttimelocale.x)

/* We do not implement alternate representations. However, we always
 * check whether a given modifier is allowed for a certain conversion.
 */

#define _ALT_E              0x01
#define _ALT_O              0x02
#define _LEGAL_ALT(x)       { if (alt_format & ~(x)) return (NULL); }

/****************************************************************************
 * Private Data
 ****************************************************************************/

static const struct
{
  const char *abday[7];
  const char *day[7];
  const char *abmon[12];
  const char *mon[12];
  const char *am_pm[2];
  const char *d_t_fmt;
  const char *d_fmt;
  const char *t_fmt;
  const char *t_fmt_ampm;
} g_defaulttimelocale =
{
  {
    "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
  },
  {
    "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday",
    "Friday", "Saturday"
  },
  {
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
  },
  {
    "January", "February", "March", "April", "May", "June", "July",
    "August", "September", "October", "November", "December"
  },
  {
    "AM", "PM"
  },
  "%a %b %d %H:%M:%S %Y",
  "%m/%d/%y",
  "%H:%M:%S",
  "%I:%M:%S %p"
};

struct century_relyear
{
  int century;
  int relyear;
};

/****************************************************************************
 * Private Functions
 ****************************************************************************/

static int _conv_num(FAR const unsigned char **buf, FAR int *dest,
                     int llim, int ulim)
{
  int result = 0;
  int rulim = ulim;

  while (isspace(**buf))
    {
      (*buf)++;
    }

  if (**buf < '0' || **buf > '9')
    {
      return 0;
    }

  /* we use rulim to break out of the loop when we run out of digits */

  do
    {
      result *= 10;
      result += *(*buf)++ - '0';
      rulim /= 10;
    }
  while ((result * 10 <= ulim) && rulim && **buf >= '0' && **buf <= '9');

  if (result < llim || result > ulim)
    {
      return 0;
    }

  *dest = result;
  return 1;
}

static FAR const unsigned char *_strptime(FAR const unsigned char *buf,
                                          FAR const char *fmt,
                                          FAR struct tm *tm,
                                          FAR struct century_relyear *cr)
{
  unsigned char c;
  FAR const unsigned char *bp;
  size_t len = 0;
  int alt_format;
  int i;

  bp = buf;
  while ((c = *fmt) != '\0')
    {
      /* Clear `alternate' modifier prior to new conversion. */

      alt_format = 0;

      /* Eat up white-space. */

      if (isspace(c))
        {
          while (isspace(*bp))
            bp++;

          fmt++;
          continue;
        }

      if ((c = *fmt++) != '%')
        {
          goto literal;
        }

again:
      switch (c = *fmt++)
        {
          case '%': /* "%%" is converted to "%". */
literal:
            if (c != *bp++)
              {
                return NULL;
              }

            break;

            /* "Alternative" modifiers. Just set the appropriate flag
            * and start over again.
            */

          case 'E': /* "%E?" alternative conversion modifier. */
            _LEGAL_ALT(0);
            alt_format |= _ALT_E;
            goto again;

          case 'O': /* "%O?" alternative conversion modifier. */
            _LEGAL_ALT(0);
            alt_format |= _ALT_O;
            goto again;

            /* "Complex" conversion rules, implemented through recursion. */

          case 'c': /* Date and time, using the locale's format. */
            _LEGAL_ALT(_ALT_E);
            if (!(bp = _strptime(bp, _ctloc(d_t_fmt), tm, cr)))
              {
                return NULL;
              }

            break;

          case 'D': /* The date as "%m/%d/%y". */
            _LEGAL_ALT(0);
            if (!(bp = _strptime(bp, "%m/%d/%y", tm, cr)))
              {
                return NULL;
              }

            break;

          case 'R': /* The time as "%H:%M". */
            _LEGAL_ALT(0);
            if (!(bp = _strptime(bp, "%H:%M", tm, cr)))
              {
                return NULL;
              }

            break;

          case 'r': /* The time as "%I:%M:%S %p". */
            _LEGAL_ALT(0);
            if (!(bp = _strptime(bp, "%I:%M:%S %p", tm, cr)))
              {
                return NULL;
              }

            break;

          case 'T': /* The time as "%H:%M:%S". */
            _LEGAL_ALT(0);
            if (!(bp = _strptime(bp, "%H:%M:%S", tm, cr)))
              {
                return NULL;
              }

            break;

          case 'X': /* The time, using the locale's format. */
            _LEGAL_ALT(_ALT_E);
            if (!(bp = _strptime(bp, _ctloc(t_fmt), tm, cr)))
              {
                return NULL;
              }

            break;

          case 'x': /* The date, using the locale's format. */
            _LEGAL_ALT(_ALT_E);
            if (!(bp = _strptime(bp, _ctloc(d_fmt), tm, cr)))
              {
                return NULL;
              }

            break;

            /* "Elementary" conversion rules. */

          case 'A': /* The day of week, using the locale's form. */
          case 'a':
            _LEGAL_ALT(0);
            for (i = 0; i < 7; i++)
              {
                /* Full name. */

                len = strlen(_ctloc(day[i]));
                if (strncasecmp(_ctloc(day[i]),
                                (const char *)bp, len) == 0)
                  break;

                /* Abbreviated name. */

                len = strlen(_ctloc(abday[i]));
                if (strncasecmp(_ctloc(abday[i]),
                                (const char *)bp, len) == 0)
                  break;
              }

            /* Nothing matched. */

            if (i == 7)
              {
                return NULL;
              }

            tm->tm_wday = i;
            bp += len;
            break;

          case 'B': /* The month, using the locale's form. */
          case 'b':
          case 'h':
            _LEGAL_ALT(0);
            for (i = 0; i < 12; i++)
              {
                /* Full name. */

                len = strlen(_ctloc(mon[i]));
                if (strncasecmp(_ctloc(mon[i]),
                                (const char *)bp, len) == 0)
                  {
                    break;
                  }

                /* Abbreviated name. */

                len = strlen(_ctloc(abmon[i]));
                if (strncasecmp(_ctloc(abmon[i]),
                                (const char *)bp, len) == 0)
                  {
                    break;
                  }
              }

            /* Nothing matched. */

            if (i == 12)
              {
                return NULL;
              }

            tm->tm_mon = i;
            bp += len;
            break;

          case 'C': /* The century number. */
            _LEGAL_ALT(_ALT_E);
            if (!(_conv_num(&bp, &i, 0, 99)))
              {
                return (NULL);
              }

            cr->century = i * 100;
            break;

          case 'd': /* The day of month. */
          case 'e':
            _LEGAL_ALT(_ALT_O);
            if (!(_conv_num(&bp, &tm->tm_mday, 1, 31)))
              {
                return (NULL);
              }

            break;

          case 'k': /* The hour (24-hour clock representation). */
            _LEGAL_ALT(0);

            /* FALLTHROUGH */

          case 'H':
            _LEGAL_ALT(_ALT_O);
            if (!(_conv_num(&bp, &tm->tm_hour, 0, 23)))
              {
                return NULL;
              }

            break;

          case 'l': /* The hour (12-hour clock representation). */
            _LEGAL_ALT(0);

            /* FALLTHROUGH */

          case 'I':
            _LEGAL_ALT(_ALT_O);
            if (!(_conv_num(&bp, &tm->tm_hour, 1, 12)))
              {
                return (NULL);
              }

            break;

          case 'j': /* The day of year. */
            _LEGAL_ALT(0);
            if (!(_conv_num(&bp, &tm->tm_yday, 1, 366)))
              {
                return NULL;
              }

            tm->tm_yday--;
            break;

          case 'M': /* The minute. */
            _LEGAL_ALT(_ALT_O);
            if (!(_conv_num(&bp, &tm->tm_min, 0, 59)))
              {
                return NULL;
              }

            break;

          case 'm': /* The month. */
            _LEGAL_ALT(_ALT_O);
            if (!(_conv_num(&bp, &tm->tm_mon, 1, 12)))
              {
                return NULL;
              }

            tm->tm_mon--;
            break;

          case 'p': /* The locale's equivalent of AM/PM. */
            _LEGAL_ALT(0);

            /* AM? */

            len = strlen(_ctloc(am_pm[0]));
            if (strncasecmp(_ctloc(am_pm[0]), (const char *)bp, len) == 0)
              {
                if (tm->tm_hour > 12) /* i.e., 13:00 AM ?! */
                  {
                    return NULL;
                  }
                else if (tm->tm_hour == 12)
                  {
                    tm->tm_hour = 0;
                  }

                bp += len;
                break;
              }

            /* PM? */

            len = strlen(_ctloc(am_pm[1]));
            if (strncasecmp(_ctloc(am_pm[1]), (const char *)bp, len) == 0)
              {
                if (tm->tm_hour > 12) /* i.e., 13:00 PM ?! */
                  {
                    return NULL;
                  }
                else if (tm->tm_hour < 12)
                  {
                    tm->tm_hour += 12;
                  }

                bp += len;
                break;
              }

            /* Nothing matched. */

            return NULL;

          case 'S': /* The seconds. */
            _LEGAL_ALT(_ALT_O);
            if (!(_conv_num(&bp, &tm->tm_sec, 0, 61)))
              {
                return NULL;
              }

            break;

          case 'U': /* The week of year, beginning on sunday. */
          case 'W': /* The week of year, beginning on monday. */
            _LEGAL_ALT(_ALT_O);
            /* XXX This is bogus, as we can not assume any valid
            * information present in the tm structure at this
            * point to calculate a real value, so just check the
            * range for now.
            */

            if (!(_conv_num(&bp, &i, 0, 53)))
              {
                return NULL;
              }

            break;

          case 'w': /* The day of week, beginning on sunday. */
            _LEGAL_ALT(_ALT_O);
            if (!(_conv_num(&bp, &tm->tm_wday, 0, 6)))
              {
                return NULL;
              }

            break;

          case 'Y': /* The year. */
            _LEGAL_ALT(_ALT_E);
            if (!(_conv_num(&bp, &i, 0, 9999)))
              {
                return NULL;
              }

            cr->relyear = -1;
            tm->tm_year = i - TM_YEAR_BASE;
            break;

          case 'y': /* The year within the century (2 digits). */
            _LEGAL_ALT(_ALT_E | _ALT_O);
            if (!(_conv_num(&bp, &cr->relyear, 0, 99)))
              {
                return NULL;
              }

            break;

            /* Miscellaneous conversions. */

          case 'n': /* Any kind of white-space. */
          case 't':
            _LEGAL_ALT(0);
            while (isspace(*bp))
              {
                bp++;
              }

            break;

          default: /* Unknown/unsupported conversion. */
            return NULL;
        }
    }

  /* We need to evaluate the two digit year spec (%y)
   * last as we can get a century spec (%C) at any time.
   */

  if (cr->relyear != -1)
  {
    if (cr->century == TM_YEAR_BASE)
    {
      if (cr->relyear <= 68)
        {
          tm->tm_year = cr->relyear + 2000 - TM_YEAR_BASE;
        }
      else
        {
          tm->tm_year = cr->relyear + 1900 - TM_YEAR_BASE;
        }
    }
    else
    {
      tm->tm_year = cr->relyear + cr->century - TM_YEAR_BASE;
    }
  }

  return bp;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: strptime
 *
 * Description:
 *   Convert a string to a time type according to a specific time format
 *   The strptime() function processes the input string from left to
 *   right.  Each of the three possible input elements (whitespace,
 *   literal, or format) are handled one after the other.  If the
 *   input cannot be matched to the format string, the function stops.
 *   The remainder of the format and input strings are not processed.
 *   The supported input field descriptors are listed below.  In case
 *   a text string (such as the name of a day of the week or a month
 *   name) is to be matched, the comparison is case insensitive.  In
 *   case a number is to be matched, leading zeros are permitted but
 *   not required.
 *   %a     The abbreviated weekday name according to the current locale.
 *   %A     The full weekday name according to the current locale.
 *   %b     The abbreviated month name according to the current locale.
 *   %B     The full month name according to the current locale.
 *   %C     The century number (year/100) as a 2-digit integer. (SU)
 *   %d     The day of the month as a decimal number (range 01 to 31).
 *   %e     Like %d, the day of the month as a decimal number, but a leading
 *          zero is replaced by a space.
 *   %h     Equivalent to %b.  (SU)
 *   %H     The hour as a decimal number using a 24-hour clock
 *          (range 00 to 23).
 *   %I     The  hour as a decimal number using a 12-hour clock
 *          (range 01 to 12).
 *   %j     The day of the year as a decimal number (range 001 to 366).
 *   %k     The hour (24-hour clock) as a decimal number (range  0  to  23);
 *          single digits are preceded by a blank.  (See also %H.)  (TZ)
 *   %l     The  hour  (12-hour  clock) as a decimal number (range 1 to 12);
 *          single digits are preceded by a blank.  (See also %I.)  (TZ)
 *   %m     The month as a decimal number (range 01 to 12).
 *   %M     The minute as a decimal number (range 00 to 59).
 *   %n     A newline character. (SU)
 *   %p     Either "AM" or "PM" according to the given time  value, or the
 *          corresponding  strings  for the current locale.  Noon is treated
 *          as "PM" and midnight as "AM".
 *   %P     Like %p but in lowercase: "am" or "pm" or a corresponding string
 *          for the current locale. (GNU)
 *   %S     The second as a decimal number (range 00 to 60).  (The range is
 *          up to 60 to allow for occasional leap seconds.)
 *   %t     A tab character. (SU)
 *   %y     The year as a decimal number without a century (range 00 to 99).
 *   %Y     The year as a decimal number including the century.
 *   %%     A literal '%' character.
 *
 * Returned Value:
 *   The return value of the function is a pointer to the first
 *   character not processed in this function call.  In case the input
 *   string contains more characters than required by the format
 *   string, the return value points right after the last consumed
 *   input character.  In case the whole input string is consumed, the
 *   return value points to the null byte at the end of the string.
 *   If strptime() fails to match all of the format string and
 *   therefore an error occurred, the function returns NULL.
 *
 ****************************************************************************/

FAR char *strptime(FAR const char *buf, FAR const char *fmt,
                   FAR struct tm *tm)
{
  struct century_relyear cr;
  cr.century = TM_YEAR_BASE;
  cr.relyear = -1;
  return (FAR char *)(_strptime((FAR const unsigned char *)buf,
                                fmt, tm, &cr));
}
