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

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

char const* PROG="caldelay";

static char const var_delay[]="$DELAY";
static char const var_weekday[]="$WEEKDAY";
static char const var_year[]="$YEAR";
static char const var_month[]="$MONTH";
static char const var_monthday[]="$MONTHDAY";
static char const var_hour[]="$HOUR";
static char const var_minute[]="$MINUTE";
static char const var_second[]="$SECOND";

typedef enum { eq, eqn, div } rw_match_type;

typedef struct constraint constraint;
struct constraint {
  constraint const* next;
  rw_match_type type;
  unsigned long spec;
};

typedef struct {
  char const* const env;
  constraint const* constraints;
  unsigned long value;
  unsigned long (* const limit)(void);
} unit;

static unsigned long limit_weekday (void) { return  6; }
static unsigned long limit_year    (void) { return -1; }
static unsigned long limit_month   (void) { return 11; }
static unsigned long limit_monthday(void);
static unsigned long limit_hour    (void) { return 23; }
static unsigned long limit_minute  (void) { return 59; }
static unsigned long limit_second  (void) { return 59; }

static unit weekday_units[]={
  { var_weekday,  null, -1, limit_weekday  },
  { var_hour,     null, -1, limit_hour     },
  { var_minute,   null, -1, limit_minute   },
  { var_second,   null, -1, limit_second   }
};

static unit monthday_units[]={
  { var_year,     null, -1, limit_year     },
  { var_month,    null, -1, limit_month    },
  { var_monthday, null, -1, limit_monthday },
  { var_hour,     null, -1, limit_hour     },
  { var_minute,   null, -1, limit_minute   },
  { var_second,   null, -1, limit_second   }
};

static unsigned long limit_monthday(void) {
  static unsigned long const limits[]={
    30, 27, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30
  };
  unsigned long leap;
  {
    unsigned long const month=monthday_units[1].value;
    if (month!=1) return limits[month];
  }
  leap=monthday_units[0].value;
  if (leap%4!=0) leap=0;
  else {
    leap/=4;
    if (leap%25!=0) leap=1;
    else {
      leap/=25;
      if (leap%4!=0) leap=0;
      else leap=1;
    }
  }
  return 27+leap;
}

static int increased=0;
static int need_increase=0;

static void do_unit(unit* const u) {
  constraint const* c;
  unsigned long const value=(increased? 0: u->value);
  unsigned long least;
  int found_any=0;
  for (c=u->constraints; c!=null; c=c->next) {
    unsigned long const spec=c->spec;
    unsigned long newval;
    if (c->type==div) {
      newval=value+(spec-1);
      newval-=newval%spec;
      if (need_increase && newval==value) newval+=spec;
      if (newval>u->limit()) continue;
    } else {
      if (c->type==eq) newval=spec;
      else newval=u->limit()-spec;
      if (newval<value) continue;
      if (need_increase && newval==value) continue;
    }
    if (found_any && newval>=least) continue;
    found_any=1;
    least=newval;
    if (least==0) break; /* can't get any lower */
  }
  if (!found_any) {
    need_increase=1;
    return;
  }
  if (least>value) {
    increased=1;
    need_increase=0;
  }
  u->value=least;
  return;
}

