/*
    TE4 - T-Engine 4
    Copyright (C) 2009, 2010, 2011 Nicolas Casalini

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    Nicolas Casalini "DarkGod"
    darkgod@te4.org
*/
#include "display.h"
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#include "auxiliar.h"
#include "types.h"
#include "serial.h"
#include "script.h"
#include "physfs.h"
#include "physfsrwops.h"

static int serial_new(lua_State *L)
{
	zipFile *zf = (zipFile*)auxiliar_checkclass(L, "physfs{zip}", 1);
	luaL_checktype(L, 2, LUA_TFUNCTION);
	luaL_checktype(L, 3, LUA_TFUNCTION);
	if (!lua_isnil(L, 4) && !lua_istable(L, 4)) { lua_pushstring(L, "argument 4 is not nil or table"); lua_error(L); }
	if (!lua_isnil(L, 5) && !lua_istable(L, 5)) { lua_pushstring(L, "argument 5 is not nil or table"); lua_error(L); }
	if (!lua_isnil(L, 6) && !lua_istable(L, 6)) { lua_pushstring(L, "argument 6 is not nil or table"); lua_error(L); }

	int d2_ref = luaL_ref(L, LUA_REGISTRYINDEX);
	int d_ref = luaL_ref(L, LUA_REGISTRYINDEX);
	int a_ref = luaL_ref(L, LUA_REGISTRYINDEX);
	int fadd_ref = luaL_ref(L, LUA_REGISTRYINDEX);
	int fname_ref = luaL_ref(L, LUA_REGISTRYINDEX);

	serial_type *s = (serial_type*)lua_newuserdata(L, sizeof(serial_type));
	auxiliar_setclass(L, "core{serial}", -1);

	s->zf = *zf;
	s->fname = fname_ref;
	s->fadd = fadd_ref;
	s->allow = a_ref;
	s->disallow = d_ref;
	s->disallow2 = d2_ref;

	return 1;
}

static int serial_free(lua_State *L)
{
	serial_type *s = (serial_type*)auxiliar_checkclass(L, "core{serial}", 1);
	luaL_unref(L, LUA_REGISTRYINDEX, s->fname);
	luaL_unref(L, LUA_REGISTRYINDEX, s->fadd);
	lua_pushnumber(L, 1);
	return 1;
}

static const char *get_name(lua_State *L, serial_type *s, int idx)
{
	lua_rawgeti(L, LUA_REGISTRYINDEX, s->fname);
	lua_pushvalue(L, idx - 1);
	lua_call(L, 1, 1);
	const char *name = lua_tostring(L, -1);
	lua_pop(L, 1);
	return name;
}

static void add_process(lua_State *L, serial_type *s, int idx)
{
	lua_rawgeti(L, LUA_REGISTRYINDEX, s->fadd);
	lua_pushvalue(L, idx - 1);
	lua_call(L, 1, 0);
}

#define writeZip(s, data) { /*printf("%s", data);*/ zipWriteInFileInZip(s->zf, data, strlen(data)); }
#define writeZipFixed(s, data, len) { /*printf("%s", data);*/ zipWriteInFileInZip(s->zf, data, len); }

static void dump_string(serial_type *s, const char *str, size_t l)
{
	while (l--) {
		switch (*str) {
		case '"': case '\\': case '\n': {
			writeZipFixed(s, "\\", 1);
			writeZipFixed(s, str, 1);
			break;
		}
		case '\r': {
			writeZipFixed(s, "\\r", 2);
			break;
		}
		case '\0': {
			writeZipFixed(s, "\\000", 4);
			break;
		}
		default: {
			writeZipFixed(s, str, 1);
			break;
		}
		}
		str++;
	}
}

static int dump_function(lua_State *L, const void* p, size_t sz, void* ud)
{
	serial_type *s = (serial_type*)ud;
//	fwrite(p, sz, 1, stdout);
//	zipWriteInFileInZip(s->zf, p, sz);
	dump_string(s, p, sz);
	return 0;
}

