/*
 * 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 <string.h>
#include <stdlib.h>
#include "gvn-value.h"
#include "gvn-misc.h"

/**
 * SECTION: gvn-value
 * @Short_description: additional value manipulation functions
 * @Title: GvnValue
 * @See_also: #DbForm, #DbConn
 * 
 * This functions are intended to expand or ease the use of the original GObject
 * library #GValue utilites.
 **/

//+++++++++++++++++++++++++++++++++++++++++++++++++++ Private

static void gvn_value_new_from_string_real (GValue * value, GType type, const gchar * string, gsize len)
{	
	switch (type)
	{
		case G_TYPE_BOOLEAN:
			g_value_set_boolean (value, atoi (string));
			break;
		case G_TYPE_CHAR:
			g_value_set_schar (value, atoi (string));
			break;
		case G_TYPE_INT:
			g_value_set_int (value, atoi (string));
			break;
		case G_TYPE_UINT:
			g_value_set_int (value, (guint) atoi (string));
			break;
		case G_TYPE_LONG:
			g_value_set_long (value, g_ascii_strtoll (string, NULL, 0));
			break;
		case G_TYPE_ULONG:
			g_value_set_ulong (value, g_ascii_strtoull (string, NULL, 0));
			break;
		case G_TYPE_FLOAT:
			g_value_set_float (value, atof (string));
			break;
		case G_TYPE_DOUBLE:
			g_value_set_double (value, g_ascii_strtod (string, NULL));
			break;
		case G_TYPE_STRING:
			g_value_set_string (value, string);
			break;
		default:
		{
			if (len == -1)
				len = strlen (string);
			
			if (type == G_TYPE_DATE)
			{
				if (len >= 10)
				{
					GDate * date = g_date_new_dmy (
						 atoi (string + 8)
						,atoi (string + 5)
						,atoi (string)
					);
					g_value_take_boxed (value, date);
				}
				else
					g_warning ("Gvn: Can't transform string to GDate");
			}
			else if (type == G_TYPE_DATE_TIME)
			{
				if (len >= 19)
				{
					GDateTime * date_time = g_date_time_new_local (
						 atoi (string)
						,atoi (string + 5)
						,atoi (string + 8)
						,atoi (string + 11)
						,atoi (string + 14)
						,atoi (string + 17)
					);
					g_value_take_boxed (value, date_time);
				}
				else
					g_warning ("Gvn: Can't transform string to GDateTime");
			}
			else if (type == G_TYPE_BYTES)
			{
				if (len > 0)
					g_value_take_boxed (value, g_bytes_new (string, len));
				else
					g_warning ("Gvn: Can't transform string to GBytes");
			}
			else if (type != GVN_TYPE_NULL)
				g_return_if_reached ();
		}
	}
}

static void gvn_value_transform_string_to_any (const GValue * src, GValue * dst)
{
	gvn_value_new_from_string_real (dst,
		G_VALUE_TYPE (dst), g_value_get_string (src), -1);
}

static void gvn_value_transform_null_to_any (const GValue * src, GValue * dst) {}

static void gvn_value_transform_any_to_null (const GValue * src, GValue * dst)
{
	gvn_value_set_null (dst);
}

static void gvn_value_transform_date_to_string (const GValue * src, GValue * dst)
{
	GDate * date = g_value_get_boxed (src);

	if (date)
	{
		g_value_take_string (dst, g_strdup_printf ("%04u-%02u-%02u"
			,g_date_get_year (date)
			,g_date_get_month (date)
			,g_date_get_day (date)
		));
	}
	else
		g_value_take_string (dst, NULL);
}

static void gvn_value_transform_date_time_to_string (const GValue * src, GValue * dst)
{
	gpointer date = g_value_get_boxed (src);

	if (date)
		g_value_take_string (dst, g_date_time_format (date, "%Y-%m-%d %T"));
	else
		g_value_take_string (dst, NULL);
}

static void gvn_value_transform_date_time_to_date (const GValue * src, GValue * dst)
{
	GDateTime * date_time = g_value_get_boxed (src);

	if (date_time)
	{
		GDate * date = g_date_new_dmy (
			 g_date_time_get_day_of_month (date_time)
			,g_date_time_get_month (date_time)
			,g_date_time_get_year (date_time)
		);
		g_value_take_boxed (dst, date);
	}
	else
		g_value_take_boxed (dst, NULL);
}

