// Copyright (c) 2006, Niels Martin Hansen
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of the Aegisub Group nor the names of its contributors
//     may be used to endorse or promote products derived from this software
//     without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// -----------------------------------------------------------------------------
//
// AEGISUB
//
// Website: http://aegisub.cellosoft.com
// Contact: mailto:jiifurusu@gmail.com
//

#ifdef WITH_AUTO4_LUA

#include "auto4_lua.h"
#include "ass_dialogue.h"
#include "ass_style.h"
#include "ass_file.h"
#include "ass_override.h"

#ifdef __WINDOWS__
#include "../lua51/src/lualib.h"
#include "../lua51/src/lauxlib.h"
#else
#include "lualib.h"
#include "lauxlib.h"
#endif

#include <assert.h>
#include <algorithm>

namespace Automation4 {

	// LuaAssFile

	void LuaAssFile::CheckAllowModify()
	{
		if (can_modify)
			return;
		lua_pushstring(L, "Attempt to modify subtitles in read-only feature context.");
		lua_error(L);
	}

	void LuaAssFile::AssEntryToLua(lua_State *L, AssEntry *e)
	{
		lua_newtable(L);

		wxString section(e->group);
		lua_pushstring(L, section.mb_str(wxConvUTF8));
		lua_setfield(L, -2, "section");

		wxString raw(e->GetEntryData());
		lua_pushstring(L, raw.mb_str(wxConvUTF8));
		lua_setfield(L, -2, "raw");

		if (raw.Trim().IsEmpty()) {
			lua_pushstring(L, "clear");

		} else if (raw[0] == _T(';')) {
			// "text" field, same as "raw" but with semicolon stripped
			wxString text(raw, 1, raw.size()-1);
			lua_pushstring(L, text.mb_str(wxConvUTF8));
			lua_setfield(L, -2, "text");

			lua_pushstring(L, "comment");

		} else if (raw[0] == _T('[')) {
			lua_pushstring(L, "head");

		} else if (section.Lower() == _T("[script info]")) {
			// assumed "info" class

			// first "key"
			wxString key = raw.BeforeFirst(_T(':'));
			lua_pushstring(L, key.mb_str(wxConvUTF8));
			lua_setfield(L, -2, "key");

			// then "value"
			wxString value = raw.AfterFirst(_T(':'));
			lua_pushstring(L, value.mb_str(wxConvUTF8));
			lua_setfield(L, -2, "value");

			lua_pushstring(L, "info");

		} else if (raw.Left(7).Lower() == _T("format:")) {

			// TODO: parse the format line; just use a tokenizer

			lua_pushstring(L, "format");

		} else if (e->GetType() == ENTRY_DIALOGUE) {
			AssDialogue *dia = e->GetAsDialogue(e);

			lua_pushboolean(L, (int)dia->Comment);
			lua_setfield(L, -2, "comment");

			lua_pushnumber(L, dia->Layer);
			lua_setfield(L, -2, "layer");

			lua_pushnumber(L, dia->Start.GetMS());
			lua_setfield(L, -2, "start_time");
			lua_pushnumber(L, dia->End.GetMS());
			lua_setfield(L, -2, "end_time");

			lua_pushstring(L, dia->Style.mb_str(wxConvUTF8));
			lua_setfield(L, -2, "style");
			lua_pushstring(L, dia->Actor.mb_str(wxConvUTF8));
			lua_setfield(L, -2, "actor");

			lua_pushnumber(L, dia->Margin[0]);
			lua_setfield(L, -2, "margin_l");
			lua_pushnumber(L, dia->Margin[1]);
			lua_setfield(L, -2, "margin_r");
			lua_pushnumber(L, dia->Margin[2]);
			lua_setfield(L, -2, "margin_t");
			lua_pushnumber(L, dia->Margin[3]);
			lua_setfield(L, -2, "margin_b");

			lua_pushstring(L, dia->Effect.mb_str(wxConvUTF8));
			lua_setfield(L, -2, "effect");

			lua_pushstring(L, ""); // tentative AS5 field
			lua_setfield(L, -2, "userdata");

			lua_pushstring(L, dia->Text.mb_str(wxConvUTF8));
			lua_setfield(L, -2, "text");

			lua_pushstring(L, "dialogue");

		} else if (e->GetType() == ENTRY_STYLE) {
			AssStyle *sty = e->GetAsStyle(e);

			lua_pushstring(L, sty->name.mb_str(wxConvUTF8));
			lua_setfield(L, -2, "name");

			lua_pushstring(L, sty->font.mb_str(wxConvUTF8));
			lua_setfield(L, -2, "fontname");
			lua_pushnumber(L, sty->fontsize);
			lua_setfield(L, -2, "fontsize");

			lua_pushstring(L, sty->primary.GetASSFormatted(true).mb_str(wxConvUTF8));
			lua_setfield(L, -2, "color1");
			lua_pushstring(L, sty->secondary.GetASSFormatted(true).mb_str(wxConvUTF8));
			lua_setfield(L, -2, "color2");
			lua_pushstring(L, sty->outline.GetASSFormatted(true).mb_str(wxConvUTF8));
			lua_setfield(L, -2, "color3");
			lua_pushstring(L, sty->shadow.GetASSFormatted(true).mb_str(wxConvUTF8));
			lua_setfield(L, -2, "color4");

			lua_pushboolean(L, (int)sty->bold);
			lua_setfield(L, -2, "bold");
			lua_pushboolean(L, (int)sty->italic);
			lua_setfield(L, -2, "italic");
			lua_pushboolean(L, (int)sty->underline);
			lua_setfield(L, -2, "underline");
			lua_pushboolean(L, (int)sty->strikeout);
			lua_setfield(L, -2, "strikeout");

			lua_pushnumber(L, sty->scalex);
			lua_setfield(L, -2, "scale_x");
			lua_pushnumber(L, sty->scaley);
			lua_setfield(L, -2, "scale_y");

			lua_pushnumber(L, sty->spacing);
			lua_setfield(L, -2, "spacing");

			lua_pushnumber(L, sty->angle);
			lua_setfield(L, -2, "angle");

			lua_pushnumber(L, sty->borderstyle);
			lua_setfield(L, -2, "borderstyle");
			lua_pushnumber(L, sty->outline_w);
			lua_setfield(L, -2, "outline");
			lua_pushnumber(L, sty->shadow_w);
			lua_setfield(L, -2, "shadow");

			lua_pushnumber(L, sty->alignment);
			lua_setfield(L, -2, "align");

			lua_pushnumber(L, sty->Margin[0]);
			lua_setfield(L, -2, "margin_l");
			lua_pushnumber(L, sty->Margin[1]);
			lua_setfield(L, -2, "margin_r");
			lua_pushnumber(L, sty->Margin[2]);
			lua_setfield(L, -2, "margin_t");
			lua_pushnumber(L, sty->Margin[3]);
			lua_setfield(L, -2, "margin_b");

			lua_pushnumber(L, sty->encoding);
			lua_setfield(L, -2, "encoding");

			lua_pushnumber(L, 2); // From STS.h: "0: window, 1: video, 2: undefined (~window)"
			lua_setfield(L, -2, "relative_to");

			lua_pushboolean(L, false); // vertical writing, tentative AS5 field
			lua_setfield(L, -2, "vertical");

			lua_pushstring(L, "style");

		} else {
			lua_pushstring(L, "unknown");
		}
		// store class of item; last thing done for each class specific code must be pushing the class name
		lua_setfield(L, -2, "class");
	}

