/*
 * Copyright (C) 2011-2013 Gary Kramlich <grim@reaperworld.com>
 *
 * 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/>.
 */

#include "gplugin-tcc-loader.h"

#include "gplugin-tcc-plugin.h"

#include <glib/gi18n.h>

#include <libtcc.h>

/******************************************************************************
 * Globals
 *****************************************************************************/
static GObjectClass *parent_class = NULL;
static GType type_real = 0;

/******************************************************************************
 * GPluginLoaderInterface API
 *****************************************************************************/
static GSList *
gplugin_tcc_loader_class_supported_extensions(GPLUGIN_UNUSED const GPluginLoaderClass *klass) {
	GSList *exts = NULL;

	exts = g_slist_append(exts, "c");

	return exts;
}

static GPluginPlugin *
gplugin_tcc_loader_query(GPluginLoader *loader, const gchar *filename,
                         GError **error)
{
	GPluginPlugin *plugin = NULL;
	GPluginPluginInfo *info = NULL;
	TCCState *s = NULL;
	gpointer memneeded = NULL;
	int memsize = -1;

	GPluginTccPluginQueryFunc gplugin_query = NULL;

	s = tcc_new();

	tcc_set_output_type(s, TCC_OUTPUT_MEMORY);

	if(tcc_add_file(s, filename) == -1) {
		if(error)
			*error = g_error_new(GPLUGIN_DOMAIN, 0, "couldn't load file %s", filename);

		tcc_delete(s);

		return NULL;
	}

	/* copy code into memory */
	if((memsize = tcc_relocate(s, NULL)) < 0) {
		if(error)
			*error = g_error_new(GPLUGIN_DOMAIN, 0, "couldn't work out how much memory is needed");

		tcc_delete(s);
		return NULL;
	}

	memneeded = g_malloc0(memsize);
	if(tcc_relocate(s, memneeded) < 0) {
		if(error)
			*error = g_error_new(GPLUGIN_DOMAIN, 0, "could not relocate plugin into memory");

		tcc_delete(s);
		g_free(memneeded);
		return NULL;
	}

	gplugin_query = (GPluginTccPluginQueryFunc) tcc_get_symbol(s, "gplugin_query");
	if (gplugin_query == NULL) {
		if(error)
			*error = g_error_new(GPLUGIN_DOMAIN, 0, "no gplugin_query function found");

		tcc_delete(s);
		g_free(memneeded);
		return NULL;
	}

	info = gplugin_query(error);
	if(info == NULL) {
		tcc_delete(s);
		g_free(memneeded);
		return NULL;
	}

	plugin = g_object_new(GPLUGIN_TYPE_TCC_PLUGIN,
	                      "filename", filename,
	                      "loader", loader,
	                      "state", s,
	                      "memory", memneeded,
	                      "info", info,
	                      NULL);

	return plugin;
}

static gboolean
gplugin_tcc_loader_load(GPLUGIN_UNUSED GPluginLoader *loader,
                        GPluginPlugin *plugin,
                        GError **error)
{
	GPluginTccPluginLoadFunc gplugin_load = NULL;
	TCCState *s = gplugin_tcc_plugin_get_state(GPLUGIN_TCC_PLUGIN(plugin));

	gplugin_load = (GPluginTccPluginLoadFunc) tcc_get_symbol(s, "gplugin_load");
	if (gplugin_load == NULL) {
		if(error)
			*error = g_error_new(GPLUGIN_DOMAIN, 0, "no gplugin_load function found");

		return FALSE;
	}

	return gplugin_load(GPLUGIN_NATIVE_PLUGIN(plugin), error);
}

static gboolean
gplugin_tcc_loader_unload(GPLUGIN_UNUSED GPluginLoader *loader,
                          GPluginPlugin *plugin,
                          GError **error)
{
	GPluginTccPluginLoadFunc gplugin_unload = NULL;
	TCCState *s = gplugin_tcc_plugin_get_state(GPLUGIN_TCC_PLUGIN(plugin));

	gplugin_unload = (GPluginTccPluginUnloadFunc) tcc_get_symbol(s, "gplugin_unload");
	if (gplugin_unload == NULL) {
		if(error)
			*error = g_error_new(GPLUGIN_DOMAIN, 0, "no gplugin_unload function found");

		return FALSE;
	}

	return gplugin_unload(GPLUGIN_NATIVE_PLUGIN(plugin), error);
}

/******************************************************************************
 * GObject Stuff
 *****************************************************************************/
static void
gplugin_tcc_loader_class_init(GPluginTccLoaderClass *klass) {
	GPluginLoaderClass *loader_class = GPLUGIN_LOADER_CLASS(klass);

	parent_class = g_type_class_peek_parent(klass);

	loader_class->supported_extensions =
		gplugin_tcc_loader_class_supported_extensions;
	loader_class->query = gplugin_tcc_loader_query;
	loader_class->load = gplugin_tcc_loader_load;
	loader_class->unload = gplugin_tcc_loader_unload;
}

/******************************************************************************
 * API
 *****************************************************************************/
void
gplugin_tcc_loader_register(GPluginNativePlugin *plugin) {
	if(g_once_init_enter(&type_real)) {
		GType type = 0;

		static const GTypeInfo info = {
			.class_size = sizeof(GPluginTccLoaderClass),
			.class_init = (GClassInitFunc)gplugin_tcc_loader_class_init,
			.instance_size = sizeof(GPluginTccLoader),
		};

		type = gplugin_native_plugin_register_type(plugin,
		                                           GPLUGIN_TYPE_LOADER,
		                                           "GPluginTccLoader",
		                                           &info,
		                                           0);

		g_once_init_leave(&type_real, type);
	}
}

GType
gplugin_tcc_loader_get_type(void) {
	if(G_UNLIKELY(type_real == 0)) {
		g_warning("gplugin_tcc_loader_get_type was called before "
		          "the type was registered!\n");
	}

	return type_real;
}

