<?php # Versao: 2.0.1
class Propriedades
{
	public $dados = array();
	public function __get($propriedade)
	{
		return $this->dados[$propriedade] ?? "";
	}
	public function __set($propriedade, $valor)
	{
		$this->dados[$propriedade]  = $valor;
	}
}

class Database extends Configuracao
{

	private static $conexao = null;

	protected $dados = array();
	protected $retorno = 'objeto';
	protected $tabela;
	protected $chave_primaria;
	protected $join;
	protected $tipo_join = 'LEFT';
	protected $distinct = false;
	protected $order_by;
	protected $group_by = false;
	protected $limite;
	protected $and = array();
	protected $or = array();
	protected $andOr = array();
	protected $orAnd = array();
	protected $in = array();
	protected $not_in = array();
	protected $between = array();
	protected $extra_sql;
	protected $row_count = false;
	protected $colunas = array();
	protected $custom_select = false;
	protected $debug_sql = false;
	public $last_id;

	public function __set($campo, $valor)
	{
		$this->dados[$campo] = $valor;
	}

	public function __get($campo)
	{
		return $this->dados[$campo] ?? null;
	}

	public function __isset($atributo)
	{
		return isset($this->$atributo);
	}
	public function __unset($atributo)
	{
		unset($this->$atributo);
	}

	public function dados($dados)
	{
		$this->dados = $dados;
	}

	public function colunas($colunas)
	{
		$this->colunas = $colunas;
		return $this;
	}

	public function customSelect($bool = false)
	{
		$this->custom_select = $bool;
		return $this;
	}
	public function debug_sql($bool = false)
	{
		$this->debug_sql = $bool;
		return $this;
	}

	public function distinct($bool = false)
	{
		$this->distinct = $bool;
		return $this;
	}

	public function retorno($retorno)
	{
		$this->retorno = $retorno;
		return $this;
	}

	public function rowCount($bool = false)
	{
		$this->row_count = $bool;
		return $this;
	}

	public function groupBy($bool = false)
	{
		$this->group_by = $bool;
		return $this;
	}

	public function conectDatabase()
	{
		if (is_null(Database::$conexao)) {
			Database::$conexao = new PDO(
				"mysql:host=" . $this->servidor . ";dbname=" . $this->banco . ";charset=utf8",
				$this->usuario,
				$this->senha,
				array(
					PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
					PDO::MYSQL_ATTR_INIT_COMMAND => 'SET sql_mode="TRADITIONAL"'
				)
			);
			Database::$conexao->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		}
	}

	protected function consultDb($sql, $params, $return)
	{
		if ($this->debug_sql) {
			echo '<div class="ui segment">';
			debug_code(SqlFormatter::format($sql), 0);
			echo '<hr> Parametros:';
			debug_code($params, 0);
			echo '</div>';
			exit();
		}
		try {

			$this->conectDatabase();
			$consulta = Database::$conexao->prepare($sql);
			foreach ($params as $param => $value) {
				$consulta->bindValue($param, $value);
			}
			$consulta->execute();
			if ($return) {
				$consulta->setFetchMode(PDO::FETCH_CLASS, 'Propriedades');
				if ($this->row_count) {
					return $consulta->rowCount();
				}
				return $consulta->fetch();
			}
			if ($this->retorno != 'array') {
				return $consulta->fetchAll(PDO::FETCH_OBJ);
			}
			return $consulta->fetchAll();
		} catch (PDOException $erro) {
			echo $erro->getMessage();
		}
	}

	protected function executeDb($sql, $params)
	{
		if ($this->debug_sql) {
			echo '<div class="ui segment">';
			debug_code(SqlFormatter::format($sql), 0);
			echo '<hr> Parametros:';
			debug_code($params, 0);
			echo '</div>';
			exit();
		}
		try {
			$this->conectDatabase();
			$consulta = Database::$conexao->prepare($sql);
			foreach ($params as $param => $value) {
				$consulta->bindValue($param, ($value == "") ? null : $value);
			}
			$resultado = $consulta->execute();
			return $resultado;
		} catch (PDOException $erro) {
			Mensagem::ErrorSQL($erro->getMessage());
		}
	}

