<?php
	/**
	* SQL class. Used for handling SQL connections
	*
	* @module sql
	* @class SQL
	* @constructor
	*/
	class SQL {
		/**
		* This is the mysqli connection beneath everything
		* 
		* @property sql
		* @type {mysqli}
		* @private
		* @required
		*/
		private $sql;
		public $queries = [];
		private static $connections = [];
		public function __construct($server,$user,$pass,$db){
			$this->sql = new mysqli('p:'.$server,$user,$pass,$db) or die('Unable to connect to mysql');
			self::$connections[] = $sql;
		}
		public function __destruct(){
			$this->sql->rollback();
			$this->sql->close();
			foreach($this->queries as $query){
				unset($query);
			}
			self::$connections = array_diff(self::$connections, [$this]);
		}
		public function __invoke(){
			return $this->sql;
		}
		public function __get($name){
			switch($name){
				case 'error':
					return $this->sql->error;
				break;
				case 'insert_id':
					return $this->sql->insert_id;
				break;
			}
		}
		/**
		* Returns a Query object based on inputs
		*
		* @method query
		* @param {String} sql The sql expression to run
		* @param {String=null} [types] A string containing all the types of arguments being passed
		* @param {Mixed} [bindings]* The bindings to use in the sql statement
		* @return {Query} Returns the query object
		*/
		public function query(...$args){
			return new Query(...array_merge([$this], $args));
		}
		public function escape($s){
			return $this->sql->escape_string($s);
		}
		public function charset($charset){
			return $this->sql->set_charset($charset);
		}
		public static function make_referenced(&$arr){
			$refs = [];
			foreach($arr as $key => $value){
				$refs[$key] = &$arr[$key];
			}
			return $refs;
		}
		public static function shutdown(){
			foreach(self::$connections as $sql){
				unset($sql);
			}
		}
	}
	/**
	* Query class. Returned by SQL::query()
	*
	* @class Query
	* @constructor
	*/
	class Query {
		private $query;
		private $sql;
		private $_result = false;
		public function __construct($sql, $source, $types=null, ...$args){
			$sql->queries[] = $this;
			$args = array_merge([$types], $args);
			$this->sql = $sql();
			$this->query = $this->sql->prepare($source);
			if(!is_null($types)){
				if(!$this->query->bind_param(...SQL::make_referenced($args))){
					throw new Exception("Unable to bind parameter {$this->query->error}");
				}
			}
		}
		public function __destruct(){
			if($this->_result){
				$this->_result = false;
				$this->query->free_result();
			}
			if($this->query){
				$this->query->close();
			}
			$sql->queries = array_diff($sql->queries, [$this]);
		}
		public function __invoke(){
			return $this->query;
		}
		public function execute(){
			if($this->query){
				$this->query->free_result();
				$this->_result = false;
				$this->query->reset();
				$r = $this->query->execute();
				$this->sql->commit();
				return $r;
			}else{
				return false;
			}
		}
		public function each_assoc(callable $fn){
			if($this->query){
				$r = $this->results;
				while($row = $r->fetch_assoc()){
					$fn($row);
				}
			}else{
				return false;
			}
		}
		public function each_num(callable $fn){
			if($this->query){
				$r = $this->results;
				while($row = $r->fetch_num()){
					$fn($row);
				}
			}else{
				return false;
			}
		}
		public function __get($name){
			switch($name){
				/**
				* Returns the mysqli::results object for the
				* query
				* 
				* @property results
				* @type {mysqli::results}
				* @public
				*/
				case 'results':
					if(!$this->_result && $this->query){
						$this->execute();
						$this->_result = $this->query->get_result();
						$this->query->close();
					}
					return $this->_result;
				break;
				/**
				* Returns an associative array of the query resulsts
				* 
				* @property assoc_results
				* @type {Array}
				* @public
				*/
				/**
				* Returns an associative array of the query resulsts
				* 
				* @property resulsts_assoc
				* @type {Array}
				* @public
				*/
				case 'assoc_results':case 'results_assoc':
					if($this->query){
						$a = [];
						$r = $this->results;
						while($row = $r->fetch_assoc()){
							array_push($a,$row);
						}
						return $a;
					}else{
						return false;
					}
				break;
				/**
				* Returns a numbered array of the query results
				* 
				* @property num_results
				* @type {Array}
				* @public
				*/
				/**
				* Returns a numbered array of the query results
				* 
				* @property resulsts_num
				* @type {Array}
				* @public
				*/
				case 'num_results':case 'results_num':
					if($this->query){
						$a = [];
						$r = $this->results;
						while($row = $r->fetch_num()){
							array_push($a,$row);
						}
						return $a;
					}else{
						return false;
					}
				break;
				case 'assoc_result':case 'result_assoc':
					if($this->query){
						$r = $this->results;
						return $r?$r->fetch_assoc():false;
					}else{
						return false;
					}
				break;
				case 'num_result':case 'result_num':
					if($this->query){
						$r = $this->results;
						return $r?$r->fetch_num():false;
					}else{
						return false;
					}
				break;
				case 'insert_id':
					return $this->sql->insert_id;
				break;
				case 'affected_rows':
					return $this->query->affected_rows;
				break;
			}
		}
	}
	register_shutdown_function(function(){
		SQL::shutdown();
	});
?>