// 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 Project http://www.aegisub.org/

/// @file auto4_lua_assfile.cpp
/// @brief Lua 5.1-based scripting engine (interface to subtitle files)
/// @ingroup scripting
///

#include "auto4_lua.h"

#include "ass_dialogue.h"
#include "ass_info.h"
#include "ass_file.h"
#include "ass_karaoke.h"
#include "ass_style.h"
#include "compat.h"

#include <libaegisub/ass/karaoke.h>
#include <libaegisub/exception.h>
#include <libaegisub/log.h>
#include <libaegisub/lua/utils.h>
#include <libaegisub/string.h>

#include <algorithm>
#include <boost/algorithm/string/case_conv.hpp>
#include <cassert>
#include <memory>

namespace {
	using namespace agi::lua;

	DEFINE_EXCEPTION(BadField, Automation4::MacroRunError);
	[[noreturn]]
	BadField bad_field(const char *expected_type, const char *name, const char *line_clasee)
	{
		throw BadField(agi::Str("Invalid or missing field '", name, "' in '", line_clasee, "' class subtitle line (expected ", expected_type, ")"));
	}

	void get_field(lua_State *L, const char *name, const char *line_class, int (*p)(lua_State *, int))
	{
		lua_getfield(L, -1, name);
		if (!p(L, -1))
			bad_field("string", name, line_class);
	}

	std::string get_string_field(lua_State *L, const char *name, const char *line_class)
	{
		get_field(L, name, line_class, lua_isstring);
		std::string ret(lua_tostring(L, -1), lua_strlen(L, -1));
		lua_pop(L, 1);
		return ret;
	}

	agi::Color get_color_field(lua_State *L, const char *name, const char *line_class)
	{
		get_field(L, name, line_class, lua_isstring);
		agi::Color ret(std::string_view(lua_tostring(L, -1), lua_strlen(L, -1)));
		lua_pop(L, 1);
		return ret;
	}

	double get_double_field(lua_State *L, const char *name, const char *line_class)
	{
		get_field(L, name, line_class, lua_isnumber);
		double ret = lua_tonumber(L, -1);
		lua_pop(L, 1);
		return ret;
	}

	long get_int_field(lua_State *L, const char *name, const char *line_class)
	{
		get_field(L, name, line_class, lua_isnumber);
		long ret = lua_tointeger(L, -1);
		lua_pop(L, 1);
		return ret;
	}

	bool get_bool_field(lua_State *L, const char *name, const char *line_class)
	{
		lua_getfield(L, -1, name);
		if (!lua_isboolean(L, -1))
			bad_field("boolean", name, line_class);
		bool ret = !!lua_toboolean(L, -1);
		lua_pop(L, 1);
		return ret;
	}

	using namespace Automation4;
	template<int (LuaAssFile::*closure)(lua_State *)>
	int closure_wrapper(lua_State *L)
	{
		return (LuaAssFile::GetObjPointer(L, lua_upvalueindex(1), false)->*closure)(L);
	}

	template<void (LuaAssFile::*closure)(lua_State *), bool allow_expired>
	int closure_wrapper_v(lua_State *L)
	{
		(LuaAssFile::GetObjPointer(L, lua_upvalueindex(1), allow_expired)->*closure)(L);
		return 0;
	}

	int modification_mask(AssEntry *e)
	{
		if (!e) return AssFile::COMMIT_SCRIPTINFO;
		switch (e->Group()) {
			case AssEntryGroup::DIALOGUE: return AssFile::COMMIT_DIAG_ADDREM;
			case AssEntryGroup::STYLE:    return AssFile::COMMIT_STYLES;
			default:                      return AssFile::COMMIT_SCRIPTINFO;
		}
	}

	template<typename T, typename U>
	const T *check_cast_constptr(const U *value) {
		return typeid(const T) == typeid(*value) ? static_cast<const T *>(value) : nullptr;
	}
}

namespace Automation4 {
	LuaAssFile::~LuaAssFile() = default;

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

	void LuaAssFile::CheckBounds(int idx)
	{
		if (idx <= 0 || idx > (int)lines.size())
			error(L, "Requested out-of-range line from subtitle file: %d", idx);
	}

