/***************************************************************************

  gbx_c_collection.c

  (c) 2000-2017 Benoît Minisini <benoit.minisini@gambas-basic.org>

  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 2, 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, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  MA 02110-1301, USA.

***************************************************************************/

#define __GBX_C_COLLECTION_C

#include "gbx_info.h"

#ifndef GBX_INFO

#include "gb_common.h"
#include "gb_error.h"

#include "gbx_variant.h"
#include "gbx_exec.h"
#include "gambas.h"
#include "gbx_api.h"
#include "gbx_class.h"
#include "gbx_object.h"
#include "gbx_compare.h"
#include "gbx_c_array.h"
#include "gbx_c_collection.h"

#define check_not_read_only(_object) ((((CCOLLECTION *)_object)->read_only) ? CARRAY_static_array(), TRUE : FALSE)

static void clear(CCOLLECTION *col)
{
	VARIANT *value;
	HASH_ENUM iter;

	CLEAR(&iter);
	col->locked = TRUE;

	for(;;)
	{
		value = HASH_TABLE_next(col->hash_table, &iter, FALSE);
		if (value == NULL)
			break;

		VARIANT_free(value);
		value->type = T_NULL;
	}

	HASH_TABLE_delete(&col->hash_table);
	col->locked = FALSE;
}

#define get_key(_col, _key, _len, _set_last) HASH_TABLE_lookup((_col)->hash_table, (_key), (_len), _set_last)

static inline void *add_key(CCOLLECTION *col, const char *key, int len)
{
	if (len == 0)
	{
		GB_Error((char *)E_VKEY);
		return NULL;
	}
	
	if (check_not_read_only(col))
		return NULL;
	
	return HASH_TABLE_insert(col->hash_table, key, len);
}

static void remove_key(CCOLLECTION *col, const char *key, int len)
{
	void *value;
	HASH_NODE *last;
	HASH_NODE *save;
	void *save_enum;

	if (len == 0)
	{
		GB_Error((char *)E_VKEY);
		return;
	}
	
	if (check_not_read_only(col))
		return;

	save = col->hash_table->last;
	
	value = HASH_TABLE_lookup(col->hash_table, key, len, TRUE);
	
	last = col->hash_table->last;
	col->hash_table->last = save;
	
	if (value == NULL)
		return;

	if (last)
	{
		save_enum = GB_BeginEnum(col);
		while (!GB_NextEnum())
		{
			HASH_ENUM *iter = (HASH_ENUM *)GB_GetEnum();
			if (iter->next == last)
				iter->next = iter->next->snext;
		}
		GB_EndEnum(save_enum);
	}

	VARIANT_free((VARIANT *)value);

	if (!col->locked)
		HASH_TABLE_remove(col->hash_table, key, len);
	else // Prevent the freed variant to be freed twice if the collection is locked (i.e. being destroyed)
		((VARIANT *)value)->type = T_NULL;
}


bool CCOLLECTION_equals(CCOLLECTION *a, CCOLLECTION *b)
{
	HASH_ENUM iter;
	char *key;
	int len;
	VARIANT *value;
	VARIANT *other_value;
	
	if (CCOLLECTION_get_count(a) != CCOLLECTION_get_count(b))
		return FALSE;
	
	if (!COMPARE_equals_variant((VARIANT *)&a->default_value, (VARIANT *)&b->default_value))
		return FALSE;
	
	CLEAR(&iter);

	for(;;)
	{
		value = HASH_TABLE_next(a->hash_table, &iter, TRUE);
		if (!value)
			break;

		HASH_TABLE_get_last_key(a->hash_table, &key, &len);
		
		other_value = HASH_TABLE_lookup(b->hash_table, key, len, FALSE);
		if (!other_value)
			return FALSE;
		
		if (!COMPARE_equals_variant(value, other_value))
			return FALSE;
	}
	
	return TRUE;
}


//-------------------------------------------------------------------------

