#define LUA_LIB

// only for debug use
#include <lua.h>
#include <lauxlib.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "spinlock.h"

#define METANAME "debugchannel"

struct command {
	struct command * next;
	size_t sz;
};

struct channel {
	struct spinlock lock;
	int ref;
	struct command * head;
	struct command * tail;
};

static struct channel *
channel_new() {
	struct channel * c = malloc(sizeof(*c));
	memset(c, 0 , sizeof(*c));
	c->ref = 1;
	SPIN_INIT(c)

	return c;
}

static struct channel *
channel_connect(struct channel *c) {
	struct channel * ret = NULL;
	SPIN_LOCK(c)
	if (c->ref == 1) {
		++c->ref;
		ret = c;
	}
	SPIN_UNLOCK(c)
	return ret;
}

static struct channel *
channel_release(struct channel *c) {
	SPIN_LOCK(c)
	--c->ref;
	if (c->ref > 0) {
		SPIN_UNLOCK(c)
		return c;
	}
	// never unlock while reference is 0
	struct command * p = c->head;
	c->head = NULL;
	c->tail = NULL;
	while(p) {
		struct command *next = p->next;
		free(p);
		p = next;
	}
	SPIN_UNLOCK(c)
	SPIN_DESTROY(c)
	free(c);
	return NULL;
}

// call free after channel_read
static struct command *
channel_read(struct channel *c, double timeout) {
	struct command * ret = NULL;
	SPIN_LOCK(c)
	if (c->head == NULL) {
		SPIN_UNLOCK(c)
		int ti = (int)(timeout * 100000);
		usleep(ti);
		return NULL;
	}
	ret = c->head;
	c->head = ret->next;
	if (c->head == NULL) {
		c->tail = NULL;
	}
	SPIN_UNLOCK(c)
	
	return ret;
}

static void
channel_write(struct channel *c, const char * s, size_t sz) {
	struct command * cmd = malloc(sizeof(*cmd)+ sz);
	cmd->sz = sz;
	cmd->next = NULL;
	memcpy(cmd+1, s, sz);
	SPIN_LOCK(c)
	if (c->tail == NULL) {
		c->head = c->tail = cmd;
	} else {
		c->tail->next = cmd;
		c->tail = cmd;
	}
	SPIN_UNLOCK(c)
}

struct channel_box {
	struct channel *c;
};

static int
lread(lua_State *L) {
	struct channel_box *cb = luaL_checkudata(L,1, METANAME);
	double ti = luaL_optnumber(L, 2, 0);
	struct command * c = channel_read(cb->c, ti);
	if (c == NULL)
		return 0;
	lua_pushlstring(L, (const char *)(c+1), c->sz);
	free(c);
	return 1;
}

static int
lwrite(lua_State *L) {
	struct channel_box *cb = luaL_checkudata(L,1, METANAME);
	size_t sz;
	const char * str = luaL_checklstring(L, 2, &sz);
	channel_write(cb->c, str, sz);
	return 0;
}

static int
lrelease(lua_State *L) {
	struct channel_box *cb = lua_touserdata(L, 1);
	if (cb) {
		if (channel_release(cb->c) == NULL) {
			cb->c = NULL;
		}
	}

	return 0;
}

static struct channel *
new_channel(lua_State *L, struct channel *c) {
	if (c == NULL) {
		c = channel_new();
	} else {
		c = channel_connect(c);
	}
	if (c == NULL) {
		luaL_error(L, "new channel failed");
		// never go here
	}
	struct channel_box * cb = lua_newuserdatauv(L, sizeof(*cb), 0);
	cb->c = c;
	if (luaL_newmetatable(L, METANAME)) {
		luaL_Reg l[]={
			{ "read", lread },
			{ "write", lwrite },
			{ NULL, NULL },
		};
		luaL_newlib(L,l);
		lua_setfield(L, -2, "__index");
		lua_pushcfunction(L, lrelease);
		lua_setfield(L, -2, "__gc");
	}
	lua_setmetatable(L, -2);
	return c;
}