static void gvn_value_transform_date_to_date_time (const GValue * src, GValue * dst)
{
	GDate * date = g_value_get_boxed (src);

	if (date)
	{
		GDateTime * date_time = g_date_time_new_utc (
			 g_date_get_year (date)
			,g_date_get_month (date)
			,g_date_get_day (date)
			,0
			,0
			,0.0
		);
		g_value_take_boxed (dst, date_time);
	}
	else
		g_value_take_boxed (dst, NULL);
}

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

/**
 * gvn_value_new:
 * @type: the type of new #GValue
 *
 * Creates a new #GValue
 *
 * Return value: a #GValue.
 **/
GValue * gvn_value_new (GType type)
{
	GValue * value = g_new0 (GValue, 1);
	return g_value_init (value, type);
}

/**
 * gvn_value_is_null:
 * @value: the value to be checked
 *
 * Checks if a @value is of type GVN_TYPE_NULL
 *
 * Return value: %TRUE if its NULL, %FALSE otherwise.
 **/

/**
 * gvn_value_set_null:
 * @value: the value to be nulled
 *
 * Checks if a @value is of type GVN_TYPE_NULL, if not, sets it.
 **/
void gvn_value_set_null (GValue * value)
{
	g_return_if_fail (G_IS_VALUE (value));

	if (!gvn_value_is_null (value))
	{
		g_value_unset (value);
		g_value_init (value, GVN_TYPE_NULL);
	}		
}

/**
 * gvn_value_new_from_string:
 * @value: an uninitilized #GValue
 * @string: a string to be converted
 * @length: length of @string or -1 if its null terminated
 * @type: the type to be converted
 * 
 * Creates a new @value from a string
 **/
void gvn_value_new_from_string (GValue * value, GType type, const gchar * string, gsize len)
{
	g_return_if_fail (string != NULL);

	g_value_init (value, type);
	gvn_value_new_from_string_real (value, type, string, len);
}

/**
 * gvn_value_new_valist:
 * @value: an uninitilized #GValue
 * @type: the type of value
 * @content: a pointer to the value content
 *
 * Initializes the @value with the specified type and content.
 **/
void gvn_value_new_with_content (GValue * value, GType type, gpointer content)
{
	g_value_init (value, type);

	switch (type)
	{
		case G_TYPE_BOOLEAN:
			g_value_set_boolean (value, *((gboolean *) content));
			break;
		case G_TYPE_CHAR:
			g_value_set_schar (value, *((gint8 *) content));
			break;
		case G_TYPE_INT:
			g_value_set_int (value, *((gint *) content));
			break;
		case G_TYPE_UINT:
			g_value_set_uint (value, *((guint *) content));
			break;
		case G_TYPE_LONG:
			g_value_set_long (value, *((glong *) content));
			break;
		case G_TYPE_ULONG:
			g_value_set_ulong (value, *((gulong *) content));
			break;
		case G_TYPE_FLOAT:
			g_value_set_float (value, *((gfloat *) content));
			break;
		case G_TYPE_DOUBLE:
			g_value_set_double (value, *((gdouble *) content));
			break;
		case G_TYPE_STRING:
			g_value_set_string (value, (gchar *) content);
			break;
		default:
		if (type == G_TYPE_DATE
		|| type == G_TYPE_BYTES)
			g_value_set_boxed (value, content);
		else if (G_TYPE_IS_OBJECT (type))
			g_value_set_object (value, content);
		else if (type != GVN_TYPE_NULL)
			g_return_if_reached ();
	}
}

/**
 * gvn_value_get_valist:
 * @value: the type of value
 * @va: a string to be converted
 * 
 * Sets the value of the initilized @value with the value of next
 * element in @va
 **/
