
var VnObject = require ('./object');
var simpleEquals = require ('./value').simpleEquals;

var Klass = new Class ();
module.exports = Klass;

var Status =
{
	 CLEAN    : 1
	,LOADING  : 2
	,READY    : 3
	,ERROR    : 4
};

var SortWay =
{
	 ASC   : 1
	,DESC  : 2 
};

Klass.extend
({
	 Status: Status
	,SortWay: SortWay
});

Klass.implement
({
	Extends: VnObject
	,Tag: 'vn-model'
	,Properties:
	{
		/**
		 * The model internal data.
		 */
		data:
		{
			type: Array
			,get: function ()
			{
				return this._data;
			}
			,set: function (x)
			{
				this._setData (x);
			}
		},
		/**
		 * The number of rows in the model.
		 */
		numRows:
		{
			type: Number
			,get: function ()
			{
				if (this._data)
					return this._data.length;

				return 0;
			}
		},
		/**
		 * The current status of the model.
		 */
		status:
		{
			type: Number
			,get: function ()
			{
				return this._status;
			}
		},
		/**
		 * Checks if the model data is ready.
		 */
		ready:
		{
			type: Boolean
			,get: function ()
			{
				return this._status === Status.READY;
			}
		}
	}

	,_data: null
	,_status: Status.CLEAN

	,_requestedSortName: null
	,_sortColumn: null
	,_sortWay: null

	,_requestedIndexes: {}
	,_indexes: []

	//+++++++++++++++++++++++++++++ Data hadling
	
	,_setData: function (data)
	{
		this._data = data;
		this._refreshRowIndexes (0);

		if (this._requestedSortName != null)
			this._realSort (this._requestedSortName, this._sortWay);
		for (column in this._requestedIndexes)
			this._buildIndex (column);

		this._setStatus (Status.READY);
	}

	/**
	 * Checks if the row exists.
	 *
	 * @param {Integer} rowIndex The row index
	 * @return {Boolean} %true if row exists, %false otherwise
	 */
	,checkRowExists: function (rowIndex)
	{
		return this._data != null
			&& rowIndex >= 0
			&& rowIndex < this._data.length;
	}

	/**
	 * Gets a value from the model.
	 *
	 * @param {Number} rowIndex The row index
	 * @param {Number} columnName The column name
	 * @return {*} The value
	 */
	,get: function (rowIndex, columnName)
	{
		if (!this.checkRowExists (rowIndex))
			return undefined;
		
		return this._data[rowIndex][columnName];
	}

	/**
	 * Updates a value on the model.
	 *
	 * @param {Number} rowIndex The row index
	 * @param {Number} columnName The column name
	 * @param {*} value The new value
	 */
	,set: function (rowIndex, columnName, value)
	{
		if (!this.checkRowExists (rowIndex))
			return;

		this.emit ('row-updated-before', rowIndex);
		this._data[rowIndex][columnName] = value;
		this.emit ('row-updated', rowIndex, [columnName]);
	}

	/**
	 * Gets a row as an object using the column index.
	 *
	 * @param {Number} rowIndex The row index
	 * @return {Object} The row as an object
	 */
	,getObject: function (rowIndex)
	{
		return this.checkRowExists (rowIndex) ?
			this._data[rowIndex] : null;
	}
	
	,_setStatus: function (status)
	{
		this._status = status;
		this.emit ('status-changed', status);
		this.emit ('status-changed-after', status);
	}
	
	/**
	* Inserts a new row on the model.
	*
	* @return The index of the inserted row
	*/
   ,insertRow: function (newRow)
   {
		var rowIndex = this._data.push (newRow) - 1;
		newRow.index = rowIndex;
		this.emit ('row-inserted', rowIndex);
		return rowIndex;
   }

	/**
	 * Deletes a row from the model. 
	 *
	 * @param {Number} rowIndex The row index
	 */
	,deleteRow: function (rowIndex)
	{
		if (!this.checkRowExists (rowIndex))
			return;

		this.emit ('row-deleted-before', rowIndex);
		this._data.splice (rowIndex, 1);
		this.emit ('row-deleted', rowIndex);
		this._refreshRowIndexes (rowIndex);
	}

	//+++++++++++++++++++++++++++++ Sorting

	/**
	 * Orders the model by the specified column name.
	 *
	 * @param {String} columnName The column name
	 * @param {SortWay} way The sort way
	 */
	,sort: function (columnName, way)
	{
		this._requestedSortName = columnName;
		this._sort (columnName, way);
	}
	 
	,_sort: function (columnName, way)
	{
		var status = this._status;
		this._setStatus (Status.LOADING);
		this._realSort (columnName, way);
		this._setStatus (status);
	}
	
	,_realSort: function (columnName, way)
	{
		if (!this._data)
			return;

		if (columnName !== this._sortColumn)
		{
			if (way === SortWay.DESC)
				var sortFunction = this.sortFunctionDesc;
			else
				var sortFunction = this.sortFunctionAsc;
	
			this._data.sort (sortFunction.bind (this, columnName));
		}
		else if (way !== this._sortWay)
			this._data.reverse ();

		this._sortColumn = columnName;
		this._sortWay = way;

		this._refreshRowIndexes (0);
	}

	,_refreshRowIndexes: function (start)
	{
		var data = this._data;

		for (var i = start; i < data.length; i++)
			data[i].index = i;
	}
	
	/*
	 * Function used to sort the model ascending.
	 */
	,sortFunctionAsc: function (column, a, b)
	{
		if (a[column] < b[column])
			return -1;
		else if (a[column] > b[column])
			return 1;
			
		return 0;
	}
	
	/*
	 * Function used to sort the model descending.
	 */
	,sortFunctionDesc: function (column, a, b)
	{
		if (a[column] > b[column])
			return -1;
		else if (a[column] < b[column])
			return 1;
			
		return 0;
	}
	
	//+++++++++++++++++++++++++++++ Searching

	/**
	 * Builds an internal hash index for the specified column, this speeds
	 * significantly searches on that column, specially when model has a lot of
	 * rows.
	 *
	 * FIXME: Not fully implemented.
	 *
	 * @param {String} column The column name
	 */
	,indexColumn: function (column)
	{
		this._requestedIndexes[column] = true;

		if (this._status === Status.READY)
			this._buildIndex (column);
	}

	,getHashValue: function (value)
	{
		if (value instanceof Date)
			return value.getTime();
		else
			return value;
	}

	,_buildIndex: function (columnName)
	{
		if (this.columnMap[columnName] === undefined)
			return;

		var data = this._data;
		var values = {};
		var nulls = [];

		for (var i = 0; i < data.length; i++)
		{
			var value = data[i][columnName];

			if (value == null)
			{
				nulls.push (data[i]);
				continue;
			}

			index[this.getHashValue (value)] = data[i];
		}

		this._indexes[columnName] = {
			values: values,
			index: index
		};
	}

	/**
	 * Searchs a value on the model and returns the row index of the first
	 * ocurrence.
	 *
	 * @param {Number} columnName The column name
	 * @param {Object} value The value to search
	 * @return {Number} The column index
	 */
	,search: function (columnName, value)
	{
		var data = this._data;

		if (data == null)
			return -1;

		// Searchs the value using an internal index.

		var index = this._indexes[columnName];
		
		if (index)
		{
			if (value == null)
			{
				if (index.nulls[0] !== undefined)
					return index.nulls[0].index;
			}
			else
			{
				var row = index.values[this.getHashValue (value)];

				if (rowIndex !== undefined)
					return row.index;
			}

			return -1;
		}

		// Searchs the value using a loop.

		for (var i = 0; i < data.length; i++)
		if (simpleEquals (data[i][columnName], value))
			return i;

		return -1;
	}
	
	//+++++++++++++++++++++++++++++ Virtual

	,refresh: function ()
	{
		this._setStatus (Status.READY);
	}
});
