<?php
class MY_Model extends CI_Model
{
	const DB_TABLE	   = 'abstract';
	const DB_TABLE_PK	= 'abstract';
	private $dirty_check = null;

	public function __construct()
	{
		parent::__construct();

		$this->load->driver('cache', array('adapter' => 'file', 'backup' => 'dummy'));

		// We must avoid loading fields on the base model because "abstract" is not a valid table.
		if(get_class($this) != "MY_Model")
		{
			$this->load_fields();
		}
	}

	private function load_fields()
	{
		// Grab each of the fields and create a class variable for it.
		// This saves us having to manually list the fields in our models.
		if(($query = $this->cache->get("describe:" . $this::DB_TABLE)) === false)
		{
			$query = $this->db->query("DESCRIBE `" . $this::DB_TABLE . "`");
			$query = $query->result();
			$this->cache->save("describe:" . $this::DB_TABLE, $query, 86400);
		}

		if(is_array($query))
		{
			foreach($query as $row)
			{
				$this->{$row->Field} = null;
			}
		}
	}

	private function get_fields()
	{
		//get table data
		if(($types = $this->cache->get("describe:" . $this::DB_TABLE)) === false)
		{
			$types = $this->db->query("DESCRIBE `" . $this::DB_TABLE . "`");
			$types = $types->result();
			$this->cache->save("describe:" . $this::DB_TABLE, $types, 86400);
		}

		$fields = array();

		if(is_array($types))
		{
			foreach($types as $type)
			{
				$fields[] = $type->Field;
			}
		}

		return $fields;
	}

	/**
	 * Check empty values and set them to their default values (ie: null, 0, etc).
	 * @return [type] [description]
	 */
	private function check_datatypes()
	{
		//get table data
		if(($types = $this->cache->get("describe:" . $this::DB_TABLE)) === false)
		{
			$types = $this->db->query("DESCRIBE `" . $this::DB_TABLE . "`");
			$types = $types->result();
			$this->cache->save("describe:" . $this::DB_TABLE, $types, 86400);
		}
		$fields = array();

		//store specific info about each field
		if(is_array($types))
		{
			foreach($types as $type)
			{
				$fields[$type->Field] 			= new StdClass();
				$fields[$type->Field]->null 	= strtolower($type->Null);
				$fields[$type->Field]->type 	= strtolower(preg_replace('/\(\d*\)/', '', $type->Type));
				$fields[$type->Field]->default 	= $type->Default;
				$fields[$type->Field]->key 		= $type->Key;
			}
		}

		//get fields for this class
		$vars = get_object_vars($this);

		foreach($vars as $name => $value)
		{
			// We only care about empty values. If the value is not empty, skip it. It will be saved as-is.
			if(!is_null($this->{$name}) && $this->{$name} !== "" && $this->{$name} !== 0 && $this->{$name} !== "0")
			{
				continue;
			}

			if(!isset($fields[$name]))
			{
				continue;
			}

			$zero = $this->{$name} === 0 || $this->{$name} === "0";

			// If this field is not a key and is zero, save it as zero.
			if($fields[$name]->key == "" && $zero)
			{
				$this->{$name} = 0;
			}
			// Regardless of what type this is, if we're allowed to be NULL or this field is a key, lets be NULL.
			else if(($fields[$name]->null == "yes" || $fields[$name]->key != "") && $fields[$name]->default !== 0)
			{
				$this->{$name} = null;
			}
			// Otherwise, set the value to the database-defined default.
			else if($fields[$name]->null == "no")
			{
				//If the data type is a (BIG|TINY)INT, our default must be zero on newer MySQL.
				//Otherwise, empty string works great.
				if(preg_match('/int( unsigned)?$/i', $fields[$name]->type))
				{
					$default = 0;
				}
				else
				{
					$default = "";
				}

				$this->{$name} = $fields[$name]->default !== "" && $fields[$name]->default != null ? $fields[$name]->default : $default;
			}
		}
	}

	/**
	 * Create record.
	 * @return int ID of the new record.
	 */
	private function insert()
	{
		$this->check_datatypes();

		$sql = $this->db->insert_string($this::DB_TABLE, $this->clean_fields($this));

	   	if($this->db->query($sql))
		{
			$this->{$this::DB_TABLE_PK} = $this->db->insert_id();

			return $this->{$this::DB_TABLE_PK};
		}
		else
		{
			return false;
		}
	}

	/**
	 * Update record.
	 */
	private function update()
	{
		$this->check_datatypes();

		$sql = $this->db->update_string($this::DB_TABLE, $this->clean_fields($this), array($this::DB_TABLE_PK => $this->{$this::DB_TABLE_PK}));
		return $this->db->query($sql);
	}

	/**
	 * Populate from an array or standard class.
	 * @param mixed $row
	 */
	public function populate($row)
	{
		if(!is_array($row) && !is_object($row))
		{
			return false;
		}

		foreach ($row as $key => $value)
		{
			$this->$key = $value;
		}

		$this->dirty_check = $this->fingerprint();

		return true;
	}

