#include "module.h"
#include "lauxlib.h"
#include "lmem.h"
#include "platform.h"
#include "user_interface.h"
#include "c_types.h"
#include <string.h>
#include "gpio.h"
#include "hw_timer.h"
#include "pin_map.h"
#include "driver/gpio16.h"

#define TIMER_OWNER (('P' << 8) + 'U')

#define xstr(s) str(s)
#define str(s) #s

// Maximum delay in microseconds
#define DELAY_LIMIT 64000000

typedef struct {
  uint32_t gpio_set;
  uint32_t gpio_clr;
  int32_t delay;
  int32_t delay_min;
  int32_t delay_max;
  uint32_t count;
  uint32_t count_left;
  uint16_t loop;
} pulse_entry_t;

typedef struct {
  uint32_t entry_count;
  volatile uint32_t steps;
  volatile uint16_t entry_pos;
  volatile uint16_t active_pos;
  volatile int16_t stop_pos;  // -1 is stop nowhere, -2 is stop everywhere, otherwise is stop point
  pulse_entry_t *entry;
  volatile uint32_t expected_end_time;
  volatile uint32_t desired_end_time;
  volatile int32_t next_adjust;
  volatile int32_t pending_delay;
  volatile int cb_ref;
} pulse_t;

static int active_pulser_ref;
static pulse_t *active_pulser;
static task_handle_t tasknumber;

static int gpio_pulse_push_state(lua_State *L, pulse_t *pulser) {
  uint32_t now;
  uint32_t expected_end_time;
  uint32_t active_pos;
  uint32_t steps;
  do {
    now = 0x7FFFFFFF & system_get_time();
    expected_end_time = pulser->expected_end_time;
    active_pos = pulser->active_pos;
    steps = pulser->steps;
  } while (expected_end_time != pulser->expected_end_time ||
           active_pos != pulser->active_pos ||
           steps != pulser->steps);

  if (active_pos >= pulser->entry_count) {
    lua_pushnil(L);
  } else {
    lua_pushinteger(L, active_pos + 1);    // Lua is 1 offset
  }
  lua_pushinteger(L, steps);

  int32_t diff = (expected_end_time - now) & 0x7fffffff;
  lua_pushinteger(L, (diff << 1) >> 1);
  lua_pushinteger(L, now);
  return 4;
}

static int gpio_pulse_getstate(lua_State *L) {
  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");

  return gpio_pulse_push_state(L, pulser);
}

static int gpio_pulse_stop(lua_State *L) {
  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");

  if (pulser != active_pulser) {
    return 0;
  }

  int argno = 2;

  int32_t stop_pos = -2;

  if (lua_type(L, argno) == LUA_TNUMBER) {
    stop_pos = luaL_checkinteger(L, 2);
    if (stop_pos != -2) {
      if (stop_pos < 1 || stop_pos > pulser->entry_count) {
        return luaL_error( L, "bad stop position: %d (valid range 1 - %d)", stop_pos, pulser->entry_count );
      }
      stop_pos = stop_pos - 1;
    }
    argno++;
  }

  if (lua_type(L, argno) == LUA_TFUNCTION || lua_type(L, argno) == LUA_TLIGHTFUNCTION) {
    lua_pushvalue(L, argno);
  } else {
    return luaL_error( L, "missing callback" );
  }

  int new_cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
  int cb_ref = pulser->cb_ref;

  pulser->cb_ref = LUA_NOREF;
  pulser->stop_pos = -1;
  pulser->cb_ref = new_cb_ref;
  pulser->stop_pos = stop_pos;

  luaL_unref(L, LUA_REGISTRYINDEX, cb_ref);

  lua_pushboolean(L, 1);
  return 1;
}

static int gpio_pulse_delete(lua_State *L) {
  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");

  if (pulser == active_pulser) {
    return 0;
  }

  luaL_unref(L, LUA_REGISTRYINDEX, pulser->cb_ref);
  return 0;
}

