<?php

/** KumbiaForms - PHP Rapid Development Framework *****************************
*	
* Copyright (C) 2005-2007 Andrés Felipe Gutiérrez (andresfelipe at vagoogle.net)
* 	
* This framework is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* 
* This framework 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
* Lesser General Public License for more details.
* 
* You should have received a copy of the GNU Lesser General Public
* License along with this framework; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
* 
* Este framework es software libre; puedes redistribuirlo y/o modificarlo
* bajo los terminos de la licencia pública general GNU tal y como fue publicada
* por la Fundación del Software Libre; desde la versión 2.1 o cualquier
* versión superior.
* 
* Este framework es distribuido con la esperanza de ser util pero SIN NINGUN 
* TIPO DE GARANTIA; sin dejar atrás su LADO MERCANTIL o PARA FAVORECER ALGUN
* FIN EN PARTICULAR. Lee la licencia publica general para más detalles.
* 
* Debes recibir una copia de la Licencia Pública General GNU junto con este
* framework, si no es asi, escribe a Fundación del Software Libre Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
*****************************************************************************/

/**
 * ActiveRecord Class maps relational to object
 * 
 * Active Record es un enfoque al problema de acceder a los datos de una 
 * base de datos. Una fila en la tabla de la base de datos (o vista) 
 * se envuelve en una clase, de manera que se asocian filas únicas de 
 * la base de datos con objetos del lenguaje de programación usado. 
 * Cuando se crea uno de estos objetos, se añade una fila a 
 * la tabla de la base de datos. Cuando se modifican los atributos del
 * objeto, se actualiza la fila de la base de datos.
 *
 * Propiedades Soportadas:
 * $db = Conexión al Motor de Base de datos
 * $db = Tabla que contiene la tabla que esta siendo mapeada
 * $fields = Listado de Campos de la tabla que han sido mapeados
 * $count = Conteo del ultimo Resultado de un Select
 * $primary_key = Listado de columnas que conforman la llave primaria
 * $non_primary = Listado de columnas que no son llave primaria
 * $not_null = Listado de campos que son not_null
 * $attributes_names = nombres de todos los campos que han sido mapeados
 * $debug = Indica si se deben mostrar los SQL enviados al RDBM en pantalla
 * $logger = Si es diferente de false crea un log utilizando la clase Logger
 * en lib/kumbia/logger.php, esta crea un archivo .txt en logs/ con todas las
 * operaciones realizadas en ActiveRecord, si $logger = "nombre", crea un 
 * archivo con ese nombre
 * 
 * Propiedades sin Soportar:
 * $dynamic_update : La idea es que en un futuro ActiveRecord solo
 * actualize los campos que han cambiado.  (En Desarrollo)
 * $dynamic_insert : Indica si los valores del insert son solo aquellos 
 * que sean no nulos. (En Desarrollo) 
 * $select_before_update: Exige realizar una sentencia SELECT anterior
 * a la actualización UPDATE para comprobar que los datos no hayan sido
 * cambiados (En Desarrollo) 
 * $subselect : Permitira crear una entidad ActiveRecord de solo lectura que
 * mapearia los resultados de un select directamente a un Objeto (En Desarrollo)
 * 
 */
class ActiveRecord {

	//Soportados
	public $db;
	public $source;
	public $fields;
	public $count;
	public $primary_key = array();
	public $non_primary = array();
	public $not_null = array();
	public $attributes_names = array();
	public $is_view;

	public $debug = false;
	public $logger = false;
	public $persistent = false;

	//Sin Soportar
	public $dynamic_update = false;
	public $dynamic_insert = false;
	public $select_before_update = false;
	public $subselect = null;

	//:Privados
	private $validates_length = array();
	private $_in = array();
	private $_at = array();
	private $where_pk;
	private $dumped = false;
	private $dump_lock = false;
	private $data_type = array();
	private $_has_one = array();
	private $_has_many = array();
	private $_belongs_to = array();
	

	// Persistance Models Meta-data
	static public $models;
	
	function ActiveRecord($table='', $dump=true){
		if(!$table){
			 $this->model_name();
		} else {
			$this->source = strtolower($table);
		}
		if($dump){
			$this->dump($table);
		}
	}
	
	/**
	 * Obtiene el nombre de la relacion en el RDBM a partir del nombre de la clase
	 *
	 */
	private function model_name(){
		$this->source = get_class($this);			
		if(ereg("([a-z])([A-Z])", $this->source, $reg)){
			$this->source = str_replace($reg[0], $reg[1]."_".strtolower($reg[2]), $this->source);			
		}
		$this->source = strtolower($this->source);
	}

	/**
	 * Pregunta si el ActiveRecord ya ha consultado la información de metadatos
	 * de la base de datos o del registro persistente
	 *
	 * @return boolean
	 */
	public function is_dumped(){
		return $dumped;
	}
	
	/**
	 * Valida que los valores que sean leidos del objeto ActiveRecord esten definidos
	 * previamente o sean atributos de la entidad
	 *
	 * @param string $property	 
	 */
	function __get($property){
		if(!$this->dumped){
			$this->dump();
		}		
		if(!$this->dump_lock){
			if(!isset($this->$property)){
				ActiveRecordException::display_warning("Propiedad no definida", "Propiedad indefinida '$property' leida de el modelo '$this->source'", $this->source);			
			}
		}
		return $this->$property;
	}

	/**
	 * Valida que los valores que sean asignados al objeto ActiveRecord esten definidos
	 * o sean atributos de la entidad
	 *
	 * @param string $property
	 * @param mixed $value
	 */
	function __set($property, $value){
		if(!$this->dumped){
			$this->dump();
		}		
		if(!$this->dump_lock){
			if(!isset($this->$property)){
				ActiveRecordException::display_warning("Propiedad no definida", "Propiedad indefinida '$property' asignada en el modelo '$this->source' ($value)", $this->source);			
			}
		}
		$this->$property = $value;
	}
	
