#include <lua.h>
#include <lauxlib.h>
#include "link/extensions/abl_link/include/abl_link.h"
#include "tinycthread.h"

#if LUA_VERSION_NUM > 501
#define luax_len(L, i) (int) lua_rawlen(L, i)
#define luax_register(L, f) luaL_setfuncs(L, f, 0)
#else
#define luax_len(L, i) (int) lua_objlen(L, i)
#define luax_register(L, f) luaL_register(L, NULL, f)
#define LUA_RIDX_MAINTHREAD 1
#endif

typedef struct {
  enum {
    EVENT_TYPE_NUM_PEERS,
    EVENT_TYPE_TEMPO,
    EVENT_TYPE_START_STOP,
  } type;
  union {
    uint64_t num_peers;
    double tempo;
    bool start_stop;
  };
} event_t;

#define NUM_EVENTS 64

typedef struct {
    event_t buffer[NUM_EVENTS];
    event_t *head;
    event_t *tail;
    event_t *end;
    bool full;
} event_queue_t;

void eq_init(event_queue_t *queue) {
    queue->head = queue->buffer;
    queue->tail = queue->buffer;
    queue->end  = queue->buffer + NUM_EVENTS;
}

static void eq_advance(event_queue_t *queue, event_t **ptr) {
    (*ptr)++;
    if (*ptr == queue->end) {
        *ptr = queue->buffer;
    }
}

void eq_push(event_queue_t *queue, event_t evt) {
    if (queue->full) return;

    *queue->head = evt;

    eq_advance(queue, &queue->head);

    if (queue->head == queue->tail) {
       queue->full = true;
    }
}

bool eq_pop(event_queue_t *queue, event_t *evt) {
    if (!queue->full && queue->head == queue->tail) {
      return false;
    }

    *evt = *(queue->tail);
    eq_advance(queue, &queue->tail);

    return true;
}

typedef struct {
  abl_link link;

  int cb_num_peers;
  int cb_tempo;
  int cb_start_stop;

  mtx_t mutex;
  event_queue_t queue;
} link_state_t;

static int l_create_link(lua_State* L) {
  double bpm = luaL_checknumber(L, 1);

  link_state_t* state = (link_state_t*)lua_newuserdata(L, sizeof(link_state_t));

  state->link = abl_link_create(bpm);
  state->cb_num_peers = LUA_NOREF;
  state->cb_tempo = LUA_NOREF;
  state->cb_start_stop = LUA_NOREF;

  mtx_init(&state->mutex, mtx_plain);
  eq_init(&state->queue);

  luaL_getmetatable(L, "abletonlink.link");
  lua_setmetatable(L, -2);

  return 1;
}

static int l_create_session_state(lua_State* L) {
  abl_link_session_state* session_state = (abl_link_session_state*)lua_newuserdata(L, sizeof(abl_link_session_state));

  *session_state = abl_link_create_session_state();

  luaL_getmetatable(L, "abletonlink.session_state");
  lua_setmetatable(L, -2);

  return 1;
}

static link_state_t *checklinkstate(lua_State *L) {
  link_state_t* state = (link_state_t*)luaL_checkudata(L, 1, "abletonlink.link");
  luaL_argcheck(L, state != NULL, 1, "`link' expected");
  luaL_argcheck(L, state->link.impl != NULL, 1, "`link' is already destroyed");
  return state;
}

static abl_link checklink(lua_State *L) {
  link_state_t* state = checklinkstate(L);
  return state->link;
}

static abl_link_session_state checksessionstate(lua_State *L, int ud) {
  abl_link_session_state* session_state = (abl_link_session_state*)luaL_checkudata(L, ud, "abletonlink.session_state");
  luaL_argcheck(L, session_state != NULL, 1, "`session_state' expected");
  luaL_argcheck(L, session_state->impl != NULL, 1, "`session_state' is already destroyed");
  return *session_state;
}

static int l_destroy_link(lua_State* L) {
  link_state_t* state = checklinkstate(L);

  if (state->link.impl) {
    abl_link_destroy(state->link);

    luaL_unref(L, LUA_REGISTRYINDEX, state->cb_num_peers);
    luaL_unref(L, LUA_REGISTRYINDEX, state->cb_tempo);
    luaL_unref(L, LUA_REGISTRYINDEX, state->cb_start_stop);
    state->cb_num_peers = LUA_NOREF;
    state->cb_tempo = LUA_NOREF;
    state->cb_start_stop = LUA_NOREF;

    mtx_destroy(&state->mutex);
    state->link.impl = NULL;
  }

  return 0;
}