	AssEntry *LuaAssFile::LuaToAssEntry(lua_State *L)
	{
		// assume an assentry table is on the top of the stack
		// convert it to a real AssEntry object, and pop the table from the stack

		if (!lua_istable(L, -1)) {
			lua_pushstring(L, "Can't convert a non-table value to AssEntry");
			lua_error(L);
			return 0;
		}

		lua_getfield(L, -1, "class");
		if (!lua_isstring(L, -1)) {
			lua_pushstring(L, "Table lacks 'class' field, can't convert to AssEntry");
			lua_error(L);
			return 0;
		}
		wxString lclass(lua_tostring(L, -1), wxConvUTF8);
		lclass.MakeLower();
		lua_pop(L, 1);

		AssEntry *result;

#define GETSTRING(varname, fieldname, lineclass)		\
	lua_getfield(L, -1, fieldname);						\
	if (!lua_isstring(L, -1)) {							\
		lua_pushstring(L, "Invalid string '" fieldname "' field in '" lineclass "' class subtitle line"); \
		lua_error(L);									\
		return 0;										\
	}													\
	wxString varname (lua_tostring(L, -1), wxConvUTF8);	\
	lua_pop(L, 1);
#define GETFLOAT(varname, fieldname, lineclass)			\
	lua_getfield(L, -1, fieldname);						\
	if (!lua_isnumber(L, -1)) {							\
		lua_pushstring(L, "Invalid number '" fieldname "' field in '" lineclass "' class subtitle line"); \
		lua_error(L);									\
		return 0;										\
	}													\
	float varname = lua_tonumber(L, -1);				\
	lua_pop(L, 1);
#define GETINT(varname, fieldname, lineclass)			\
	lua_getfield(L, -1, fieldname);						\
	if (!lua_isnumber(L, -1)) {							\
		lua_pushstring(L, "Invalid number '" fieldname "' field in '" lineclass "' class subtitle line"); \
		lua_error(L);									\
		return 0;										\
	}													\
	int varname = lua_tointeger(L, -1);					\
	lua_pop(L, 1);
#define GETBOOL(varname, fieldname, lineclass)			\
	lua_getfield(L, -1, fieldname);						\
	if (!lua_isboolean(L, -1)) {						\
		lua_pushstring(L, "Invalid boolean '" fieldname "' field in '" lineclass "' class subtitle line"); \
		lua_error(L);									\
		return 0;										\
	}													\
	bool varname = !!lua_toboolean(L, -1);				\
	lua_pop(L, 1);

		GETSTRING(section, "section", "common")

		if (lclass == _T("clear")) {
			result = new AssEntry(_T(""));
			result->group = section;

		} else if (lclass == _T("comment")) {
			GETSTRING(raw, "text", "comment")
			raw.Prepend(_T(";"));
			result = new AssEntry(raw);
			result->group = section;

		} else if (lclass == _T("head")) {
			result = new AssEntry(section);
			result->group = section;

		} else if (lclass == _T("info")) {
			GETSTRING(key, "key", "info")
			GETSTRING(value, "value", "info")
			result = new AssEntry(wxString::Format(_T("%s: %s"), key.c_str(), value.c_str()));
			result->group = _T("[Script Info]"); // just so it can be read correctly back

		} else if (lclass == _T("format")) {
			// ohshi- ...
			// *FIXME* maybe ignore the actual data and just put some default stuff based on section?
			result = new AssEntry(_T("Format: Auto4,Is,Broken"));
			result->group = section;

		} else if (lclass == _T("style")) {
			GETSTRING(name, "name", "style")
			GETSTRING(fontname, "fontname", "style")
			GETFLOAT(fontsize, "fontsize", "style")
			GETSTRING(color1, "color1", "style")
			GETSTRING(color2, "color2", "style")
			GETSTRING(color3, "color3", "style")
			GETSTRING(color4, "color4", "style")
			GETBOOL(bold, "bold", "style")
			GETBOOL(italic, "italic", "style")
			GETBOOL(underline, "underline", "style")
			GETBOOL(strikeout, "strikeout", "style")
			GETFLOAT(scale_x, "scale_x", "style")
			GETFLOAT(scale_y, "scale_y", "style")
			GETINT(spacing, "spacing", "style")
			GETFLOAT(angle, "angle", "style")
			GETINT(borderstyle, "borderstyle", "style")
			GETFLOAT(outline, "outline", "style")
			GETFLOAT(shadow, "shadow", "style")
			GETINT(align, "align", "style")
			GETINT(margin_l, "margin_l", "style")
			GETINT(margin_r, "margin_r", "style")
			GETINT(margin_t, "margin_t", "style")
			GETINT(margin_b, "margin_b", "style")
			GETINT(encoding, "encoding", "style")
			// leaving out relative_to and vertical

			AssStyle *sty = new AssStyle();
			sty->name = name;
			sty->font = fontname;
			sty->fontsize = fontsize;
			sty->primary.Parse(color1);
			sty->secondary.Parse(color2);
			sty->outline.Parse(color3);
			sty->shadow.Parse(color4);
			sty->bold = bold;
			sty->italic = italic;
			sty->underline = underline;
			sty->strikeout = strikeout;
			sty->scalex = scale_x;
			sty->scaley = scale_y;
			sty->spacing = spacing;
			sty->angle = angle;
			sty->borderstyle = borderstyle;
			sty->outline_w = outline;
			sty->shadow_w = shadow;
			sty->alignment = align;
			sty->Margin[0] = margin_l;
			sty->Margin[1] = margin_r;
			sty->Margin[2] = margin_t;
			sty->Margin[3] = margin_b;
			sty->encoding = encoding;
			sty->UpdateData();

			result = sty;

		} else if (lclass == _T("styleex")) {
			lua_pushstring(L, "Found line with class 'styleex' which is not supported. Wait until AS5 is a reality.");
			lua_error(L);
			return 0;

		} else if (lclass == _T("dialogue")) {
			GETBOOL(comment, "comment", "dialogue")
			GETINT(layer, "layer", "dialogue")
			GETINT(start_time, "start_time", "dialogue")
			GETINT(end_time, "end_time", "dialogue")
			GETSTRING(style, "style", "dialogue")
			GETSTRING(actor, "actor", "dialogue")
			GETINT(margin_l, "margin_l", "dialogue")
			GETINT(margin_r, "margin_r", "dialogue")
			GETINT(margin_t, "margin_t", "dialogue")
			GETINT(margin_b, "margin_b", "dialogue")
			GETSTRING(effect, "effect", "dialogue")
			//GETSTRING(userdata, "userdata", "dialogue")
			GETSTRING(text, "text", "dialogue")

			AssDialogue *dia = new AssDialogue();
			dia->Comment = comment;
			dia->Layer = layer;
			dia->Start.SetMS(start_time);
			dia->End.SetMS(end_time);
			dia->Style = style;
			dia->Actor = actor;
			dia->Margin[0] = margin_l;
			dia->Margin[1] = margin_r;
			dia->Margin[2] = margin_t;
			dia->Margin[3] = margin_b;
			dia->Effect = effect;
			dia->Text = text;
			dia->UpdateData();

			result = dia;

		} else {
			lua_pushfstring(L, "Found line with unknown class: %s", lclass.mb_str(wxConvUTF8).data());
			lua_error(L);
			return 0;
		}

#undef GETSTRING
#undef GETFLOAT
#undef GETINT
#undef GETBOOL

		//lua_pop(L, 1); // the function shouldn't eat the table it converted
		return result;
	}