BEGIN_METHOD(Collection_new, GB_INTEGER mode)

	int mode = MISSING(mode) ? 0 : VARG(mode);

	HASH_TABLE_create(&THIS->hash_table, TYPE_sizeof(T_VARIANT), mode);
	THIS->last = NULL;
	THIS->default_value.type = GB_T_NULL;

END_METHOD


BEGIN_METHOD_VOID(Collection_free)

	clear(THIS);
	GB_StoreVariant(NULL, POINTER(&THIS->default_value));

END_METHOD


BEGIN_PROPERTY(Collection_Count)

	GB_ReturnInt(CCOLLECTION_get_count(THIS));

END_PROPERTY


BEGIN_PROPERTY(Collection_Empty)

	GB_ReturnBoolean(CCOLLECTION_get_count(THIS) == 0);

END_PROPERTY


BEGIN_PROPERTY(Collection_Key)

	char *key;
	int len;

	if (READ_PROPERTY)
	{
		if (HASH_TABLE_get_last_key(THIS->hash_table, &key, &len))
			GB_ReturnVoidString();
		else
			GB_ReturnNewString(key, len);
	}
	else
		HASH_TABLE_set_last_key(THIS->hash_table, PSTRING(), PLENGTH());

END_PROPERTY


BEGIN_METHOD(Collection_Add, GB_VARIANT value; GB_STRING key)

	void *data;

	data = add_key(THIS, STRING(key), LENGTH(key));
	if (!data)
		return;

	GB_StoreVariant(ARG(value), data);

END_METHOD


BEGIN_METHOD(Collection_Exist, GB_STRING key)

	GB_ReturnBoolean(get_key(THIS, STRING(key), LENGTH(key), FALSE) != NULL);

END_METHOD


BEGIN_METHOD(Collection_Remove, GB_STRING key)

	remove_key(THIS, STRING(key), LENGTH(key));

END_METHOD


BEGIN_METHOD_VOID(Collection_Clear)

	int mode = THIS->hash_table->mode;

	if (check_not_read_only(THIS))
		return;

	/* Stops all iterators */
	GB_StopAllEnum(THIS);

	clear(THIS);
	HASH_TABLE_create(&THIS->hash_table, TYPE_sizeof(T_VARIANT), mode);

END_METHOD


BEGIN_METHOD_VOID(Collection_next)

	void *value;
	HASH_TABLE *hash_table = OBJECT(CCOLLECTION)->hash_table;
	HASH_ENUM *iter = (HASH_ENUM *)GB_GetEnum();

	value = HASH_TABLE_next(hash_table, iter, TRUE);

	if (value == NULL)
		GB_StopEnum();
	else
		GB_ReturnVariant(value);

END_METHOD


BEGIN_METHOD(Collection_get, GB_STRING key)

	void *value = get_key(THIS, STRING(key), LENGTH(key), TRUE);
	if (!value)
	{
		if (THIS->has_default)
			value = (void *)&THIS->default_value;
	}

	GB_ReturnVariant(value);

END_METHOD


BEGIN_METHOD(Collection_put, GB_VARIANT value; GB_STRING key)

	void *data;

	if (VARIANT_is_null((VARIANT *)&VARG(value)))
		remove_key(THIS, STRING(key), LENGTH(key));
	else
	{
		data = add_key(THIS, STRING(key), LENGTH(key));
		if (!data)
			return;
		GB_StoreVariant(ARG(value), data);
	}

END_METHOD


BEGIN_METHOD_VOID(Collection_Copy)

	GB_COLLECTION col;
	GB_COLLECTION_ITER iter;;
	GB_VARIANT value;
	char *key;
	int len;

	GB_CollectionNew(&col, THIS->mode);

	CLEAR(&iter);

	for(;;)
	{
		if (GB_CollectionEnum(THIS, &iter, &value, &key, &len))
			break;

		GB_CollectionSet(col, key, len, &value);
	}

	GB_ReturnObject(col);