	void LuaAssFile::AssEntryToLua(lua_State *L, size_t idx)
	{
		lua_newtable(L);

		const AssEntry *e = lines[idx];
		if (!e)
			e = &ass->Info[idx];

		set_field(L, "section", e->GroupHeader());

		if (auto info = check_cast_constptr<AssInfo>(e)) {
			set_field(L, "raw", info->GetEntryData());
			set_field(L, "key", info->Key());
			set_field(L, "value", info->Value());
			set_field(L, "class", "info");
		}
		else if (auto dia = check_cast_constptr<AssDialogue>(e)) {
			set_field(L, "raw", dia->GetEntryData());
			set_field(L, "comment", dia->Comment);

			set_field(L, "layer", dia->Layer);

			set_field(L, "start_time", dia->Start);
			set_field(L, "end_time", dia->End);

			set_field(L, "style", dia->Style);
			set_field(L, "actor", dia->Actor);
			set_field(L, "effect", dia->Effect);

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

			set_field(L, "text", dia->Text);

			// create extradata table
			lua_newtable(L);
			for (auto const& ed : ass->GetExtradata(dia->ExtradataIds)) {
				push_value(L, ed.key);
				push_value(L, ed.value);
				lua_settable(L, -3);
			}
			lua_setfield(L, -2, "extra");

			set_field(L, "class", "dialogue");
		}
		else if (auto sty = check_cast_constptr<AssStyle>(e)) {
			set_field(L, "raw", sty->GetEntryData());
			set_field(L, "name", sty->name);

			set_field(L, "fontname", sty->font);
			set_field(L, "fontsize", sty->fontsize);

			set_field(L, "color1", sty->primary.GetAssStyleFormatted() + "&");
			set_field(L, "color2", sty->secondary.GetAssStyleFormatted() + "&");
			set_field(L, "color3", sty->outline.GetAssStyleFormatted() + "&");
			set_field(L, "color4", sty->shadow.GetAssStyleFormatted() + "&");

			set_field(L, "bold", sty->bold);
			set_field(L, "italic", sty->italic);
			set_field(L, "underline", sty->underline);
			set_field(L, "strikeout", sty->strikeout);

			set_field(L, "scale_x", sty->scalex);
			set_field(L, "scale_y", sty->scaley);

			set_field(L, "spacing", sty->spacing);

			set_field(L, "angle", sty->angle);

			set_field(L, "borderstyle", sty->borderstyle);
			set_field(L, "outline", sty->outline_w);
			set_field(L, "shadow", sty->shadow_w);

			set_field(L, "align", sty->alignment);

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

			set_field(L, "encoding", sty->encoding);

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

			set_field(L, "class", "style");
		}
		else {
			assert(false);
		}
	}