	/**
	 * Read class variables from POST and assign them to our class variables.
	 * Password fields are automatically hashed.
	 *
	 * @access public
	 * @param mixed $super
	 * @return void
	 */
	public function readPostVars($data = null)
	{
		if($data == null)
		{
			$data = $this->input->post();
		}

		foreach($this as $key => $value)
		{
			// Skip any arrays in our model classes.
			// if(!isset($data["$key"]) || is_array($this->$key))
			if(is_array($this->$key))
			{
				continue;
			}

			//special action for password fields
			if($key == "password")
			{
				$this->load->helper("password");

				//if there's a new value, hash it
				if($this->input->post("$key"))
				{
					$new_value = password_hash($data["$key"], PASSWORD_BCRYPT);
				}
				//otherwise, keep the old (already hashed) value
				else
				{
					$new_value = $value;
				}
			}
			else
			{
				$new_value = isset($data["$key"]) ? $data["$key"] : false;

				// Any fields NOT sent by the browser will return false.
				// This includes un-checked checkboxes!  We must set these values to 0.
				if($new_value === false)
				{
					$new_value = 0;
				}
			}

			$this->$key = $new_value;
		}
	}

	/**
	 * Load from the database.
	 * @param int $id
	 */
	public function load($id)
	{
		$key = $this::DB_TABLE . ":" . $id;

		if(($query = $this->cache->get($key)) === false)
		{
			$query = $this->db->query("SELECT * FROM `" . $this::DB_TABLE . "` WHERE `" . $this::DB_TABLE_PK . "`=?", array($id));

			if(is_object($query))
			{
				$query = $query->row();
				$this->cache->save($key, $query, 86400);
			}
		}

		if(is_object($query))
		{
			return $this->populate($query);
		}
		else
		{
			return false;
		}
	}

	/**
	 * Delete the current record.
	 */
	public function delete()
	{
		$this->db->query("DELETE FROM `" . $this::DB_TABLE . "` WHERE `" . $this::DB_TABLE_PK . "`=?", array($this->{$this::DB_TABLE_PK}));

		if($this->db->affected_rows() != -1)
		{
			$this->cache->clean();
			unset($this->{$this::DB_TABLE_PK});
			return true;
		}

		return false;
	}

	/**
	 * Save the record.
	 */
	public function save()
	{
		// Nothing changed, no need to save.
		if($this->fingerprint() === $this->dirty_check)
		{
			return true;
		}

		// $this->db->trans_start();
		if (isset($this->{$this::DB_TABLE_PK}) && $this->{$this::DB_TABLE_PK})
		{
			$this->update();
			$id = $this->{$this::DB_TABLE_PK};
		}
		else
		{
			$id = $this->insert();
		}
		// $this->db->trans_complete();

		if($this->db->affected_rows() === -1)
		{
			return false;
		}
		else
		{
			$this->cache->clean();
			return $this->load($id);
		}
	}

	/**
	 * Get an array of Models with an optional limit, offset.
	 *
	 * @param int $limit Optional.
	 * @param int $offset Optional; if set, requires $limit.
	 * @return array Models populated by database, keyed by PK.
	 */
	public function get($limit = 0, $offset = 0)
	{
		if ($limit)
		{
			$query = $this->db->query("SELECT * FROM `" . $this::DB_TABLE . "` LIMIT $limit,$offset");// $this->db->get($this::DB_TABLE, $limit, $offset);
		}
		else
		{
			$query = $this->db->query("SELECT * FROM `" . $this::DB_TABLE . "`"); //$this->db->get($this::DB_TABLE);
		}
		$ret_val = array();
		$class = get_class($this);
		foreach ($query->result() as $row)
		{
			$model = new $class;
			$model->populate($row);
			$ret_val[$row->{$this::DB_TABLE_PK}] = $model;
		}
		return $ret_val;
	}

	/**
	 * Remove public member variables that don't map to any fields but break the SQL.
	 * @param  [type] $obj [description]
	 * @return [type]	  [description]
	 */
	private function clean_fields($obj)
	{
		$vars 	= get_object_vars($obj);
		$fields = $this->get_fields();

		foreach($vars as $name => $value)
		{
			try
			{
				$rp = new ReflectionProperty($this, $name);

				if($rp->isPublic() && !in_array($name, $fields))
				{
					unset($obj->{$name});
				}
			}
			catch(Exception $e)
			{}

			// FYI: This breaks things.
			// It seemingly fixed something once, but breaks attached objects (ie: tags).
			// if(is_array($obj->{$name}) || is_object($obj->{$name}))
			// {
			// 	unset($obj->{$name});
			// }
		}

		return $obj;
	}

	public function tokenize($result, $id_field, $name_field)
	{
		$list = array();

		foreach($result as $item)
		{
			$list[] = array("id" => (int)$item->$id_field, "name" => $item->$name_field);
		}

		return $list;
	}

	private function fingerprint()
	{
		$fingerprint = "";

		if(($query = $this->cache->get("describe:" . $this::DB_TABLE)) === false)
		{
			$query = $this->db->query("DESCRIBE `" . $this::DB_TABLE . "`");
			$query = $query->result();
			$this->cache->save("describe:" . $this::DB_TABLE, $query, 86400);
		}

		if(is_array($query))
		{
			foreach($query as $row)
			{
				if(is_array($this->{$row->Field}) || is_object($this->{$row->Field}))
				{
					$fingerprint .= json_encode($this->{$row->Field});
				}
				else
				{
					$fingerprint .= $this->{$row->Field};
				}
			}
		}

		return sha1($fingerprint);
	}
}