	void LuaAssFile::GetAssEntry(int n)
	{
		entryIter e;
		if (n < last_entry_id/2) {
			// fastest to search from start
			e = ass->Line.begin();
			last_entry_id = n;
			while (n-- > 0) e++;
			last_entry_ptr = e;

		} else if (last_entry_id + n > last_entry_id + ((int)ass->Line.size() - last_entry_id)/2) {
			// fastest to search from end
			int i = (int)ass->Line.size();
			e = ass->Line.end();
			last_entry_id = n;
			while (i-- > n) e--;
			last_entry_ptr = e;

		} else if (last_entry_id > n) {
			// search backwards from last_entry_id
			e = last_entry_ptr;
			while (n < last_entry_id) e--, last_entry_id--;
			last_entry_ptr = e;
			
		} else {
			// search forwards from last_entry_id
			e = last_entry_ptr;
			// reqid and last_entry_id might be equal here, make sure the loop will still work
			while (n > last_entry_id) e++, last_entry_id++;
			last_entry_ptr = e;
		}
	}

	int LuaAssFile::ObjectIndexRead(lua_State *L)
	{
		LuaAssFile *laf = GetObjPointer(L, 1);

		switch (lua_type(L, 2)) {

			case LUA_TNUMBER:
				{
					// read an indexed AssEntry

					// get requested index
					int reqid = lua_tointeger(L, 2);
					if (reqid <= 0 || reqid > (int)laf->ass->Line.size()) {
						lua_pushfstring(L, "Requested out-of-range line from subtitle file: %d", reqid);
						lua_error(L);
						return 0;
					}

					laf->GetAssEntry(reqid-1);
					laf->AssEntryToLua(L, *laf->last_entry_ptr);
					return 1;
				}

			case LUA_TSTRING:
				{
					// either return n or a function doing further stuff
					const char *idx = lua_tostring(L, 2);

					if (strcmp(idx, "n") == 0) {
						// get number of items
						lua_pushnumber(L, laf->ass->Line.size());
						return 1;

					} else if (strcmp(idx, "delete") == 0) {
						// make a "delete" function
						lua_pushvalue(L, 1);
						lua_pushcclosure(L, ObjectDelete, 1);
						return 1;

					} else if (strcmp(idx, "deleterange") == 0) {
						// make a "deleterange" function
						lua_pushvalue(L, 1);
						lua_pushcclosure(L, ObjectDeleteRange, 1);
						return 1;

					} else if (strcmp(idx, "insert") == 0) {
						// make an "insert" function
						lua_pushvalue(L, 1);
						lua_pushcclosure(L, ObjectInsert, 1);
						return 1;

					} else if (strcmp(idx, "append") == 0) {
						// make an "append" function
						lua_pushvalue(L, 1);
						lua_pushcclosure(L, ObjectAppend, 1);
						return 1;

					} else {
						// idiot
						lua_pushfstring(L, "Invalid indexing in Subtitle File object: '%s'", idx);
						lua_error(L);
						// should never return
					}
					assert(false);
				}

			default:
				{
					// crap, user is stupid!
					lua_pushfstring(L, "Attempt to index a Subtitle File object with value of type '%s'.", lua_typename(L, lua_type(L, 2)));
					lua_error(L);
				}
		}

		assert(false);
		return 0;
	}