static void fill_entry_from_table(lua_State *L, pulse_entry_t *entry) {
  if (lua_type(L, -1) != LUA_TTABLE) {
    luaL_error(L, "All entries must be tables");
  }

  lua_pushnil(L);             // key position
  while (lua_next(L, -2)) {
    // stack now contains: -1 => value; -2 => key; -3 => table
    if (lua_type(L, -2) == LUA_TNUMBER) {
      int pin = luaL_checkint(L, -2);
      int value = luaL_checkint(L, -1);

      if (pin < 0 || pin >= GPIO_PIN_NUM) {
        luaL_error(L, "pin number %d must be in range 0 .. %d", pin, GPIO_PIN_NUM - 1);
      }

      if (value) {
        entry->gpio_set |= BIT(pin_num[pin]);
      } else {
        entry->gpio_clr |= BIT(pin_num[pin]);
      }
    } else {
      const char *str = luaL_checkstring(L, -2);

      if (strcmp(str, "delay") == 0) {
        entry->delay = luaL_checkint(L, -1);
        if (entry->delay < 0 || entry->delay > DELAY_LIMIT) {
          luaL_error(L, "delay of %d must be in the range 0 .. " xstr(DELAY_LIMIT) " microseconds", entry->delay);
        }
      } else if (strcmp(str, "min") == 0) {
        entry->delay_min = luaL_checkint(L, -1);
        if (entry->delay_min < 0 || entry->delay_min > DELAY_LIMIT) {
          luaL_error(L, "delay minimum of %d must be in the range 0 .. " xstr(DELAY_LIMIT) " microseconds", entry->delay_min);
        }
      } else if (strcmp(str, "max") == 0) {
        entry->delay_max = luaL_checkint(L, -1);
        if (entry->delay_max < 0 || entry->delay_max > DELAY_LIMIT) {
          luaL_error(L, "delay maximum of %d must be in the range 0 .. " xstr(DELAY_LIMIT) " microseconds", entry->delay_max);
        }
      } else if (strcmp(str, "count") == 0) {
        entry->count = luaL_checkint(L, -1);
      } else if (strcmp(str, "loop") == 0) {
        entry->loop = luaL_checkint(L, -1);
      } else {
        luaL_error(L, "Unrecognized key found: %s", str);
      }
    }
    lua_pop(L, 1);
  }

  if (entry->delay_min != -1 || entry->delay_max != -1) {
    if (entry->delay_min == -1) {
      entry->delay_min = 0;
    }
    if (entry->delay_min > entry->delay ||
        entry->delay_max < entry->delay) {
      luaL_error(L, "Delay of %d must be between min and max", entry->delay);
    }
  }

  lua_pop(L, 1);
}

static int gpio_pulse_build(lua_State *L) {
  // Take a table argument
  luaL_checktype(L, 1, LUA_TTABLE);

  // First figure out how big we need the block to be
  size_t size = luaL_getn(L, 1);

  if (size > 100) {
    return luaL_error(L, "table is too large: %d entries is more than 100", size);
  }

  size_t memsize = sizeof(pulse_t) + size * sizeof(pulse_entry_t);
  pulse_t *pulser = (pulse_t *) lua_newuserdata(L, memsize);
  memset(pulser, 0, memsize);
  //
  // Associate its metatable
  luaL_getmetatable(L, "gpio.pulse");
  lua_setmetatable(L, -2);

  pulser->entry = (pulse_entry_t *) (pulser + 1);
  pulser->entry_count = size;

  size_t i;
  for (i = 0; i < size; i++) {
    pulse_entry_t *entry = pulser->entry + i;

    entry->delay_min = -1;
    entry->delay_max = -1;

    lua_rawgeti(L, 1, i + 1);

    fill_entry_from_table(L, entry);
  }

  return 1;
}

static int gpio_pulse_update(lua_State *L) {
  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");
  int entry_pos = luaL_checkinteger(L, 2);

  if (entry_pos < 1 || entry_pos > pulser->entry_count) {
    return luaL_error(L, "entry number must be in range 1 .. %d", pulser->entry_count);
  }

  pulse_entry_t *entry = pulser->entry + entry_pos - 1;

  pulse_entry_t new_entry = *entry;

  lua_pushvalue(L, 3);

  fill_entry_from_table(L, &new_entry);

  // Now do the update
  ETS_FRC1_INTR_DISABLE();
  *entry = new_entry;
  ETS_FRC1_INTR_ENABLE();

  return 0;
}

