/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8                                :vi│
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright (c) 1989 The Regents of the University of California.              │
│ All rights reserved.                                                         │
│                                                                              │
│ Redistribution and use in source and binary forms are permitted              │
│ provided that the above copyright notice and this paragraph are              │
│ duplicated in all such forms and that any documentation,                     │
│ advertising materials, and other materials related to such                   │
│ distribution and use acknowledge that the software was developed             │
│ by the University of California, Berkeley.  The name of the                  │
│ University may not be used to endorse or promote products derived            │
│ from this software without specific prior written permission.                │
│ THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR               │
│ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED               │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.          │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/assert.h"
#include "libc/calls/calls.h"
#include "libc/fmt/fmt.h"
#include "libc/macros.internal.h"
#include "libc/nexgen32e/nexgen32e.h"
#include "libc/time/struct/tm.h"
#include "libc/time/time.h"
#include "libc/time/tzfile.internal.h"

asm(".ident\t\"\\n\\n\
strftime (BSD-3)\\n\
Copyright 1989 The Regents of the University of California\"");
asm(".include \"libc/disclaimer.inc\"");

static char *strftime_add(char *p, const char *pe, const char *str) {
  while (p < pe && (*p = *str++) != '\0') ++p;
  return p;
}

static char *strftime_conv(char *p, const char *pe, int n, const char *format) {
  char buf[INT_STRLEN_MAXIMUM(int) + 1];
  (snprintf)(buf, sizeof(buf), format, n);
  return strftime_add(p, pe, buf);
}

static char *strftime_secs(char *p, const char *pe, const struct tm *t) {
  static char buf[INT_STRLEN_MAXIMUM(int) + 1];
  struct tm tmp;
  int64_t s;
  tmp = *t; /* Make a copy, mktime(3) modifies the tm struct. */
  s = mktime(&tmp);
  (snprintf)(buf, sizeof(buf), "%ld", s);
  return strftime_add(p, pe, buf);
}