	std::unique_ptr<AssEntry> LuaAssFile::LuaToAssEntry(lua_State *L, AssFile *ass)
	{
		// 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))
			error(L, "Can't convert a non-table value to AssEntry");

		lua_getfield(L, -1, "class");
		if (!lua_isstring(L, -1))
			error(L, "Table lacks 'class' field, can't convert to AssEntry");

		std::string lclass(lua_tostring(L, -1));
		boost::to_lower(lclass);
		lua_pop(L, 1);

		std::unique_ptr<AssEntry> result;
		if (lclass == "info")
			result = std::make_unique<AssInfo>(get_string_field(L, "key", "info"),
			                                   get_string_field(L, "value", "info"));
		else if (lclass == "style") {
			auto sty = new AssStyle;
			result.reset(sty);
			sty->name = get_string_field(L, "name", "style");
			sty->font = get_string_field(L, "fontname", "style");
			sty->fontsize = get_double_field(L, "fontsize", "style");
			sty->primary = get_color_field(L, "color1", "style");
			sty->secondary = get_color_field(L, "color2", "style");
			sty->outline = get_color_field(L, "color3", "style");
			sty->shadow = get_color_field(L, "color4", "style");
			sty->bold = get_bool_field(L, "bold", "style");
			sty->italic = get_bool_field(L, "italic", "style");
			sty->underline = get_bool_field(L, "underline", "style");
			sty->strikeout = get_bool_field(L, "strikeout", "style");
			sty->scalex = get_double_field(L, "scale_x", "style");
			sty->scaley = get_double_field(L, "scale_y", "style");
			sty->spacing = get_double_field(L, "spacing", "style");
			sty->angle = get_double_field(L, "angle", "style");
			sty->borderstyle = get_int_field(L, "borderstyle", "style");
			sty->outline_w = get_double_field(L, "outline", "style");
			sty->shadow_w = get_double_field(L, "shadow", "style");
			sty->alignment = get_int_field(L, "align", "style");
			sty->Margin[0] = get_int_field(L, "margin_l", "style");
			sty->Margin[1] = get_int_field(L, "margin_r", "style");
			sty->Margin[2] = get_int_field(L, "margin_t", "style");
			sty->encoding = get_int_field(L, "encoding", "style");
			sty->UpdateData();
		}
		else if (lclass == "dialogue") {
			assert(ass != 0); // since we need AssFile::AddExtradata
			auto dia = new AssDialogue;
			result.reset(dia);

			dia->Comment = get_bool_field(L, "comment", "dialogue");
			dia->Layer = get_int_field(L, "layer", "dialogue");
			dia->Start = get_int_field(L, "start_time", "dialogue");
			dia->End = get_int_field(L, "end_time", "dialogue");
			dia->Style = get_string_field(L, "style", "dialogue");
			dia->Actor = get_string_field(L, "actor", "dialogue");
			dia->Margin[0] = get_int_field(L, "margin_l", "dialogue");
			dia->Margin[1] = get_int_field(L, "margin_r", "dialogue");
			dia->Margin[2] = get_int_field(L, "margin_t", "dialogue");
			dia->Effect = get_string_field(L, "effect", "dialogue");
			dia->Text = get_string_field(L, "text", "dialogue");

			std::vector<uint32_t> new_ids;

			lua_getfield(L, -1, "extra");
			auto type = lua_type(L, -1);
			if (type == LUA_TTABLE) {
				lua_for_each(L, [&] {
					if (lua_type(L, -2) != LUA_TSTRING) return;
					new_ids.push_back(ass->AddExtradata(
						get_string_or_default(L, -2),
						get_string_or_default(L, -1)));
				});
				std::sort(begin(new_ids), end(new_ids));
				dia->ExtradataIds = std::move(new_ids);
			}
			else if (type != LUA_TNIL) {
				error(L, "dialogue extradata must be a table");
			}
		}
		else {
			error(L, "Found line with unknown class: %s", lclass.c_str());
			return nullptr;
		}

		return result;
	}

	std::unique_ptr<AssEntry> LuaAssFile::LuaToTrackedAssEntry(lua_State *L) {
		std::unique_ptr<AssEntry> e = LuaToAssEntry(L, ass);
		allocated_lines.push_back(e.get());
		return e;
	}

	int LuaAssFile::ObjectIndexRead(lua_State *L)
	{
		switch (lua_type(L, 2)) {
			case LUA_TNUMBER:
			{
				// read an indexed AssEntry
				int idx = lua_tointeger(L, 2);
				CheckBounds(idx);
				AssEntryToLua(L, idx - 1);
				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, lines.size());
					return 1;
				}

				lua_pushvalue(L, 1);
				if (strcmp(idx, "delete") == 0)
					lua_pushcclosure(L, closure_wrapper_v<&LuaAssFile::ObjectDelete, false>, 1);
				else if (strcmp(idx, "deleterange") == 0)
					lua_pushcclosure(L, closure_wrapper_v<&LuaAssFile::ObjectDeleteRange, false>, 1);
				else if (strcmp(idx, "insert") == 0)
					lua_pushcclosure(L, closure_wrapper_v<&LuaAssFile::ObjectInsert, false>, 1);
				else if (strcmp(idx, "append") == 0)
					lua_pushcclosure(L, closure_wrapper_v<&LuaAssFile::ObjectAppend, false>, 1);
				else if (strcmp(idx, "script_resolution") == 0)
					lua_pushcclosure(L, closure_wrapper<&LuaAssFile::LuaGetScriptResolution>, 1);
				else {
					// idiot
					lua_pop(L, 1);
					return error(L, "Invalid indexing in Subtitle File object: '%s'", idx);
				}

				return 1;
			}

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