static void basic_serialize(lua_State *L, serial_type *s, int type, int idx)
{
	if (type == LUA_TBOOLEAN) {
		if (lua_toboolean(L, idx)) { writeZipFixed(s, "true", 4); }
		else { writeZipFixed(s, "false", 5); }
	} else if (type == LUA_TNUMBER) {
		lua_pushvalue(L, idx);
		size_t len;
		const char *n = lua_tolstring(L, -1, &len);
		writeZipFixed(s, n, len);
		lua_pop(L, 1);
	} else if (type == LUA_TSTRING) {
		size_t len;
		const char *str = lua_tolstring(L, idx, &len);
		writeZipFixed(s, "\"", 1);
		dump_string(s, str, len);
		writeZipFixed(s, "\"", 1);
	} else if (type == LUA_TFUNCTION) {
		writeZipFixed(s, "loadstring(\"", 12);
		lua_dump(L, dump_function, s);
		writeZipFixed(s, "\")", 2);
	} else if (type == LUA_TTABLE) {
		lua_pushstring(L, "__CLASSNAME");
		lua_rawget(L, idx - 1);
		// This is an object, register for saving later
		if (!lua_isnil(L, -1))
		{
			lua_pop(L, 1);
			writeZipFixed(s, "loadObject('", 12);
			writeZip(s, get_name(L, s, idx));
			writeZipFixed(s, "')", 2);
			add_process(L, s, idx);
		}
		// This is just a table, save it
		else
		{
			lua_pop(L, 1);
			int ktype, etype;

			writeZipFixed(s, "{", 1);
			/* table is in the stack at index 't' */
			lua_pushnil(L);  /* first key */

			while (lua_next(L, idx - 1) != 0)
			{
				ktype = lua_type(L, -2);
				etype = lua_type(L, -1);

				// Only save allowed types
				if (
					((ktype == LUA_TBOOLEAN) || (ktype == LUA_TNUMBER) || (ktype == LUA_TSTRING) || (ktype == LUA_TFUNCTION) || (ktype == LUA_TTABLE)) &&
					((etype == LUA_TBOOLEAN) || (etype == LUA_TNUMBER) || (etype == LUA_TSTRING) || (etype == LUA_TFUNCTION) || (etype == LUA_TTABLE))
					)
				{
					writeZipFixed(s, "[", 1);
					basic_serialize(L, s, ktype, -2);
					writeZipFixed(s, "]=", 2);
					basic_serialize(L, s, etype, -1);
					writeZipFixed(s, ",\n", 2);
				}

				/* removes 'value'; keeps 'key' for next iteration */
				lua_pop(L, 1);
			}
			writeZipFixed(s, "}\n", 2);
		}
	} else {
		printf("*WARNING* can not save value of type %s\n", lua_typename(L, type));
	}
}

static int serial_tozip(lua_State *L)
{
	serial_type *s = (serial_type*)auxiliar_checkclass(L, "core{serial}", 1);

	int ktype, etype;
	bool skip;

	/* Allows & disallows */
	lua_rawgeti(L, LUA_REGISTRYINDEX, s->allow);     // -5
	lua_rawgeti(L, LUA_REGISTRYINDEX, s->disallow);  // -4
	lua_rawgeti(L, LUA_REGISTRYINDEX, s->disallow2); // -3

	/* table is in the stack at index 't' */
	lua_pushvalue(L, 2);  /* table */
	lua_pushnil(L);  /* first key */

	/* Init the zip entry */
	int err=0;
	int opt_compress_level = 4;
	zip_fileinfo zi;
	unsigned long crcFile=0;
	zi.tmz_date.tm_sec = zi.tmz_date.tm_min = zi.tmz_date.tm_hour =
	zi.tmz_date.tm_mday = zi.tmz_date.tm_mon = zi.tmz_date.tm_year = 0;
	zi.dosDate = 0;
	zi.internal_fa = 0;
	zi.external_fa = 0;
	err = zipOpenNewFileInZip3(s->zf, get_name(L, s, -2), &zi,
		NULL,0,NULL,0,NULL /* comment*/,
		(opt_compress_level != 0) ? Z_DEFLATED : 0,
		opt_compress_level,0,
		-MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
		NULL,crcFile);
	if (err != ZIP_OK)
	{
		lua_pushnil(L);
		lua_pushstring(L, "could not add file to zip");
		return 2;
	}

	writeZipFixed(s, "d={}\n", 5);
	writeZipFixed(s, "setLoaded('", 11);
	writeZip(s, get_name(L, s, -2));
	writeZipFixed(s, "', d)\n", 6);
	while (lua_next(L, -2) != 0)
	{
		skip = FALSE;
		ktype = lua_type(L, -2);
		etype = lua_type(L, -1);

		if (s->allow != LUA_REFNIL)
		{
			lua_pushvalue(L, -2); lua_rawget(L, -7);
			skip = lua_isnil(L, -1); lua_pop(L, 1);
		}
		else if (s->disallow != LUA_REFNIL)
		{
			lua_pushvalue(L, -2); lua_rawget(L, -6);
			skip = !lua_isnil(L, -1); lua_pop(L, 1);
		}
		if (s->disallow2 != LUA_REFNIL)
		{
			lua_pushvalue(L, -2); lua_rawget(L, -5);
			skip = !lua_isnil(L, -1); lua_pop(L, 1);
		}

		if (!skip)
		{
			writeZipFixed(s, "d[", 2);
			basic_serialize(L, s, ktype, -2);
			writeZipFixed(s, "]=", 2);
			basic_serialize(L, s, etype, -1);
			writeZipFixed(s, "\n", 1);
		}

		/* removes 'value'; keeps 'key' for next iteration */
		lua_pop(L, 1);
	}
	writeZipFixed(s, "\nreturn d", 9);

	zipCloseFileInZip(s->zf);

	lua_pushboolean(L, TRUE);
	return 1;
}