	/**
	 * Devuelve un valor o un listado dependiendo del tipo de Relación
	 *
	 */
	public function __call($method, $args=array()){
		$has_relation = false;
		$model = ereg_replace("^get", "", $method);
		$mmodel = strtolower($model);
		if(in_array($mmodel, $this->_belongs_to)){
			$has_relation = true;
			if(kumbia::$models[$model]){
				return kumbia::$models[$model]->find_first($this->{$mmodel."_id"});
			} 
		}
		if(in_array($mmodel, $this->_has_many)){
			$has_relation = true;
			if(kumbia::$models[$model]){
				if($this->id){					
					return kumbia::$models[$model]->find($this->source."_id={$this->id}");
				} else {
					return array();
				}
			} 
		}		
		if(in_array($mmodel, $this->_has_one)){
			$has_relation = true;
			if(kumbia::$models[$model]){
				if($this->id){					
					return kumbia::$models[$model]->find_first($this->source."_id={$this->id}");
				} else {
					return array();
				}
			} 
		}		
		if(method_exists($this, $method)){
			call_user_func_array($method, $args);
		} else {
			if($has_relation){
				throw new ActiveRecordException("No existe el modelo '$model' para relacionar con ActiveRecord::{$this->source}");
			} else {
				throw new ActiveRecordException("No existe el método '$method' en ActiveRecord::{$this->source}");
			}
		}
		return $this->$method($args);
		
	}

	/**
	 * Verifica si la tabla definida en $this->source existe
	 * en la base de datos y la vuelca en dump_info
	 *
	 * @return boolean
	 */
	public function dump(){
		if($this->source) {
			$this->source = strtolower($this->source);						
		} else {
			$this->model_name();
			if(!$this->source){
				return false;	
			}						
		}	
		$table = $this->source;	
		if(!count(ActiveRecord::get_meta_data($this->source))){		
			if(!$this->db){
				$this->db = db::raw_connect();
			}
			$this->db->debug = $this->debug;
			$this->db->logger = $this->logger;
			$this->dumped = true;			
			if($this->db->table_exists($table)){
				$this->dump_info($table);
			} else {
				$config = Config::read();
				throw new ActiveRecordException("No existe la tabla '$table' en la base de datos '{$config->database->name}'");
				return false;
			}
			if(!count($this->primary_key)){
				if(!$this->is_view){
					throw new ActiveRecordException("No se ha definido una llave primaria para la tabla '$table' esto imposibilita crear el ActiveRecord para esta entidad");
					return false;
				}
			}			
		} else {			
			if(!$this->dumped){
				$this->dumped = true;
				$this->dump_info($table);	
			}
		}
		return true;
	}

