<?php defined('SYSPATH') or die('No direct access allowed.');

/**
 * This class overrides Kohana's ORM
 *
 * This file contains enhancements for Kohana, that should be considered upstream and maybe havent been yet.
 *
 * @package    lnApp
 * @category   Modifications
 * @author     Deon George
 * @copyright  (c) 2014 Deon George
 * @license    http://dev.leenooks.net/license.html
 */
abstract class lnApp_ORM extends Kohana_ORM {
	protected $_table_names_plural = FALSE;
	protected $_model_names_plural = FALSE;
	private $_object_formated = array();
	private $_formated = FALSE;

	protected $_created_column = array('column'=>'date_orig','format'=>TRUE);
	protected $_updated_column = array('column'=>'date_last','format'=>TRUE);

	// Our filters used to display values in a friendly format
	protected $_display_filters = array();

	// Our attributes used in forms.
	protected $_form = array();

	// Our attribute blobs that should be compressed
	protected $_compress_column = array();

	// Our attributes that should be converted to NULL when empty
	protected $_nullifempty = array();

	// Our attribute values that need to be stored as serialized
	protected $_serialize_column = array();

	// If we need to load any sub items on loading this model
	protected $_sub_items = array();
	protected $_sub_items_load = array();
	protected $_sub_items_sorted = FALSE;

	// Whether to show a SystemMessage when a record is saved.
	protected $_save_message = FALSE;

	/**
	 * Auto process some data as it comes from the database
	 * @see parent::__get()
	 */
	public function __get($column) {
		if (array_key_exists($column,$this->_table_columns)) {
			// If the column is a blob, we'll decode it automatically
			if (
				$this->_table_columns[$column]['data_type'] == 'blob'
				AND in_array($column,$this->_compress_column)
				AND ! is_null($this->_object[$column])
				AND ! isset($this->_changed[$column])
				AND (! isset($this->_table_columns[$column]['auto_convert']) OR ! $this->_table_columns[$column]['auto_convert'])
			) {

				// In case our blob hasnt been saved as one.
				try {
					 $this->_object[$column] = $this->_blob($this->_object[$column]);
				}
				catch(Exception $e) {
					HTTP_Exception::factory(501,Kohana_Exception::text($e));
				}

				$this->_table_columns[$column]['auto_convert'] = TRUE;
			}

			// If the column is a serialized object, we'll unserialize it.
			if (
				in_array($column,$this->_serialize_column)
				AND is_string($this->_object[$column])
				AND ! is_null($this->_object[$column])
				AND ! isset($this->_changed[$column])
				AND (! isset($this->_table_columns[$column]['unserialized']) OR ! $this->_table_columns[$column]['unserialized'])
			) {

				// In case our object hasnt been saved as serialized.
				try {
					$this->_object[$column] = unserialize($this->_object[$column]);
				}
				catch(Exception $e) {
					HTTP_Exception::factory(501,Kohana_Exception::text($e));
				}

				$this->_table_columns[$column]['unserialized'] = TRUE;
			}
		}

		return parent::__get($column);
	}

	/**
	 * Retrieve and Store DB BLOB data in compressed format.
	 */
	private function _blob($data,$set=FALSE) {
		try {
			return $set ? gzcompress($this->_serialize($data,$set)) : $this->_serialize(gzuncompress($data));

		// Maybe the data isnt compressed?
		} catch (Exception $e) {
			return $this->_serialize($data,$set);
		}
	}

	/**
	 * Format fields for display purposes
	 *
	 * @param string column name
	 * @return mixed
	 */
	private function _format() {
		foreach ($this->_display_filters as $column => $formats)
			$this->_object_formated[$column] = $this->run_filter($column,$this->__get($column),array($column=>$formats));

		$this->_formated = TRUE;
	}

	/**
	 * Intercept our object load, so that we can load our subitems
	 */
	protected function _load_values(array $values) {
		parent::_load_values($values);

		if ($this->_sub_items_load AND count($this->_sub_items_load) > 1)
			throw HTTP_Exception::factory('501','Sub Items doesnt support more than 1 load');

		$sort = array();
		if ($this->_loaded AND $this->_sub_items_load)
			foreach ($this->_sub_items_load as $item => $sort)
				$this->_sub_items = $this->$item->find_all()->as_array();

		if ($sort) {
			Sort::MAsort($this->_sub_items,$sort);
			$this->_sub_items_sorted = TRUE;
		}

		return $this;
	}

	/**
	 * If a column is marked to be nullified if it is empty, this is where it is done.
	 */
	private function _nullifempty(array $array) {
		foreach ($array as $k=>$v) {
			if (is_array($v)) {
				if (is_null($x=$this->_nullifempty($v)))
					unset($array[$k]);
				else
					$array[$k] = $x;

			} elseif (! $v AND $v !== 0 AND $v !== '0')
				unset($array[$k]);

		}

		return count($array) ? $array : NULL;
	}

	/**
	 * Try and (un)serialize our data, and if it fails, just return it.
	 */
	private function _serialize($data,$set=FALSE) {
		try {
			return $set ? serialize($data) : unserialize($data);

		// Maybe the data serialized?
		} catch (Exception $e) {
			return $data;
		}
	}

	/**
	 * Overrides Kohana cache so that it can be globally disabled.
	 */
	public function cached($lifetime=NULL) {
		return $this->_db->caching($this->_table_name) ? parent::cached($lifetime) : $this;
	}

	public function clear() {
		$this->_formated = FALSE;
		$this->_object_formated = array();

		return parent::clear();
	}