int main(int argc, char** argv) {
  struct tai now;
  struct caltime ct;
  int weekday;
  char const* x;
  unit* units;
  size_t n_units;
  size_t i;

  if (argc<2)
    strerr_die3x(111, usage, PROG, " program [arg ...]");

  x=env_get(var_weekday+1);
  if (x!=null) {
    units=weekday_units;
    n_units=sizeof weekday_units/sizeof (unit);
  } else {
    units=monthday_units;
    n_units=sizeof monthday_units/sizeof (unit);
  }

  {
    int seen_any=0;
    for (i=0; i!=n_units; ++i) {
      constraint const** clink;
      constraint* c;
      x=env_get(units[i].env+1);
      clink=&units[i].constraints;
      c=(void*)alloc(sizeof *c);
      if (c==null)
        strerr_die4sys(111, PROG, err_unable, err_alloc, ": ");
      if (x!=null && *x!='\0') {
        seen_any=1;
        for (;;) {
          unsigned int len;
          switch (*x) {
            case '=': c->type=eq;  break;
            case '-': c->type=eqn; break;
            case '/': c->type=div; break;
            default:
              strerr_die4x(111, PROG, ": ", units[i].env, err_malformed);
          }
          ++x;
          len=scan_ulong(x, &c->spec);
          if (len==0)
            strerr_die4x(111, PROG, ": ", units[i].env, err_malformed);
          if (c->spec==0) c->type=eq;
          else if (c->type==eqn) c->spec--;
          x+=len;
          c->next=null;
          *clink=c;
          clink=&c->next;
          if (*x=='\0') break;
          c=(void*)alloc(sizeof *c);
          if (c==null)
            strerr_die4sys(111, PROG, err_unable, err_alloc, ": ");
        }
      } else {
        if (seen_any) {
          c->type=eq;
          c->spec=0;
        } else {
          c->type=div;
          c->spec=1;
        }
        units[i].constraints=c;
      }
    }
  }

  x=env_get(var_delay+1);
  if (x!=null) {
    {
      struct tai d;
      {
        unsigned char taibuf[TAI_PACK]={ 0, 0, 0, 0, 0, 0, 0, 0 };
        {
          unsigned char* bufp;
          unsigned long delay;
          {
            unsigned int const len=scan_ulong(x, &delay);
            if (len==0 || x[len]!='\0')
              strerr_die4x(111, PROG, ": ", var_delay, err_malformed);
          }
          bufp=taibuf+sizeof taibuf;
          while (delay!=0) {
            if (bufp==taibuf)
              strerr_die5x(100, PROG, ": ", var_delay, err_oflow,
                           "struct tai");
            --bufp;
            *bufp=delay&(unsigned long)0xff;
            delay>>=8;
          }
        }
        tai_unpack((char*)taibuf, &d);
      }
      tai_now(&now);
      tai_add(&now, &now, &d);
      caltime_utc(&ct, &now, &weekday, null);
      tai_sub(&now, &now, &d);
    }

    if (units==weekday_units) {
      weekday_units[0].value=weekday;
      weekday_units[1].value=ct.hour;
      weekday_units[2].value=ct.minute;
      weekday_units[3].value=ct.second;
    } else {
      monthday_units[0].value=ct.date.year;
      monthday_units[1].value=ct.date.month-1;
      monthday_units[2].value=ct.date.day-1;
      monthday_units[3].value=ct.hour;
      monthday_units[4].value=ct.minute;
      monthday_units[5].value=ct.second;
    }

    for (i=0; i!=n_units;) {
      do_unit(&units[i]);
      while (need_increase) {
        if (i==0) {
          if (units==monthday_units)
            strerr_die2x(99, PROG, ": all run times are in the past");
          units[0].value=0;
          ct.date.day+=7;
          increased=1;
          need_increase=0;
          continue;
        }
        --i;
        do_unit(&units[i]);
      }
      ++i;
    }

    if (units==weekday_units) {
      ct.date.day+=weekday_units[0].value;
      ct.date.day-=weekday;
      ct.hour     =weekday_units[1].value;
      ct.minute   =weekday_units[2].value;
      ct.second   =weekday_units[3].value;
    } else {
      ct.date.year =monthday_units[0].value;
      ct.date.month=monthday_units[1].value+1;
      ct.date.day  =monthday_units[2].value+1;
      ct.hour      =monthday_units[3].value;
      ct.minute    =monthday_units[4].value;
      ct.second    =monthday_units[5].value;
    }

    {
      char delaybuf[(sizeof (unsigned long)*CHAR_BIT+2)/3+1];
      {
        unsigned long delay;
        {
          unsigned char taibuf[TAI_PACK];
          {
            struct tai then;
            caltime_tai(&ct, &then);
            tai_sub(&then, &then, &now);
            tai_pack((char*)taibuf, &then);
          }
          delay=0;
          for (i=0; i<TAI_PACK; ++i) {
            unsigned long const d=delay<<8;
            if (d>>8!=delay)
              strerr_die5x(100, PROG, ": ", var_delay, err_oflow, "long");
            delay=d+taibuf[i];
            if (delay<d)
              strerr_die5x(100, PROG, ": ", var_delay, err_oflow, "long");
          }
        }
        delaybuf[fmt_ulong(delaybuf, delay)]='\0';
      }
      if (pathexec_env(var_delay+1, delaybuf)==0)
        strerr_die5sys(111, PROG, err_unable, err_setvar, var_delay, ": ");
    }
  }

  ++argv;
  pathexec((char const**)argv);
  strerr_die5sys(errstat, PROG, err_unable, err_exec, argv[0], ": ");
}


syntax highlighted by Code2HTML, v. 0.9.1