		assert(false);
		return 0;
	}

	void LuaAssFile::InitScriptInfoIfNeeded()
	{
		if (script_info_copied) return;
		size_t i = 0;
		for (auto const& info : ass->Info) {
			// Just in case an insane user inserted non-info lines into the
			// script info section...
			while (lines[i]) ++i;
			lines_to_delete.emplace_back(std::make_unique<AssInfo>(info));
			lines[i++] = lines_to_delete.back().get();
		}
		script_info_copied = true;
	}

	void LuaAssFile::QueueLineForDeletion(size_t idx)
	{
		if (!lines[idx] || lines[idx]->Group() == AssEntryGroup::INFO)
			InitScriptInfoIfNeeded();
		else
			lines_to_delete.emplace_back(lines[idx]);
	}

	void LuaAssFile::AssignLine(size_t idx, std::unique_ptr<AssEntry> e)
	{
		auto group = e->Group();
		if (group == AssEntryGroup::INFO)
			InitScriptInfoIfNeeded();
		lines[idx] = e.get();
		if (group == AssEntryGroup::INFO)
			lines_to_delete.emplace_back(std::move(e));
		else
			e.release();
	}

	void LuaAssFile::InsertLine(std::vector<AssEntry *> &vec, size_t idx, std::unique_ptr<AssEntry> e)
	{
		auto group = e->Group();
		if (group == AssEntryGroup::INFO)
			InitScriptInfoIfNeeded();
		vec.insert(vec.begin() + idx, e.get());
		if (group == AssEntryGroup::INFO)
			lines_to_delete.emplace_back(std::move(e));
		else
			e.release();
	}

	void 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

		CheckAllowModify();

		int n = check_int(L, 2);
		if (n < 0) {
			// insert line so new index is n
			lua_remove(L, 1);
			lua_pushinteger(L, -n);
			lua_replace(L, 1);
			ObjectInsert(L);
		}
		else if (n == 0) {
			// append line to list
			lua_remove(L, 1);
			lua_remove(L, 1);
			ObjectAppend(L);
		}
		else {
			// replace line at index n or delete
			if (!lua_isnil(L, 3)) {
				// insert
				CheckBounds(n);

				auto e = LuaToTrackedAssEntry(L);
				modification_type |= modification_mask(e.get());
				QueueLineForDeletion(n - 1);
				AssignLine(n - 1, std::move(e));
			}
			else {
				// delete
				lua_remove(L, 1);
				lua_remove(L, 1);
				ObjectDelete(L);
			}
		}
	}

	int LuaAssFile::ObjectGetLen(lua_State *L)
	{
		lua_pushnumber(L, lines.size());
		return 1;
	}

	void LuaAssFile::ObjectDelete(lua_State *L)
	{
		CheckAllowModify();

		// get number of items to delete
		int itemcount = lua_gettop(L);
		if (itemcount == 0) return;

		std::vector<size_t> ids;
		if (itemcount == 1 && lua_istable(L, 1)) {
			lua_pushvalue(L, 1);
			lua_for_each(L, [&] {
				size_t n = check_uint(L, -1);
				argcheck(L, n > 0 && n <= lines.size(), 1, "Out of range line index");
				ids.push_back(n - 1);
			});
		}
		else {
			ids.reserve(itemcount);
			while (itemcount > 0) {
				size_t n = check_uint(L, -1);
				argcheck(L, n > 0 && n <= lines.size(), itemcount, "Out of range line index");
				ids.push_back(n - 1);
				--itemcount;
			}
		}

		sort(ids.begin(), ids.end());

		size_t id_idx = 0, out = 0;
		for (size_t i = 0; i < lines.size(); ++i) {
			if (id_idx < ids.size() && ids[id_idx] == i) {
				modification_type |= modification_mask(lines[i]);
				QueueLineForDeletion(i);
				++id_idx;
			}
			else {
				lines[out++] = lines[i];
			}
		}

		lines.erase(lines.begin() + out, lines.end());
	}

	void LuaAssFile::ObjectDeleteRange(lua_State *L)
	{
		CheckAllowModify();

		size_t a = std::max<size_t>(check_uint(L, 1), 1) - 1;
		size_t b = std::min<size_t>(check_uint(L, 2), lines.size());

		if (a >= b) return;

		for (size_t i = a; i < b; ++i) {
			modification_type |= modification_mask(lines[i]);
			QueueLineForDeletion(i);
		}

		lines.erase(lines.begin() + a, lines.begin() + b);
	}

	void LuaAssFile::ObjectAppend(lua_State *L)
	{
		CheckAllowModify();

		int n = lua_gettop(L);

		for (int i = 1; i <= n; i++) {
			lua_pushvalue(L, i);
			auto e = LuaToTrackedAssEntry(L);
			modification_type |= modification_mask(e.get());

			if (lines.empty()) {
				InsertLine(lines, 0, std::move(e));
				continue;
			}

			// Find the appropriate place to put it
			auto group = e->Group();
			for (size_t i = lines.size(); i > 0; --i) {
				auto cur_group = lines[i - 1] ? lines[i - 1]->Group() : AssEntryGroup::INFO;
				if (cur_group == group) {
					InsertLine(lines, i, std::move(e));
					break;
				}
			}

			// No lines of this type exist already, so just append it to the end
			if (e) InsertLine(lines, lines.size(), std::move(e));
		}
	}

	void LuaAssFile::ObjectInsert(lua_State *L)
	{
		CheckAllowModify();

		size_t before = check_uint(L, 1);

		// + 1 to allow appending at the end of the file
		argcheck(L, before > 0 && before <= lines.size() + 1, 1,
			"Out of range line index");

		if (before == lines.size() + 1) {
			lua_remove(L, 1);
			ObjectAppend(L);
			return;
		}

		int n = lua_gettop(L);
		std::vector<AssEntry *> new_entries;
		new_entries.reserve(n - 1);
		for (int i = 2; i <= n; i++) {
			lua_pushvalue(L, i);
			auto e = LuaToTrackedAssEntry(L);
			modification_type |= modification_mask(e.get());
			InsertLine(new_entries, i - 2, std::move(e));
			lua_pop(L, 1);
		}
		lines.insert(lines.begin() + before - 1, new_entries.begin(), new_entries.end());
	}

	void LuaAssFile::ObjectGarbageCollect(lua_State *L)
	{
		references--;
		if (!references) delete this;
		LOG_D("automation/lua") << "Garbage collected LuaAssFile";
	}

	int LuaAssFile::ObjectIPairs(lua_State *L)
	{
		lua_pushvalue(L, lua_upvalueindex(1)); // push 'this' as userdata
		lua_pushcclosure(L, closure_wrapper<&LuaAssFile::IterNext>, 1);
		lua_pushnil(L);
		push_value(L, 0);
		return 3;
	}

	int LuaAssFile::IterNext(lua_State *L)
	{
		size_t i = check_uint(L, 2);
		if (i >= lines.size()) {
			lua_pushnil(L);
			return 1;
		}

		push_value(L, i + 1);
		AssEntryToLua(L, i);
		return 2;
	}

	int LuaAssFile::LuaParseKaraokeData(lua_State *L)
	{
		auto e = LuaToAssEntry(L, ass);
		auto dia = check_cast_constptr<AssDialogue>(e.get());
		argcheck(L, !!dia, 1, "Subtitle line must be a dialogue line");

		int idx = 0;

		// 2.1.x stored everything before the first syllable at index zero
		// There's no longer any such thing with the new parser, but scripts
		// may rely on kara[0] existing so add an empty syllable
		lua_createtable(L, 0, 6);
		set_field(L, "duration", 0);
		set_field(L, "start_time", 0);
		set_field(L, "end_time", 0);
		set_field(L, "tag", "");
		set_field(L, "text", "");
		set_field(L, "text_stripped", "");
		lua_rawseti(L, -2, idx++);

		agi::ass::Karaoke kara;
		SetKaraokeLine(kara, dia, false, false);
		for (auto const& syl : kara) {
			lua_createtable(L, 0, 6);
			set_field(L, "duration", syl.duration);
			set_field(L, "start_time", syl.start_time - dia->Start);
			set_field(L, "end_time", syl.start_time + syl.duration - dia->Start);
			set_field(L, "tag", syl.tag_type);
			set_field(L, "text", syl.GetText(false));
			set_field(L, "text_stripped", syl.text);
			lua_rawseti(L, -2, idx++);
		}

		return 1;
	}

	int LuaAssFile::LuaGetScriptResolution(lua_State *L)
	{
		int w, h;
		ass->GetResolution(w, h);
		push_value(L, w);
		push_value(L, h);
		return 2;
	}

	void LuaAssFile::LuaSetUndoPoint(lua_State *L)
	{
		if (!can_set_undo)
			error(L, "Attempt to set an undo point in a context where it makes no sense to do so.");

		if (modification_type) {
			pending_commits.emplace_back();
			PendingCommit& back = pending_commits.back();

			back.modification_type = modification_type;
			back.mesage = to_wx(check_string(L, 1));
			back.lines = lines;
			modification_type = 0;
		}
	}

	LuaAssFile *LuaAssFile::GetObjPointer(lua_State *L, int idx, bool allow_expired)
	{
		assert(lua_type(L, idx) == LUA_TUSERDATA);
		auto ud = lua_touserdata(L, idx);
		auto laf = *static_cast<LuaAssFile **>(ud);
		if (!allow_expired && laf->references < 2)
			error(L, "Subtitles object is no longer valid");
		return laf;
	}

	std::vector<AssEntry *> LuaAssFile::ProcessingComplete(wxString const& undo_description)
	{
		auto apply_lines = [&](std::vector<AssEntry *> const& lines) {
			if (script_info_copied)
				ass->Info.clear();
			ass->Styles.clear();
			ass->Events.clear();

			for (auto line : lines) {
				if (!line) continue;
				switch (line->Group()) {
					case AssEntryGroup::INFO:     ass->Info.push_back(*static_cast<AssInfo *>(line)); break;
					case AssEntryGroup::STYLE:    ass->Styles.push_back(*static_cast<AssStyle *>(line)); break;
					case AssEntryGroup::DIALOGUE: ass->Events.push_back(*static_cast<AssDialogue *>(line)); break;
					default: break;
				}
			}
		};
		// Apply any pending commits
		for (auto const& pc : pending_commits) {
			apply_lines(pc.lines);
			ass->Commit(pc.mesage, pc.modification_type);
		}

		// Commit any changes after the last undo point was set
		if (modification_type)
			apply_lines(lines);
		if (modification_type && can_set_undo && !undo_description.empty())
			ass->Commit(undo_description, modification_type);

		lines_to_delete.clear();

		auto ret = std::move(lines);
		references--;
		if (!references) delete this;
		return ret;
	}

	void LuaAssFile::Cancel()
	{
		for (auto& line : lines_to_delete) line.release();
		for (AssEntry *line : allocated_lines) delete line;
		references--;
		if (!references) delete this;
	}

	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)
	{
		for ([[maybe_unused]] auto& line : ass->Info)
			lines.push_back(nullptr);
		for (auto& line : ass->Styles)
			lines.push_back(&line);
		for (auto& line : ass->Events)
			lines.push_back(&line);

		// prepare userdata object
		*static_cast<LuaAssFile**>(lua_newuserdata(L, sizeof(LuaAssFile*))) = this;

		// make the metatable
		lua_createtable(L, 0, 5);
		set_field<closure_wrapper<&LuaAssFile::ObjectIndexRead>>(L, "__index");
		set_field<closure_wrapper_v<&LuaAssFile::ObjectIndexWrite, false>>(L, "__newindex");
		set_field<closure_wrapper<&LuaAssFile::ObjectGetLen>>(L, "__len");
		set_field<closure_wrapper_v<&LuaAssFile::ObjectGarbageCollect, true>>(L, "__gc");
		set_field<closure_wrapper<&LuaAssFile::ObjectIPairs>>(L, "__ipairs");
		lua_setmetatable(L, -2);

		// register misc functions
		// assume the "aegisub" global table exists
		lua_getglobal(L, "aegisub");

		set_field<closure_wrapper<&LuaAssFile::LuaParseKaraokeData>>(L, "parse_karaoke_data");
		set_field<closure_wrapper_v<&LuaAssFile::LuaSetUndoPoint, false>>(L, "set_undo_point");

		lua_pop(L, 1); // pop "aegisub" table

		// Leaves userdata object on stack
	}
}