static int l_is_enabled(lua_State* L) {
  abl_link link = checklink(L);
  bool enabled = abl_link_is_enabled(link);
  lua_pushboolean(L, enabled);
  return 1;
}

static int l_enable(lua_State* L) {
  abl_link link = checklink(L);
  bool enable = lua_isnoneornil(L, 2) ? true : lua_toboolean(L, 2);
  abl_link_enable(link, enable);
  return 0;
}

static int l_is_start_stop_sync_enabled(lua_State* L) {
  abl_link link = checklink(L);
  bool enabled = abl_link_is_start_stop_sync_enabled(link);
  lua_pushboolean(L, enabled);
  return 1;
}

static int l_enable_start_stop_sync(lua_State* L) {
  abl_link link = checklink(L);
  bool enable = lua_isnoneornil(L, 2) ? true : lua_toboolean(L, 2);
  abl_link_enable_start_stop_sync(link, enable);
  return 0;
}

static int l_num_peers(lua_State* L) {
  abl_link link = checklink(L);
  uint64_t peers = abl_link_num_peers(link);
  lua_pushinteger(L, peers);
  return 1;
}

static int l_clock_micros(lua_State* L) {
  abl_link link = checklink(L);
  int64_t micros = abl_link_clock_micros(link);
  lua_pushinteger(L, micros);
  return 1;
}

static int l_capture_audio_session_state(lua_State* L) {
  abl_link link = checklink(L);
  abl_link_session_state session_state = checksessionstate(L, 2);
  abl_link_capture_audio_session_state(link, session_state);
  return 0;
}

static int l_commit_audio_session_state(lua_State* L) {
  abl_link link = checklink(L);
  abl_link_session_state session_state = checksessionstate(L, 2);
  abl_link_commit_audio_session_state(link, session_state);
  return 0;
}

static int l_capture_app_session_state(lua_State* L) {
  abl_link link = checklink(L);
  abl_link_session_state session_state = checksessionstate(L, 2);
  abl_link_capture_app_session_state(link, session_state);
  return 0;
}

static int l_commit_app_session_state(lua_State* L) {
  abl_link link = checklink(L);
  abl_link_session_state session_state = checksessionstate(L, 2);
  abl_link_commit_app_session_state(link, session_state);
  return 0;
}

static void cb_num_peers(uint64_t num_peers, void *context) {
  link_state_t* state = (link_state_t *)context;
  event_t evt;
  evt.type = EVENT_TYPE_NUM_PEERS;
  evt.num_peers = num_peers;
  eq_push(&state->queue, evt);
}
static int l_set_num_peers_callback(lua_State* L) {
  link_state_t* state = checklinkstate(L);

  luaL_unref(L, LUA_REGISTRYINDEX, state->cb_num_peers);

  if (lua_isnil(L, 2)) {
    state->cb_num_peers = LUA_NOREF;
    abl_link_set_num_peers_callback(state->link, NULL, NULL);
    return 0;
  }

  luaL_checktype(L, 2, LUA_TFUNCTION);
  lua_pushvalue(L, 2);
  state->cb_num_peers = luaL_ref(L, LUA_REGISTRYINDEX);
  abl_link_set_num_peers_callback(state->link, cb_num_peers, state);
  return 0;
}

static void cb_tempo(double tempo, void *context) {
  link_state_t* state = (link_state_t *)context;
  event_t evt;
  evt.type = EVENT_TYPE_TEMPO;
  evt.tempo = tempo;
  eq_push(&state->queue, evt);
}
static int l_set_tempo_callback(lua_State* L) {
  link_state_t* state = checklinkstate(L);

  luaL_unref(L, LUA_REGISTRYINDEX, state->cb_tempo);

  if (lua_isnil(L, 2)) {
    state->cb_tempo = LUA_NOREF;
    abl_link_set_tempo_callback(state->link, NULL, NULL);
    return 0;
  }

  luaL_checktype(L, 2, LUA_TFUNCTION);
  lua_pushvalue(L, 2);
  state->cb_tempo = luaL_ref(L, LUA_REGISTRYINDEX);
  abl_link_set_tempo_callback(state->link, cb_tempo, state);
  return 0;
}

