<?php

/*

  Пример работы:

  class User extends Model
  {
  function __construct($fromArray = array())
  {
  $this->_tableName_ = 'Users';
  parent::__construct($fromArray);
  }

  static function model()
  {
  return new self();
  }
  }

  Просто новый пользователь:
  new User();

  Новый пользователь из массива данных:
  new User(array('ID'=>12, 'Name'=>'John', 'Fname'=>'Doe'));

  Берём пользователя по ID:
  User::model()->getByPK(2);

  Всех пользователей:
  User::model()->getAll()

  Или всех пользователей с параметрами:
  User::model()->getAll(array('where'=>'`ID` > 5', 'order'=>'`Name` ASC'))

 */

abstract class Model
{

	// Relations
	const TO_ONE = 'TO_ONE';
	const TO_MANY = 'TO_MANY';

	private $_primaryKey_;
	protected $_tableName_;
	private $_id_;
	private $_vals_ = array();
	protected $_relations_ = array();

	function __construct($fromArray = array())
	{
		$this->_primaryKey_ = $this->getPk();

		if ($fromArray) {
			$this->setValues($fromArray);
		}
	}

	/**
	 * TODO: Проверять, есть ли такие поля в БД.
	 * @param type $name
	 * @param type $value
	 */
	function __set($name, $value)
	{
		$this->_vals_[$name] = $value;

		if ($name == $this->_primaryKey_) {
			$this->_id_ = $value;
		}
	}

	function __get($name)
	{
		if (isset($this->_vals_[$name])) {
			return $this->_vals_[$name];
		} else {
			if (isset($this->_relations_[$name])) {
				extract($this->_relations_[$name]);
				$model = new $targetModel;

				switch ($relationType) {
					case self::TO_ONE:
						return $model::model()->getOne($targetField, $this->$localField);
						break;
					case self::TO_MANY:
						return $model::model()->getAll(array('where' => "`$targetField` = '" . $this->$localField . "'"));
						break;
					default:
						throw new Exception('Undefined relation type');
						break;
				}
			}
		}
		return null;
	}

	/**
	 * Получаем Primary Key
	 * @return type
	 */
	private function getPk()
	{
		$res = App::DB()->query("SHOW KEYS FROM " . $this->_tableName_ . " WHERE Key_name = 'PRIMARY'")->fetch(PDO::FETCH_ASSOC);
		return $res["Column_name"];
	}

	/**
	 * Устанавливает отношение с другой таблицей
	 * @param string $nameOfRelation - название отношения. В дальнейшем по этому имени будт происходить обращение.
	 * @param string $localField - локальные данные по которым происходит связывание.
	 * @param string $targetModel - имя внешней модели (с которой хотим связаться).
	 * @param string $targetField - имя поля в удалённой таблице.
	 * @param const $relationType - тип связи (к одному / ко многим).
	 * @return obkect/array
	 */
	public function setRelation($nameOfRelation, $localField, $targetModel, $targetField, $relationType)
	{
		return $this->_relations_[$nameOfRelation] = array(
			'localField' => $localField,
			'targetModel' => $targetModel,
			'targetField' => $targetField,
			'relationType' => $relationType,
		);
	}

	/**
	 * Возвращает объект - запись в БД.
	 * @param int $id
	 * @return \Model
	 */
	function getByPK($id)
	{
		$this->_vals_ = App::DB()->query('SELECT * FROM ' . $this->_tableName_ . ' WHERE ' . $this->_primaryKey_ . ' = ' . $id)->fetch(PDO::FETCH_ASSOC);
		$this->_id_ = $this->_vals_[$this->_primaryKey_];
		return $this;
	}

	/**
	 * Возвращает массив с объектами, соответствующими записями в БД.
	 * @param array $params ['where'] просто строка с условиями: (`id` > 1) AND (`name` == 'TEST'); ['order'] строка с именем толбца и напрабления: `summ` DESC
	 * @return array с объектами
	 */
	function getAll($params = array())
	{
		$sql = 'SELECT * FROM `' . $this->_tableName_ . '`';
		$sql .= isset($params['where']) && $params['where'] ? ' WHERE ' . $params['where'] : '';
		$sql .= isset($params['order']) && $params['order'] ? ' ORDER BY ' . $params['order'] : '';
		$sql .= isset($params['limit']) && $params['limit'] ? ' LIMIT ' . $params['limit'] : '';
		$res = App::DB()->query($sql);

		$ready = array();

		$calledClass = get_called_class();
		while ($res && $row = $res->fetch(PDO::FETCH_ASSOC)) {
			if (isset($params['noKeys'])) {
				$ready[] = isset($params['asArray']) && $params['asArray'] ? $row : new $calledClass($row);
			} else {
				if (isset($params['asArrayOf'])) {
					$ready[] = isset($params['asArray']) && $params['asArray'] ? $row : new $calledClass($row);
				} else {
					$ready[$row[$this->_primaryKey_]] = isset($params['asArray']) && $params['asArray'] ? $row : new $calledClass($row);
				}
			}
		}

		return (isset($params['limit']) && $params['limit'] == 1) ? array_shift($ready) : $ready;
	}