	int LuaAssFile::ObjectIndexWrite(lua_State *L)
	{
		// instead of implementing everything twice, just call the other modification-functions from here
		// after modifying the stack to match their expectations

		if (!lua_isnumber(L, 2)) {
			lua_pushstring(L, "Attempt to write to non-numeric index in subtitle index");
			lua_error(L);
			return 0;
		}

		LuaAssFile *laf = GetObjPointer(L, 1);
		laf->CheckAllowModify();

		int n = lua_tointeger(L, 2);

		if (n < 0) {
			// insert line so new index is n
			lua_pushvalue(L, 1);
			lua_pushcclosure(L, ObjectInsert, 1);
			lua_pushinteger(L, -n);
			lua_pushvalue(L, 3);
			lua_call(L, 2, 0);
			return 0;

		} else if (n == 0) {
			// append line to list
			lua_pushvalue(L, 1);
			lua_pushcclosure(L, ObjectAppend, 1);
			lua_pushvalue(L, 3);
			lua_call(L, 1, 0);
			return 0;

		} else {
			// replace line at index n or delete
			if (!lua_isnil(L, 3)) {
				// insert
				AssEntry *e = LuaToAssEntry(L);
				laf->GetAssEntry(n-1);
				delete *laf->last_entry_ptr;
				*laf->last_entry_ptr = e;
				return 0;

			} else {
				// delete
				lua_pushvalue(L, 1);
				lua_pushcclosure(L, ObjectDelete, 1);
				lua_pushvalue(L, 2);
				lua_call(L, 1, 0);
				return 0;

			}
		}
	}

