<?php

// Special data types
define("MODEL_SERIAL", 			0x1000); // auto increment id
define("MODEL_OBJECT", 			0x1001); // Link to another object type by id
define("MODEL_CREATED",			0x1002); // CREATED timestamp
define("MODEL_UPDATED",			0x1003); // UPDATED timestamp

// Numeric data types
define("MODEL_BIT", 			0x2000);
define("MODEL_TINYINT", 		0x2001);
define("MODEL_BOOL",			0x2002);
define("MODEL_SMALLINT",		0x2003);
define("MODEL_MEDIUMINT",		0x2004);
define("MODEL_INT", 			0x2005);
define("MODEL_BIGINT", 			0x2006);
define("MODEL_FLOAT",			0x2007);
define("MODEL_DOUBLE",			0x2008);
define("MODEL_DECIMAL",			0x2009);

// String data types
define("MODEL_CHAR", 			0x3000);
define("MODEL_VARCHAR",			0x3001);
define("MODEL_BINARY",			0x3002);
define("MODEL_VARBINARY",		0x3003);
define("MODEL_TINYBLOB",		0x3004);
define("MODEL_TINYTEXT",		0x3005);
define("MODEL_TEXT",			0x3006);
define("MODEL_BLOB",			0x3007);
define("MODEL_MEDIUMTEXT",		0x3008);
define("MODEL_MEDIUMBLOB",		0x3009);
define("MODEL_LONGTEXT",		0x300A);
define("MODEL_LONGBLOB",		0x300B);
define("MODEL_ENUM",			0x300C);
define("MODEL_SET",				0x300D);

// Date types
define("MODEL_DATE",			0x4000);
define("MODEL_DATETIME",		0x4001);
define("MODEL_TIMESTAMP",		0x4002);
define("MODEL_TIME",			0x4003);
define("MODEL_YEAR",			0x4004);

define("MODEL_INDEX", 12);
define("MODEL_FULLTEXT", 13);
define("MODEL_UNIQUE", 14);



class Model implements JsonSerializable {

	public $_fields = []; // List of fields in the DB table
	public $_virgin = []; // Virgin, untouched data loaded from the DB
	public $_data = []; // Current (modified) data as stored in the DB
	public $_objects = []; // Objects constructed from the values in _data

	public $_timeout = 1;

	public $_valid = false;
	public $_loaded = false;

	public static $_cache = null;

	static protected $_raw = false;

	public function __construct($id = null, $deep = true) {
		$this->_valid = false;
		if ($id !== null) {
			$this->_load_record($id);
			$this->_loaded = $this->_valid;
		} else {
			$this->_get_fields();
			$this->_loaded = false;
			$this->_valid = true;
		}
	}

	public static function cache_connect($host, $port, $weight = 50) {
		if (Model::$_cache === null) {
			Model::$_cache = new Memcached();
		}
		Model::$_cache->addServer($host, $port, $weight);
	}

	private function _load_record($id) {

		$class = get_called_class();
		if (property_exists($class, "table")) {
			$v = get_class_vars($class);
			$table = $v["table"];
		} else {
			$table = strtolower($class);
		}

		$r = DB::getInstance()->select($table, $id);

		if ($r === false) {
			$this->_valid = false;
			return;
		}


		foreach ($r as $k=>$v) {
			$this->_fields[] = $k;
			$this->_virgin[$k] = $v;
		}

		if (property_exists($class, "transform")) {
			foreach ($this->_fields as $k) {
				if (array_key_exists($k, $this->transform)) {
					$v = $this->transform[$k];
					$f = "from_" . $v;
					if (method_exists($this, $f)) {
						$this->_data[$k] = $this->$f($this->_virgin[$k]);
					} else {
						$this->_data[$k] = $this->_virgin[$k];
					}
				} else {
					$this->_data[$k] = $this->_virgin[$k];
				}
			}
		} else {
			foreach ($this->_fields as $k) {
				$this->_data[$k] = $this->_virgin[$k];
			}
		}
		$this->_valid = true;
	}