	/**
	 * Возвращает один элемент, соотбетствующий параметрам
	 * @param string $whereField - имя поля по которому производим поиск
	 * @param type $whereValue - искомое значение
	 * @return type
	 */
	function getOne($whereField, $whereValue)
	{
		return $this->getAll(array(
					'where' => '`' . $whereField . '` = "' . $whereValue . '"',
					'limit' => 1
		));
	}

	/**
	 * Удаляет запись из БД.
	 * @param   int $id
	 * @return boolean
	 */
	function delete($id)
	{
		return $id ? App::DB()->query('DELETE FROM `' . $this->_tableName_ . '` WHERE `' . $this->_primaryKey_ . '` = ' . $id) : false;
	}

	/**
	 * Добавляет новую запись в БД.
	 * @param array $values - ассоциативный массив соо значениями.
	 * @return object - id добавленной записи или false.
	 */
	function add2($values)
	{
		$values = $this->_vals_;
		unset($values[$this->_primaryKey_]);

		$q = array();
		foreach ($values as $val) {
			$q[] = '?';
		}

		var_dump('INSERT INTO `' . $this->_tableName_ . '` (`' . implode('`, `', array_keys($values)) . '`) VALUES (' . implode(', ', array_values($q)) . ') ');
		die;
		$res = App::DB()->prepare_query('INSERT INTO `' . $this->_tableName_ . '` (`' . implode('`, `', array_keys($values)) . '`) VALUES (' . implode(', ', array_values($q)) . ') ', self::arrayWithTypes($values));

		if ($res) {
			throw new Exception('MySQL error #' . $res->errno . ' - ' . $res->error);
		}

		return $this->getByPK(App::DB()->insert_id);
	}

	function add($values)
	{
		$request = App::DB()->prepare('INSERT INTO ' . $this->_tableName_ . ' (' . implode(',', array_keys($values)) . ') values (:' . implode(', :', array_keys($values)) . ')');
		if ($request->execute((array) $values)) {
			return $this->getByPK(App::DB()->lastInsertId());
		} else {
			return false;
		}
	}

	/**
	 * Устанавливает значения полей из массива. Нужно проверять, есть ли такие поля в таблице.
	 * @param array $values - ассоциативный массив со значениями.
	 */
	function setValues($values)
	{
		if (isset($values[$this->_primaryKey_])) {
			$this->_id_ = $values[$this->_primaryKey_];
		}

		$this->_vals_ = $values;
	}

	/**
	 * Сохраняет объект в БД.
	 * @return object/boolean
	 */
	function save2()
	{
		if (!$this->_id_ || !$this->_vals_) {
			if ($this->_vals_) {
				return $this->add($this->_vals_);
			} else {
				return FALSE;
			}
		}

		$values = $this->_vals_;
		unset($values[$this->_primaryKey_]);
		$keys = array_keys($values);
		$values = array_values($values);
		$values[] = $this->_id_;
		$res = App::DB()->prepare_query('UPDATE `' . $this->_tableName_ . '` SET `' . implode('`=?, `', array_values($keys)) . '`=? WHERE `' . $this->_primaryKey_ . '`=?', self::arrayWithTypes($values));

		return $res ? $this : FALSE;
	}

	function save()
	{
		if (!$this->_id_ || !$this->_vals_) {
			if ($this->_vals_) {
				return $this->add($this->_vals_);
			} else {
				return FALSE;
			}
		}

		$values = $this->_vals_;
		unset($values[$this->_primaryKey_]);

		$request = App::DB()->prepare('UPDATE ' . $this->_tableName_ . ' SET ' . implode('=?, ', array_keys($values)) . '=? WHERE ' . $this->_primaryKey_ . '=' . $this->_id_);
		$res = $request->execute(array_values($values));

		return $res ? $this : FALSE;
	}

	/**
	 * Возвращает значения полей объекта в виде ассоциативного массива.
	 * @return array
	 */
	function getValues()
	{
		return $this->_vals_;
	}

	function removeId()
	{
		$this->_vals_[$this->_primaryKey_] = $this->_id_ = NULL;
	}

}
