#include <lua.h>
#include <lauxlib.h>
#include "link/extensions/abl_link/include/abl_link.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

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

  abl_link* link = (abl_link*)lua_newuserdata(L, sizeof(abl_link));

  *link = abl_link_create(bpm); 

  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 abl_link checklink(lua_State *L) {
  abl_link* link = (abl_link*)luaL_checkudata(L, 1, "abletonlink.link");
  luaL_argcheck(L, link != NULL, 1, "`link' expected");
  luaL_argcheck(L, link->impl != NULL, 1, "`link' is already destroyed");
  return *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) {
  abl_link* link = (abl_link*)luaL_checkudata(L, 1, "abletonlink.link");
  luaL_argcheck(L, link != NULL, 1, "`link' expected");
  if (link->impl) {
    abl_link_destroy(*link); 
    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 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 },
  { "__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;
}