	public static function find($where = []) {
		$class = get_called_class();
		if (property_exists($class, "table")) {
			$v = get_class_vars($class);
			$table = $v["table"];
		} else {
			$table = strtolower($class);
		}
		return new Query($class, $table, $where);
	}


	private function _get_fields() {
		$class = get_called_class();
		if ($class == "Model") { // Light model being used for xfer
			return;
		}
		if (property_exists($class, "table")) {
			$v = get_class_vars($class);
			$table = $v["table"];
		} else {
			$table = strtolower($class);
		}

		$this->_fields = [];

		$q = DB::getInstance()->query("describe `" . $table . "`");
		while ($r = DB::getInstance()->nextRecord($q)) {
			$this->_fields[] = $r->Field;
			$this->_data[$r->Field] = null;
		}
		$this->_valid = true;
	}

	public function from_json($data) {
		$d = json_decode($data);
		if ($d) return $d;
		return new stdClass;
	}

	public function to_json($data) {
		return json_encode($data);
	}

	public function valid() {
		return $this->_valid;
	}

	public function loaded() {
		return $this->_loaded;
	}

	public function save() {
		Model::raw_on();
		$class = get_called_class();
		if (property_exists($class, "table")) {
			$v = get_class_vars($class);
			$table = $v["table"];
		} else {
			$table = strtolower($class);
		}

		$_save = [];

		if (property_exists($class, "transform")) {
			foreach ($this->_fields as $k) {
				if (array_key_exists($k, $this->transform)) {
					$v = $this->transform[$k];
					$f = "to_" . $v;
					if (method_exists($this, $f)) {
						$_save[$k] = $this->$f(@$this->$k);
					} else {
						$v = __unref($this->$k);
						if (is_string($v)) $v = trim($v);
						$_save[$k] = $v;
					}
				} else {
					$v = __unref($this->$k);
					if (is_string($v)) $v = trim($v);
					$_save[$k] = $v;
				}
			}
		} else {
			foreach ($this->_fields as $k) {
				$v = __unref($this->$k);
				if (is_string($v)) $v = trim($v);
				$_save[$k] = $v;
			}
		}

		$_update = [];

		foreach ($_save as $k=>$v) {
			if (array_key_exists($k, $this->_virgin)) {
				if ($_save[$k] != $this->_virgin[$k]) {
					$_update[$k] = $v;
					$this->_virgin[$k] = $v;
				}
			} else {
				$_update[$k] = $v;
				$this->_virgin[$k] = $v;
			}
		}

		$triggers = [];
		if ($this->_loaded) {
			if (count($_update) > 0) {
				if (in_array("updated", $this->_fields)) {
					$_update['updated'] = time();
				}
	
				DB::getInstance()->update($table, $this->id, $_update);
			}
		} else {
			$_update = $this->_virgin;
			if (in_array("created", $this->_fields)) {
				$_update['created'] = time();
			}
			$this->id = DB::getInstance()->insert($table, $_update);
			$this->_loaded = true;
		}

		foreach ($_update as $k=>$v) {
			if (property_exists($class, "_triggers")) {
				if (array_key_exists($k, $this->_triggers)) {
					$triggers[$this->_triggers[$k]] = 1;
				}
			}
		}

		foreach ($triggers as $trigger=>$count) {

                        if ($trigger instanceof \Closure) {
				$trigger->call($this);
				continue;
                        }

                        if (is_array($trigger)) {
                                $c = $trigger[0];
                                $f = $trigger[1];
                                $c::$f();
				continue;
                        }

			if (is_string($trigger)) {
				$this->$trigger();
				continue;
			}
		}

		Model::raw_off();

	}

	public static function raw_on() {
		Model::$_raw = true;
	}

	public static function raw_off() {
		Model::$_raw = false;
	}

	static public function raw() {
		return Model::$_raw;
	}


	public function delete() {
        $class = get_called_class();
        if (property_exists($class, "table")) {
            $v = get_class_vars($class);
            $table = $v["table"];
        } else {
            $table = strtolower($class);
        }		

		if (method_exists($this, "on_delete")) {
			$this->on_delete();
		}

		DB::getInstance()->query("delete from `" . $table . "` where id=:id", ["id" => $this->_data["id"]]);
	}