	/**
	 * Vuelca la información de la tabla $table en la base de datos
	 * para armar los atributos y meta-data del ActiveRecord, 
	 * actualmente solo esta soportado MySQL
	 *
	 * @param string $table
	 */
	private function dump_info($table){
		$config = Config::read();
		$this->dump_lock = true;
		if($config->database->type=='mysql'){
			if(!count(ActiveRecord::get_meta_data($table))){
				$meta_data = $this->db->in_query("desc $table");
				if($meta_data){
					ActiveRecord::set_meta_data($table, $meta_data);
				}
			}
			foreach(ActiveRecord::get_meta_data($table) as $field){
				if(!isset($this->$field['Field'])){
					$this->$field['Field'] = "";
				}
				$this->fields[] = $field['Field'];
				if($field['Key']=='PRI'){
					$this->primary_key[] = $field['Field'];
				} else $this->non_primary[] = $field['Field'];
				if($field['Null']=='NO'){
					$this->not_null[] = $field['Field'];
				}
				if($field['type']){
					$this->data_type[$field['Field']] = strtolower($field['Type']);
				}
				if(substr($field['Field'], strlen($field['Field'])-3, 3)=='_at'){
					$this->_at[] = $field['Field'];
				}
				if(substr($field['Field'], strlen($field['Field'])-3, 3)=='_in'){
					$this->_in[] = $field['Field'];
				}
			}
			$this->attributes_names = $this->fields;
			$this->dump_lock = false;
			return true;
		}
		if($config->database->type=='postgresql'){
			if(!count(ActiveRecord::get_meta_data($table))){
				$meta_data = $this->db->in_query("SELECT a.attname AS Field, t.typname AS Type,
			 	CASE WHEN attnotnull=false THEN 'YES' ELSE 'NO' END AS null,			 
			 	CASE WHEN (select cc.contype FROM pg_catalog.pg_constraint cc WHERE
			 	cc.conrelid = c.oid AND cc.conkey[1] = a.attnum)='p' THEN 'PRI' ELSE '' 
			 	END AS Key FROM pg_catalog.pg_class c, pg_catalog.pg_attribute a, 
			 	pg_catalog.pg_type t WHERE c.relname = '$table' AND c.oid = a.attrelid 
			 	AND a.attnum > 0 AND t.oid = a.atttypid");
				if($meta_data){
					ActiveRecord::set_meta_data($table, $meta_data);
				}				
			}
			foreach(ActiveRecord::get_meta_data($table) as $field){				
				if(!isset($this->$field['field'])){
					$this->$field['field'] = "";
				}
				$this->fields[] = $field['field'];
				if($field['key']=='PRI'){
					$this->primary_key[] = $field['field'];
				} else {
					$this->non_primary[] = $field['field'];
				}
				if($field['null']=='NO'){
					$this->not_null[] = $field['field'];
				}
				if($field['type']){
					$this->data_type[$field['field']] = strtolower($field['type']);
				}
				if(substr($field['field'], strlen($field['field'])-3, 3)=='_at'){
					$this->_at[] = $field['field'];
				}
				if(substr($field['field'], strlen($field['field'])-3, 3)=='_in'){
					$this->_in[] = $field['field'];
				}
			}			
			$this->attributes_names = $this->fields;
			$this->dump_lock = false;
			return true;
		}		
		if($config->database->type=='oracle'){			
			if(!count(ActiveRecord::get_meta_data($table))){				
				$meta_data = $this->db->in_query("SELECT LOWER(ALL_TAB_COLUMNS.COLUMN_NAME) AS FIELD, LOWER(ALL_TAB_COLUMNS.DATA_TYPE) AS TYPE, ALL_TAB_COLUMNS.DATA_LENGTH AS LENGTH, (SELECT COUNT(*) FROM ALL_CONS_COLUMNS WHERE TABLE_NAME = '".strtoupper($table)."' AND ALL_CONS_COLUMNS.COLUMN_NAME = ALL_TAB_COLUMNS.COLUMN_NAME AND ALL_CONS_COLUMNS.POSITION IS NOT NULL) AS KEY, ALL_TAB_COLUMNS.NULLABLE AS ISNULL FROM ALL_TAB_COLUMNS WHERE ALL_TAB_COLUMNS.TABLE_NAME = '".strtoupper($table)."'");
				if($meta_data){
					ActiveRecord::set_meta_data($table, $meta_data);
				}				
			}			
			foreach(ActiveRecord::get_meta_data($table) as $field){
				if(!isset($this->$field['field'])){
					$this->$field['field'] = "";
				}
				$this->fields[] = $field['field'];
				if($field['key']){
					$this->primary_key[] = $field['field'];
				} else {
					$this->non_primary[] = $field['field'];
				}
				if($field['isnull']=='N'){
					$this->not_null[] = $field['field'];
				}
				if($field['type']){
					$this->data_type[$field['field']] = $field['type'];
				}
				if(substr($field['field'], strlen($field['field'])-3, 3)=='_at'){
					$this->_at[] = $field['field'];
				}
				if(substr($field['field'], strlen($field['field'])-3, 3)=='_in'){
					$this->_in[] = $field['field'];
				}
			}				
			$this->attributes_names = $this->fields;
			$this->dump_lock = false;
			return true;
		}
		$this->dump_lock = false;
		return false;
	}

	/**
	 * Commit a Transaction
	 *
	 * @return success
	 */	
	public function commit(){
		if(!$this->dumped){
			$this->dump();
		}
		return $this->sql("commit");
	}

	/**
	 * Rollback a Transaction
	 *
	 * @return success
	 */	
	public function rollback(){
		if(!$this->dumped){
			$this->dump();
		}
		return $this->sql("rollback");
	}

	/**
	 * Start a transaction in RDBM
	 *
	 * @return success
	 */	
	public function begin(){
		if(!$this->dumped){
			$this->dump();
		}
		return $this->sql("begin");
	}

	/**
	 * Find record in this table using a SQL Statement
	 *
	 * @param string $sqlQuery
	 * @return ActiveRecord Cursor
	 */
	public function find_by_sql($sqlQuery){
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		if(!$this->dumped) $this->dump();
		$results = array();
		foreach($this->db->in_query($sqlQuery) as $result){
			$results[] = $this->dump_result($result);
		}
		return $results;
	}

	/**
	 * Execute a SQL Statement directly
	 *
	 * @param string $sqlQuery
	 * @return int affected
	 */
	public function sql($sqlQuery){
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->logger = $this->logger;
		$this->db->debug = $this->debug;
		return $this->db->query($sqlQuery, $this->debug);
	}

	/**
	 * Return Fist Record 
	 *
	 * @param mixed $what
	 * @param boolean $debug
	 * @return ActiveRecord Cursor
	 */
	public function find_first($what=''){
		if(!$this->dumped) {
			$this->dump();
		}
		$config = Config::read();
		$what = get_params(func_get_args());
		if(is_array($what)){
			$select = "select ";
			if(!$what['conditions']){
				if(!is_param($what[0])){
					if(is_numeric($what[0])){
						if(!$this->primary_key[0]){
							if(isset($this->id)){
								$this->primary_key[0] = "id";
							}
						}
						if($this->is_view&&!$this->primary_key[0]){
							$this->primary_key[0] = 'id';
						}
						$what['conditions'] = "{$this->primary_key[0]} = '{$what[0]}'";
					} else {
						$what['conditions'] = $what[0];
					}
				}
			}
			if($what["columns"]) {
				$select.="{$what["columns"]} from {$this->source} where " ;
			} else {
				$select.="* from {$this->source} where " ;
			}
			if($what["conditions"]) {
				if($config->database->type=='oracle'){
					/**
					 * Bug: Solo cambia la primera aparición de una fecha
					 */
					if(ereg("'[0-9]{4}-[0-9]{2}-[0-9]{2}'", $what['conditions'], $reg)){
						$what['conditions'] = str_replace($reg[0], "TO_DATE({$reg[0]}, 'YYYY-MM-DD')", $what['conditions']);
					}
				}
				$select.=" {$what["conditions"]} ";				
			} else {
				$select.=" 1=1 ";
			}
			if($config->database->type=='oracle'){
				if($what["limit"]) {								
					$select.=" and rownum <= {$what["limit"]} ";			
				} else {			
					$select.=" and rownum = 1";			
				}
			}
			if($what["order"]) {
				$select.=" order by {$what["order"]} ";
			} else {
				$select.=" order by 1 ";
			}
			if($config->database->type!='oracle'){
				if($what["limit"]) {								
					$select.=" limit {$what["limit"]} ";			
				} else {			
					$select.=" limit 1";			
				}
			}
			if(!$this->db){
				$this->db = db::raw_connect();
			}
			$this->db->debug = $this->debug;
			$this->db->logger = $this->logger;			
			$result = $this->db->fetch_one($select);
			if($result){
				$this->dump_result_self($this->db->fetch_one($select));
				return $this->dump_result($result);
			}
			return false;
		}
		return false;
	}

	/**
	 * Find data on Relational Map table
	 *
	 * @param string $what	 
	 * @return ActiveRecord Cursor
	 */
	public function find($what=''){
		$return = "?";
		if(!$this->dumped) {
			$this->dump();
		}
		$what = get_params(func_get_args());		
		if(is_array($what)){
			$select = "select ";
			if(!$what['conditions']){
				if(!is_param($what[0])){
					if(is_numeric($what[0])){
						if(!$this->primary_key[0]){
							if(isset($this->id)){
								$this->primary_key[0] = "id";
							}
						}
						if($this->is_view&&!$this->primary_key[0]){
							$this->primary_key[0] = 'id';
						}
						$what['conditions'] = "{$this->primary_key[0]} = '{$what[0]}'";
						$return = '1';
					} else {
						$return = 'n';
						$what['conditions'] = $what[0];
					}
				}
			}
			if($what["columns"]) {
				$select.="{$what["columns"]} from {$this->source} where " ;
			} else {
				$select.="* from {$this->source} where " ;
			}
			if($what["conditions"]) {
				if($config->database->type=='oracle'){
					/**
					 * Bug: Solo cambia la primera aparición de una fecha
					 */
					if(ereg("'[0-9]{4}-[0-9]{2}-[0-9]{2}'", $what['conditions'], $reg)){
						$what['conditions'] = str_replace($reg[0], "TO_DATE({$reg[0]}, 'YYYY-MM-DD')", $what['conditions']);
					}
				}
				$select.=" {$what["conditions"]} ";
			} else {
				$select.=" 1=1 ";
			}
			if($config->database->type=='oracle'){
				if($what["limit"]) {								
					$select.=" and rownum <= {$what["limit"]} ";			
				} 
			}
			if($what["order"]) {
				$select.=" order by {$what["order"]} ";
			} 
			if($config->database->type!='oracle'){
				if($what["limit"]) {								
					$select.=" limit {$what["limit"]} ";			
				} 
			}
			if(!$this->db){
				$this->db = db::raw_connect();
			}
			$this->db->debug = $this->debug;
			$this->db->logger = $this->logger;
			if($return=='?'||$return=='n'){
				$results = array();
				foreach($this->db->in_query($select) as $result){
					$results[] = $this->dump_result($result);
				}
				$this->count = $this->db->num_rows();
				return $results;
			} else {
				$result = $this->db->fetch_one($select);
				if($result){
					$this->dump_result_self($this->db->fetch_one($select));
					return $this->dump_result($result);
				}
				return false;
			}
		}
		return false;
	}

	public function distinct($what=''){
		if(!$this->dumped) $this->dump();
		if(func_num_args()>1){
			$what = get_params(func_get_args());
		}
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		if(is_array($what)){
			if(!$what['column']) $what['column'] = $what['0'];
			$select = "select distinct {$what["column"]} from {$this->source} where " ;
			if($what["conditions"]) {
				$select.=" {$what["conditions"]} ";
			} else {
				$select.=" 1=1 ";
			}
			if($what["order"]) {
				$select.=" order by {$what["order"]} ";
			} else {
				$select.=" order by 1 ";
			}
			if($what["limit"]) {
				$select.=" limit {$what["limit"]} ";
			}
		} else {
			if($what!==''){
				$select = "select distinct $what from {$this->source} order by 1";
			}
		}
		$results = array();
		foreach($this->db->in_query($select) as $result){
			$results[] = $result[0];
		}
		return $results;
	}

	/**
	 * Ejecuta una consulta en el RDBM directamente
	 *
	 * @param string $sql
	 * @return resource
	 */
	public function select_one($sql){
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		if(substr(ltrim($sql), 0, 7)!="select") $sql = "select ".$sql;
		$num = $this->db->fetch_one($sql);
		return $num[0];
	}

	static public function static_select_one($sql){
		$db = db::raw_connect();
		if(substr(ltrim($sql), 0, 7)!="select") $sql = "select ".$sql;
		$num = $db->fetch_one($sql);
		return $num[0];
	}

	/**
	 * Realiza un conteo de filas
	 *
	 * @param string $what
	 * @return integer
	 */
	public function count($what=''){
		if(!$this->dumped) $this->dump();
		if(func_num_args()>1){
			$what = get_params(func_get_args());
		}
		if(is_array($what)){
			if(!$what['distinct']) {
				$select = "select count(*) from {$this->source} where " ;
			} else {
				$select = "select count(distinct {$what['distinct']}) from {$this->source} where " ;
			}
			if($what["conditions"]) $select.=" {$what["conditions"]} "; else $select.=" 1=1 ";
			if($what["order"]) $select.=" order by {$what["order"]} "; else $select.=" order by 1 ";
			if($what["limit"]) $select.=" limit {$what["limit"]} ";
		} else {
			if($what!==''){
				if(is_numeric($what)){
					if($this->is_view&&!$this->primary_key[0]){
						$this->primary_key[0] = 'id';
					}
					$select = "select count(*) from {$this->source} where {$this->primary_key[0]} = '$what' order by 1";
				} else {
					$select = "select count(*) from {$this->source} where $what order by 1";
				}
			} else {
				$select = "select count(*) from {$this->source} order by 1";
			}
		}
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		$num = $this->db->fetch_one($select);
		return $num[0];
	}

	/**
	 * Realiza un promedio sobre el campo $what
	 *
	 * @param string $what
	 * @return array
	 */
	public function average($what=''){
		if(!$this->dumped) {
			$this->dump();
		}
		if(func_num_args()>1){
			$what = get_params(func_get_args());
		}
		if(is_array($what)){
			if(!$what['column']) $what['column'] = $what[0];
			$select = "select avg({$what['column']}) from {$this->source} where " ;
			if($what["conditions"]) $select.=" {$what["conditions"]} "; else $select.=" 1=1 ";
			if($what["order"]) $select.=" order by {$what["order"]} "; else $select.=" order by 1 ";
			if($what["limit"]) $select.=" limit {$what["limit"]} ";
		} else {
			if($what!==''){
				$select = "select avg($what) from {$this->source} order by 1";
			}
		}
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		$num = $this->db->fetch_one($select);
		return $num[0];
	}

	public function sum($what=''){
		if(!$this->dumped) {
			$this->dump();
		}
		if(func_num_args()>1){
			$what = get_params(func_get_args());
		}
		if(is_array($what)){
			$select = "select sum({$what['column']}) from {$this->source} where " ;
			if($what["conditions"]) $select.=" {$what["conditions"]} "; else $select.=" 1=1 ";
			if($what["order"]) $select.=" order by {$what["order"]} "; else $select.=" order by 1 ";
			if($what["limit"]) $select.=" limit {$what["limit"]} ";
		} else {
			if($what!==''){
				$select = "select sum($what) from {$this->source} order by 1";
			}
		}
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		$num = $this->db->fetch_one($select);
		return $num[0];
	}

	/**
	 * Busca el valor maximo para el campo $what
	 *
	 * @param string $what
	 * @return mixed
	 */
	public function maximum($what=''){
		if(!$this->dumped) {
			$this->dump();
		}
		if(func_num_args()>1){
			$what = get_params(func_get_args());
		}
		if(is_array($what)){
			if(!$what['column']) $what['column'] = $what[0];
			$select = "select max({$what['column']}) from {$this->source} where " ;
			if($what["conditions"]) $select.=" {$what["conditions"]} "; else $select.=" 1=1 ";
			if($what["order"]) $select.=" order by {$what["order"]} "; else $select.=" order by 1 ";
			if($what["limit"]) $select.=" limit {$what["limit"]} ";
		} else {
			if($what!==''){
				$select = "select max($what) from {$this->source} order by 1";
			}
		}
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		$num = $this->db->fetch_one($select);
		return $num[0];
	}

	/**
	 * Busca el valor minimo para el campo $what
	 *
	 * @param string $what
	 * @return mixed
	 */
	public function minimum($what=''){
		if(!$this->dumped) {
			$this->dump();
		}
		if(func_num_args()>1){
			$what = get_params(func_get_args());
		}
		if(is_array($what)){
			$select = "select min({$what['column']}) from {$this->source} where " ;
			if($what["conditions"]) $select.=" {$what["conditions"]} "; else $select.=" 1=1 ";
			if($what["order"]) $select.=" order by {$what["order"]} "; else $select.=" order by 1 ";
			if($what["limit"]) $select.=" limit {$what["limit"]} ";
		} else {
			if($what!==''){
				$select = "select min($what) from {$this->source} order by 1";
			}
		}
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		$num = $this->db->fetch_one($select);
		return $num[0];
	}

	/**
	 * Realiza un conteo directo mediante $sql
	 *
	 * @param string $sqlQuery
	 * @return mixed
	 */
	public function count_by_sql($sqlQuery){
		if(!$this->dumped) $this->dump();
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		$num = $this->db->fetch_one($sqlQuery);
		return $num[0];
	}

	/**
	 * Iguala los valores de un resultado de la base de datos
	 * en un nuevo objeto con sus correspondientes 
	 * atributos de la clase
	 *
	 * @param array $result
	 * @return ActiveRecord
	 */
	function dump_result($result){
		if(!$this->dumped) $this->dump();
		$obj = clone $this;
		$this->dump_lock = true;
		if(is_array($result)){
			foreach($result as $k => $r){
				if(!is_numeric($k)){
					$obj->$k = stripslashes($r);
				}
			}
		}
		$this->dump_lock = false;
		return $obj;
	}

	/**
	 * Iguala los valores de un resultado de la base de datos
	 * con sus correspondientes atributos de la clase
	 *
	 * @param array $result
	 * @return ActiveRecord
	 */
	public function dump_result_self($result){
		if(!$this->dumped) $this->dump();
		$this->dump_lock = true;
		if(is_array($result)){
			foreach($result as $k => $r){
				if(!is_numeric($k)){
					$this->$k = stripslashes($r);
				}
			}
		}
		$this->dump_lock = false;
	}

	/**
	 * Create a new Row using values from $_REQUEST
	 *
	 * @return boolean success
	 */
	public function create_from_request(){
		if(!$this->dumped) $this->dump();
		$values = array();
		foreach($_REQUEST as $k => $r){
			if(isset($this->$k)) {
				$values[$k] = $r;
			}
		}
		if(count($values)){
			return $this->create($values);
		} else{
			return false;
		}
	}

	/**
	 * Saves a new Row using values from $_REQUEST
	 *
	 * @return boolean success
	 */
	public function save_from_request(){
		if(!$this->dumped) $this->dump();
		foreach($_REQUEST as $k => $r){
			if(isset($this->$k)) {
				$this->$k = $r;
			}
		}
		return $this->save();
	}

	/**
	 * Creates a new Row in map table
	 *
	 * @param mixed $values
	 * @return success boolean
	 */
	public function create($values=''){
		if(!$this->dumped) $this->dump();
		if(func_num_args()>1){
			$values = get_params(func_get_args());
		}
		if(is_array($values)){
			if(is_array($values[0])){
				foreach($values as $v){
					foreach($this->fields as $f){
						$this->$f = "";
					}
					foreach($v as $k => $r){
						if(!is_numeric($k)){
							if(isset($this->$k)){
								$this->$k = $r;
							} else {
								throw new ActiveRecordException("No existe el Atributo '$k' en la entidad '{$this->source}' al ejecutar la inserción");
								return false;
							}
						}
					}
					if(!$this->exists()){
						return $this->save();
					} else {
						Flash::error('No se puede crear el registro ya existe');
						return false;
					}
				}
			} else {
				foreach($this->fields as $f){
					$this->$f = "";
				}
				foreach($values as $k => $r){
					if(!is_numeric($k)){
						if(isset($this->$k)){
							$this->$k = $r;
						} else {
							throw new ActiveRecordException("No existe el Atributo '$k' en la entidad '{$this->source}' al ejecutar la inserción");
							return false;
						}
					}
				}
				if(!$this->exists()){
					return $this->save();
				} else{
					Flash::error('No se puede crear el registro ya existe');
				}
			}
		} else {
			if($values!==''){
				Flash::warning("Parámetro incompatible en acción 'create'. No se pudo crear ningun registro");
				return false;
			} else {
				if(!$this->exists()){
					return $this->save();
				}
			}
		}
		return true;
	}

	/**
	 * Consulta si un determinado registro existe o no
	 * en la entidad de la base de datos
	 *
	 * @return boolean
	 */
	function exists(){
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		if(!$this->dumped){
			$this->dump();
		}
		$this->where_pk = "";
		foreach($this->primary_key as $key){
			if($this->$key){
				$this->where_pk.= " $key = '{$this->$key}' and ";
			} else {
				$this->where_pk.= " 0=1 and ";
			}
		}
		if($this->where_pk){
			$this->where_pk = substr($this->where_pk, 0, strlen($this->where_pk)-4);
		}
		$query = "select count(*) from {$this->source} where {$this->where_pk}";
		$num = $this->db->fetch_one($query);		
		return $num[0];
	}

	/**
	 * Saves Information on the ActiveRecord Properties
	 *
	 * @return boolean success
	 */
	public function save(){
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		if(!$this->dumped) {
			$this->dump();
		}
		$ex = $this->exists();
		#Run Validation Callbacks Before
		if(method_exists($this, 'before_validation')){
			if($this->before_validation()=='cancel') return false;
		} else {
			if(isset($this->before_validation)){
				$method = $this->before_validation;
				if($this->$method()=='cancel') return false;
			}
		}
		if(!$ex&&method_exists($this, "before_validation_on_create")){
			if($this->before_validation_on_create()=='cancel') return false;
		} else {
			if(isset($this->before_validation_on_create)){
				$method = $this->before_validation_on_create;
				if($this->$method()=='cancel') return false;
			}
		}
		if($ex&&method_exists($this, "before_validation_on_update")){
			if($this->before_validation_on_update()=='cancel') return false;
		} else {
			if(isset($this->before_validation_on_update)){
				$method = $this->before_validation_on_update;
				if($this->$method()=='cancel') return false;
			}
		}

		if(is_array($this->not_null)){
			$e = false;
			for($i=0;$i<=count($this->not_null)-1;$i++){
				$f = $this->not_null[$i];
				if(is_null($this->$f)||$this->$f===''){
					if(!$ex&&$f=='id'){
						continue;
					}
					if(!$ex&&in_array($f, $this->_at)){
						continue;
					}
					if($ex&&in_array($f, $this->_in)){
						continue;
					}
					Flash::error("Error: El campo $f no puede ser nulo en $this->source");
					$e = true;
				}
			}
			if($e){
				return false;
			}
		}
		if(is_array($this->validates_length)){
			$e = false;
			reset($this->validates_length);
			for($i=0;$i<=count($this->validates_length)-1;$i++){
				$f = key($this->validates_length);
				if(is_numeric($this->validates_length[$f]['minimum'])){
					$n = $this->validates_length[$f]['minimum'];
					if(strlen($this->$f)<$n){
						Flash::error("Error: El campo $f debe tener como m&iacute;nimo $n caracteres");
						$e = true;
					}
				}
				if(is_numeric($this->validates_length[$f]['maximum'])){
					$n = $this->validates_length[$f]['maximum'];
					if(strlen($this->$f)>$n){
						Flash::error("Error: El campo $f debe tener como m&aacute;ximo $n caracteres");
						$e = true;
					}
				}
			}
			if($e){
				return false;
			}
			unset($f);
			unset($n);
		}

		#Run Validation Callbacks After
		if(!$ex&&method_exists($this, "after_validation_on_create")){
			if($this->after_validation_on_create()=='cancel') return false;
		} else {
			if(isset($this->after_validation_on_create)){
				$method = $this->after_validation_on_create;
				if($this->$method()=='cancel') return false;
			}
		}
		if($ex&&method_exists($this, "after_validation_on_update")){
			if($this->after_validation_on_update()=='cancel') return false;
		} else {
			if(isset($this->after_validation_on_update)){
				$method = $this->after_validation_on_update;
				if($this->$method()=='cancel') return false;
			}
		}
		if(method_exists($this, 'after_validation')){
			if($this->after_validation()=='cancel') return false;
		} else {
			if(isset($this->after_validation)){
				$method = $this->after_validation;
				if($this->$method()=='cancel') return false;
			}
		}
		# Run Before Callbacks
		if(method_exists($this, "before_save")){
			if($this->before_save()=='cancel') return false;
		} else {
			if(isset($this->before_save)){
				$method = $this->before_save;
				if($this->$method()=='cancel') return false;
			}
		}
		if($ex&&method_exists($this, "before_update")){
			if($this->before_update()=='cancel') return false;
		} else {
			if(isset($this->before_update)){
				$method = $this->before_update;
				if($this->$method()=='cancel') return false;
			}
		}
		if(!$ex&&method_exists($this, "before_create")){
			if($this->before_create()=='cancel') return false;
		} else {
			if(isset($this->before_create)){
				$method = $this->before_create;
				if($this->$method()=='cancel') return false;
			}
		}
		
		$config = Config::read();		
		if($ex){
			$set = "";			
			foreach($this->non_primary as $np){
				if(in_array($np, $this->_in)){
					if($config->database->type=='oracle'){
						$this->$np = date("Y-m-d");
					} else {
						$this->$np = date("Y-m-d G:i:s");
					}
				}				
				if(substr($this->$np, 0, 1)=="%"){
					$set.= "$np = ".str_replace("%", "", $this->$np).",";
				} else {
					if(!$this->is_a_numeric_type($np)){
						/**
						 * Se debe especificar el formato de fecha en Oracle
						 */
						if($this->data_type[$np]=='date'&&$config->database->type=='oracle'){
							$set.="$np = TO_DATE('".addslashes($this->$np)."', 'YYYY-MM-DD'),";
						} else {
							$set.="$np = '".addslashes($this->$np)."',";
						}
					} else {												
						$set.="$np = ".addslashes($this->$np ? $this->$np : "NULL").",";
					}
				}
			}
			$set = substr($set, 0, strlen($set)-1);
			$query = "update {$this->source} set $set where {$this->where_pk}";
		} else {							
			$insert = "insert into {$this->source} (";
			$values = ") values (";									
			foreach($this->fields as $field){
				if($field!='id'&&!$this->id){
					if(in_array($field, $this->_at)){
						if($config->database->type=='oracle'){
							$this->$field = date("Y-m-d");
						} else {
							$this->$field = date("Y-m-d G:i:s");
						}
					}
					if(in_array($field, $this->_in)){
						$this->$field = "NULL";
					}
					$insert.=$field.",";
					if(substr($this->$field, 0, 1)=="%"){
						$values.= str_replace("%", "", $this->$field).",";
					} else {
						if($this->is_a_numeric_type($field)||$this->$field=="NULL"){
							$values.=addslashes($this->$field ? $this->$field : "NULL").",";
						} else {							
							if($this->data_type[$field]=='date'&&$config->database->type=='oracle'){
								/**
								 * Se debe especificar el formato de fecha en Oracle
								 */
								$values.="TO_DATE('".addslashes($this->$field)."', 'YYYY-MM-DD'),";
							} else {
								$values.="'".addslashes($this->$field)."',";	
							}							
						}
					}
				} else {
					/**
					 * Campos autonumericos en Oracle deben utilizar una sequencia auxiliar
					 */
					if($config->database->type=='oracle'){
						if(!$this->id){
							$insert.=$field.",";
							$values.=$this->source."_sequence.NextVal,";
						}
					}
				}
			}
			$insert = substr($insert, 0, strlen($insert)-1);
			$values = substr($values, 0, strlen($values)-1);
			$query = "$insert $values)";
		}
		
		$val = $this->db->query($query);
		if($config->database->type=='oracle'){			
			$this->commit();	
		}		
		if(!$ex){
			$m = $this->maximum('id');
			$this->find_first($m);
		}

		if($val){
			if($ex&&method_exists($this, "after_update")){
				if($this->after_update()=='cancel') return false;
			} else {
				if(isset($this->after_update)){
					$method = $this->after_update;
					if($this->$method()=='cancel') return false;
				}
			}
			if(!$ex&&method_exists($this, "after_create")){
				if($this->after_create()=='cancel') return false;
			} else {
				if(isset($this->after_create)){
					$method = $this->after_create;
					if($this->$method()=='cancel') return false;
				}
			}
			if(method_exists($this, "after_save")){
				if($this->after_save()=='cancel') return false;
			} else {
				if(isset($this->after_save)){
					$method = $this->after_save;
					if($this->$method()=='cancel') return false;
				}
			}
			return $val;
		} else return false;
	}

	/**
	 * Find All data in the Relational Table
	 *
	 * @param string $field
	 * @param string $value
	 * @return ActiveRecod Cursor
	 */
	function find_all_by($field, $value){
		return $this->find(array("conditions" => "$field = '$value'"));
	}

	/**
	 * Updates Data in the Relational Table
	 *
	 * @param mixed $values
	 * @return boolean sucess
	 */
	function update($values=''){
		if(!$this->dumped) {
			$this->dump();
		}
		if(func_num_args()>1){
			$values = get_params(func_get_args());
		}
		if(is_array($values)){
			foreach($values as $k => $r){
				if(!is_numeric($k)){
					if(isset($this->$k)){
						$this->$k = $r;
					} else {
						throw new ActiveRecordException("No existe el Atributo '$k' en la entidad '{$this->source}' al ejecutar la inserción");
						return false;
					}
				}
			}
			if($this->exists()){
				return $this->save();
			} else {
				Flash::error('No se puede actualizar porque el registro no existe');
				return false;
			}
		} else {
			if($this->exists()){
				return $this->save();
			} else {
				Flash::error('No se puede actualizar porque el registro no existe');
				return false;
			}
		}
		return false;
	}

	/**
	 * Deletes data from Relational Map Table
	 *
	 * @param mixed $what
	 */
	function delete($what=''){

		if(!$this->dumped){
			$this->dump();
		}
		if(func_num_args()>1){
			$what = get_params(func_get_args());
		}
		if(is_array($what)){
			$delete = "delete from {$this->source} ";
			if($what["conditions"]) $delete.=" where {$what["conditions"]} ";
		} else {
			if(is_numeric($what)){
				$delete = "delete from {$this->source} where {$this->primary_key[0]} = '$what'";
			} else{
				if($what){
					$delete = "delete from {$this->source} where $what";
				} else {
					$delete = "delete from {$this->source} where {$this->primary_key[0]} = '{$this->{$this->primary_key[0]}}'";
				}
			}
		}
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		if(method_exists($this, "before_delete")){
			if($this->id) $this->find($this->id);
			if($this->before_delete()=='cancel') {
				return false;
			}
		} else {
			if(isset($this->before_delete)){
				if($this->id) $this->find($this->id);
				$method = $this->before_delete;
				if($this->$method()=='cancel') {
					return false;
				}
			}
		}
		$val = $this->db->query($delete);
		if($val){
			if(method_exists($this, "after_delete")){
				if($this->after_delete()=='cancel') {
					return false;
				}
			} else {
				if(isset($this->after_delete)){
					$method = $this->after_delete;
					if($this->$method()=='cancel') {
						return false;
					}
				}
			}
		}
		return $val;
	}

	/**
	 * Delete All data from Relational Map Table
	 *
	 */
	function delete_all(){
		if(!$this->db){
			$this->db = db::raw_connect();
		}
		$this->db->debug = $this->debug;
		$this->db->logger = $this->logger;
		if(!$this->dumped) {
			$this->dump();
		}
		$this->db->query("delete from {$this->source}");
	}

	/**
	 * Validate that Attributes cannot have a null value
	 */
	function validates_presence_of(){
		if(!$this->dumped){
			$this->dump();
		}
		if(!is_array($this->not_null)){
			$this->not_null = array();
		}
		if(func_num_args()){
			$params = func_get_args();
		} else return true;
		if(is_array($params[0])) {
			$params = $params[0];
		}
		foreach($params as $p){
			if(!in_array($p, $this->fields)){
				throw new ActiveRecordException('No se puede validar presencia de "'.$p.'"
					en el modelo '.$this->source.' porque no existe el atributo</u><br>
					Verifique que el atributo este bien escrito y/o exista en la relación ');
				return false;
			}
			if(!in_array($p, $this->not_null)){
				$this->not_null[] = $p;
			}
		}
		return true;
	}

	/**
	 * Valida el tamaño de ciertos campos antes de insertar
	 * o actualizar
	 *
	 * @return boolean
	 */
	public function validates_length_of(){
		if(!$this->dumped){
			$this->dump();
		}
		if(func_num_args()){
			$params = func_get_args();
		} else return true;
		if(!is_array($this->validates_length)){
			$this->validates_length = array();
		}
		if(is_array($params[0])){
			foreach($params as $param){
				$param = get_params($param);
				$this->validates_length[$param[0]] = array(
				"minimum" => $param['minimum'],
				"maximum" => $param['maximum']
				);
			}
		} else {
			$param = get_params($params);
			$this->validates_length[$param[0]] = array(
			"minimum" => $param['minimum'],
			"maximum" => $param['maximum']
			);
		}
		return true;
	}

	/**
	 * Verifica si un campo es de tipo de dato numerico o no
	 *
	 * @param string $field
	 * @return boolean
	 */
	public function is_a_numeric_type($field){		
		if(
		strpos(" ".$this->data_type[$field], "int")||
		strpos(" ".$this->data_type[$field], "decimal")||
		strpos(" ".$this->data_type[$field], "number")
		){
			return true;
		} else return false;
	}

	/**
	 * Obtiene los datos de los metadatos generados por Primera vez en la Sesión
	 *
	 * @param string $table
	 * @return array
	 */
	static function get_meta_data($table){
		if(isset(self::$models[$table])){
			return self::$models[$table];
		} else {
			if(isset($_SESSION['KUMBIA_META_DATA'][$_SESSION['KUMBIA_PATH']][$table])){
				self::set_meta_data($table, $_SESSION['KUMBIA_META_DATA'][$_SESSION['KUMBIA_PATH']][$table]);
				return self::$models[$table];
			}
			return array();
		}
	}

	/**
	 * Crea un registro de meta datos para la tabla especificada
	 *
	 * @param string $table
	 * @param array $meta_data
	 */
	static function set_meta_data($table, $meta_data){
		if(!isset($_SESSION['KUMBIA_META_DATA'][$_SESSION['KUMBIA_PATH']][$table])){
			$_SESSION['KUMBIA_META_DATA'][$_SESSION['KUMBIA_PATH']][$table] = $meta_data;
		}
		self::$models[$table] = $meta_data;
		return true;
	}
	
	
	/*******************************************************************************************
	 * Metodos para generacion de relaciones
	 *******************************************************************************************/
	
	/**
	 * Crea una relacion 1-1 entre dos modelos
	 *
	 * @param string $relation
	 */
	public function has_one($relation){
		if(!in_array($relation, $this->_has_one)){
			$this->_has_one[] = $relation;
		}
	}
	
	/**
	 * Crea una relacion 1-1 inversa entre dos modelos
	 *
	 * @param string $relation
	 */
	public function belongs_to($relation){		
		if(!in_array($relation, $this->_belongs_to)){
			$this->_belongs_to[] = $relation;			
		}
	}
	
	/**
	 * Crea una relacion 1-n entre dos modelos
	 *
	 * @param string $relation
	 */
	public function has_many($relation){
		if(!in_array($relation, $this->_has_many)){
			$this->_has_many[] = $relation;
		}
	}
	
}


/**
 * Clase para manejar errores ocurridos en operaciones de
 * ActiveRecord
 *
 */
class ActiveRecordException extends Exception {
	
	public $message;
	public $extended_message;
	
	/**
	 * Constructor de la clase;
	 *
	 */
	public function __construct($message, $extended_message=''){
		$this->message = $message;
		$this->extended_message = $extended_message;
		parent::__construct($message);
	}
	
	/**
	 * Muestra el mensaje de error de la excepción
	 *
	 */
	public function show_message(){
		Flash::error("
		<span style='font-size:24px'>KumbiaException: ".htmlentities($this->message)."</span><br/>
		<div>".htmlentities($this->extended_message)."<br>
		<span style='font-size:12px'>En el archivo <i>{$this->getFile()}</i> en la línea: <i>{$this->getLine()}</i></span></div>", true);
		print "<pre style='border:1px solid #969696; background: #FFFFE8'>";
		print $this->getTraceAsString()."\n";
		print "</pre>";
	}
	
	static function display_warning($title, $message, $source){
		Flash::warning("
		<span style='font-size:24px'>KumbiaWarning: $title</span><br/>
		<div>$message<br>
		<span style='font-size:12px'>En el modelo <i>{$source}</i> al ejecutar <i>{$_REQUEST['controller']}/{$_REQUEST['action']}/{$_REQUEST['id']}</i></span></div>", true);
		print "<pre style='border:1px solid #969696; background: #FFFFE8'>";
		print debug_print_backtrace()."\n";
		print "</pre>";
	}
	
}

?>