var Connection = require('./connection');

/**
 * Class to handle the Database select results. Also allows
 * updates, insertions and deletions on tables where the primary key is 
 * selected.
 *
 * Note that table and column names must be unique in the selection query,
 * otherwise updates are not allowed on that table/column. If two tables or
 * columns have the same name, an alias should be used to make it updatable.
 */
var Model = new Class();
module.exports = Model;

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

var Mode = {
	 ON_CHANGE : 1
	,ON_DEMAND : 2
};

var Operation = {
	 INSERT : 1 << 1
	,UPDATE : 1 << 2
	,DELETE : 1 << 3
};

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

Model.extend({
	 Status: Status
	,Mode: Mode
	,Operation: Operation
	,SortWay: SortWay
});

Model.implement({
	Extends: Vn.Object
	,Tag: 'db-model'
	,Properties: {
		/**
		 * The connection used to execute the statement.
		 */
		conn: {
			type: Connection
			,set: function(x) {
				this._conn = x;
				this._autoLoad();
			}
			,get: function() {
				return this._conn;
			}
		},
		/**
		 * The result index.
		 */
		resultIndex: {
			type: Number
			,set: function(x) {
				this._resultIndex = x;
			}
			,get: function() {
				return this._resultIndex;
			}
		},
		/**
		 * The lot used to execute the statement.
		 */
		lot: {
			type: Vn.LotIface
			,set: function(x) {
				this.link({_lot: x}, {'change': this._onLotChange});
				this._onLotChange();
			}
			,get: function() {
				return this._lot;
			}
		},
		/**
		 * The model select statement.
		 */
		stmt: {
			type: Sql.Stmt
			,set: function(x) {
				this._stmt = x;
				this._autoLoad();
			}
			,get: function() {
				return this._stmt;
			}
		},
		/**
		 * The model query.
		 */
		query: {
			type: String
			,set: function(x) {
				this.stmt = new Sql.String({query: x});
			}
			,get: function() {
				if (this._stmt)
					return this._stmt.render(null);
				else
					return null;
			}
		},
		/**
		 * The main table.
		 */
		mainTable: {
			type: String
			,set: function(x) {
				this._mainTable = null;
				this._requestedMainTable = x;
				this._refreshMainTable();
			}
			,get: function() {
				return this._mainTable;
			}
		},
		/**
		 * Determines if the model is updatable.
		 */
		updatable: {
			type: Boolean
			,set: function(x) {
				this._updatable = false;
				this._requestedUpdatable = x;
				this._refreshUpdatable();
			}
			,get: function() {
				return this._updatable;
			}
		},
		/**
		 * 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;
			}
		},
		/**
		 * Update mode.
		 */
		mode: {
			enumType: Mode
			,value: Mode.ON_CHANGE
		},
		/**
		 * Wether to execute the model query automatically.
		 */
		autoLoad: {
			type: Boolean
			,value: true
		}
	}

	,_conn: null
	,_resultIndex: 0
	,_lot: null
	,_stmt: null
	,_status: Status.CLEAN
	,data: null
	,tables: null
	,columns: null
	,columnMap: null
	,_updatable: false
	,_paramsChanged: true

	,_requestedSortIndex: -1
	,_requestedSortName: null
	,_sortColumn: -1
	,_sortWay: null

	,_requestedIndexes: {}
	,_indexes: []	

	,_requestedUpdatable: false
	,_operations: null
	,_operationsMap: null
	,_defaults: []
	,_requestedMainTable: null

	,initialize: function(props) {
		Vn.Object.prototype.initialize.call(this, props);
		this._cleanData();
		this._setStatus(Status.CLEAN);
	}

	,appendChild: function(child) {
		if (child.nodeType === Node.TEXT_NODE)
			this.query = child.textContent;
	}

	,loadXml: function(builder, node) {
		Vn.Object.prototype.loadXml.call(this, builder, node);

		var query = node.firstChild.nodeValue;
		
		if (query)
			this.query = query;
	}

	,_getHolders(stmt) {
		if (!stmt) return null;
		let holders = this._stmt.findHolders();
		if (!holders) return null;

		if (this._lot) {
			const params = this._lot.params;
			for (const holder of holders)
			if (params[holder] instanceof Sql.Object) {
				const paramHolders = params[holder].findHolders();
				if (paramHolders)
					holders = holders.concat(paramHolders);
			}
		}

		return holders;
	}

	,_getHolderValues: function() {
		let holders = this._getHolders(this._stmt);
		if (!holders) return null;

		const lotParams = this._lot ? this._lot.params : {};
		const params = {};
		for (const holder of holders)
		if (!(lotParams[holder] instanceof Sql.Object))
			params[holder] = lotParams[holder];

		return params;
	}

	,_getHolderParams: function() {
		let holders = this._getHolders(this._stmt);
		if (!holders) return null;

		const lotParams = this._lot ? this._lot.params : {};
		const params = {};
		for (const holder of holders)
			params[holder] = lotParams[holder];

		return params;
	}

	,_onLotChange: function() {
		const params = this._getHolderValues();
		this._paramsChanged = !Vn.Value.equals(params, this._lastParams);

		if (this.autoLoad)
			this.lazyRefresh();
	}

	,_autoLoad: function() {
		if (this.autoLoad)
			this.refresh();
		else
			this.clean();
	}

	,_isReady: function(params) {
		if (!this._stmt || !this._conn)
			return false;

		for (const param in params)
			if (params[param] === undefined)
				return false;

		return true;
	}

	,lazyRefresh: function() {
		if (this._paramsChanged)
			this.refresh();
	}

	/**
	 * Refresh the model data reexecuting the query on the database.
	 */
	,refresh: function() {
		const params = this._getHolderParams();

		if (this._isReady(params)) {
			this._setStatus(Status.LOADING);
			this._lastParams = this._getHolderValues();
			this._paramsChanged = false;
			this._conn.execStmt(this._stmt,
				this._selectDone.bind(this), params);
		} else
			this.clean();
	}

	,clean: function() {
		this._cleanData();
		this._setStatus(Status.CLEAN);
	}

	,_selectDone: function(resultSet) {
		var result;
		var dataResult;
		
		this._cleanData();

		try {
			for (var i = 0; result = resultSet.fetchResult(); i++)
			if (i == this._resultIndex)
				dataResult = result;

			if (!dataResult || typeof dataResult !== 'object')
				throw new Error('The provided statement doesn\'t return a result set');
		} catch (e) {
			this._setStatus(Status.ERROR);
			throw e;
		}

		this.data = dataResult.data;
		this.tables = dataResult.tables;
		this.columns = dataResult.columns;
		this.columnMap = dataResult.columnMap;
		this._repairColumns();
		this._refreshRowIndexes(0);
		this._refreshMainTable();

		for (column in this._requestedIndexes)
			this._buildIndex(column);

		var sortColumn = -1;

		if (this._requestedSortName)
			sortColumn = this.getColumnIndex(this._requestedSortName);
		else if (this._requestedSortIndex !== -1
		&& this.checkColExists(this._requestedSortIndex))
			sortColumn = this._requestedSortIndex;

		if (sortColumn !== -1)
			this._realSort(sortColumn, this._sortWay);
	
		this._setStatus(Status.READY);
	}
	
	,_refreshRowIndexes: function(start) {
		for (var i = start; i < this.data.length; i++)
			this.data[i].index = i;
		
		if (this._operationsMap) {
			this._operationsMap = {};
			
			for (var i = 0; i < this._operations.length; i++)
				this._operationsMap[i] = this._operations[i];
		}
	}

	,_cleanData: function() {
		this.data = null;
		this.tables = null;
		this.columns = null;
		this.columnMap = null;
		this._sortColumn = -1;
		this._indexes = [];
		this._resetOperations();
	}
	
	,_refreshUpdatable: function() {
		var oldValue = this._updatable;
		this._updatable = this._mainTable !== null && this._requestedUpdatable;
		
		if (oldValue != this._updatable)
			this.emit('updatable-changed');
	}
	
	,_refreshMainTable: function() {
		var newMainTable = null;
		var tables = this.tables;

		if (tables)
		for (var i = 0; i < tables.length; i++)
		if (tables[i].pks.length > 0)
		if (!this._requestedMainTable
		|| tables[i].name === this._requestedMainTable) {
			newMainTable = i;
			break;
		}

		this._mainTable = newMainTable;
		this._refreshUpdatable();
	}

	/**
	 * Sets the default value for inserted fields.
	 *
	 * @param {String} field The destination field name
	 * @param {String} table The destination table name
	 * @param {Sql.Expr} srcColumn The default value expression
	 */
	,setDefault: function(field, table, expr) {
		this._defaults.push({field, table, expr});
	}

	/**
	 * Sets the default value for inserted fields.
	 *
	 * @param {String} field The destination field name
	 * @param {String} table The destination table name
	 * @param {Object} value The default value
	 */
	,setDefaultFromValue: function(field, table, value) {
		this._defaults.push({field, table, value});
	}

	/**
	 * Sets the default value for inserted fields from another column in the
	 * model.
	 *
	 * @param {String} field The destination field name
	 * @param {String} table The destination table name
	 * @param {String} srcColumn The source column
	 */
	,setDefaultFromColumn: function(field, table, srcColumn) {
		this._defaults.push({field, table, srcColumn});
	}

	/**
	 * Checks if column index exists.
	 *
	 * @param {integer} column The column index
	 * @return {Boolean} %true if column exists, %false otherwise
	 */
	,checkColExists: function(column) {
		return this.columns
			&& column >= 0
			&& column < this.columns.length;
	}

	/**
	 * Checks if column name exists.
	 *
	 * @param {string} columnName The column name
	 * @return {Boolean} %true if column exists, %false otherwise
	 */
	,checkColName: function(columnName) {
		return this.columnMap
			&& this.columnMap[columnName] != null;
	}

	/**
	 * 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
			&& rowIndex >= 0
			&& rowIndex < this.data.length;
	}
	
	,_checkTableUpdatable: function(tableIndex) {
		var tableUpdatable = tableIndex !== null
			&& this.tables[tableIndex].pks.length > 0;

		if (!tableUpdatable && !tableIndex) {
			if (tableIndex)
				console.warn("Db.Model: Table %s is not updatable",
					this.tables[tableIndex].name);
			else
				console.warn("Db.Model: Model not updatable");
		}
				
		return tableUpdatable;
	}

	/**
	 * Get the index of the column from its name.
	 *
	 * @param {string} columnName The column name
	 * @return {number} The column index or -1 if column not exists
	 */
	,getColumnIndex: function(columnName) {
		if (this.checkColName(columnName))
			return this.columnMap[columnName].index;

		return -1;
	}

	/**
	 * Get the index of the column from its name.
	 *
	 * @param {number} columnIndex The column name
	 * @return {string} The column index or -1 if column not exists
	 */
	,getColumnName: function(columnIndex) {
		if (this.checkColExists(columnIndex))
			return this.columns[columnIndex].name;

		return null;
	}

	/**
	 * Gets the row as object.
	 *
	 * @param {number} rowIndex The row index
	 * @return {Object} The row
	 */
	,getObject: function(rowIndex) {
		if (!this.checkRowExists(rowIndex))
			return undefined;

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

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

		var tableIndex = this.columnMap[columnName].table;
			
		if (!this._checkTableUpdatable(tableIndex))
			return;

		var row = this.data[rowIndex];

		var op = this._createOperation(rowIndex);
		op.type |= Operation.UPDATE;

		if (!op.oldValues)
			op.oldValues = [];
		if (!op.tables)
			op.tables = {};
			
		var tableOp = op.tables[tableIndex];
		
		if (!tableOp) {
			tableOp = Operation.UPDATE;
			var pks = this.tables[tableIndex].pks;
			
			for (const pk of pks)
			if (!row[pk] && !op.oldValues[pk]) {
				tableOp = Operation.INSERT;
				break;
			}

			op.tables[tableIndex] = tableOp;
		}
		
		if (tableOp & Operation.UPDATE
		&& op.oldValues[columnName] === undefined)
			op.oldValues[columnName] = row[columnName];

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

		if (this.mode == Mode.ON_CHANGE
		&& !(op.type & Operation.INSERT))
			this.performOperations();
	}

	/**
	 * Gets a value from the model using the column index.
	 *
	 * @param {number} rowIndex The row index
	 * @param {number} columnIndex The column index
	 * @return {mixed} The value
	 */
	,getByIndex: function(rowIndex, columnIndex) {
		var columnName = this.getColumnName(columnIndex);
	
		if (columnName)
			return this.get(rowIndex, columnName);

		return undefined;
	}

	/**
	 * Updates a value on the model using the column index.
	 *
	 * @param {number} rowIndex The row index
	 * @param {number} columnIndex The column index
	 * @param {mixed} value The new value
	 */
	,setByIndex: function(rowIndex, columnIndex, value) {
		var columnName = this.getColumnName(columnIndex);
		
		if (columnName)
			this.set(rowIndex, columnName, value);
		else
			console.warn('Db.Model: Column %d doesn\'t exist', columnIndex);
	}

	/**
	 * Deletes a row from the model. 
	 *
	 * @param {number} rowIndex The row index
	 */
	,deleteRow: function(rowIndex) {
		if (!this.checkRowExists(rowIndex)
		|| !this._checkTableUpdatable(this._mainTable))
			return;
	
		var op = this._createOperation(rowIndex);
		op.type |= Operation.DELETE;

		if (!this._requestedMainTable) {
			this.emit('row-deleted-before', rowIndex);
			this.data.splice(rowIndex, 1);
			this.emit('row-deleted', rowIndex);
			this._refreshRowIndexes(rowIndex);
		} else {
			this.emit('row-updated-before', rowIndex);

			if (!op.oldValues)
				op.oldValues = [];

			var updatedCols = [];

			for (var i = 0; i < this.columns.length; i++)
			if (this.columns[i].table == this._mainTable) {
				const colName = this.columns[i].name;

				if (op.oldValues[colName] === undefined)
					op.oldValues[colName] = op.row[colName];

				op.row[colName] = null;
				updatedCols.push(i);
			}

			this.emit('row-updated', rowIndex, updatedCols);
		}
		
		if (this.mode === Mode.ON_CHANGE)
			this.performOperations();
	}

	/**
	 * Inserts a new row on the model.
	 *
	 * @return The index of the inserted row
	 */
	,insertRow: function() {
		if (!this._checkTableUpdatable(this._mainTable))
			return -1;

		var cols = this.columns;
		var newRow = {};
		
		for (var i = 0; i < cols.length; i++)
		if (cols[i].table === this._mainTable)
			newRow[cols[i].name] = cols[i].def;
		else
			newRow[cols[i].name] = null;
			
		var rowIndex = this.data.push(newRow) - 1;
		newRow.index = rowIndex;
			
		var op = this._createOperation(rowIndex);
		op.type |= Operation.INSERT;

		this.emit('row-inserted', rowIndex);
	
		return rowIndex;
	}
	
	/**
	 * Performs all model changes on the database.
	 */
	,performOperations: function() {
		var ops = this._operations;
	
		if (ops.length === 0) {
			this.emit('operations-done');
			return;
		}
		
		var stmts = new Sql.MultiStmt();
		
		var query = new Sql.String({query: 'START TRANSACTION'});
		stmts.push(query);

		for (var i = 0; i < ops.length; i++) {
			query = null;
			var op = ops[i];

			if (op.type & Operation.DELETE) {
				if (op.type & Operation.INSERT)
					continue;
		
				var where = this._createWhere(this._mainTable, op, true);

				if (where) {
					query = new Sql.Delete({where});
					query.addTarget(this._createTarget(this._mainTable));
				}
			} else if (op.type & (Operation.INSERT | Operation.UPDATE)) {
				query = new Sql.MultiStmt();

				for (var tableIndex in op.tables) {
					var stmt = this._createDmlQuery(op, parseInt(tableIndex));
					query.push(stmt);
				}
			}

			if (query) {
				stmts.push(query);
			} else {
				console.warn('Db.Model: %s', _('ErrorSavingChanges'));
				return;
			}
		}

		var query = new Sql.String({query: 'COMMIT'});
		stmts.push(query);
	
		this._conn.execStmt(stmts,
			this._onOperationsDone.bind(this, ops));

		this._resetOperations();
	}

	,_createDmlQuery: function(op, tableIndex) {
		var where = this._createWhere(tableIndex, op, false);

		if (!where)
			return null;

		var multiStmt = new Sql.MultiStmt();
		var target = this._createTarget(tableIndex);
		
		var select = new Sql.Select({where});
		select.addTarget(target);

		var row = op.row;
		var cols = this.columns;

		if (op.tables[tableIndex] & Operation.INSERT) {
			var dmlQuery = new Sql.Insert();
			var table = this.tables[tableIndex];

			for (const def of this._defaults)
			if (def.table === table.name) {
				if (def.value)
					dmlQuery.addSet(def.field, def.value);
				else if (def.expr)
					dmlQuery.addExpr(def.field, def.expr);
				else if (def.srcColumn)
					dmlQuery.addSet(def.field, row[def.srcColumn]);
			}

			for (const col of cols)
			if (col.table === tableIndex) {
				if (row[col.name] !== null)
					dmlQuery.addSet(col.orgname, row[col.name]);
				select.addField(col.orgname);
			}
		} else {
			var updateWhere = this._createWhere(tableIndex, op, true);
			
			if (!updateWhere)
				return null;
		
			var dmlQuery = new Sql.Update({where: updateWhere});
		
			for (const col of cols)
			if (col.table === tableIndex && op.oldValues[col.name] !== undefined) {
				var fieldName = col.orgname;
				dmlQuery.addSet(fieldName, row[col.name]);
				select.addField(fieldName);
			}
		}

		dmlQuery.addTarget(target);

		multiStmt.push(dmlQuery);
		multiStmt.push(select);
		return multiStmt;
	}
	
	,_onOperationsDone: function(ops, resultSet) {
		var error = resultSet.getError();
	
		if (error) {
			this._operations = this._operations.concat(ops);
			
			for (var i = 0; i < ops.length; i++)
				this._operationsMap[ops[i].row.index] = ops[i];
		
			throw error;
		}

		resultSet.fetchResult();
		var isOperation = false;
	
		for (var i = 0; i < ops.length; i++) {
			var op = ops[i];
			var row = op.row;
			
			if (!(op.type & Operation.DELETE
			&& op.type & Operation.INSERT))
				// eslint-disable-next-line no-unused-vars
				isOperation = true;
		
			if (op.type & Operation.DELETE) {
				resultSet.fetchResult();
			} else if (op.type & (Operation.INSERT | Operation.UPDATE)) {
				this.emit('row-updated-before', row.index);

				var updatedCols = [];
				var cols = this.columns;
		
				for (var tableIndex in op.tables) {	
					var j = 0;
					tableIndex = parseInt(tableIndex);

					resultSet.fetchResult();
					var newValues = resultSet.fetchRow();		

					if (op.tables[tableIndex] & Operation.INSERT) {
						for (var i = 0; i < cols.length; i++)
						if (cols[i].table === tableIndex) {
							row[cols[i].name] = newValues[j++];
							updatedCols.push(i);
						}
					} else {
						for (var i = 0; i < cols.length; i++)
						if (cols[i].table === tableIndex
						&& op.oldValues[i] !== undefined) {
							row[cols[i].name] = newValues[j++];
							updatedCols.push(i);
						}
					}
				}

				this.emit('row-updated', row.index, updatedCols);
			}
		}
		
		resultSet.fetchResult();
			
//		if (isOperation)
			this.emit('operations-done');
	}
	
	/**
	 * Undoes all unsaved changes made to the model.
	 */
	,reverseOperations: function() {
		for (var i = 0; i < this._operations.length; i++) {
			var op = this._operations[i];
			var row = op.row;

			if (op.type & Operation.DELETE
			&& !(op.type & Operation.INSERT)) {
				this.data.splice(row.index, 0, row);
				this.emit('row-inserted', row.index);
			} else if (op.type & Operation.UPDATE) {
				this.emit('row-updated-before', row.index);

				var updatedCols = [];
				var cols = this.columns;

				for (var i = 0; i < cols.length; i++)
				if (op.oldValues[i] !== undefined) {
					const colName = cols[i].name;
					row[colName] = op.oldValues[colName];
					updatedCols.push(i);
				}

				this.emit('row-updated', row.index, updatedCols);
			}
		}

		this._resetOperations();
		this._refreshRowIndexes(0);
	}
	
	,_resetOperations: function() {
		this._operations = [];
		this._operationsMap = {};
	}
	
	/*
	 * 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;
	}
	
	/**
	 * Orders the model by the specified column name.
	 *
	 * @param {integer} columnName The column name
	 * @param {SortWay} way The sort way
	 */
	,sortByName: function(columnName, way) {
		this._requestedSortIndex = -1;
		this._requestedSortName = columnName;

		if (this.checkColName(columnName))
			this._sort(columnName, way);
	}

	/**
	 * Orders the model by the specified column.
	 *
	 * @param {integer} column The column index
	 * @param {SortWay} way The sort way
	 */
	,sort: function(column, way) {
		this._requestedSortIndex = column;
		this._requestedSortName = null;

		const columnName = this.getColumnName(column);
		if (columnName) return;

		this._sort(columnName, way);
	}
	 
	,_sort: function(column, way) {
		this._setStatus(Status.LOADING);
		this._realSort(column, way);
		this._setStatus(Status.READY);
	}
	
	,_realSort: function(column, way) {
		if (column !== this._sortColumn) {
			if (way === SortWay.DESC)
				var sortFunction = this.sortFunctionDesc;
			else
				var sortFunction = this.sortFunctionAsc;
	
			this.data.sort(sortFunction.bind(this, column));
		} else if (way !== this._sortWay)
			this.data.reverse();

		this._sortColumn = column;
		this._sortWay = way;

		this._refreshRowIndexes(0);
	}

	/**
	 * 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);
	}

	,_buildIndex: function(columnName) {
		if (this.checkColName(columnName)) {
			var index = {};
			var data = this.data;

			switch (this.columns[columnName].type) {
				case Connection.Type.TIMESTAMP:
				case Connection.Type.DATE_TIME:
				case Connection.Type.DATE:
					for (var i = 0; i < data.length; i++)
						index[data[i][columnName].toString()] = i;
					break;
				default:
					for (var i = 0; i < data.length; i++)
						index[data[i][columnName]] = i;
			}

			this._indexes[columnName] = index;
		}
	}

	/**
	 * Searchs a value on the model and returns the row index of the first
	 * ocurrence.
	 * If an index have been built on that column, it will be used, for more
	 * information see the indexColumn() method.
	 *
	 * @param {String} columnName The column name
	 * @param {Object} value The value to search
	 * @return {integer} The column index
	 */
	,search: function(columnName, value) {
		if (!this.checkColName(columnName))
			return -1;
		
		if (value)
		switch (this.columnMap[columnName].type) {
			case Connection.Type.BOOLEAN:
				value = !!value;
				break;
			case Connection.Type.INTEGER:
				value = parseInt(value);
				break;
			case Connection.Type.DOUBLE:
				value = parseFloat(value);
				break;
			default:
				value = value.toString();
		}

		let rowIndex = -1;
		const index = this._indexes[columnName];
		
		if (index) {
			// Searchs the value using an internal index

			if (index[value] !== undefined)
				rowIndex = index[value];
		} else {
			// Searchs the value using a loop

			var data = this.data;
		
			switch (this.columnMap[columnName].type) {
			case Connection.Type.TIMESTAMP:
			case Connection.Type.DATE_TIME:
			case Connection.Type.DATE:
				for (var i = 0; i < data.length; i++)
				if (value === data[i][columnName].toString()) {
					rowIndex = i;
					break;
				}
				break;
			default:
				for (var i = 0; i < data.length; i++)
				if (value === data[i][columnName]) {
					rowIndex = i;
					break;
				}
			}
		}

		return rowIndex;
	}

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

	,_setStatus: function(status) {
		this._status = status;
		this.emit('status-changed', status);
		this.emit('status-changed-after', status);
	}
	
	,_createTarget: function(tableIndex) {
		var table = this.tables[tableIndex];

		return new Sql.Table({
			 name: table.orgname
			,schema: table.schema
		});
	}

	,_createWhere: function(tableIndex, op, useOldValues) {
		const where = new Sql.Operation({type: Sql.Operation.Type.AND});
		const pks = this.tables[tableIndex].pks;
		
		if (pks.length === 0)
			return null;

		for (const pk of pks) {
			const column = this.columnMap[pk];
		
			const equalOp = new Sql.Operation({type: Sql.Operation.Type.EQUAL});
			equalOp.push(new Sql.Field({name: column.orgname}));
			where.push(equalOp);

			let pkValue = null;
			
			if (useOldValues && op.oldValues
			&& op.oldValues[pk] !== undefined)
				pkValue = op.oldValues[pk];
			else
				pkValue = op.row[pk];

			if (pkValue)
				equalOp.push(new Sql.Value({value: pkValue}));
			else if (column.flags & Connection.Flag.AI && !useOldValues)
				equalOp.push(new Sql.Function({name: 'LAST_INSERT_ID'}));
			else
				return null;
		}
		
		return where;
	}

	,_createOperation: function(rowIndex) {
		var op = this._operationsMap[rowIndex];

		if (!op) {		
			op = {
				type: 0,
				row: this.data[rowIndex]
			};
			this._operations.push(op);
			this._operationsMap[rowIndex] = op;
		}
			
		return op;
	}

	/**
	 * Overrides information about a table and its columns. If a parameter is
	 * not provided, the original will be preserved. This method should be used
	 * primarily to avoid the mysql bug that causes this information will not
	 * be set correctly.
	 * For more information see the following links:
	 *  - https://bugs.mysql.com/bug.php?id=44660
	 *  - https://bugs.mysql.com/bug.php?id=26894
	 *
	 * @param {String} table The table alias
	 * @param {String} orgtable The original table name
	 * @param {String} schema The original table schema
	 * @param {Array} pks Array with the names of primary keys
	 * @param {String} ai The autoincrement column name
	 */
	,setInfo: function(table, orgname, schema, pks, ai) {
		if (!this.tableInfo)
			this.tableInfo = {};
	
		this.tableInfo[table] = {
			orgname,
			schema,
			pks,
			ai
		};
		
		this._repairColumns();
	}

	,_repairColumns: function() {
		// Repairs wrong table info

		if (this.tableInfo && this.tables)
		for (var i = 0; i < this.tables.length; i++) {
			var table = this.tables[i];
			var tableInfo = this.tableInfo[table.name];
		
			if (!tableInfo)
				continue;

			table.orgname = tableInfo.orgname;
			table.schema = tableInfo.schema;

			if (tableInfo.pks) {
				table.pks = [];
			
				for (const pk of tableInfo.pks) {
					if (this.checkColName(pk))
						table.pks.push(pk);
					else
						console.warn('Db.Model: Can\'t repair primary key: `%s`.`%s`'
							,tableInfo.orgname
							,pk
						);
				}
			}

			if (tableInfo.ai) {
				if (this.checkColName(tableInfo.ai))
					this.columnMap[tableInfo.ai].flags |= Connection.Flag.AI;
				else
					console.warn('Db.Model: Can\'t repair autoincrement column: `%s`.`%s`'
						,tableInfo.orgname
						,tableInfo.ai
					);
			}
		}
	}
});