#include "prjlibs-include/standards.h"
#include <unistd.h>
#include <limits.h>
#include <stddef.h>
#include <errno.h>
#include <time.h>
#include <sys/time.h>

#include "skalibs/include/stddjb.h"
#include "prjlibs-include/constants.h"
#include "runwhen.h"

char const* PROG="rw-match";

static void die_malformed(char const*, char const*) gccattr_noreturn;
static void die_malformed(char const* str, char const* exp) {
  strerr_die5x(100, PROG, ": malformed constraints (expected ", exp, "): ",
               str);
}

static void die_range(void) gccattr_noreturn;
static void die_range(void) {
  strerr_die2x(100, PROG, ": timestamp out of range");
}

static void die_unit_range(char const*) gccattr_noreturn;
static void die_unit_range(char const* x) {
  strerr_die3x(100, PROG, ": value out of range for unit: ", x);
}

enum { unset, eq, eqn, div };

typedef struct {
  unsigned int type;
  unsigned int spec;
  unsigned int value;
  unsigned int const min;
  unsigned int (* const max)(void);
  unsigned int const maxmax;
  char id;
} unit;

static struct taia const zero=TAIA_ZERO;

static unsigned int max_weekday (void) { return  6; }
static unsigned int max_year    (void) { return -1; }
static unsigned int max_month   (void) { return 12; }
static unsigned int max_monthday(void);
static unsigned int max_hour    (void) { return 23; }
static unsigned int max_minute  (void) { return 59; }
static unsigned int max_second  (void) { return 59; }

static unit units_weekday[]={
  { unset, 0, -1, 0, max_weekday,  6, 'w' },
  { unset, 0, -1, 0, max_hour   , 23, 'H' },
  { unset, 0, -1, 0, max_minute , 59, 'M' },
  { unset, 0, -1, 0, max_second , 59, 'S' },
  { unset, 0, -1, 0, null       ,  0,  0  }
};

static unit units_monthday[]={
  { unset, 0, -1, 0, max_year    , -1, 'y' },
  { unset, 0, -1, 1, max_month   , 12, 'm' },
  { unset, 0, -1, 1, max_monthday, 31, 'd' },
  { unset, 0, -1, 0, max_hour    , 23, 'H' },
  { unset, 0, -1, 0, max_minute  , 59, 'M' },
  { unset, 0, -1, 0, max_second  , 59, 'S' },
  { unset, 0, -1, 0, null        ,  0,  0  }
};