	public function cadastrar($dados = null)
	{
		if (!is_null($dados)) {
			$this->dados = array_merge($this->dados, $dados);
		}
		$sql = "INSERT INTO {$this->tabela} (";
		foreach ($this->dados as $coluna => $valor) {
			$sql .= "$coluna, ";
		}
		$sql = substr($sql, 0, -2);
		$sql .= ") VALUES (";
		foreach ($this->dados as $coluna => $valor) {
			$sql .= ":$coluna, ";
		}
		$sql = substr($sql, 0, -2);
		$sql .= ")";
		$resultado = $this->executeDb($sql, $this->dados);
		$this->last_id = Database::$conexao->lastInsertId();
		if (isset($_FILES)) {
			foreach ($_FILES as $indice => $arquivo) {
				unset($_FILES[$indice]);
				$this->enviarArquivo($this->last_id, $indice, $arquivo);
			}
		}
		return $resultado;
	}


	public function alterar($dados = null)
	{
		if (!is_null($dados)) {
			$this->dados = array_merge($this->dados, $dados);
		}
		$sql = "UPDATE {$this->tabela} SET ";
		foreach ($this->dados as $coluna => $valor) {
			$sql .= "$coluna = :$coluna, ";
		}

		$sql = substr($sql, 0, -2);
		$sql .= " WHERE {$this->tabela}.{$this->chave_primaria} = :{$this->chave_primaria}";
		$resultado = $this->executeDb($sql, $this->dados);
		if (isset($_FILES)) {
			foreach ($_FILES as $indice => $arquivo) {
				if (isset($_FILES[$indice])) {
					unset($_FILES[$indice]);
					$this->enviarArquivo($this->dados[$this->chave_primaria], $indice, $arquivo);
				}
			}
		}
		return $resultado;
	}

	private function enviarArquivo($id, $coluna, $arquivo)
	{

		// debug_code($id, 0);
		// debug_code($coluna, 0);
		// debug_code($this->tabela);
		if ($arquivo["error"] == 0) {

			$dir = ROOT . "/files/{$this->tabela}";
			UploadFile::removeFiles("$dir/", $id . "_" . $coluna);
			$caminho_arquivo = UploadFile::sendFiles($dir, $arquivo, "files/{$this->tabela}/" . $id . "_" . $coluna);
			$this->dados = array($coluna => $caminho_arquivo);
			$this->pk($id);
			$this->alterar();
		} else {
			if ($arquivo["error"] != 4) {
				throw new Exception("Erro ao enviar arquivo: \"$coluna\"");
			}
		}
	}

	public function salvar()
	{
		$dados = $this->dados;
		if ($this->consultar()) {
			$this->dados = $dados;
			return $this->alterar();
		} else {
			return $this->cadastrar();
		}
	}

	public function excluir($pk = null)
	{
		if (!is_null($pk)) {
			$this->pk($pk);
		}
		$sql = "DELETE FROM {$this->tabela} WHERE {$this->tabela}.{$this->chave_primaria} = :{$this->chave_primaria}";
		$resultado = $this->executeDb($sql, array(
			$this->chave_primaria => $this->dados[$this->chave_primaria]
		));
		return $resultado;
	}

	public function listar(...$tabelas)
	{
		$valores = array();
		$sql = $this->gerarSelect($tabelas);
		$valores = $this->gerarWhere($sql);
		$sql .= "ORDER BY $this->order_by";
		if (!is_null($this->limite)) {
			$sql .= " LIMIT {$this->limite}";
		}
		return $this->consultDb($sql, $valores, false);
	}

	public function consultar(...$tabelas)
	{
		$sql = $this->gerarSelect($tabelas);
		$valores = $this->gerarWhere($sql);
		$sql .= "ORDER BY $this->order_by";
		$dados =  $this->consultDb($sql, $valores, true);
		return ($dados === false) ? new Propriedades() : $dados;
	}

	public function contar(...$tabelas)
	{
		$valores = array();
		$colunas = array();
		if (isset($this->colunas)) {
			$colunas = ((array) $this->colunas) ?? array();
		}


		$sql = "SELECT " . ($this->distinct ? 'DISTINCT ' : '');
		if (count($colunas) > 0) {
			for ($i = 0; $i < count($colunas); $i++) {
				if ($this->custom_select == true) {
					$sql .= "$colunas[$i], ";
				} else {
					$sql .= "{$this->tabela}.$colunas[$i], ";
				}
			}
		}
		$sql .= " COUNT(*) AS total FROM {$this->tabela} ";

		$sql .= $this->gerarJoin($tabelas, $this->tipo_join);
		$valores = $this->gerarWhere($sql);
		$data = $this->consultDb($sql, $valores, true);

		if ($data) {
			if ($this->row_count) {
				return $data;
			}
			return $data->total;
		} else {
			return 0;
		}
	}