void gvn_value_get_valist (const GValue * value, va_list va)
{
	GType type = G_VALUE_TYPE (value);

	switch (type)
	{
		case G_TYPE_BOOLEAN:
			*va_arg (va, gboolean*) = g_value_get_boolean (value);
			break;
		case G_TYPE_CHAR:
			*va_arg (va, gint8*) = g_value_get_schar (value);
			break;
		case G_TYPE_INT:
			*va_arg (va, gint*) = g_value_get_int (value);
			break;
		case G_TYPE_UINT:
			*va_arg (va, guint*) = g_value_get_uint (value);
			break;
		case G_TYPE_LONG:
			*va_arg (va, glong*) = g_value_get_long (value);
			break;
		case G_TYPE_ULONG:
			*va_arg (va, gulong*) = g_value_get_ulong (value);
			break;
		case G_TYPE_FLOAT:
			*va_arg (va, gfloat*) = g_value_get_float (value);
			break;
		case G_TYPE_DOUBLE:
			*va_arg (va, gdouble*) = g_value_get_double (value);
			break;
		case G_TYPE_STRING:
			*va_arg (va, gchar**) = g_value_dup_string (value);
			break;
		default:
		if (type == G_TYPE_DATE
		|| type == G_TYPE_BYTES)
			*va_arg (va, gpointer*) = g_value_dup_boxed (value);
		else if (G_TYPE_IS_OBJECT (type))
			*va_arg (va, gpointer*) = g_value_dup_object (value);
		else if (type != GVN_TYPE_NULL)
			g_return_if_reached ();
	}
}

/**
 * gvn_value_compare:
 * @a: a #GValue to be compared
 * @b: a #GValue to be compared
 * 
 * Does the same as g_value_compare0(), but returns a %gboolean.
 *
 * Return value: %TRUE if two values are equal, %FALSE otherwise.
 **/

/**
 * gvn_value_compare0:
 * @a: #GValue to be compared
 * @b: #GValue to be compared
 * 
 * Compares a and b. The two values ​​must be of the same type or GVN_TYPE_NULL.
 *
 * Return value: -1, 0 or 1, if @a is <, == or > than @b.
 **/
gint gvn_value_compare0 (const GValue * a, const GValue * b)
{
	GType a_type = G_VALUE_TYPE (a);
	gboolean a_is_val = G_IS_VALUE (a);
	gboolean b_is_val = G_IS_VALUE (b);
	
	if (!(a_is_val && b_is_val))
	{
		if (a_is_val)
			return 1;
		if (b_is_val)
			return -1;
	}
	else if (a_type == G_VALUE_TYPE (b))
	{
		switch (a_type)
		{
			case G_TYPE_FLOAT:
			{
				gfloat aux = g_value_get_float (a) - g_value_get_float (b);
				return (aux > 0.0) ? 1 : (aux < 0.0) ? -1 : 0;
			}
			case G_TYPE_DOUBLE:
			{
				gdouble aux = g_value_get_double (a) - g_value_get_double (b);
				return (aux > 0.0) ? 1 : (aux < 0.0) ? -1 : 0;
			}
			case G_TYPE_INT:
				return g_value_get_int (a) - g_value_get_int (b);
			case G_TYPE_UINT:
				return (gint) (g_value_get_uint (a) - g_value_get_uint (b));
			case G_TYPE_LONG:
				return (gint) (g_value_get_long (a) - g_value_get_long (b));
			case G_TYPE_ULONG:
				return (gint) (g_value_get_ulong (a) - g_value_get_ulong (b));
			case G_TYPE_BOOLEAN:
				return (gint) (g_value_get_boolean (a) - g_value_get_boolean (b));
			case G_TYPE_CHAR:
				return (gint) (g_value_get_schar (a) - g_value_get_schar (b));
			case G_TYPE_STRING:
				return g_strcmp0 (g_value_get_string (a), g_value_get_string (b));
			default:
			if (a_type == GVN_TYPE_NULL)
				return 0;
			if (G_TYPE_FUNDAMENTAL (a_type) == G_TYPE_BOXED)
			{
				gpointer a_boxed = g_value_get_boxed (a);
				gpointer b_boxed = g_value_get_boxed (b);
			
				if (!a_boxed)
					return (gint) (a_boxed - b_boxed);
				if (!b_boxed)
					return (gint) (a_boxed - b_boxed);
				if (a_type == G_TYPE_DATE)
					return g_date_compare (a_boxed, b_boxed);
				if (a_type == G_TYPE_DATE_TIME)
					return g_date_time_compare (a_boxed, b_boxed);
				else if (a_type == G_TYPE_BYTES)
					return (gint) (a_boxed - b_boxed);
			}

			g_warning (_("Attempting to compare invalid types: %s\n"),
				g_type_name (a_type));
		}
	}
	else if (gvn_value_is_null (a))
		return -1;
	else if (gvn_value_is_null (b))
		return 1;

	return 1;
}

