/*
 * Copyright (C) 2012 - Juan Ferrer Toribio
 *
 * 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 "sql-render.h"
#include <string.h>

/**
 * SECTION: sql-render
 * @Short_description: renders an #SqlObject to a string
 * @Title: SqlRender
 * 
 * #SqlRender takes care of rendering the different types of #SqlObject to a 
 * valid SQL string, ready to pass to a Database. In most cases the user won't
 * need to use any method besides the creation and destruction ones and
 * sql_render_get_string().
 **/
G_DEFINE_TYPE (SqlRender, sql_render, G_TYPE_OBJECT);

/**
 * sql_render_new:
 * @delimiter: the delimiter character
 *
 * Creates an #SqlRender object
 *
 * Return value: the new #SqlRender
 **/
SqlRender * sql_render_new (gchar delimiter)
{
	return g_object_new (SQL_TYPE_RENDER, "delimiter", delimiter, NULL);	
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++ Public

/**
 * sql_render_get_string:
 * @obj: a #SqlRender
 * @object: the #SqlObject
 * @data: pointer to custom data
 * @err: (out) (allow-none): the return location for a #GError or %NULL
 *
 * Transforms the #SqlObject into a SQL string.
 *
 * Return value: a string with the rendered statement of %NULL if error.
 **/
gchar * sql_render_get_string (SqlRender * obj, gpointer object, gpointer data, GError ** err)
{
	gchar * sql;

	g_return_val_if_fail (SQL_IS_RENDER (obj), NULL);
	g_return_val_if_fail (G_IS_OBJECT (object), NULL);

	obj->data = data;
	obj->object = g_object_ref (object);
	obj->buffer = g_string_sized_new (SQL_BUFFER_SIZE);

	sql_render_add_object (obj, object);
	
	if (obj->error)
	{
		if (!err)
			g_warning ("%s",obj->error->message);
		else
			g_propagate_error (err, obj->error);

		g_clear_error (&obj->error);
		sql = g_string_free (obj->buffer, TRUE);
	}
	else
		sql = g_string_free (obj->buffer, FALSE);
		
	g_clear_object (&obj->object);
	obj->buffer = NULL;
	obj->data = NULL;
	return sql;
}

/**
 * sql_render_register_function:
 * @obj: a #SqlRender
 * @type: #GType for which the function will be used
 * @function: (scope async): render function to use with @type
 * 
 * Registers @function as the function to be used to render the objects of type
 * @type. Users most likely won't need to use it.
 **/
void sql_render_register_function (SqlRender * obj, GType type, SqlRenderFunc function)
{
	g_return_if_fail (SQL_IS_RENDER (obj));

	g_hash_table_insert (obj->symbol_table, GUINT_TO_POINTER (type), function);
}

/**
 * sql_render_add_object:
 * @obj: a #SqlRender
 * @object: a #gpointer to an object
 * 
 * Adds an object to a render.
 **/
void sql_render_add_object (SqlRender * obj, gpointer object)
{
	SqlRenderFunc function;
	
	g_return_if_fail (SQL_IS_RENDER (obj));
	g_return_if_fail (G_IS_OBJECT (object) || !object);

	if (object)
	{
		function = g_hash_table_lookup (obj->symbol_table,
			GUINT_TO_POINTER (G_OBJECT_TYPE (object)));
	
		if (function)
			function (object, obj);
		else
			sql_object_render (object, obj);
	}
}

/**
 * sql_render_add_espace:
 * @obj: a #SqlRender
 * 
 * Adds an space character to a render.
 **/
void sql_render_add_espace (SqlRender * obj)
{
	g_return_if_fail (SQL_IS_RENDER (obj));

	gsize len = obj->buffer->len;

	if (len > 0)
	switch (obj->buffer->str[len-1])
	{
		case ' ':
		case '(':
		case '.':
			return;
		default:
			g_string_append_c (obj->buffer, ' ');
	}
}

/**
 * sql_render_printf:
 * @obj: a #SqlRender
 * @string: a format string.
 * @...: a %NULL terminated list of variables.
 * 
 * Creates an string from a format string.
 **/
void sql_render_printf (SqlRender * obj, const gchar * string, ...)
{
	va_list vl;

	g_return_if_fail (SQL_IS_RENDER (obj));
	g_return_if_fail (string);
	
	va_start (vl, string);
	sql_render_add_espace (obj);
	g_string_append_vprintf (obj->buffer, string, vl);
	va_end (vl);
}

/**
 * sql_render_append:
 * @obj: a #SqlRender
 * @string: a character string.
 * 
 * Appends @string to the current contents of @obj. 
 **/
void sql_render_append (SqlRender * obj, const gchar * string)
{
	g_return_if_fail (SQL_IS_RENDER (obj));
	g_return_if_fail (string);

	g_string_append (obj->buffer, string);
}

/**
 * sql_render_append_with_delimiter:
 * @obj: a #SqlRender
 * @string: a character string.
 * @delimiter: the delimiter character to use.
 * 
 * Appends @string to the current contents of @obj. @delimiter is used to add
 * spaces into the string depending where required.
 **/
void sql_render_append_with_delimiter (SqlRender * obj, const gchar * string, gchar delimiter)
{
	g_return_if_fail (SQL_IS_RENDER (obj));

	if (string)
	{
		sql_render_add_espace (obj);
		g_string_append_c (obj->buffer, delimiter);
		g_string_append (obj->buffer, string);
		g_string_append_c (obj->buffer, delimiter);
	}
}

/**
 * sql_render_add_token:
 * @obj: a #SqlRender
 * @token: an SQL reserved keyword
 * 
 * Adds @token to @obj. Note that @token must be a reserved SQL keyword. 
 **/
void sql_render_add_token (SqlRender * obj, const gchar * token)
{
	g_return_if_fail (SQL_IS_RENDER (obj));

	if (token)
	{
		sql_render_add_espace (obj);
		g_string_append (obj->buffer, token);
	}
}

/**
 * sql_render_add_identifier:
 * @obj: a #SqlRender
 * @identifier: a character string.
 * 
 * Adds @identifier to @obj. @identifier is taken as the name of an identifier
 * for an expression in an SQL string.
 **/
void sql_render_add_identifier (SqlRender * obj, const gchar * identifier)
{
	g_return_if_fail (SQL_IS_RENDER (obj));

	sql_render_append_with_delimiter (obj, identifier,
		obj->delimiter);
}

/**
 * sql_render_add_item:
 * @obj: a #SqlRender
 * @required: 
 * @token: 
 * @item: 
 * 
 * 
 **/
void sql_render_add_item (SqlRender * obj, gboolean required, const gchar * token, gpointer item)
{
	g_return_if_fail (SQL_IS_RENDER (obj));
	g_return_if_fail (G_IS_OBJECT (item) || !item);

	if (item)
	{
		sql_render_add_token (obj, token);
		sql_render_add_object (obj, item);
	}
	else if (required)
		sql_render_set_error (obj);
}

/**
 * sql_render_add_list:
 * @obj: a #SqlRender
 * @required: 
 * @token: 
 * @list: (element-type GObject.Object): a list of objects to add
 * @separator: 
 * 
 * 
 **/
void sql_render_add_list (SqlRender * obj, gboolean required, const gchar * token,
	GSList * list, const gchar * separator)
{
	g_return_if_fail (SQL_IS_RENDER (obj));

	if (list)
	{
		GSList * n;
		sql_render_add_token (obj, token);
	
		for (n = list; n; n = n->next)
		{
			sql_render_add_object (obj, n->data);
		
			if (n->next)
				g_string_append_printf (obj->buffer, " %s", separator);
		}
	}
	else if (required)
		sql_render_set_error (obj);
}

/**
 * sql_render_add_list_with_func:
 * @obj: a #SqlRender
 * @required:
 * @token:
 * @list: (element-type GObject.Object):
 * @separator:
 * @function: (scope call): 
 * 
 * 
 **/
void sql_render_add_list_with_func (SqlRender * obj, gboolean required, const gchar * token,
	GSList * list, const gchar * separator, SqlRenderFunc function)
{
	g_return_if_fail (SQL_IS_RENDER (obj));

	if (list)
	{
		GSList * n;
		sql_render_add_token (obj, token);
	
		for (n = list; n; n = n->next)
		{
			function (n->data, obj);
		
			if (n->next)
				g_string_append_printf (obj->buffer, " %s", separator);
		}
	}
	else if (required)
		sql_render_set_error (obj);
}

/**
 * sql_render_set_error:
 * @obj: a #SqlRender
 **/
void sql_render_set_error (SqlRender * obj)
{
	g_return_if_fail (SQL_IS_RENDER (obj));

	obj->error = g_error_new (
		 SQL_RENDER_LOG_DOMAIN
		,SQL_RENDER_ERROR
		,"An error ocurred during query render: [ %s ] <-- Error is here"
		,obj->buffer->str
	);
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++ Properties

enum
{
	 PROP_DELIMITER = 1
};

static void sql_render_set_property (SqlRender * obj, guint id,
	const GValue * value, GParamSpec * pspec)
{
	switch (id)
	{
		case PROP_DELIMITER:
			obj->delimiter = g_value_get_schar (value);
			break;
		default:		
			G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, id, pspec);
	}
}

static void sql_render_get_property (SqlRender * obj, guint id,
	GValue * value, GParamSpec * pspec)
{	
	switch (id)
	{
		case PROP_DELIMITER:
			g_value_set_schar (value, obj->delimiter);
			break;
		default:		
			G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, id, pspec);
	}
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++ Class

static void sql_render_init (SqlRender * obj)
{
	obj->buffer = NULL;
	obj->error = NULL;
	obj->symbol_table = g_hash_table_new (
		 g_direct_hash
		,g_direct_equal
	);
}

static void sql_render_finalize (SqlRender * obj)
{
	g_hash_table_unref (obj->symbol_table);
	G_OBJECT_CLASS (sql_render_parent_class)->finalize (G_OBJECT (obj));
}

static void sql_render_class_init (SqlRenderClass * klass)
{
	GObjectClass * k = G_OBJECT_CLASS (klass);
	k->finalize = (GObjectFinalizeFunc) sql_render_finalize;
	k->set_property = (GObjectSetPropertyFunc) sql_render_set_property;
	k->get_property = (GObjectGetPropertyFunc) sql_render_get_property;

	g_object_class_install_property (k, PROP_DELIMITER,
		g_param_spec_char ("delimiter"
			,_("Delimiter")
			,_("The character used for delimite the name of fields, tables...")
			,G_MININT8 ,G_MAXINT8 ,'`'
			,G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE
	));
}