<?php
	namespace Juju;
	require_once('Data/securestring.class.php');
	require_once('PDO/transaction.class.php');
	require_once('PDO/defaultvalue.class.php');
	require_once('PDO/filter.class.php');
	require_once('PDO/table.class.php');
	use Juju\{Data\SecureString, PDO\DefaultValue, PDO\Filter, PDO\Table, PDO\Transaction};

	class PDO {
		public static function from(string $dsnstring){
			$parts = explode(':', $dsnstring);
			$dsnstring = $parts[1];
			$dsn = explode(';', $dsnstring);
			$dsn = array_reduce($dsn, function($dsn, $item){
				$item = explode('=', $item);
				$dsn[$item[0]] = $item[1];
				return $dsn;
			});
			if(!isset($dsn['host'])){
				throw new \Exception("DSN '{$dsnstring}' missing host");
			}
			if(!isset($dsn['dbname'])){
				throw new \Exception("DSN '{$dsnstring}' missing dbname");
			}
			if(!isset($dsn['user'])){
				$user = $dsn['dbname'];
			}else{
				$user = $dsn['user'];
			}
			if(!isset($dsn['pass'])){
				$dsn['pass'] = $user;
			}
			$pass = SecureString::from($dsn['pass']);
			unset($dsn['pass'], $dsn['user']);
			$dsn = array_reduce(array_keys($dsn), function($a, $key) use($dsn){
				$a[] = "{$key}={$dsn[$key]}";
				return $a;
			});
			$dsnstring = $parts[0].':'.implode(';', $dsn);
			return new PDO($dsnstring, $user, $pass);
		}

		private $pdo;
		private function __construct(string $dsn, string $user, SecureString $pass){
			$mysql = explode(':', $dsn)[0] === 'mysql';
			$options = [];
			if($mysql){
				$options[\PDO::MYSQL_ATTR_INIT_COMMAND] = "SET NAMES 'UTF8'";
				$options[\PDO::MYSQL_ATTR_MULTI_STATEMENTS] = false;
				$options[\PDO::MYSQL_ATTR_FOUND_ROWS] = true;
			}
			$pdo = new \PDO($dsn, $user, (string)$pass, $options);
			$pdo->setAttribute(\PDO::ATTR_AUTOCOMMIT, true);
			$pdo->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_LOWER);
			$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
			$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
			$this->pdo = $pdo;
		}
		public function transaction(callable $fn){
			$pdo = $this->pdo;
			if($pdo->inTransaction()){
				throw new \Exception("Unable to start a new transaction");
			}
			$transaction = new Transaction($this);
			if($fn($transaction) === false){
				do{
					$transaction->rollback();
				}while($transaction->savepoint);
				$pdo->rollback();
			}else{
				$transaction->commit();
				if($pdo->inTransaction()){
					$pdo->commit();
				}
			}
			unset($transaction);
			$pdo->setAttribute(\PDO::ATTR_AUTOCOMMIT, true);
			return $this;
		}
		public function table(string $name){
			return new Table($this, $name);
		}
		public function prepare(string $statement, array $options = []){
			$pdo = $this->pdo;
			if($pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'mysql'){
				$options[\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY] = true;
			}
			$query = $pdo->prepare($statement);
			if($query === false){
				throw $this->getError();
			}
			return $query;
		}
		public function exec(string $statement){
			$query = $this->prepare($statement);
			$query->execute();
			$count = 0;
			while($query->fetch() !== false){
				$count++;
			}
			if($count === 0){
				$query2 = $this->query("select ROW_COUNT()");
				$count = $query2->fetchColumn();
				$query2->closeCursor();
				if($count === 0){
					$count = $query->rowCount();
				}
			}
			$query->closeCursor();
			return $count;
		}
		public function query(string $statement, int $mode = null, ...$args){
			$query = $this->prepare($statement);
			if(!is_null($mode)){
				$query->setFetchMode($mode, ...$args);
			}
			$query->execute();
			return $query;
		}
		public function quote($value, $type = \PDO::PARAM_STR){
			if($type == \PDO::PARAM_INT){
				return (int)$value;
			}elseif($type == \PDO::PARAM_BOOL){
				return (bool)$value ? "b'1'" : "b'0'";
			}elseif($type == \PDO::PARAM_NULL){
				return 'null';
			}
			return $this->pdo->quote((string)$value, $type);
		}
		public function beginTransaction(...$args){
			return $this->pdo->beginTransaction(...$args);
		}
		public function commit(...$args){
			return $this->pdo->commit(...$args);
		}
		public function rollback(...$args){
			return $this->pdo->rollback(...$args);
		}
		public function setAttribute(...$args){
			return $this->pdo->setAttribute(...$args);
		}
		public function getAttribute(...$args){
			return $this->pdo->getAttribute(...$args);
		}
		public function lastInsertId(...$args){
			$id = $this->pdo->lastInsertId(...$args);
			if($id === 0){
				$query = $this->pdo->query("select LAST_INSERT_ID()");
				$id = $query->fetchColumn();
				$query->closeCursor();
			}
			return $id;
		}
		public function getError(){
			$error = $this->pdo->errorInfo();
			return new \Exception($error[2], $error[1]);
		}
		public function stringFilter(array $filter = null){
			$where = '';
			if(!is_null($filter)){
				$where = 'where ';
				foreach($filter as $name => $value){
					if($value instanceof Filter){
						$where .= "`{$name}` {$value} and";
					}else{
						if(is_integer($value)){
							$type = \PDO::PARAM_INT;
						}elseif(is_bool($value)){
							$type = \PDO::PARAM_BOOL;
						}elseif(is_null($value)){
							$type = \PDO::PARAM_NULL;
						}else{
							$type = \PDO::PARAM_STR;
						}
						$where .= "`{$name}` = {$this->quote($value, $type)} and";
					}
				}
				$where = rtrim($where, ' and');
			}
			return $where;
		}
		public function stringSet(array $data){
			$sets = '';
			foreach($data as $name => $value){
				$sets .= "`{$name}` = {$this->quote($value)},";
			}
			if(count($sets) > 0){
				$sets = 'set '.rtrim($sets, ',');
			}
			return $sets;
		}
		public function stringColumn(string $name, array $column){
			$default = '';
			if(!is_null($column['default'])){
				$value = $column['default'];
				if($value instanceof DefaultValue){
					$default .= " DEFAULT {$value}";
				}else{
					if(is_integer($value)){
						$type = \PDO::PARAM_INT;
					}elseif(is_bool($value)){
						$type = \PDO::PARAM_BOOL;
					}else{
						$type = \PDO::PARAM_STR;
					}
					$default .= " DEFAULT {$this->quote($value, $type)}";
				}
			}
			$null = $column['null'] ? ' NULL' : ' NOT NULL';
			$ai = $column['increment'] ? ' AUTO_INCREMENT' : '';
			return "`{$name}` {$column['type']}{$null}{$default}{$ai}";
		}
		public function stringIndex(string $name, array $idx){
			if($idx['unique']){
				return "constraint unique index {$name} (".implode(',', $idx['columns']).")";
			}else{
				return "index {$name} (".implode(',', $idx['columns']).")";
			}
		}
		public function stringForeignKey(string $name, array $foreignKey){
			$cols0 = '';
			$cols1 = '';
			foreach($foreignKey['columns'] as $row){
				$cols0 .= "{$row[0]},";
				$cols1 .= "{$row[1]},";
			}
			$cols0 = rtrim($cols0, ',');
			$cols1 = rtrim($cols1, ',');
			return "constraint `{$name}` foreign key ({$cols0}) references `{$foreignKey['references']}` ({$cols1})";
		}
	}
?>