/**
 * gvn_value_copy:
 * @src: source #GValue
 * @dst: destination #GValue
 * 
 * Copies the value of @src into @dst. This function also works with #GvnNull
 * values.
 **/
void gvn_value_copy (const GValue * src, GValue * dst)
{
	g_return_if_fail (G_IS_VALUE (src));
	g_return_if_fail (G_IS_VALUE (dst));

	if (G_VALUE_TYPE (src) != G_VALUE_TYPE (dst))
	{
		g_value_unset (dst);
		g_value_init (dst, G_VALUE_TYPE (src));
	}
	
	g_value_copy (src, dst);
}

/**
 * gvn_value_ccopy:
 * @src: source #GValue
 * @dst: destination #GValue
 * 
 * Compares @src with @dst, if they are different copies the value of @src into
 * @dst. This function also works with #GvnNull values.
 * 
 * Return value: %TRUE if they are copied, %FALSE if they are equals.
 **/
gboolean gvn_value_ccopy (const GValue * src, GValue * dst)
{
	if (gvn_value_compare (src, dst))
		return FALSE;

	gvn_value_copy (src, dst);
	return TRUE;
}	

/**
 * gvn_value_to_format_string:
 **/
void gvn_value_to_format_string (const GValue * src, guint digits, GValue * dst)
{
	GType type = G_VALUE_TYPE (src);

	g_value_init (dst, G_TYPE_STRING);
		
	switch (type)
	{
		case G_TYPE_FLOAT:
		case G_TYPE_DOUBLE:
		{
			gdouble dec;
			gchar * format;
			gchar buffer [G_ASCII_DTOSTR_BUF_SIZE];
		
			if (type == G_TYPE_FLOAT)
				dec = (gdouble) g_value_get_float (src);
			else
				dec = g_value_get_double (src);
				
			format = g_strdup_printf ("%%.%df", digits);
			g_ascii_formatd (buffer, G_ASCII_DTOSTR_BUF_SIZE, format, dec);
			g_value_set_string (dst, buffer);
			g_free (format);
			break;
		}
		default:
		if (!gvn_value_is_null (src))
			g_value_transform (src, dst);
		else
			g_value_set_string (dst, "");
	}
}

/**
 * gvn_type_init:
 *
 * Initializes library types, also calls g_type_init().
 **/
void gvn_type_init ()
{
	gint n;

	GType types[] = {
		 G_TYPE_BOOLEAN
		,G_TYPE_CHAR
		,G_TYPE_INT
		,G_TYPE_UINT
		,G_TYPE_LONG
		,G_TYPE_ULONG
		,G_TYPE_FLOAT
		,G_TYPE_DOUBLE
		,G_TYPE_STRING
		,G_TYPE_DATE
		,G_TYPE_DATE_TIME
		,G_TYPE_BYTES
		,G_TYPE_NONE
	};

	for (n = 0; types[n] != G_TYPE_NONE; n++)
	{
		g_value_register_transform_func (types[n], GVN_TYPE_NULL, 
			gvn_value_transform_any_to_null
		);
		g_value_register_transform_func (GVN_TYPE_NULL, types[n],
			gvn_value_transform_null_to_any
		);
		g_value_register_transform_func (G_TYPE_STRING, types[n],
			gvn_value_transform_string_to_any
		);
	}

	g_value_register_transform_func (G_TYPE_DATE, G_TYPE_STRING,
		gvn_value_transform_date_to_string
	);
	g_value_register_transform_func (G_TYPE_DATE_TIME, G_TYPE_STRING,
		gvn_value_transform_date_time_to_string
	);
	g_value_register_transform_func (G_TYPE_DATE_TIME, G_TYPE_DATE,
		gvn_value_transform_date_time_to_date
	);
	g_value_register_transform_func (G_TYPE_DATE, G_TYPE_DATE_TIME,
		gvn_value_transform_date_to_date_time
	);
}