// Cron module

#include "module.h"
#include "lauxlib.h"
#include "lmem.h"
#include "user_interface.h"
#include "c_types.h"
#include "c_string.h"
#include "ets_sys.h"
#include "time.h"
#include "rtc/rtctime_internal.h"
#include "rtc/rtctime.h"
#include "stdlib.h"
#include "mem.h"

struct cronent_desc {
  uint64_t min;  // Minutes repeat - bits 0-59
  uint32_t hour; // Hours repeat - bits 0-23
  uint32_t dom;  // Day-of-month repeat - bits 0-30
  uint16_t mon;  // Monthly repeat - bits 0-11
  uint8_t  dow;  // Day-of-week repeat - bits 0-6
};

typedef struct cronent_ud {
  struct cronent_desc desc;
  int cb_ref;
} cronent_ud_t;

static ETSTimer cron_timer;

static int *cronent_list = 0;
static size_t cronent_count = 0;

static uint64_t lcron_parsepart(lua_State *L, char *str, char **end, uint8_t min, uint8_t max) {
  uint64_t res = 0;
  if (str[0] == '*') {
    uint32_t each = 1;
    *end = str + 1;
    if (str[1] == '/') {
      each = strtol(str + 2, end, 10);
      if (end != 0)
      if (each == 0 || each >= max - min) {
        return luaL_error(L, "invalid spec (each %d)", each);
      }
    }
    for (int i = 0; i <= (max - min); i++) {
      if (((min + i) % each) == 0) res |= (uint64_t)1 << i;
    }
  } else {
    uint32_t val;
    while (1) {
      val = strtol(str, end, 10);
      if (val < min || val > max) {
        return luaL_error(L, "invalid spec (val %d out of range %d..%d)", val, min, max);
      }
      res |= (uint64_t)1 << (val - min);
      if (**end != ',') break;
      str = *end + 1;
    }
  }
  return res;
}

static int lcron_parsedesc(lua_State *L, char *str, struct cronent_desc *desc) {
  char *s = str;
  desc->min = lcron_parsepart(L, s, &s, 0, 59);
  if (*s != ' ') return luaL_error(L, "invalid spec (separator @%d)", s - str);
  desc->hour = lcron_parsepart(L, s + 1, &s, 0, 23);
  if (*s != ' ') return luaL_error(L, "invalid spec (separator @%d)", s - str);
  desc->dom = lcron_parsepart(L, s + 1, &s, 1, 31);
  if (*s != ' ') return luaL_error(L, "invalid spec (separator @%d)", s - str);
  desc->mon = lcron_parsepart(L, s + 1, &s, 1, 12);
  if (*s != ' ') return luaL_error(L, "invalid spec (separator @%d)", s - str);
  desc->dow = lcron_parsepart(L, s + 1, &s, 0, 6);
  if (*s != 0) return luaL_error(L, "invalid spec (trailing @%d)", s - str);
  return 0;
}

static int lcron_create(lua_State *L) {
  // Check arguments
  char *strdesc = (char*)luaL_checkstring(L, 1);
  luaL_checkanyfunction(L, 2);
  // Parse description
  struct cronent_desc desc;
  lcron_parsedesc(L, strdesc, &desc);
  // Allocate userdata
  cronent_ud_t *ud = lua_newuserdata(L, sizeof(cronent_ud_t));
  // Set metatable
  luaL_getmetatable(L, "cron.entry");
  lua_setmetatable(L, -2);
  // Set callback
  lua_pushvalue(L, 2);
  ud->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
  // Set entry
  ud->desc = desc;
  // Store entry
  lua_pushvalue(L, -1);
  cronent_list = os_realloc(cronent_list, sizeof(int) * (cronent_count + 1));
  cronent_list[cronent_count++] = luaL_ref(L, LUA_REGISTRYINDEX);
  return 1;
}

static size_t lcron_findindex(lua_State *L, cronent_ud_t *ud) {
  cronent_ud_t *eud;
  size_t i;
  for (i = 0; i < cronent_count; i++) {
    lua_rawgeti(L, LUA_REGISTRYINDEX, cronent_list[i]);
    eud = lua_touserdata(L, 1);
    lua_pop(L, 1);
    if (eud == ud) break;
  }
  if (i == cronent_count) return -1;
  return i;
}

static int lcron_schedule(lua_State *L) {
  cronent_ud_t *ud = luaL_checkudata(L, 1, "cron.entry");
  char *strdesc = (char*)luaL_checkstring(L, 2);
  struct cronent_desc desc;
  lcron_parsedesc(L, strdesc, &desc);
  ud->desc = desc;
  size_t i = lcron_findindex(L, ud);
  if (i == -1) {
    lua_pushvalue(L, 1);
    cronent_list = os_realloc(cronent_list, sizeof(int) * (cronent_count + 1));
    cronent_list[cronent_count++] = lua_ref(L, LUA_REGISTRYINDEX);
  }
  return 0;
}