static char *strftime_timefmt(char *p, const char *pe, const char *format,
                              const struct tm *t) {
  int i;
  long diff;
  char const *sign;
  /* size_t z1, z2, z3; */
  for (; *format; ++format) {
    if (*format == '%') {
    label:
      switch (*++format) {
        case '\0':
          --format;
          break;
        case 'A':
          p = strftime_add(p, pe,
                           (t->tm_wday < 0 || t->tm_wday > 6)
                               ? "?"
                               : kWeekdayName[t->tm_wday]);
          continue;
        case 'a':
          p = strftime_add(p, pe,
                           (t->tm_wday < 0 || t->tm_wday > 6)
                               ? "?"
                               : kWeekdayNameShort[t->tm_wday]);
          continue;
        case 'B':
          p = strftime_add(
              p, pe,
              (t->tm_mon < 0 || t->tm_mon > 11) ? "?" : kMonthName[t->tm_mon]);
          continue;
        case 'b':
        case 'h':
          p = strftime_add(p, pe,
                           (t->tm_mon < 0 || t->tm_mon > 11)
                               ? "?"
                               : kMonthNameShort[t->tm_mon]);
          continue;
        case 'c':
          p = strftime_timefmt(p, pe, "%D %X", t);
          continue;
        case 'C':
          /*
          ** %C used to do a...
          **	strftime_timefmt("%a %b %e %X %Y", t);
          ** ...whereas now POSIX 1003.2 calls for
          ** something completely different.
          ** (ado, 5/24/93)
          */
          p = strftime_conv(p, pe, div100int64(t->tm_year + TM_YEAR_BASE),
                            "%02d");
          continue;
        case 'D':
          p = strftime_timefmt(p, pe, "%m/%d/%y", t);
          continue;
        case 'x':
          /*
          ** Version 3.0 of strftime from Arnold Robbins
          ** (arnold@skeeve.atl.ga.us) does the
          ** equivalent of...
          **	strftime_timefmt("%a %b %e %Y");
          ** ...for %x; since the X3J11 C language
          ** standard calls for "date, using locale's
          ** date format," anything goes.  Using just
          ** numbers (as here) makes Quakers happier.
          ** Word from Paul Eggert (eggert@twinsun.com)
          ** is that %Y-%m-%d is the ISO standard date
          ** format, specified in ISO 2014 and later
          ** ISO 8601:1988, with a summary available in
          ** pub/doc/ISO/english/ISO8601.ps.Z on
          ** ftp.uni-erlangen.de.
          ** (ado, 5/30/93)
          */
          p = strftime_timefmt(p, pe, "%m/%d/%y", t);
          continue;
        case 'd':
          p = strftime_conv(p, pe, t->tm_mday, "%02d");
          continue;
        case 'E':
        case 'O':
          /*
          ** POSIX locale extensions, a la
          ** Arnold Robbins' strftime version 3.0.
          ** The sequences
          **	%Ec %EC %Ex %Ey %EY
          **	%Od %oe %OH %OI %Om %OM
          **	%OS %Ou %OU %OV %Ow %OW %Oy
          ** are supposed to provide alternate
          ** representations.
          ** (ado, 5/24/93)
          */
          goto label;
        case 'e':
          p = strftime_conv(p, pe, t->tm_mday, "%2d");
          continue;
        case 'H':
          p = strftime_conv(p, pe, t->tm_hour, "%02d");
          continue;
        case 'I':
          p = strftime_conv(p, pe, (t->tm_hour % 12) ? (t->tm_hour % 12) : 12,
                            "%02d");
          continue;
        case 'j':
          p = strftime_conv(p, pe, t->tm_yday + 1, "%03d");
          continue;
        case 'k':
          /*
          ** This used to be...
          **	strftime_conv(t->tm_hour % 12 ?
          **		t->tm_hour % 12 : 12, 2, ' ');
          ** ...and has been changed to the below to
          ** match SunOS 4.1.1 and Arnold Robbins'
          ** strftime version 3.0.  That is, "%k" and
          ** "%l" have been swapped.
          ** (ado, 5/24/93)
          */
          p = strftime_conv(p, pe, t->tm_hour, "%2d");
          continue;
#ifdef KITCHEN_SINK
        case 'K':
          /*
          ** After all this time, still unclaimed!
          */
          p = strftime_add(p, pe, "kitchen sink");
          continue;
#endif /* defined KITCHEN_SINK */
        case 'l':
          /*
          ** This used to be...
          **	strftime_conv(t->tm_hour, 2, ' ');
          ** ...and has been changed to the below to
          ** match SunOS 4.1.1 and Arnold Robbin's
          ** strftime version 3.0.  That is, "%k" and
          ** "%l" have been swapped.
          ** (ado, 5/24/93)
          */
          p = strftime_conv(p, pe, (t->tm_hour % 12) ? (t->tm_hour % 12) : 12,
                            "%2d");
          continue;
        case 'M':
          p = strftime_conv(p, pe, t->tm_min, "%02d");
          continue;
        case 'm':
          p = strftime_conv(p, pe, t->tm_mon + 1, "%02d");
          continue;
        case 'n':
          p = strftime_add(p, pe, "\n");
          continue;
        case 'p':
          p = strftime_add(p, pe, t->tm_hour >= 12 ? "PM" : "AM");
          continue;
        case 'R':
          p = strftime_timefmt(p, pe, "%H:%M", t);
          continue;
        case 'r':
          p = strftime_timefmt(p, pe, "%I:%M:%S %p", t);
          continue;
        case 'S':
          p = strftime_conv(p, pe, t->tm_sec, "%02d");
          continue;
        case 's':
          p = strftime_secs(p, pe, t);
          continue;
        case 'T':
        case 'X':
          p = strftime_timefmt(p, pe, "%H:%M:%S", t);
          continue;
        case 't':
          p = strftime_add(p, pe, "\t");
          continue;
        case 'U':
          p = strftime_conv(p, pe, (t->tm_yday + 7 - t->tm_wday) / 7, "%02d");
          continue;
        case 'u':
          /*
          ** From Arnold Robbins' strftime version 3.0:
          ** "ISO 8601: Weekday as a decimal number
          ** [1 (Monday) - 7]"
          ** (ado, 5/24/93)
          */
          p = strftime_conv(p, pe, (t->tm_wday == 0) ? 7 : t->tm_wday, "%d");
          continue;
        case 'V':
          /*
          ** From Arnold Robbins' strftime version 3.0:
          ** "the week number of the year (the first
          ** Monday as the first day of week 1) as a
          ** decimal number (01-53).  The method for
          ** determining the week number is as specified
          ** by ISO 8601 (to wit: if the week containing
          ** January 1 has four or more days in the new
          ** year, then it is week 1, otherwise it is
          ** week 53 of the previous year and the next
          ** week is week 1)."
          ** (ado, 5/24/93)
          */
          /*
          ** XXX--If January 1 falls on a Friday,
          ** January 1-3 are part of week 53 of the
          ** previous year.  By analogy, if January
          ** 1 falls on a Thursday, are December 29-31
          ** of the PREVIOUS year part of week 1???
          ** (ado 5/24/93)
          **
          ** You are understood not to expect this.
          */
          i = (t->tm_yday + 10 - (t->tm_wday ? (t->tm_wday - 1) : 6)) / 7;
          if (i == 0) {
            /*
            ** What day of the week does
            ** January 1 fall on?
            */
            i = t->tm_wday - (t->tm_yday - 1);
            /*
            ** Fri Jan 1: 53
            ** Sun Jan 1: 52
            ** Sat Jan 1: 53 if previous
            ** 		 year a leap
            **		 year, else 52
            */
            if (i == TM_FRIDAY)
              i = 53;
            else if (i == TM_SUNDAY)
              i = 52;
            else
              i = isleap(t->tm_year + TM_YEAR_BASE) ? 53 : 52;
#ifdef XPG4_1994_04_09
            /*
            ** As of 4/9/94, though,
            ** XPG4 calls for 53
            ** unconditionally.
            */
            i = 53;
#endif /* defined XPG4_1994_04_09 */
          }
          p = strftime_conv(p, pe, i, "%02d");
          continue;
        case 'v':
          /*
          ** From Arnold Robbins' strftime version 3.0:
          ** "date as dd-bbb-YYYY"
          ** (ado, 5/24/93)
          */
          p = strftime_timefmt(p, pe, "%e-%b-%Y", t);
          continue;
        case 'W':
          p = strftime_conv(
              p, pe, (t->tm_yday + 7 - (t->tm_wday ? (t->tm_wday - 1) : 6)) / 7,
              "%02d");
          continue;
        case 'w':
          p = strftime_conv(p, pe, t->tm_wday, "%d");
          continue;
        case 'y':
          p = strftime_conv(p, pe, (t->tm_year + TM_YEAR_BASE) % 100, "%02d");
          continue;
        case 'Y':
          p = strftime_conv(p, pe, t->tm_year + TM_YEAR_BASE, "%04d");
          continue;
        case 'Z':
          if (t->tm_zone) {
            p = strftime_add(p, pe, t->tm_zone);
          } else {
            if (t->tm_isdst == 0 || t->tm_isdst == 1) {
              p = strftime_add(p, pe, tzname[t->tm_isdst]);
            } else {
              p = strftime_add(p, pe, "?");
            }
          }
          continue;
        case 'z':
          if (t->tm_isdst < 0) continue;
#ifdef TM_GMTOFF
          diff = t->TM_GMTOFF;
#else /* !defined TM_GMTOFF */
          if (t->tm_isdst == 0)
#ifdef USG_COMPAT
            diff = -timezone;
#else /* !defined USG_COMPAT */
            continue;
#endif /* !defined USG_COMPAT */
          else
#ifdef ALTZONE
            diff = -altzone;
#else /* !defined ALTZONE */
            continue;
#endif /* !defined ALTZONE */
#endif /* !defined TM_GMTOFF */
          if (diff < 0) {
            sign = "-";
            diff = -diff;
          } else {
            sign = "+";
          }
          p = strftime_add(p, pe, sign);
          diff /= SECSPERMIN;
          diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR);
          p = strftime_conv(p, pe, diff, "%04d");
          continue;
        case '%':
        /*
         * X311J/88-090 (4.12.3.5): if conversion char is
         * undefined, behavior is undefined.  Print out the
         * character itself as printf(3) also does.
         */
        default:
          break;
      }
    }
    if (p >= pe) break;
    *p++ = *format;
  }
  return p;
}

/**
 * Converts time to string, e.g.
 *
 *     char b[64];
 *     int64_t sec;
 *     struct tm tm;
 *     time(&sec);
 *     localtime_r(&sec, &tm);
 *     strftime(b, sizeof(b), "%Y-%m-%dT%H:%M:%S%z", &tm);       // ISO8601
 *     strftime(b, sizeof(b), "%a, %d %b %Y %H:%M:%S %Z", &tm);  // RFC1123
 *
 * @return bytes copied excluding nul, or 0 on error
 */
size_t strftime(char *s, size_t size, const char *f, const struct tm *t) {
  char *p;
  assert(t);
  p = strftime_timefmt(s, s + size, f, t);
  if (p < s + size) {
    *p = '\0';
    return p - s;
  } else {
    s[size - 1] = '\0';
    return 0;
  }
}