static unsigned int max_monthday(void) {
  static unsigned int const limits[]=
    { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  unsigned int const year =units_monthday[0].value;
  unsigned int const month=units_monthday[1].value;
  int leapday;
  if (month!=2)         leapday=0;
  else if (year%400==0) leapday=1;
  else if (year%100==0) leapday=0;
  else if (year%  4==0) leapday=1;
  else                  leapday=0;
  return limits[month-1]+leapday;
}

int main(int argc, char** argv) {
  char const* constraint_str;
  unit* units;
  unsigned int i;
  int flag_increased=0;
  int flag_need_increase=0;
  struct tm tm;

  RW_ARG_CHECK(2, " stamp constraints");

  constraint_str=argv[2];
  if (constraint_str[str_chr(constraint_str, 'w')]=='w')
    units=units_weekday;
  else units=units_monthday;

  while (*constraint_str!='\0') {
    char const* const constraint=constraint_str;
    unsigned int len;
    if (*constraint_str!=',') die_malformed(constraint_str, ",");
    ++constraint_str;
    for (i=0;; ++i) {
      if (units[i].max==null)
        die_malformed(constraint_str,
                      "\"y\", \"m\", \"d\", \"w\", \"H\", \"M\", or \"S\"");
      if (*constraint_str==units[i].id) break;
    }
    if (units[i].type!=unset)
      strerr_die3x(100, PROG, ": duplicate constraint for unit: ", constraint);
    ++constraint_str;
    switch (*constraint_str) {
      case '=': units[i].type=eq;  break;
      case '-': units[i].type=eqn; break;
      case '/': units[i].type=div; break;
      default: die_malformed(constraint_str, "\"=\", \"-\", \"/\", or \",\"");
    }
    ++constraint_str;
    len=scan_uint(constraint_str, &units[i].spec);
    if (len==0) die_malformed(constraint_str, "number");
    if (units[i].spec==0) units[i].type=eq;
    else if (units[i].type==eqn) units[i].spec--;
    if (units[i].spec>units[i].maxmax) {
      if (units[i].type!=div || units[i].min>0)
        die_unit_range(constraint);
      units[i].type=eq;
      units[i].spec=0;
    } else if (units[i].spec<units[i].min && units[i].type==eq)
      die_unit_range(constraint);
    constraint_str+=len;
  }

  {
    struct taia stamp;
    struct tm* ptm;
    struct timeval tv;
    time_t t;
    rw_scan(&stamp, argv[1]);
    if (timeval_from_taia(&tv, &stamp)==0) die_range();
    t=tv.tv_sec;
    ptm=localtime(&t);
    if (ptm==null) die_range();
    tm=*ptm;
    if (units==units_weekday) {
      units_weekday[0].value=tm.tm_wday;
      units_weekday[1].value=tm.tm_hour;
      units_weekday[2].value=tm.tm_min;
      units_weekday[3].value=tm.tm_sec;
    } else {
      units_monthday[0].value=tm.tm_year+1900;
      units_monthday[1].value=tm.tm_mon+1;
      units_monthday[2].value=tm.tm_mday;
      units_monthday[3].value=tm.tm_hour;
      units_monthday[4].value=tm.tm_min;
      units_monthday[5].value=tm.tm_sec;
    }
  }

  for (i=0; units[i].max!=null && units[i].type==unset; ++i)
    units[i].type=div, units[i].spec=1;
  for (; units[i].max!=null; ++i)
    if (units[i].type==unset)
      units[i].type=eq, units[i].spec=units[i].min;

  for (i=0; units[i].max!=null;) {
    unsigned int const value=(flag_increased? units[i].min: units[i].value);
    unsigned int const spec=units[i].spec;
    unsigned int newval;
    if (units[i].type==div) {
      newval=value+(spec-1);
      newval-=newval%spec;
      if (flag_need_increase && newval==value) newval+=spec;
    } else {
      if (units[i].type==eq) newval=spec;
      else newval=units[i].max()-spec;
      if (newval<value) goto need_increase;
      if (flag_need_increase && newval==value) goto need_increase;
    }
    if (newval>units[i].max()) goto need_increase;
    if (newval>value) {
      flag_increased=1;
      flag_need_increase=0;
    } else if (flag_need_increase) goto need_increase;
    units[i].value=newval;
    if (!flag_need_increase) { ++i; continue; }
  need_increase:
    if (i!=0) { flag_need_increase=1; flag_increased=0; --i; continue; }
    if (units!=units_weekday)
      strerr_die2x(99, PROG, ": no future times match constraints");
    tm.tm_mday+=7-units[0].value;
    units[0].value=tm.tm_wday=0;
    flag_increased=1;
    flag_need_increase=0;
  }

  {
    struct taia stamp;
    struct timeval tv;
    time_t t;
    if (units==units_weekday) {
      tm.tm_mday+=units_weekday[0].value-tm.tm_wday;
      tm.tm_hour =units_weekday[1].value;
      tm.tm_min  =units_weekday[2].value;
      tm.tm_sec  =units_weekday[3].value;
    } else {
      tm.tm_year=units_monthday[0].value-1900;
      tm.tm_mon =units_monthday[1].value-1;
      tm.tm_mday=units_monthday[2].value;
      tm.tm_hour=units_monthday[3].value;
      tm.tm_min =units_monthday[4].value;
      tm.tm_sec =units_monthday[5].value;
    }
    tm.tm_isdst=-1;
    t=mktime(&tm);
    if (t==(time_t)-1) die_range();
    tv.tv_sec=t;
    tv.tv_usec=0;
    taia_from_timeval(&stamp, &tv);
    rw_pass(&stamp, argv+3);
  }
}


syntax highlighted by Code2HTML, v. 0.9.1