#define CLONETABLE 2
#define CLONETABLE_LIST 3

static int serial_clonefull_recurs(lua_State *L, int idx)
{
	int ktype, etype;
	int nb = 0;
	// We are called with the newtable on top of the stack

	lua_pushnil(L);  /* first key */
	while (lua_next(L, idx - 2) != 0)
	{
		ktype = lua_type(L, -2);
		etype = lua_type(L, -1);

		// Forbid cloning of fields named __threads
		if (ktype == LUA_TSTRING)
		{
			const char *s = lua_tostring(L, -2);
			if (!strcmp(s, "__threads"))
			{
				lua_pop(L, 1);
				continue;
			}
		}

		if (ktype == LUA_TTABLE)
		{
			// Check clonetable first
			lua_pushvalue(L, -2);
			lua_rawget(L, CLONETABLE);
			if (lua_isnil(L, -1))
			{
				// If not found, clone it
				lua_pop(L, 1);
				lua_newtable(L);

				// Store in the clonetable
				lua_pushvalue(L, -3);
				lua_pushvalue(L, -2);
				lua_rawset(L, CLONETABLE);
				lua_pushvalue(L, -3);
				lua_pushvalue(L, -2);
				lua_rawset(L, CLONETABLE_LIST);
			}
		}
		else
		{
			lua_pushvalue(L, -2);
		}

		if (etype == LUA_TTABLE)
		{
			// Check clonetable first
			lua_pushvalue(L, -2);
			lua_rawget(L, CLONETABLE);
			if (lua_isnil(L, -1))
			{
				// If not found, clone it
				lua_pop(L, 1);
				lua_newtable(L);

				// Store in the clonetable
				lua_pushvalue(L, -3);
				lua_pushvalue(L, -2);
				lua_rawset(L, CLONETABLE);
				lua_pushvalue(L, -3);
				lua_pushvalue(L, -2);
				lua_rawset(L, CLONETABLE_LIST);
			}
		}
		else
		{
			lua_pushvalue(L, -2);
		}

		// Now set in the new table
		lua_rawset(L, -5);

		/* removes 'value'; keeps 'key' for next iteration */
		lua_pop(L, 1);
	}

	// Setup metatable
	if (lua_getmetatable(L, idx - 1))
	{
		lua_setmetatable(L, -2); // -2 because -1 was the newtable before we push the metatable
	}

	// Check for class
	lua_pushstring(L, "__CLASSNAME");
	lua_rawget(L, -2);
	if (lua_isstring(L, -1))
	{
		lua_pop(L, 1);
		nb++;

		lua_getfield(L, -1, "cloned");
		if (lua_isfunction(L, -1))
		{
			lua_pushvalue(L, -2);
			lua_pushvalue(L, idx-2);
			lua_call(L, 2, 0);
		}
		else lua_pop(L, 1);
	}
	else lua_pop(L, 1);

	return nb;
}

static int serial_clonefull(lua_State *L)
{
	luaL_checktype(L, 1, LUA_TTABLE);
	lua_newtable(L); // idx 2 == clonetable_all
	lua_newtable(L); // idx 3 == clonetable

	// Store in the clonetable
	lua_pushvalue(L, 1);
	lua_newtable(L);
	lua_rawset(L, CLONETABLE);
	lua_pushvalue(L, 1);
	lua_newtable(L);
	lua_rawset(L, CLONETABLE_LIST);

	int nb = 0;
	lua_pushnil(L);  /* first key */
	while (lua_next(L, CLONETABLE_LIST) != 0)
	{
//		printf("<TOP %d\n", lua_gettop(L));
		nb += serial_clonefull_recurs(L, -1);
//		printf(">TOP %d // %d\n", lua_gettop(L), nb);

		// Remove from list
		lua_pop(L, 1); // remove value
		lua_pushnil(L); // set to nil
		lua_rawset(L, CLONETABLE_LIST);

		// Reset the next()
		lua_pushnil(L);
	}

	lua_pushnumber(L, nb);
	return 2;
}

static const struct luaL_reg seriallib[] =
{
	{"new", serial_new},
	{"cloneFull", serial_clonefull},
	{NULL, NULL},
};

static const struct luaL_reg serial_reg[] =
{
	{"__gc", serial_free},
	{"toZip", serial_tozip},
	{NULL, NULL},
};

int luaopen_serial(lua_State *L)
{
	auxiliar_newclass(L, "core{serial}", serial_reg);
	luaL_openlib(L, "core.serial", seriallib, 0);
	lua_pop(L, 1);
	return 1;
}