	/**
	 * Return a formated columns, as per the model definition
	 */
	public function display($column,$default='') {
		// Trigger a load of the record.
		$value = $this->__get($column);

		if (! $value)
			$value = $default;

		// If some of our fields need to be formated for display purposes.
		if (! $this->_formated AND $this->_display_filters)
			$this->_format();

		if (isset($this->_object_formated[$column]))
			return $this->_object_formated[$column];
		else
			return is_array($value) ? join(', ',$value) : $value;
	}

	public function display_filters(array $filters) {
		$this->_display_filters = Arr::merge($this->_display_filters,$filters);
	}

	public function dump() {
		$result = array();

		$result['this'] = $this->object();

		if (isset($this->_sub_items))
			foreach ($this->_sub_items as $o)
				$result['sub'][] = $o->dump();

		return $result;
	}

	/**
	 * This function is our AJAX helper, used by module list_autocomplete()
	 */
	public function list_autocomplete($term,$index,$value,array $label,array $limit=array(),array $options=NULL) {
		$result = array();

		$query = empty($options['object']) ? $this : $options['object'];

		foreach ($limit as $w) {
			list($k,$s,$v) = $w;

			$query->and_where($k,$s,$v);
		}

		$c = 0;
		foreach ((empty($options['object']) ? $query->find_all() : $query->execute()) as $o) {
			// If we got here via a DB query, we need to reload our ORM object from the result.
			if (! is_object($o)) {
				if (empty($options['key']))
					throw new Kohana_Exception('Missing key for non object');

				$o = $this->clear()->where($options['key'],'=',$o[$options['key']])->find();
			}

			switch ($index) {
				case 'url':
					if (empty($options['urlprefix']))
						throw new Kohana_Exception('Missing URL Prefix');

					$v = $options['urlprefix'].$o->resolve($value);

					break;

				default: $v = $o->resolve($value);
			}

			$k = '';
			foreach ($label as $k => $details)
				foreach ($details as $lvalue)
					$k = preg_replace('/%s/',$o->resolve($lvalue),$k,1);

			$result[$c++] = array(
				'value'=>$v,
				'label'=>$k,
			);
		}

		return $result;
	}

	/**
	 * Return an array of data that can be used in a SELECT statement.
	 * The ID and VALUE is defined in the model for the select.
	 */
	public function list_select($blank=FALSE,$object=NULL) {
		$result = array();

		if (is_null($object))
			$object = $this->find_all();

		if ($blank)
			$result[NULL] = '';

		if ($this->_form AND array_intersect(array('id','value'),$this->_form))
			foreach ($object as $o)
				$result[$o->{$this->_form['id']}] = $o->resolve($this->_form['value']);

		return $result;
	}

	/**
	 * Replace this ORM object with one in the database
	 */
	public function replace(array $key,$id='id') {
		$o = ORM::factory(ucfirst($this->_object_name),$key);

		foreach ($this->changed() as $k)
			$o->{$k} = $this->{$k};

		$o->_sub_items = $this->_sub_items;

		return $o->save();
	}

	/**
	 * This function is used so that methods can be called via variables
	 */
	public function resolve($key) {
		eval("\$x = \$this->$key;");

		return $x;
	}

	public function save(Validation $validation=NULL) {
		// Find any fields that have changed, and process them.
		if ($this->_changed)
			foreach ($this->_changed as $c) {
				// Any fields that are blobs, and encode them.
				if (! is_null($this->_object[$c]) AND $this->_table_columns[$c]['data_type'] == 'blob' AND in_array($c,$this->_compress_column)) {
					$this->_object[$c] = $this->_blob($this->_object[$c],TRUE);

					// We need to reset our auto_convert flag
					if (isset($this->_table_columns[$c]['auto_convert']))
						$this->_table_columns[$c]['auto_convert'] = FALSE;

				// Any fields that should be seriailzed, we'll do that.
				} elseif (is_array($this->_object[$c]) AND in_array($c,$this->_serialize_column)) {
					$this->_object[$c] = serialize($this->_object[$c]);
				}

				// Test if the value has still changed
				if ($this->_original_values AND $this->_object[$c] == $this->_original_values[$c])
					unset($this->_changed[$c]);
			}

		parent::save();

		if ($this->saved() AND $this->_save_message AND (PHP_SAPI !== 'cli'))
			SystemMessage::factory()
				->title('Record Updated')
				->type('success')
				->body(sprintf('Record %s:%s Updated',$this->_table_name,$this->id));

		return $this;
	}

	public function subitems() {
		return $this->_sub_items;
	}

	public function subitem_add(Model $item) {
		array_push($this->_sub_items,$item);
	}

	public function subitem_get($model,array $key) {
		$class = 'Model_'.$model;

		foreach ($this->_sub_items as $o)
			if ($o instanceof $class AND array_intersect($o->object(),$key) === $key)
				return $o;

		$item = ORM::factory($model);
		array_push($this->_sub_items,$item);

		return $item;
	}

	/**
	 * Override the Kohana processing so we can null values if required.
	 * We override this function, because we do set our own primary key value
	 */
	public function values(array $values, array $expected = NULL) {
		foreach ($values as $k=>$v) {
			// Convert to NULL
			if (in_array($k,$this->_nullifempty)) {
				if (is_array($v))
					$values[$k] = $this->_nullifempty($v);

				elseif (! $v AND $v !== 0 AND $v !== '0')
					$values[$k] = NULL;
			}
		}

		parent::values($values,$expected);

		if (isset($values[$this->_primary_key]))
			$this->{$this->_primary_key} = $values[$this->_primary_key];

		return $this;
	}

	public function what_changed() {
		$result = array();

		foreach ($this->changed() as $k) {
			$result[$k]['old'] = ($x=Arr::get($this->_original_values,$k)) ? $x : serialize($x);
			$result[$k]['new'] = ($x=Arr::get($this->_object,$k)) ? $x : serialize($x);
		}

		return $result;
	}
}
?>