static int lcron_handler(lua_State *L) {
  cronent_ud_t *ud = luaL_checkudata(L, 1, "cron.entry");
  luaL_checkanyfunction(L, 2);
  lua_pushvalue(L, 2);
  luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_ref);
  ud->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
  return 0;
}

static int lcron_unschedule(lua_State *L) {
  cronent_ud_t *ud = luaL_checkudata(L, 1, "cron.entry");
  size_t i = lcron_findindex(L, ud);
  if (i == -1) return 0;
  luaL_unref(L, LUA_REGISTRYINDEX, cronent_list[i]);
  memmove(cronent_list + i, cronent_list + i + 1, sizeof(int) * cronent_count - i - 1);
  cronent_count--;
  return 0;
}

static int lcron_delete(lua_State *L) {
  cronent_ud_t *ud = luaL_checkudata(L, 1, "cron.entry");
  lcron_unschedule(L);
  luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_ref);
  return 0;
}

static int lcron_reset(lua_State *L) {
  for (size_t i = 0; i < cronent_count; i++) {
    luaL_unref(L, LUA_REGISTRYINDEX, cronent_list[i]);
  }
  cronent_count = 0;
  os_free(cronent_list);
  cronent_list = 0;
  return 0;
}

static void cron_handle_time(uint8_t mon, uint8_t dom, uint8_t dow, uint8_t hour, uint8_t min) {
  lua_State *L = lua_getstate();
  struct cronent_desc desc;
  desc.mon  = (uint16_t)1 << (mon - 1);
  desc.dom  = (uint32_t)1 << (dom - 1);
  desc.dow  = ( uint8_t)1 << dow;
  desc.hour = (uint32_t)1 << hour;
  desc.min  = (uint64_t)1 << min;
  for (size_t i = 0; i < cronent_count; i++) {
    lua_rawgeti(L, LUA_REGISTRYINDEX, cronent_list[i]);
    cronent_ud_t *ent = lua_touserdata(L, 1);
    lua_pop(L, 1);
    if ((ent->desc.mon  & desc.mon ) == 0) continue;
    if ((ent->desc.dom  & desc.dom ) == 0) continue;
    if ((ent->desc.dow  & desc.dow ) == 0) continue;
    if ((ent->desc.hour & desc.hour) == 0) continue;
    if ((ent->desc.min  & desc.min ) == 0) continue;
    lua_rawgeti(L, LUA_REGISTRYINDEX, ent->cb_ref);
    lua_rawgeti(L, LUA_REGISTRYINDEX, cronent_list[i]);
    lua_call(L, 1, 0);
  }
}

static void cron_handle_tmr() {
  struct rtc_timeval tv;
  rtc_time_gettimeofday(&tv);
  if (tv.tv_sec == 0) { // Wait for RTC time
    ets_timer_arm_new(&cron_timer, 1000, 0, 1);
    return;
  }
  time_t t = tv.tv_sec;
  struct tm tm;
  gmtime_r(&t, &tm);
  uint32_t diff = 60000 - tm.tm_sec * 1000 - tv.tv_usec / 1000;
  if (tm.tm_sec == 59) {
    t++;
    diff += 60000;
    gmtime_r(&t, &tm);
  }
  ets_timer_arm_new(&cron_timer, diff, 0, 1);
  cron_handle_time(tm.tm_mon + 1, tm.tm_mday, tm.tm_wday, tm.tm_hour, tm.tm_min);
}

static const LUA_REG_TYPE cronent_map[] = {
  { LSTRKEY( "schedule" ),   LFUNCVAL( lcron_schedule ) },
  { LSTRKEY( "handler" ),    LFUNCVAL( lcron_handler ) },
  { LSTRKEY( "unschedule" ), LFUNCVAL( lcron_unschedule ) },
  { LSTRKEY( "__gc" ),       LFUNCVAL( lcron_delete ) },
  { LSTRKEY( "__index" ),    LROVAL( cronent_map ) },
  { LNILKEY, LNILVAL }
};

static const LUA_REG_TYPE cron_map[] = {
  { LSTRKEY( "schedule" ),   LFUNCVAL( lcron_create ) },
  { LSTRKEY( "reset" ),      LFUNCVAL( lcron_reset ) },
  { LNILKEY, LNILVAL }
};

int luaopen_cron( lua_State *L ) {
  ets_timer_disarm(&cron_timer);
  ets_timer_setfn(&cron_timer, cron_handle_tmr, 0);
  ets_timer_arm_new(&cron_timer, 1000, 0, 1);
  luaL_rometatable(L, "cron.entry", (void *)cronent_map);
  return 0;
}

NODEMCU_MODULE(CRON, "cron", cron_map, luaopen_cron);