static int gpio_pulse_adjust(lua_State *L) {
  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");

  if (active_pulser != pulser) {
    return 0;
  }

  int offset = luaL_checkinteger(L, 2);
  // This will alter the next adjustable
  pulser->next_adjust = offset;

  int rc = gpio_pulse_push_state(L, active_pulser);

  return rc;
}

static int gpio_pulse_cancel(lua_State *L) {
  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");

  if (active_pulser != pulser) {
    return 0;
  }

  // Shut off the timer
  platform_hw_timer_close(TIMER_OWNER);

  int rc = gpio_pulse_push_state(L, active_pulser);

  active_pulser = NULL;

  int pulser_ref = active_pulser_ref;
  active_pulser_ref = LUA_NOREF;
  luaL_unref(L, LUA_REGISTRYINDEX, pulser_ref);

  return rc;
}

static void ICACHE_RAM_ATTR gpio_pulse_timeout(os_param_t p) {
  (void) p;

  uint32_t now = system_get_time();

  int delay;

  if (active_pulser) {
    delay = active_pulser->pending_delay;
    if (delay > 0) {
      if (delay > 1200000) {
        delay = 1000000;
      }
      active_pulser->pending_delay -= delay;
      platform_hw_timer_arm_us(TIMER_OWNER, delay);
      return;
    }
  }

  do {
    active_pulser->active_pos = active_pulser->entry_pos;

    if (!active_pulser || active_pulser->entry_pos >= active_pulser->entry_count) {
      if (active_pulser) {
        active_pulser->steps++;
      }
      platform_hw_timer_close(TIMER_OWNER);
      task_post_low(tasknumber, (task_param_t)0);
      return;
    }
    active_pulser->steps++;

    pulse_entry_t *entry = active_pulser->entry + active_pulser->entry_pos;

    // Yes, this means that there is more skew on D0 than on other pins....
    if (entry->gpio_set & 0x10000) {
      gpio16_output_set(1);
    }
    GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, entry->gpio_set);
    GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, entry->gpio_clr);
    if (entry->gpio_clr & 0x10000) {
      gpio16_output_set(0);
    }

    int16_t stop = active_pulser->stop_pos;
    if (stop == -2 || stop == active_pulser->entry_pos) {
      platform_hw_timer_close(TIMER_OWNER);
      task_post_low(tasknumber, (task_param_t)0);
      return;
    }

    if (entry->loop) {
      if (entry->count_left == 0) {
        entry->count_left = entry->count + 1;
      }
      if (--entry->count_left >= 1) {
        active_pulser->entry_pos = entry->loop - 1;       // zero offset
      } else {
        active_pulser->entry_pos++;
      }
    } else {
      active_pulser->entry_pos++;
    }

    delay = entry->delay;

    int delay_offset = 0;

    if (entry->delay_min != -1) {
      int offset = active_pulser->next_adjust;
      active_pulser->next_adjust = 0;
      delay_offset = ((0x7fffffff & (now - active_pulser->desired_end_time)) << 1) >> 1;
      delay -= delay_offset;
      delay += offset;
      //dbg_printf("%d(et %d diff %d): Delay was %d us, offset = %d, delay_offset = %d, new delay = %d, range=%d..%d\n",
      //           now, active_pulser->desired_end_time, now - active_pulser->desired_end_time,
      //           entry->delay, offset, delay_offset, delay, entry->delay_min, entry->delay_max);
      if (delay < entry->delay_min) {
        // we can't delay as little as 'delay', so we need to adjust
        // the next period as well.
        active_pulser->next_adjust = (entry->delay - entry->delay_min) + offset;
        delay = entry->delay_min;
      } else if (delay > entry->delay_max) {
        // we can't delay as much as 'delay', so we need to adjust
        // the next period as well.
        active_pulser->next_adjust = (entry->delay - entry->delay_max) + offset;
        delay = entry->delay_max;
      }
    }

    active_pulser->desired_end_time += delay + delay_offset;
    active_pulser->expected_end_time = system_get_time() + delay;
  } while (delay < 3);

  if (delay > 1200000) {
    active_pulser->pending_delay = delay - 1000000;
    delay = 1000000;
  }
  platform_hw_timer_arm_us(TIMER_OWNER, delay);
}