static void cb_start_stop(bool start_stop, void *context) {
  link_state_t* state = (link_state_t *)context;
  event_t evt;
  evt.type = EVENT_TYPE_START_STOP;
  evt.start_stop = start_stop;
  eq_push(&state->queue, evt);
}
static int l_set_start_stop_callback(lua_State* L) {
  link_state_t* state = checklinkstate(L);

  luaL_unref(L, LUA_REGISTRYINDEX, state->cb_start_stop);

  if (lua_isnil(L, 2)) {
    state->cb_start_stop = LUA_NOREF;
    abl_link_set_start_stop_callback(state->link, NULL, NULL);
    return 0;
  }

  luaL_checktype(L, 2, LUA_TFUNCTION);
  lua_pushvalue(L, 2);
  state->cb_start_stop = luaL_ref(L, LUA_REGISTRYINDEX);
  abl_link_set_start_stop_callback(state->link, cb_start_stop, state);
  return 0;
}

static int l_pump_events(lua_State* L) {
  link_state_t *state = checklinkstate(L);

  bool was_full = state->queue.full;
  state->queue.full = false;

  event_t evt;
  while (eq_pop(&state->queue, &evt)) {
    mtx_lock(&state->mutex);

    switch (evt.type) {
      case EVENT_TYPE_NUM_PEERS:
        if (state->cb_num_peers == LUA_NOREF) continue;

        lua_rawgeti(L, LUA_REGISTRYINDEX, state->cb_num_peers);
        lua_pushinteger(L, evt.num_peers);
        break;

      case EVENT_TYPE_TEMPO:
        if (state->cb_tempo == LUA_NOREF) continue;

        lua_rawgeti(L, LUA_REGISTRYINDEX, state->cb_tempo);
        lua_pushnumber(L, evt.tempo);
        break;

      case EVENT_TYPE_START_STOP:
        if (state->cb_start_stop == LUA_NOREF) continue;

        lua_rawgeti(L, LUA_REGISTRYINDEX, state->cb_start_stop);
        lua_pushinteger(L, evt.start_stop);
        break;
    }

    mtx_unlock(&state->mutex);
    lua_call(L, 1, 0);
  }

  lua_pushboolean(L, was_full);
  return 1;
}

static const luaL_Reg abl_link_r[] = {
  { "destroy", l_destroy_link },
  { "is_enabled", l_is_enabled},
  { "enable", l_enable },
  { "is_start_stop_sync_enabled", l_is_start_stop_sync_enabled },
  { "enable_start_stop_sync", l_enable_start_stop_sync},
  { "num_peers", l_num_peers },
  { "clock_micros", l_clock_micros },
  { "capture_audio_session_state", l_capture_audio_session_state },
  { "commit_audio_session_state", l_commit_audio_session_state },
  { "capture_app_session_state", l_capture_app_session_state },
  { "commit_app_session_state", l_commit_app_session_state },
  { "set_num_peers_callback", l_set_num_peers_callback },
  { "set_tempo_callback", l_set_tempo_callback },
  { "set_start_stop_callback", l_set_start_stop_callback },
  { "pump_events", l_pump_events },
  { "__gc", l_destroy_link },
  { NULL, NULL },
};

static int l_destroy_session_state(lua_State* L) {
  abl_link_session_state* session_state = (abl_link_session_state*)luaL_checkudata(L, 1, "abletonlink.session_state");
  luaL_argcheck(L, session_state != NULL, 1, "`session_state' expected");
  if (session_state->impl) {
    abl_link_destroy_session_state(*session_state);
    session_state->impl = NULL;
  }
  return 0;
}

static int l_tempo(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  double bpm = abl_link_tempo(session_state);
  lua_pushnumber(L, bpm);
  return 1;
}

static int l_set_tempo(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  double bpm = luaL_checknumber(L, 2);
  int64_t at_time = luaL_checkinteger(L, 3);
  abl_link_set_tempo(session_state, bpm, at_time);
  return 0;
}

static int l_beat_at_time(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  int64_t time = luaL_checkinteger(L, 2);
  double quantum = luaL_checknumber(L, 3);
  double beat = abl_link_beat_at_time(session_state, time, quantum);
  lua_pushnumber(L, beat);
  return 1;
}