	private function gerarSelect($tabelas)
	{
		$colunas = array();
		if (isset($this->colunas)) {
			$colunas = ((array) $this->colunas) ?? array();
		}

		if ($this->distinct) {
			$sql = "SELECT DISTINCT ";
		} else {
			$sql = "SELECT ";
		}
		if (count($colunas) > 0) {
			for ($i = 0; $i < count($colunas); $i++) {
				if ($this->custom_select == true) {
					$sql .= "$colunas[$i], ";
				} else {
					$sql .= "{$this->tabela}.$colunas[$i], ";
				}
			}
			$sql = substr($sql, 0, -2);
		} else {
			$sql .= "{$this->tabela}.*";
		}
		if ($this->custom_select == false) {
			if (count($tabelas) == 0) {
				foreach ($this->join as $join) {
					$sql .= $this->getSelect($join);
				}
			} else if ($tabelas[0] != "0") {
				if (is_float($tabelas[0])) {
					$joins = $this->join;
					foreach ($tabelas as $n) {
						unset($joins[intval($n)]);
					}
					foreach ($joins as $join) {
						$sql .= $this->getSelect($join);
					}
				} else {
					foreach ($tabelas as $key => $n) {
						$sql .= $this->getSelect($this->join[$n]);
					}
				}
			}
		}
		$sql .= " FROM {$this->tabela} ";
		$sql .= $this->gerarJoin($tabelas, $this->tipo_join);
		return $sql;
	}


	private function getSelect($join)
	{
		$str = ", ";
		if (isset($join["apelido"])) {
			$str .= $join["apelido"];
		} else {
			$str .= $join["tabela"];
		}
		$str .= "." . $join["colunas"];
		return $str;
	}

	private function getJoin($join)
	{
		$str = "{$this->tipo_join} JOIN {$join["tabela"]} ";
		if (isset($join["apelido"])) {
			$str .= "AS {$join["apelido"]} ";
			$join["tabela"] = $join["apelido"];
		}
		if (isset($join["tabelafk"])) {
			$str .= "ON {$join["tabelafk"]}";
		} else {
			$str .= "ON {$this->tabela}";
		}
		$str .= ".{$join["fk"]} = {$join["tabela"]}.{$join["pk"]} ";
		return $str;
	}

	private function gerarJoin($tabelas)
	{
		$sql = "";
		if (count($tabelas) == 0) {
			foreach ($this->join as $join) {
				$sql .= $this->getJoin($join);
			}
		} else if ($tabelas[0] != "0") {
			if (is_float($tabelas[0])) {
				$joins = $this->join;
				foreach ($tabelas as $n) {
					unset($joins[intval($n)]);
				}
				foreach ($joins as $join) {
					$sql .= $this->getJoin($join);
				}
			} else {
				foreach ($tabelas as $n) {

					$sql .= "{$this->tipo_join} JOIN {$this->join[$n]["tabela"]} ";
					if (isset($this->join[$n]["apelido"])) {
						$sql .= "AS {$this->join[$n]["apelido"]} ";
					}
					if (isset($this->join[$n]["tabelafk"])) {
						$sql .= "ON {$this->join[$n]["tabelafk"]}";
					} else {
						$sql .= "ON {$this->tabela}";
					}
					if (isset($this->join[$n]["apelido"])) {
						$sql .= ".{$this->join[$n]["fk"]} = {$this->join[$n]["apelido"]}.{$this->join[$n]["pk"]} ";
					} else {
						$sql .= ".{$this->join[$n]["fk"]} = {$this->join[$n]["tabela"]}.{$this->join[$n]["pk"]} ";
					}
				}
			}
		}
		return $sql;
	}