END_METHOD


BEGIN_PROPERTY(Collection_Keys)

	GB_ARRAY keys;
	GB_COLLECTION_ITER iter;;
	char *key;
	int len;
	int i;

	GB_ArrayNew(&keys, GB_T_STRING, CCOLLECTION_get_count(THIS));

	CLEAR(&iter);

	for(i = 0;; i++)
	{
		if (GB_CollectionEnum(THIS, &iter, NULL, &key, &len))
			break;

		*((char **)GB_ArrayGet(keys, i)) = STRING_new(key, len);
	}

	GB_ReturnObject(keys);

END_METHOD


BEGIN_PROPERTY(Collection_Default)

	if (READ_PROPERTY)
		GB_ReturnVariant(&THIS->default_value);
	else
	{
		if (check_not_read_only(THIS))
			return;
		GB_StoreVariant(PROP(GB_VARIANT), POINTER(&THIS->default_value));
		THIS->has_default = THIS->default_value.type != GB_T_NULL;
	}

END_PROPERTY


static void return_node_key(HASH_TABLE *hash_table, HASH_NODE *node)
{
	char *key;
	int len;
	
	HASH_TABLE_get_key(hash_table, node, &key, &len);
	if (len)
		GB_ReturnNewString(key, len);
	else
		GB_ReturnNull();

}

BEGIN_PROPERTY(Collection_First)

	return_node_key(THIS->hash_table, THIS->hash_table->sfirst);

END_PROPERTY


BEGIN_PROPERTY(Collection_Last)

	return_node_key(THIS->hash_table, THIS->hash_table->slast);

END_PROPERTY


BEGIN_PROPERTY(Collection_ReadOnly)

	if (READ_PROPERTY)
		GB_ReturnBoolean(THIS->read_only);
	else
	{
		bool read_only = VPROP(GB_BOOLEAN);
		if (THIS->read_only == !!read_only)
			return;
		
		if (THIS->read_only)
			CARRAY_static_array();
		else
			THIS->read_only = TRUE;
	}

END_PROPERTY


BEGIN_METHOD(Collection_Equals, GB_OBJECT collection)

	CCOLLECTION *other = (CCOLLECTION *)VARG(collection);
	
	if (GB_CheckObject(other))
		return;
	
	GB_ReturnBoolean(CCOLLECTION_equals(THIS, other));
	
END_METHOD

static CCOLLECTION *_insert_col;

static void insert_one(const char *key, int len, GB_VALUE *value)
{
	void *data;

	data = add_key(_insert_col, key, len);
	if (!data)
		return;

	GB_StoreVariant(&value->_variant, data);
}

BEGIN_METHOD(Collection_Insert, GB_OBJECT collection)

	GB_COLLECTION other = (GB_COLLECTION)VARG(collection);
	
	if (GB_CheckObject(other))
		return;
	
	_insert_col = THIS;
	GB_CollectionBrowse(other, insert_one);

END_METHOD

#endif


