/*
** binding-mri.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2013 Jonas Kulla <Nyocurio@gmail.com>
**
** mkxp 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 2 of the License, or
** (at your option) any later version.
**
** mkxp 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 mkxp.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "binding.h"
#include "binding-util.h"
#include "sharedstate.h"
#include "eventthread.h"
#include "filesystem.h"

#include "./ruby/include/ruby.h"
#include "./ruby/include/ruby/encoding.h"

#include "zlib.h"

#include "SDL_filesystem.h"

#include <QFile>
#include <QByteArray>

#include <QDebug>

extern const char module_rpg[];

static void mriBindingExecute();
static void mriBindingTerminate();

ScriptBinding scriptBindingImpl =
{
	mriBindingExecute,
	mriBindingTerminate
};

ScriptBinding *scriptBinding = &scriptBindingImpl;

void tableBindingInit();
void etcBindingInit();
void fontBindingInit();
void bitmapBindingInit();
void spriteBindingInit();
void viewportBindingInit();
void planeBindingInit();
void windowBindingInit();
void tilemapBindingInit();

void inputBindingInit();
void audioBindingInit();
void graphicsBindingInit();

void fileIntBindingInit();

RB_METHOD(mriPrint);
RB_METHOD(mriP);
RB_METHOD(mriDataDirectory);
RB_METHOD(mkxpPuts);

static void mriBindingInit()
{
	tableBindingInit();
	etcBindingInit();
	fontBindingInit();
	bitmapBindingInit();
	spriteBindingInit();
	viewportBindingInit();
	planeBindingInit();
	windowBindingInit();
	tilemapBindingInit();

	inputBindingInit();
	audioBindingInit();
	graphicsBindingInit();

	fileIntBindingInit();

	_rb_define_module_function(rb_mKernel, "print", mriPrint);
	_rb_define_module_function(rb_mKernel, "p",     mriP);

	rb_eval_string(module_rpg);

	VALUE mod = rb_define_module("System");
	_rb_define_module_function(mod, "data_directory", mriDataDirectory);
	_rb_define_module_function(mod, "puts", mkxpPuts);

	rb_define_global_const("MKXP", Qtrue);
}

static void
showMsg(const QByteArray &msg)
{
	shState->eThread().showMessageBox(msg.constData());
}

RB_METHOD(mkxpPuts)
{
	RB_UNUSED_PARAM;

	const char *str;
	rb_get_args(argc, argv, "z", &str, RB_ARG_END);

	qDebug() << str;

	return Qnil;
}

static void printP(int argc, VALUE *argv,
                   const char *convMethod, const char *sep)
{
	VALUE dispString = rb_str_buf_new(128);
	ID conv = rb_intern(convMethod);

	for (int i = 0; i < argc; ++i)
	{
		VALUE str = rb_funcall(argv[i], conv, 0);
		rb_str_buf_append(dispString, str);

		if (i < argc)
			rb_str_buf_cat2(dispString, sep);
	}

	showMsg(RSTRING_PTR(dispString));
}

RB_METHOD(mriPrint)
{
	RB_UNUSED_PARAM;

	printP(argc, argv, "to_s", "");

	return Qnil;
}

RB_METHOD(mriP)
{
	RB_UNUSED_PARAM;

	printP(argc, argv, "inspect", "\n");

	return Qnil;
}

RB_METHOD(mriDataDirectory)
{
	RB_UNUSED_PARAM;

	const char *org, *app;

	rb_get_args(argc, argv, "zz", &org, &app, RB_ARG_END);

	char *path = SDL_GetPrefPath(org, app);

	VALUE pathStr = rb_str_new_cstr(path);

	SDL_free(path);

	return pathStr;
}

static void runCustomScript(const char *filename)
{
	QFile scriptFile(filename);
	if (!scriptFile.open(QFile::ReadOnly))
	{
		showMsg(QByteArray("Unable to open '") + filename + "'");
		return;
	}

	QByteArray scriptData = scriptFile.readAll();
	scriptFile.close();

	scriptData.prepend("#encoding:utf-8\n");

	rb_eval_string_protect(scriptData.constData(), 0);
}

VALUE kernelLoadDataInt(const char *filename);

struct Script
{
	QByteArray name;
	QByteArray encData;
	uint32_t unknown;

	QByteArray decData;
};

static void runRMXPScripts()
{
	const QByteArray &scriptPack = shState->rtData().config.game.scripts;

	if (scriptPack.isEmpty())
	{
		showMsg("No game scripts specified (missing Game.ini?)");
		return;
	}

	if (!shState->fileSystem().exists(scriptPack.constData()))
	{
		showMsg("Unable to open '" + scriptPack + "'");
		return;
	}

	VALUE scriptArray = kernelLoadDataInt(scriptPack.constData());

	if (rb_type(scriptArray) != RUBY_T_ARRAY)
	{
		showMsg("Failed to read script data");
		return;
	}

	int scriptCount = RARRAY_LEN(scriptArray);

	QByteArray decodeBuffer;
	decodeBuffer.resize(0x1000);

	QVector<Script> encScripts(scriptCount);

	for (int i = 0; i < scriptCount; ++i)
	{
		VALUE script = rb_ary_entry(scriptArray, i);

		if (rb_type(script) != RUBY_T_ARRAY)
		{
			continue;
		}

		VALUE scriptUnknown = rb_ary_entry(script, 0);
		VALUE scriptName    = rb_ary_entry(script, 1);
		VALUE scriptString  = rb_ary_entry(script, 2);

		Script &sc = encScripts[i];
		sc.name = RSTRING_PTR(scriptName);
		sc.encData = QByteArray(RSTRING_PTR(scriptString), RSTRING_LEN(scriptString));
		sc.unknown = FIX2UINT(scriptUnknown);
	}

	for (int i = 0; i < scriptCount; ++i)
	{
		Script &sc = encScripts[i];

		int result = Z_OK;
		ulong bufferLen;

		while (true)
		{
			unsigned char *bufferPtr =
			        reinterpret_cast<unsigned char*>(const_cast<char*>(decodeBuffer.constData()));
			const unsigned char *sourcePtr =
			        reinterpret_cast<const unsigned char*>(sc.encData.constData());

			bufferLen = decodeBuffer.length();

			result = uncompress(bufferPtr, &bufferLen,
			                    sourcePtr, sc.encData.length());

			bufferPtr[bufferLen] = '\0';

			if (result != Z_BUF_ERROR)
				break;

			decodeBuffer.resize(decodeBuffer.size()*2);
		}

		if (result != Z_OK)
		{
			static char buffer[256];
			snprintf(buffer, sizeof(buffer), "Error decoding script %d: '%s'",
			         i, sc.name.constData());

			showMsg(buffer);
			break;
		}

		sc.decData = QByteArray(decodeBuffer.constData(), bufferLen);
		sc.decData.prepend("#encoding:utf-8\n");

		ruby_script(sc.name.constData());

		rb_gc_start();

		/* Execute code */
		rb_eval_string_protect(sc.decData.constData(), 0);

		VALUE exc = rb_gv_get("$!");
		if (rb_type(exc) != RUBY_T_NIL)
			break;
	}
}

static void mriBindingExecute()
{
	ruby_setup();
	rb_enc_set_default_external(rb_enc_from_encoding(rb_utf8_encoding()));

	RbData rbData;
	shState->setBindingData(&rbData);

	mriBindingInit();

	QByteArray &customScript = shState->rtData().config.customScript;
	if (!customScript.isEmpty())
		runCustomScript(customScript.constData());
	else
		runRMXPScripts();

	VALUE exc = rb_gv_get("$!");
	if (rb_type(exc) != RUBY_T_NIL && !rb_eql(rb_obj_class(exc), rb_eSystemExit))
	{
		qDebug() << "Had exception:" << rb_class2name(rb_obj_class(exc));
		VALUE bt = rb_funcall(exc, rb_intern("backtrace"), 0);
		rb_p(bt);
		VALUE msg = rb_funcall(exc, rb_intern("message"), 0);
		if (RSTRING_LEN(msg) < 256)
			showMsg(RSTRING_PTR(msg));
		else
			qDebug() << (RSTRING_PTR(msg));
	}

	ruby_cleanup(0);

	shState->rtData().rqTermAck = true;
}

static void mriBindingTerminate()
{
	rb_raise(rb_eSystemExit, " ");
}