static int gpio_pulse_start(lua_State *L) {
  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");

  if (active_pulser) {
    return luaL_error(L, "pulse operation already in progress");
  }

  int argno = 2;
  int initial_adjust;

  if (lua_type(L, argno) == LUA_TNUMBER) {
    initial_adjust = luaL_checkinteger(L, argno);
    argno++;
  }

  if (lua_type(L, argno) == LUA_TFUNCTION || lua_type(L, argno) == LUA_TLIGHTFUNCTION) {
    lua_pushvalue(L, argno);
  } else {
    return luaL_error( L, "missing callback" );
  }

  luaL_unref(L, LUA_REGISTRYINDEX, pulser->cb_ref);
  pulser->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);

  active_pulser = pulser;
  lua_pushvalue(L, 1);
  active_pulser_ref = luaL_ref(L, LUA_REGISTRYINDEX);

  size_t i;
  for (i = 0; i < pulser->entry_count; i++) {
    pulser->entry[i].count_left = 0;
  }
  pulser->entry_pos = 0;
  pulser->active_pos = 0;
  pulser->steps = 0;
  pulser->stop_pos = -1;
  pulser->next_adjust = initial_adjust;

  // Now start things up
  if (!platform_hw_timer_init(TIMER_OWNER, FRC1_SOURCE, FALSE)) {
    // Failed to init the timer
    luaL_error(L, "Unable to initialize timer");
  }

  active_pulser->expected_end_time = 0x7fffffff & system_get_time();
  platform_hw_timer_set_func(TIMER_OWNER, gpio_pulse_timeout, 0);
  gpio_pulse_timeout(0);

  return 0;
}

static void gpio_pulse_task(os_param_t param, uint8_t prio)
{
  (void) param;
  (void) prio;

  if (active_pulser) {
    lua_State *L = lua_getstate();
    // Invoke the callback
    lua_rawgeti(L, LUA_REGISTRYINDEX, active_pulser->cb_ref);

    int rc = gpio_pulse_push_state(L, active_pulser);

    active_pulser = NULL;
    int pulser_ref = active_pulser_ref;
    active_pulser_ref = LUA_NOREF;
    luaL_unref(L, LUA_REGISTRYINDEX, pulser_ref);

    lua_call(L, rc, 0);
  }
}

LROT_BEGIN(pulse)
  LROT_FUNCENTRY( getstate, gpio_pulse_getstate )
  LROT_FUNCENTRY( stop, gpio_pulse_stop )
  LROT_FUNCENTRY( cancel, gpio_pulse_cancel )
  LROT_FUNCENTRY( start, gpio_pulse_start )
  LROT_FUNCENTRY( adjust, gpio_pulse_adjust )
  LROT_FUNCENTRY( update, gpio_pulse_update )
  LROT_FUNCENTRY( __gc, gpio_pulse_delete )
  LROT_TABENTRY( __index, pulse )
LROT_END( pulse, pulse, LROT_MASK_GC_INDEX )


LROT_PUBLIC_BEGIN(gpio_pulse)
  LROT_FUNCENTRY( build, gpio_pulse_build )
  LROT_TABENTRY( __index, gpio_pulse )
LROT_END( gpio_pulse, gpio_pulse, LROT_MASK_INDEX )


int gpio_pulse_init(lua_State *L)
{
  luaL_rometatable(L, "gpio.pulse", LROT_TABLEREF(pulse));
  tasknumber = task_get_id(gpio_pulse_task);
  return 0;
}

NODEMCU_MODULE(GPIOPULSE, "gpiopulse", gpio_pulse, gpio_pulse_init);