	private function gerarWhere(&$sql)
	{
		$valores = array();
		if (
			count($this->and) > 0 ||
			count($this->andOr) > 0 ||
			count($this->or) > 0 ||
			count($this->orAnd) > 0 ||
			count($this->in) > 0 ||
			count($this->not_in) > 0 ||
			count($this->between) > 0
		) {
			$sql .= "WHERE";
			foreach ($this->and as $coluna => $valor) {
				$parametro = str_replace(".", "", $coluna);
				if (is_null($valor[0])) {
					$sql .= " $coluna {$valor[1]} AND";
				} else {
					$sql .= " $coluna {$valor[1]} :$parametro AND";
					$valores[$parametro] = $valor[0];
				}
			}
			if (count($this->andOr) > 0) {
				foreach ($this->andOr as $condicoes) {
					$sql .= " (";
					foreach ($condicoes as $indice => $condicao) {
						$condicao[2] = $condicao[2] ?? '=';
						$parametro = str_replace(".", "", $condicao[0]);
						$parametro .= $indice;
						if (is_null($condicao[1])) {
							$sql .= " {$condicao[0]} {$condicao[2]} OR";
						} else {
							$sql .= " {$condicao[0]} {$condicao[2]} :$parametro OR";
							$valores[$parametro] = $condicao[1];
						}
					}
					$sql = substr($sql, 0, -2);
					$sql .= ") AND";
				}
			}
			if (count($this->or) > 0) {
				$sql .= " (";
				foreach ($this->or as $coluna => $valor) {
					$parametro = str_replace(".", "", $coluna);
					$sql .= " $coluna {$valor[1]} :$parametro OR";
					$valores[$parametro] = $valor[0];
				}
				$sql = substr($sql, 0, -2);
				$sql .= ") AND";
			}
			if (count($this->orAnd) > 0) {
				$sql .= " (";
				foreach ($this->orAnd as $grupo) {
					$sql .= " (";
					foreach ($grupo as $indice => $condicao) {
						$condicao[2] = $condicao[2] ?? '=';
						$parametro = str_replace(".", "", $condicao[0]);
						if (is_null($condicao[1])) {
							$sql .= " $condicao[0] {$condicao[2]} AND";
						} else {
							$sql .= " $condicao[0] {$condicao[2]} :$parametro AND";
							$valores[$parametro] = $condicao[1];
						}
					}
					$sql = substr($sql, 0, -3);
					$sql .= ") OR";
				}
				$sql = substr($sql, 0, -2);
				$sql .= ") AND";
			}

			if (count($this->in) > 0) {
				foreach ($this->in as $coluna => $valor) {
					$parametro = str_replace(".", "", $coluna);
					$sql .= " $coluna {$valor[1]} (";
					foreach ($valor[0] as $i => $v) {
						$sql .= ":$parametro$i,";
						$valores[$parametro . $i] = $v;
					}
					$sql = substr($sql, 0, -1);
					$sql .= ") AND";
				}
			}

			if (count($this->not_in) > 0) {
				foreach ($this->not_in as $coluna => $valor) {
					$parametro = str_replace(".", "", $coluna);
					$sql .= " $coluna {$valor[1]} (";
					foreach ($valor[0] as $i => $v) {
						$sql .= ":$parametro$i,";
						$valores[$parametro . $i] = $v;
					}
					$sql = substr($sql, 0, -1);
					$sql .= ") AND";
				}
			}

			if (!is_null($this->extra_sql)) {
				$sql .= $this->extra_sql . " AND";
			}
			if (count($this->between) > 0) {
				foreach ($this->between as $valor) {
					if ($valor[0]) {
						$sql .= " (:{$valor[2]}1 BETWEEN {$valor[2]} AND {$valor[3]}) AND";
						$valores[$valor[2] . "1"] = $valor[1];
						// valor coluna coluna
					} else {
						$sql .= " ({$valor[1]} BETWEEN :{$valor[1]}1 AND :{$valor[1]}2) AND";
						$valores[$valor[1] . "1"] = $valor[2];
						$valores[$valor[1] . "2"] = $valor[3];
					}
				}
			}
			$sql = substr($sql, 0, -3);
		}
		return $valores;
	}

	public function pk($pk = null)
	{
		$this->chave_primaria = $pk;
		if (is_null($pk)) {
			return $this->dados[$this->chave_primaria];
		} else {
			$this->dados[$this->chave_primaria] = $pk;
			$this->and($this->tabela . "." . $this->chave_primaria, $pk);
		}
		return $this;
	}