static int
lcreate(lua_State *L) {
	struct channel *c = new_channel(L, NULL);
	lua_pushlightuserdata(L, c);
	return 2;
}

static int
lconnect(lua_State *L) {
	struct channel *c = lua_touserdata(L, 1);
	if (c == NULL)
		return luaL_error(L, "Invalid channel pointer");
	new_channel(L, c);

	return 1;
}

static const int HOOKKEY = 0;

/*
** Auxiliary function used by several library functions: check for
** an optional thread as function's first argument and set 'arg' with
** 1 if this argument is present (so that functions can skip it to
** access their other arguments)
*/
static lua_State *getthread (lua_State *L, int *arg) {
  if (lua_isthread(L, 1)) {
    *arg = 1;
    return lua_tothread(L, 1);
  }
  else {
    *arg = 0;
    return L;  /* function will operate over current thread */
  }
}

/*
** Call hook function registered at hook table for the current
** thread (if there is one)
*/
static void hookf (lua_State *L, lua_Debug *ar) {
  static const char *const hooknames[] =
    {"call", "return", "line", "count", "tail call"};
  lua_rawgetp(L, LUA_REGISTRYINDEX, &HOOKKEY);
  lua_pushthread(L);
  if (lua_rawget(L, -2) == LUA_TFUNCTION) {  /* is there a hook function? */
    lua_pushstring(L, hooknames[(int)ar->event]);  /* push event name */
    if (ar->currentline >= 0)
      lua_pushinteger(L, ar->currentline);  /* push current line */
    else lua_pushnil(L);
    lua_call(L, 2, 1);  /* call hook function */
	int yield = lua_toboolean(L, -1);
	lua_pop(L,1);
	if (yield) {
		lua_yield(L, 0);
	}
  }
}

/*
** Convert a string mask (for 'sethook') into a bit mask
*/
static int makemask (const char *smask, int count) {
  int mask = 0;
  if (strchr(smask, 'c')) mask |= LUA_MASKCALL;
  if (strchr(smask, 'r')) mask |= LUA_MASKRET;
  if (strchr(smask, 'l')) mask |= LUA_MASKLINE;
  if (count > 0) mask |= LUA_MASKCOUNT;
  return mask;
}

static int db_sethook (lua_State *L) {
  int arg, mask, count;
  lua_Hook func;
  lua_State *L1 = getthread(L, &arg);
  if (lua_isnoneornil(L, arg+1)) {  /* no hook? */
    lua_settop(L, arg+1);
    func = NULL; mask = 0; count = 0;  /* turn off hooks */
  }
  else {
    const char *smask = luaL_checkstring(L, arg+2);
    luaL_checktype(L, arg+1, LUA_TFUNCTION);
    count = (int)luaL_optinteger(L, arg + 3, 0);
    func = hookf; mask = makemask(smask, count);
  }
  if (lua_rawgetp(L, LUA_REGISTRYINDEX, &HOOKKEY) == LUA_TNIL) {
    lua_createtable(L, 0, 2);  /* create a hook table */
    lua_pushvalue(L, -1);
    lua_rawsetp(L, LUA_REGISTRYINDEX, &HOOKKEY);  /* set it in position */
    lua_pushstring(L, "k");
    lua_setfield(L, -2, "__mode");  /** hooktable.__mode = "k" */
    lua_pushvalue(L, -1);
    lua_setmetatable(L, -2);  /* setmetatable(hooktable) = hooktable */
  }
  lua_pushthread(L1); lua_xmove(L1, L, 1);  /* key (thread) */
  lua_pushvalue(L, arg + 1);  /* value (hook function) */
  lua_rawset(L, -3);  /* hooktable[L1] = new Lua hook */
  lua_sethook(L1, func, mask, count);
  return 0;
}

LUAMOD_API int
luaopen_skynet_debugchannel(lua_State *L) {
	luaL_Reg l[] = {
		{ "create", lcreate },	// for write
		{ "connect", lconnect },	// for read
		{ "release", lrelease },
		{ "sethook", db_sethook },
		{ NULL, NULL },
	};
	luaL_checkversion(L);
	luaL_newlib(L,l);
	return 1;
}