GB_DESC NATIVE_Collection[] =
{
	GB_DECLARE("Collection", sizeof(CCOLLECTION)),

	GB_METHOD("_new", NULL, Collection_new, "[(Mode)i]"),
	GB_METHOD("_free", NULL, Collection_free, NULL),

	GB_PROPERTY_READ("Count", "i", Collection_Count),
	GB_PROPERTY_READ("Empty", "b", Collection_Empty),
	GB_PROPERTY_READ("Length", "i", Collection_Count),
	GB_PROPERTY_READ("First", "s", Collection_First),
	GB_PROPERTY_READ("Last", "s", Collection_Last),
	GB_PROPERTY("Key", "s", Collection_Key),
	GB_PROPERTY("Default", "v", Collection_Default),
	GB_PROPERTY_READ("Keys", "String[]", Collection_Keys),
	GB_PROPERTY("ReadOnly", "b", Collection_ReadOnly),

	GB_METHOD("Add", NULL, Collection_Add, "(Value)v(Key)s"),
	GB_METHOD("Exist", "b", Collection_Exist, "(Key)s"),
	GB_METHOD("Remove", NULL, Collection_Remove, "(Key)s"),
	GB_METHOD("Clear", NULL, Collection_Clear, NULL),
	/*GB_METHOD("_first", NULL, collection_first, NULL),*/
	GB_METHOD("_next", "v", Collection_next, NULL),
	GB_METHOD("_get", "v", Collection_get, "(Key)s"),
	GB_METHOD("_put", NULL, Collection_put, "(Value)v(Key)s"),
	GB_METHOD("Copy", "Collection", Collection_Copy, NULL),
	GB_METHOD("Equals", "b", Collection_Equals, "(Collection)Collection;"),
	GB_METHOD("Insert", NULL, Collection_Insert, "(Collection)Collection;"),

	GB_END_DECLARE
};

#ifndef GBX_INFO

void GB_CollectionNew(GB_COLLECTION *col, int mode)
{
	VALUE param;

	param._integer.type = GB_T_INTEGER;
	param._integer.value = mode;

	*col = OBJECT_create_native(CLASS_Collection, &param);
}

int GB_CollectionCount(GB_COLLECTION col)
{
	return HASH_TABLE_size(((CCOLLECTION *)col)->hash_table);
}

bool GB_CollectionSet(GB_COLLECTION col, const char *key, int len, GB_VARIANT *value)
{
	VARIANT *data;

	if (VARIANT_is_null((VARIANT *)&value->value))
		remove_key((CCOLLECTION *)col, key, len);
	else
	{
		data = (VARIANT *)add_key((CCOLLECTION *)col, key, len);
		if (!data)
			return TRUE;
		VALUE_write_variant((VALUE *)value, data);
	}
	return FALSE;
}

bool GB_CollectionGet(GB_COLLECTION col, const char *key, int len, GB_VARIANT *value)
{
	CCOLLECTION *_object = (CCOLLECTION *)col;
	VARIANT *var;
	bool ret;

	value->type = T_VARIANT;

	var = (VARIANT *)get_key(THIS, key, len, TRUE);

	if (!var)
	{
		if (!THIS->has_default)
		{
			value->value.type = GB_T_NULL;
			return TRUE;
		}

		var = (VARIANT *)&THIS->default_value;
		ret = TRUE;
	}
	else
	{
		ret = FALSE;
	}

	value->value.type = var->type;
	value->value.value.data = var->value.data;
	return ret;
}

bool GB_CollectionEnum(GB_COLLECTION col, GB_COLLECTION_ITER *iter, GB_VARIANT *value, char **key, int *len)
{
	VARIANT *val;
	HASH_TABLE *hash_table = ((CCOLLECTION *)col)->hash_table;

	if (!key)
	{
		CLEAR(iter);
		return FALSE;
	}

	val = HASH_TABLE_next(hash_table, (HASH_ENUM *)iter, TRUE);
	if (!val)
		return TRUE;

	if (value)
	{
		value->type = T_VARIANT;
		value->value.type = val->type;
		value->value.value.data = val->value.data;
	}

	HASH_TABLE_get_last_key(hash_table, key, len);
	return FALSE;
}

void GB_CollectionBrowse(GB_COLLECTION col, GB_COLLECTION_BROWSE_CALLBACK func)
{
	GB_COLLECTION_ITER iter;
	GB_VALUE value;
	char *key;
	int len;
	
	GB_CollectionEnum(col, &iter, NULL, NULL, NULL);
	for(;;)
	{
		if (GB_CollectionEnum(col, &iter, (GB_VARIANT *)&value, &key, &len))
			break;
		
		BORROW((VALUE *)&value);
		(*func)(key, len, &value);
		RELEASE((VALUE *)&value);
	}
}

#endif