	public function and($coluna, $valor, $operador = "=")
	{
		if ($operador == "LIKE" || $operador == "NOT LIKE") {
			$valor = "%$valor%";
		}
		$this->and[$coluna] = array($valor, $operador);
		return $this;
	}

	/**
	 * $condicoes = [coluna,valor,operador],[coluna,valor,operador]...
	 */
	public function andOr(...$condicoes)
	{
		foreach ($condicoes as $i => $condicao) {
			$condicao[2] = $condicao[2] ?? '=';
			if ($condicao[2] == "LIKE") {
				$condicoes[$i][1] = "%{$condicao[1]}%";
			}
		}
		$this->andOr[] = $condicoes;
		return $this;
	}

	/**
	 * $inverso = false: coluna valor1 valor2
	 * $inverso = true: valor coluna1 coluna2
	 */
	public function between($param1, $param2, $param3, $inverso = false)
	{
		$this->between[] = array($inverso, $param1, $param2, $param3);
		return $this;
	}

	public function andIsNull($coluna)
	{
		$this->and[$coluna] = array(null, "IS NULL");
		return $this;
	}

	public function andIsNotNull($coluna)
	{
		$this->and[$coluna] = array(null, "IS NOT NULL");
		return $this;
	}

	public function or($coluna, $valor, $operador = "LIKE")
	{
		if ($operador == "LIKE" || $operador == "NOT LIKE") {
			$valor = "%$valor%";
		}
		$this->or[$coluna] = array($valor, $operador);
		return $this;
	}

	public function orAnd(...$condicoes)
	{
		foreach ($condicoes as $indice => $condicao) {
			$condicao[2] = $condicao[2] ?? '=';
			if ($condicao[2] == "LIKE") {
				$condicoes[$indice][1] = "%$condicao[1]%";
			}
		}
		$this->orAnd[] = $condicoes;
		return $this;
	}

	/**
	 * @desc  $valores => array (unidimensional);
	 * @return $in
	 */
	public function in($coluna, $valores)
	{
		foreach ($valores as $valor) {
			$this->in[$coluna] = array($valores, " IN ");
		}
		return $this;
	}

	/**
	 * @desc  $valores => array (unidimensional);
	 * @return $not_in
	 */
	public function notIn($coluna, $valores)
	{
		foreach ($valores as $valor) {
			$this->not_in[$coluna] = array($valores, " NOT IN ");
		}
		return $this;
	}

	public function orderBy($order_by)
	{
		$this->order_by = $order_by;
		return $this;
	}

	public function tipoJoin($tipo_join)
	{
		$this->tipo_join = $tipo_join;
		return $this;
	}

	public function extraSQL($extra_sql)
	{
		$this->extra_sql = " " . $extra_sql;
		return $this;
	}


	/**
	 * @desc [0] => Quantidade de linhas;
	 * @desc [1] => Offset (Linha que iniciará os dados);
	 * @param array $limite (unidimensional)
	 * @return $limite
	 */
	public function limite($limite)
	{
		$this->limite = $limite;
		return $this;
	}

	public function getUltimoId()
	{
		$dados = $this->consultDb(
			"SELECT {$this->chave_primaria} AS ID FROM {$this->tabela}
			ORDER BY {$this->chave_primaria} DESC LIMIT 1",
			array(),
			true
		);

		return $dados->ID;
	}

	public function listarColunas()
	{
		return $this->consultDb(
			"SELECT COLUMN_NAME as Field FROM information_schema.columns 
			WHERE TABLE_NAME = '{$this->tabela}' AND TABLE_SCHEMA = '{$this->banco}'",
			array(),
			false
		);
	}

	protected function GeradorConsulta($sql, $params, $return)
	{
		if ($this->debug_sql) {
			echo "<pre class='code'>\n";
			echo SqlFormatter::format($sql);
			echo "</pre>";
			exit();
		}
		try {
			$this->conectDatabase();
			$consulta = Database::$conexao->prepare($sql);
			foreach ($params as $param => $value) {
				$consulta->bindValue($param, $value);
			}
			$consulta->execute();
			if ($return) {
				$retorno = $consulta->fetch();
				return $retorno;
			}
			return $consulta->fetchAll();
		} catch (PDOException $erro) {
			echo $erro->getMessage();
		}
	}
}