	public function __toString() {
		return "" . $this->_data["id"];
	}

	public function __toInt() {
		return (int)$this->_data["id"];
	}

	public function jsonSerialize() : mixed {
		
		$out = [];
	
		foreach ($this->_fields as $f) {
			$out[$f] = $this->_data["$f"];
		}

		foreach ($this->_fields as $f) {
			if (array_key_exists($f, $this->_objects)) {
				$out[$f] = $this->_objects["$f"];
			}
		}

		if (property_exists($this, "_computed")) {
			foreach ($this->_computed as $k=>$v) {
				$out[$k] = $this->$v();
			}
		}

		return $out;
	}

	private function __get_field_class($f) {
		if (!property_exists($this, "_classes")) return null;
		if (!array_key_exists($f, $this->_classes)) return null;
		return $this->_classes[$f];
	}

	public function __get($k) {
		if (property_exists($this, "_computed")) {
			if (array_key_exists($k, $this->_computed)) {

				$c = $this->cache_get($k);
				if ($c) return $c;

				$func = $this->_computed[$k];
				$c = $this->$func();
				$this->cache_set($k, $c);
				return $c;
			}
		}


		if (!in_array($k, $this->_fields)) {
			return null;
		}

		$class = $this->__get_field_class($k);
		if ($class != null) {
			if (array_key_exists($k, $this->_objects)) return $this->_objects[$k];
		}
		return $this->_data[$k];
	}

	public function __set($key, $val) {
		if (!in_array($key, $this->_fields)) return false;
		$class = $this->__get_field_class($key);

		if ($class != null) {
			if ($val instanceof Model) {

				if ($val instanceof $class) {
					$this->_data[$val] = $val->id;
					$this->_objects[$key] = $val;
					return true;
				}
			
				throw new Exception('Class mismatch');
				return false;
			}

			if (is_numeric($val)) {
				$this->_data[$key] = $val;
				$this->_objects[$key] = new $class($val);
				return true;
			}

			$val = (int)$val;
			$this->_data[$key] = $val;
			$this->_objects[$key] = new $class($val);
			return true;
		}

		$this->_data[$key] = $val;
		unset($this->_objects[$key]);
		return true;
	}

	public function __isset($key) {
		if (!in_array($key, $this->_fields)) return false;
		return true;
	}

	public function __unset($key) {
		if (!in_array($key, $this->_fields)) return;
		$this->_data[$key] = null;
		unset($this->_objects[$key]);
	}

	public function load($key = null) {

		if ($key === null) {
			foreach ($this->_fields as $key) {
				$this->load($key);
			}
			return true;
		}
		if (!in_array($key, $this->_fields)) {
			return false;
		}
		$class = $this->__get_field_class($key);
		if ($class == null) {
			return false;
		}

		if ($this->_data[$key] === null) {
			return false;
		}

		$ob = new $class($this->_data[$key]);
		if (!$ob->valid()) return false;
		$this->_objects[$key] = $ob;
		return true;
	}

	private function cache_key($key) {
		$class = get_called_class();
		return sprintf("%s[%d]::%s", $class, $this->id, $key);
	}
		
	public function cache_set($key, $val) {
		$key = $this->cache_key($key);
		Model::$_cache->set($key, $val, $this->_timeout);
	}

	public function cache_get($key) {
		$key = $this->cache_key($key);
		return Model::$_cache->get($key);
	}

	public function cache_invalidate($key) {
		$key = $this->cache_key($key);
		Model::$_cache->delete($key);
		
	}
	
	public static function get_all_models() {
		$res = [];
		foreach (get_declared_classes() as $class) {
			if (is_subclass_of($class, "Model")) {
				$res[] = $class;
			}
		}
	}

	public function create_or_update_table() {
		
	}

	public function get_table_name() {
        $class = get_called_class();
        if (property_exists($class, "table")) {
            $v = get_class_vars($class);
            return($v["table"]);
        }
		return (strtolower($class));
	}

}