	int LuaAssFile::ObjectGetLen(lua_State *L)
	{
		LuaAssFile *laf = GetObjPointer(L, 1);
		lua_pushnumber(L, laf->ass->Line.size());
		return 1;
	}

	int LuaAssFile::ObjectDelete(lua_State *L)
	{
		LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1));

		laf->CheckAllowModify();
		
		// get number of items to delete
		int itemcount = lua_gettop(L);
		std::vector<int> ids;
		ids.reserve(itemcount);

		// sort the item id's so we can delete from last to first to preserve original numbering
		while (itemcount > 0) {
			if (!lua_isnumber(L, itemcount)) {
				lua_pushstring(L, "Attempt to delete non-numeric line id from Subtitle Object");
				lua_error(L);
				return 0;
			}
			int n = lua_tointeger(L, itemcount);
			if (n > (int)laf->ass->Line.size() || n < 1) {
				lua_pushstring(L, "Attempt to delete out of range line id from Subtitle Object");
				lua_error(L);
				return 0;
			}
			ids.push_back(n-1); // make C-style line ids
			--itemcount;
		}
		std::sort(ids.begin(), ids.end());

		// now delete the id's backwards
		// start with the last one, to initialise things
		laf->GetAssEntry(ids.back());
		// get an iterator to it, and increase last_entry_ptr so it'll still be valid after deletion, and point to the right index
		entryIter e = laf->last_entry_ptr++;
		laf->ass->Line.erase(e);
		int n = laf->last_entry_id;
		for (int i = (int)ids.size()-2; i >= 0; --i) {
			int id = ids[i];
			while (id > n--) laf->last_entry_ptr--;
			e = laf->last_entry_ptr++;
			delete *e;
			laf->ass->Line.erase(e);
		}
		laf->last_entry_id = n;

		return 0;
	}

	int LuaAssFile::ObjectDeleteRange(lua_State *L)
	{
		LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1));

		laf->CheckAllowModify();
		
		if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2)) {
			lua_pushstring(L, "Non-numeric argument given to deleterange");
			lua_error(L);
			return 0;
		}

		int a = lua_tointeger(L, 1), b = lua_tointeger(L, 2);

		if (a < 1) a = 1;
		if (b > (int)laf->ass->Line.size()) b = (int)laf->ass->Line.size();

		if (b < a) return 0;

		if (a == b) {
			laf->GetAssEntry(a-1);
			entryIter e = laf->last_entry_ptr++;
			delete *e;
			laf->ass->Line.erase(e);
			return 0;
		}

		entryIter ai, bi;
		laf->GetAssEntry(a-1);
		ai = laf->last_entry_ptr;
		laf->GetAssEntry(b-1);
		bi = laf->last_entry_ptr;
		laf->last_entry_ptr++;

		laf->ass->Line.erase(ai, bi);

		return 0;
	}

	int LuaAssFile::ObjectAppend(lua_State *L)
	{
		LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1));

		laf->CheckAllowModify();
		
		int n = lua_gettop(L);

		if (laf->last_entry_ptr != laf->ass->Line.begin()) {
			laf->last_entry_ptr--;
			laf->last_entry_id--;
		}

		for (int i = 1; i <= n; i++) {
			lua_pushvalue(L, i);
			AssEntry *e = LuaToAssEntry(L);
			laf->ass->Line.push_back(e);
		}

		return 0;
	}

	int LuaAssFile::ObjectInsert(lua_State *L)
	{
		LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1));

		laf->CheckAllowModify();
		
		if (!lua_isnumber(L, 1)) {
			lua_pushstring(L, "Can't insert at non-numeric index");
			lua_error(L);
			return 0;
		}

		int n = lua_gettop(L);

		laf->GetAssEntry(int(lua_tonumber(L, 1)-1));

		for (int i = 2; i <= n; i++) {
			lua_pushvalue(L, i);
			AssEntry *e = LuaToAssEntry(L);
			lua_pop(L, 1);
			laf->ass->Line.insert(laf->last_entry_ptr, e);
			laf->last_entry_id++;
		}

		return 0;
	}

	int LuaAssFile::ObjectGarbageCollect(lua_State *L)
	{
		LuaAssFile *laf = GetObjPointer(L, 1);
		delete laf;
		wxLogDebug(_T(">>gc<< Garbage collected LuaAssFile"));
		return 0;
	}

	int LuaAssFile::LuaParseTagData(lua_State *L)
	{
		lua_newtable(L);
		// TODO
		return 1;
	}

	int LuaAssFile::LuaUnparseTagData(lua_State *L)
	{
		lua_pushstring(L, "");
		// TODO
		return 1;
	}

	int LuaAssFile::LuaParseKaraokeData(lua_State *L)
	{
		AssEntry *e = LuaToAssEntry(L);
		if (e->GetType() != ENTRY_DIALOGUE) {
			delete e;
			lua_pushstring(L, "Attempt to create karaoke table from non-dialogue subtitle line");
			lua_error(L);
			return 0;
		}

		AssDialogue *dia = e->GetAsDialogue(e);
		dia->ParseASSTags();

		int kcount = 0;
		int kdur = 0;
		int ktime = 0;
		wxString ktag = _T("");
		wxString ktext = _T("");
		wxString ktext_stripped = _T("");

		lua_newtable(L);

		for (int i = 0; i < (int)dia->Blocks.size(); i++) {
			AssDialogueBlock *block = dia->Blocks[i];

			switch (block->GetType()) {

				case BLOCK_BASE:
					break;

				case BLOCK_PLAIN:
					ktext += block->text;
					ktext_stripped += block->text;
					break;

				case BLOCK_DRAWING:
					// a drawing is regarded as a kind of control code here, so it's just stripped away
					ktext += block->text;
					break;

				case BLOCK_OVERRIDE: {
					bool brackets_open = false;
					AssDialogueBlockOverride *ovr = block->GetAsOverride(block);

					for (int j = 0; j < (int)ovr->Tags.size(); j++) {
						AssOverrideTag *tag = ovr->Tags[j];

						if (tag->IsValid() && tag->Name.Mid(0,2).CmpNoCase(_T("\\k")) == 0) {
							// karaoke tag
							if (brackets_open) {
								ktext += _T("}");
								brackets_open = false;
							}

							// store to lua
							lua_newtable(L);
							lua_pushnumber(L, kdur);
							lua_setfield(L, -2, "duration");
							lua_pushnumber(L, ktime);
							lua_setfield(L, -2, "start_time");
							lua_pushnumber(L, ktime+kdur);
							lua_setfield(L, -2, "end_time");
							lua_pushstring(L, ktag.mb_str(wxConvUTF8));
							lua_setfield(L, -2, "tag");
							lua_pushstring(L, ktext.mb_str(wxConvUTF8));
							lua_setfield(L, -2, "text");
							lua_pushstring(L, ktext_stripped.mb_str(wxConvUTF8));
							lua_setfield(L, -2, "text_stripped");
							lua_rawseti(L, -2, kcount);

							// prepare new syllable
							kcount++;
							ktag = tag->Name.Mid(1);
							// check if it's a "set time" tag, special handling for that (depends on previous syllable duration)
							if (ktag == _T("kt")) {
								ktime = tag->Params[0]->AsInt() * 10;
								kdur = 0;
							} else {
								ktime += kdur; // duration of previous syllable
								kdur = tag->Params[0]->AsInt() * 10;
							}
							ktext.clear();
							ktext_stripped.clear();

						} else {
							// not karaoke tag
							if (!brackets_open) {
								ktext += _T("{");
								brackets_open = true;
							}
							ktext += tag->ToString();
						}

					}

					if (brackets_open) {
						ktext += _T("}");
						brackets_open = false;
					}

					break;
				}

			}

		}

		dia->ClearBlocks();

		// store final syllable/block to lua
		lua_newtable(L);
		lua_pushnumber(L, kdur);
		lua_setfield(L, -2, "duration");
		lua_pushnumber(L, ktime);
		lua_setfield(L, -2, "start_time");
		lua_pushnumber(L, ktime+kdur);
		lua_setfield(L, -2, "end_time");
		lua_pushstring(L, ktag.mb_str(wxConvUTF8));
		lua_setfield(L, -2, "tag");
		lua_pushstring(L, ktext.mb_str(wxConvUTF8));
		lua_setfield(L, -2, "text");
		lua_pushstring(L, ktext_stripped.mb_str(wxConvUTF8));
		lua_setfield(L, -2, "text_stripped");
		lua_rawseti(L, -2, kcount);

		delete dia;
		return 1;
	}

	int LuaAssFile::LuaSetUndoPoint(lua_State *L)
	{
		LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1));
		if (!laf->can_set_undo) {
			lua_pushstring(L, "Attempt to set an undo point in a context without undo-support.");
			lua_error(L);
			return 0;
		}

		wxString description;
		if (lua_isstring(L, 1)) {
			description = wxString(lua_tostring(L, 1), wxConvUTF8);
			lua_pop(L, 1);
		}
		AssFile::top->FlagAsModified(description);

		laf->ass = AssFile::top; // make sure we're still working on the most recent undo point
		return 0;
	}

	LuaAssFile *LuaAssFile::GetObjPointer(lua_State *L, int idx)
	{
		assert(lua_type(L, idx) == LUA_TUSERDATA);
		void *ud = lua_touserdata(L, idx);
		return *((LuaAssFile**)ud);
	}

	LuaAssFile::~LuaAssFile()
	{
	}

	LuaAssFile::LuaAssFile(lua_State *_L, AssFile *_ass, bool _can_modify, bool _can_set_undo)
		: ass(_ass)
		, L(_L)
		, can_modify(_can_modify)
		, can_set_undo(_can_set_undo)
	{
		// prepare cursor
		last_entry_ptr = ass->Line.begin();
		last_entry_id = 0;

		// prepare userdata object
		void *ud = lua_newuserdata(L, sizeof(LuaAssFile*));
		*((LuaAssFile**)ud) = this;

		// make the metatable
		lua_newtable(L);
		lua_pushcfunction(L, ObjectIndexRead);
		lua_setfield(L, -2, "__index");
		lua_pushcfunction(L, ObjectIndexWrite);
		lua_setfield(L, -2, "__newindex");
		lua_pushcfunction(L, ObjectGetLen);
		lua_setfield(L, -2, "__len");
		lua_pushcfunction(L, ObjectGarbageCollect);
		lua_setfield(L, -2, "__gc");
		lua_setmetatable(L, -2);

		// register misc functions
		// assume the "aegisub" global table exists
		lua_getglobal(L, "aegisub");
		assert(lua_type(L, -2) == LUA_TUSERDATA);
		lua_pushvalue(L, -2); // the userdata object
		lua_pushcclosure(L, LuaParseTagData, 1);
		lua_setfield(L, -2, "parse_tag_data");
		assert(lua_type(L, -2) == LUA_TUSERDATA);
		lua_pushvalue(L, -2);
		lua_pushcclosure(L, LuaUnparseTagData, 1);
		lua_setfield(L, -2, "unparse_tag_data");
		assert(lua_type(L, -2) == LUA_TUSERDATA);
		lua_pushvalue(L, -2);
		lua_pushcclosure(L, LuaParseKaraokeData, 1);
		lua_setfield(L, -2, "parse_karaoke_data");
		assert(lua_type(L, -2) == LUA_TUSERDATA);
		lua_pushvalue(L, -2);
		lua_pushcclosure(L, LuaSetUndoPoint, 1);
		lua_setfield(L, -2, "set_undo_point");
		lua_pop(L, 1);
	}

};

#endif // WITH_AUTO4_LUA