static int l_phase_at_time(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  int64_t time = luaL_checkinteger(L, 2);
  double quantum = luaL_checknumber(L, 3);
  double phase = abl_link_phase_at_time(session_state, time, quantum);
  lua_pushnumber(L, phase);
  return 1;
}

static int l_time_at_beat(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  double beat = luaL_checknumber(L, 2);
  double quantum = luaL_checknumber(L, 3);
  int64_t time = abl_link_time_at_beat(session_state, beat, quantum);
  lua_pushinteger(L, time);
  return 1;
}

static int l_request_beat_at_time(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  double beat = luaL_checknumber(L, 2);
  int64_t time = luaL_checkinteger(L, 3);
  double quantum = luaL_checknumber(L, 4);
  abl_link_request_beat_at_time(session_state, beat, time, quantum);
  return 0;
}

static int l_force_beat_at_time(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  double beat = luaL_checknumber(L, 2);
  int64_t time = luaL_checkinteger(L, 3);
  double quantum = luaL_checknumber(L, 4);
  abl_link_force_beat_at_time(session_state, beat, time, quantum);
  return 0;
}

static int l_is_playing(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  bool playing = abl_link_is_playing(session_state);
  lua_pushboolean(L, playing);
  return 1;
}

static int l_set_is_playing(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  bool playing = lua_toboolean(L, 2);
  int64_t time = luaL_checkinteger(L, 3);
  abl_link_set_is_playing(session_state, playing, time);
  return 0;
}

static int l_time_for_is_playing(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  int64_t time = abl_link_time_for_is_playing(session_state);
  lua_pushinteger(L, time);
  return 1;
}

static int l_request_beat_at_start_playing_time(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  double beat = luaL_checknumber(L, 2);
  double quantum = luaL_checknumber(L, 3);
  abl_link_request_beat_at_start_playing_time(session_state, beat, quantum);
  return 0;
}

static int l_set_is_playing_and_request_beat_at_time(lua_State* L) {
  abl_link_session_state session_state = checksessionstate(L, 1);
  bool playing = lua_toboolean(L, 2);
  int64_t time = luaL_checkinteger(L, 3);
  double beat = luaL_checknumber(L, 4);
  double quantum = luaL_checknumber(L, 5);
  abl_link_set_is_playing_and_request_beat_at_time(session_state, playing, time, beat, quantum);
  return 0;
}

static const luaL_Reg abl_link_session_state_r[] = {
  { "destroy", l_destroy_session_state },
  { "tempo", l_tempo },
  { "set_tempo", l_set_tempo },
  { "beat_at_time", l_beat_at_time },
  { "phase_at_time", l_phase_at_time },
  { "time_at_beat", l_time_at_beat },
  { "request_beat_at_time", l_request_beat_at_time },
  { "force_beat_at_time", l_force_beat_at_time },
  { "is_playing", l_is_playing },
  { "set_is_playing", l_set_is_playing },
  { "time_for_is_playing", l_time_for_is_playing },
  { "request_beat_at_start_playing_time", l_request_beat_at_start_playing_time },
  { "set_is_playing_and_request_beat_at_time", l_set_is_playing_and_request_beat_at_time },
  { "__gc", l_destroy_session_state },
  { NULL, NULL },
};

static const luaL_Reg abletonlink[] = {
  { "create", l_create_link },
  { "create_session_state", l_create_session_state },
  { NULL, NULL },
};

int luaopen_abletonlink(lua_State* L) {
  luaL_newmetatable(L, "abletonlink.link");
  lua_getmetatable(L, -1);

  // m.__index = m
  lua_pushvalue(L, -1);
  lua_setfield(L, -1, "__index");

  // register
  luax_register(L, abl_link_r);
  lua_pop(L, 1);

  luaL_newmetatable(L, "abletonlink.session_state");
  lua_getmetatable(L, -1);

  // m.__index = m
  lua_pushvalue(L, -1);
  lua_setfield(L, -1, "__index");

  // register
  luax_register(L, abl_link_session_state_r);
  lua_pop(L, 1);
  
  lua_newtable(L); 
  lua_pushstring(L, "v1.0.0"); 
  lua_setfield(L, -2, "_VERSION");
  luax_register(L, abletonlink);
  return 1;
}
