<?php

/**
 * @author Peter Csubak (chuby)
 * @copyright Peter Csubak (chuby)
 */

/**
 *
 * A MySQL alapú EHInterface interfészt implementáló osztály, aminek a feladata a megadott egyed(ek) adatainak 
 * kezelése az adatbázisban. Ezt az osztályt elsősorban a PersistenceHandler használja, de természetesen 
 * használható a framework-től független rendszerekben is, viszont ezekben az esetekben figyelni kell bizonyos
 * alapvető szabályokra. (Ezeket lásd a tutorialban.)
 * (Megjegyzés: Az adatbáziselem a dokumentációban általában az adatbázisban tárolható tuple asszociatív 
 * tömb reprezentációját jelenti.)
 */
class MySQLEntityHandler implements EHInterface {

	private $tableName;
	private $dbElement;
	private $dbElementFrom;
	private $dbElementTo;
	private $connID;

	public function setTableName($value){
		$this->tableName = $value;
	}

	public function setDBElement($value){
		$this->dbElement = $value;
	}

	public function setDBElementFrom($value){
		$this->dbElementFrom = $value;
	}

	public function setDBElementTo($value){
		$this->dbElementTo = $value;
	}

	public function setConnectionId($value){
		$this->connID = $value;
	}

	/**
	 * A megadott asszociatív tömb kulcsát adja vissza a megadott pozíciónál.
	 * Erre azért van szükség, mert az asszociatív tömb csak foreach-el vagy
	 * iterátorral járható be, aminek az a következménye, hogy sima for,
	 * esetén nyílván nem tudok hozzáférni a kulcsokhoz, vagy az értékekhez.
	 *
	 * (lehet hogy refactoring kell majd, mert lehet hogy meg lehet majd
	 * oldani foreach-el is a problémát)
	 *
	 * @param $myArray Az asszociatív tömb.
	 * @param $pos A pozícioó, ahol a kulcs érdekel.
	 * @return A kulcs nevét adja vissza string formában.
	 */
	private function keyNameAtPos($myArray, $pos){
		if (($pos < 0) || ($pos >= count($myArray)))
		return "NULL";

		reset($myArray);
		for($i = 0; $i < $pos; $i++) next($myArray);

		$k = (string) key($myArray);

		reset($myArray);

		return $k;
	}

	/**
	 * A megadott asszociatív tömbből adja vissza az értéket a megadott 
	 * pozíción. 
	 * (Az okokat lásd a keyNameAtPos metódusnál)
	 *
	 * @param $myArray Az asszociatív tömb.
	 * @param $pos A pozíció.
	 * @return Az érték a megadott pozíción.
	 */
	private function getAtPos($myArray, $pos){
		if (($pos < 0) || ($pos >= count($myArray)))
		return "NULL";

		reset($myArray);
		if ($pos == 0) $res = current($myArray);
		for($i = 0; $i < $pos; $i++) $res = next($myArray);

		return $res;
	}

	/**
	 * Vissza adja a kulcsneveket, úgy ahogyan az INSERT SQL utasításhoz 
	 * szükséges.
	 *
	 * @return Az SQL utasításban felhasználható, string.
	 */
	private function getKeysAsInsertString(){
		$res = "";
		for ($i = 0; $i < count($this->dbElement); $i++){
			$key = $this->keyNameAtPos($this->dbElement, $i);
			if ($i < (count($this->dbElement) - 1)){
				$res .= ' `'.$key.'`, ';
			}else{
				$res .= ' `'.$key.'`';
			}
		}
		return $res;
	}

	/**
	 * Vissza adja az értékeket, úgy ahogyan az INSERT SQL utasításnak van
	 * szüksége rá.
	 *
	 * @return Az értékek, SQL String formában.
	 */
	private function getValuesAsInsertString(){
		$res = "";
		for ($i = 0; $i < count($this->dbElement); $i++){
			$val = $this->getAtPos($this->dbElement, $i);
			if ($i < (count($this->dbElement) - 1)){
				$res .= ' \''.$val.'\', ';
			}else{
				$res .= ' \''.$val.'\'';
			}
		}
		return $res;
	}

	/**
	 * Elvégzi az INSERT műveletet a példányban szereplő adatok alapján.
	 *
	 * @return boolean Ha a $dbElement változó be van állítva, akkor az INSERT művelet 
	 * végrehajtásakor a "lekérdezés" eredményétől függően igaz ha sikeresen végbement, 
	 * illetve hamis amennyiben nme ment végbe sikeresen a művelet. Amennyiben a
	 * $dbElement változó nincs beállítva, akkor mindig hamis érték.
	 */
	public function insert(){
		if (isset($this->dbElement)){
			$ins = "INSERT INTO ";
			$ins .= "`".$this->tableName."` ";
			$ins .= '('.$this->getKeysAsInsertString().')';
			$ins .= " VALUES ";
			$ins .= '('.$this->getValuesAsInsertString().');';

			$res = mysql_query($ins, $this->connID);
			$this->clearMe();
			if ($res != false){
				return true;
			}else{
				return false;
			}
		}else{
			$this->clearMe();
			return false;
		}
	}

	/**
	 * Visszaadja az adatbáziselemet reprezentáló asszociatív tömböt olyan formában, hogy azt 
	 * egy UPDATE művelet során fel lehessen használni. (Tehát egy UPDATE stringet hoz létre 
	 * belőle.
	 *
	 * @return string A létrehozott UPDATE string.
	 */
	private function getDBAsUpdateString(){
		$res = "";
		for ($i = 0; $i < count($this->dbElementTo); $i++){
			$key = $this->keyNameAtPos($this->dbElementTo, $i);
			$val = $this->getAtPos($this->dbElementTo, $i);
			if ($i < (count($this->dbElementTo) - 1)){
				$res .= '`'.$key.'` = \''.$val.'\', ';
			}else{
				$res .= '`'.$key.'` = \''.$val.'\'';
			}
		}
		return $res;
	}

	/**
	 * Visszaadja a megadott adatbáziselemet, mint egy olyan stringet, amit egy WHERE záradékban 
	 * fel lehet használni. 
	 *
	 * @param array $dbelement Az adatbáziselemet reprezentáló asszociatív tömb. 
	 * @return string A WHERE záradékban felhasználható string.
	 */
	private function getDBEAsWhereString($dbelement){
		$res = "(";
		$i = 0;
		if (count($dbelement) == 0) return "1";
		foreach ($dbelement as $key => $value){
			if(($value == null) || is_float($value)){
				continue;
			}

			if ($i++ != 0){
				if ($i <= (count($dbelement))){
					$res .= "AND";
				}
			}

			$res .= " `".$key."`";
			if (is_int($value)){
				$res .= ' = '.$value.' ';
			}else{
				$res .= ' = \''.$value.'\' ';
			}
		}

		return $res.");";
	}

	/**
	 * A $dbElementFrom és $dbElementTo adatbáziselemek által meghatározott változtatást 
	 * hajtja végre egy UPDATE művelettel. (Mivel mind a két elem esetén minden adat rendelkezésre
	 * áll, így lehetne azt is tenni, hogy egyszerűen kicseréljük a két elemet, de egyelőre nem 
	 * biztos, hogy az gyorsabb, vagy bármilyen módon jóbb megoldás lenne, ha igazolódik, hogy
	 * mégis, akkor át lesz írva ez a metódus is.
	 *
	 * @return boolean Ha a fent említett két attribútum nincs beállítva, akkor hamis, ellenkező 
	 * esetben az UPDATE művelet sikerességétől függően igaz vagy hamis érték. 
	 */
	public function change(){
		if (isset($this->dbElementFrom) && isset($this->dbElementTo)){


			$upd = 'UPDATE ';
			$upd .= "`".$this->tableName."` ";
			$upd .= 'SET '.$this->getDBAsUpdateString();
			$upd .= ' WHERE '.$this->getDBEAsWhereString($this->dbElementFrom);

			$res = mysql_query($upd, $this->connID);
			$this->clearMe();
			if ($res != false){
				return true;
			}else{
				return false;
			}
		}else{
			$this->clearMe();
			return false;
		}
	}

	/**
	 * Az adatbázisból törli a megadott adatbáziselemet. 
	 *
	 * @return boolean Ha a $dbElement attribútum nincs beállítva, akkor hamis, ellenkező esetben
	 * a DELETE utasítás sikersességétől függően igaz vagy hamis érték.
	 */
	public function delete(){
		if (isset($this->dbElement)){
			$del = 'DELETE FROM ';
			$del .= "`".$this->tableName."` ";
			$del .= "WHERE ".$this->getDBEAsWhereString($this->dbElement);

			$res = mysql_query($del, $this->connID);
			$this->clearMe();
			if ($res != false){
				return true;
			}else{
				return false;
			}
		}
	}

	/**
	 * Magasszinten a where záradékot leíró Where osztály példányát alakítja 
	 * where string alakúra, ami azt jelenti, hogy a WHERE záradékban felhasználható
	 * formára. 
	 *
	 * @param Where $where A magasszinten a where záradékot leíró Where osztály példánya.
	 * @return string A WHERE záradékban felhasználható string.
	 */
	private static function whereToString($where){
		$res = "";
		$res .= "`".$where->fieldName."`";
		if ($where->value1 !== null){
			if ($where->operator == "like"){
				$res .= " LIKE ";
				$res .= "'".$where->value1."'";
			}elseif ($where->operator == "between"){
				$res .= " BETWEEN ";
				$res .= "('".$where->value1."' AND '".$where->value2."')";
			}else{
				$res .= $where->operator;
				$res .= "'".$where->value1."'";
			}
		}else{
			if ($where->operator == "="){
				$res .= " IS NULL";
			}else{
				$res .= " IS NOT NULL";
			}
		}
		if ($where->whereElements !== null){
			foreach ($where->whereElements as $whr){
				if ($whr->logicalOp == "and"){
					$res .= " AND ";
				}elseif ($whr->logicalOp == "or"){
					$res .= " OR ";
				}elseif ($whr->logicalOp == "andnot"){
					$res .= " AND NOT ";
				}elseif ($whr->logicalOp == "ornot"){
					$res .= " OR NOT ";
				}else{
					$res .= " AND ";
				}
				if (count($whr->whereElements) > 0){
					$res .= "(";
					$res .= self::whereToString($whr);
					$res .= ")";
				}else{
					$res .= self::whereToString($whr);
				}
			}
		}
		return $res;
	}

	/**
	 * Szabályos SELECT utasítást végrehajtó metódus, amely a magasszintű lekérdezések 
	 * adatbázisszintre hozásához készült. Használatáról bővebben lásd a tutorialt
	 *
	 * @param mixed $where Lehet asszociatív tömb, ebben az esetben a tömb elemenk közt értelmezett 
	 * logikai kapcsolatot a $logicConn az asszociatív tömb kulcsai és értékei közt értelmezett 
	 * hasonlító operátort pedig a $tester fogja meghatározni. Lehet Where osztály példánya, ekkor 
	 * egyértelműen a where záradékot a magasszintű where záradék alapján alakítja ki a whereToString
	 * metódus. Lehet továbbá string, ekkor a string egy az egyben kerül a WHERE záradékban.
	 * @param string $logicConn Az asszociatív tömb elemei közt értelmezett logikai kapcsolatot definiálja.
	 * @param string $tester Az asszociatav tömb kulcsai és értékei közti hasonlító operátorokat definiálja.
	 * @param string $cols A lekérdezésben résztvevő oszlopokat definiálja, amennyiben az értéke
	 * //dbelementdef\\-el egyenlő akkor az adatbáziselem összes kulcsértéke, mint oszlop szerepelni 
	 * fog a lekérdezésben. Amennyiben ez nem áll fent akkor csak az itt (SQL specifikusan) megadott 
	 * oszlopok értékei kerülnek az eredménybe. 
	 * @return array Adatbáziselemeket tartalmazó tömb. (Ha a lekérdezés sikertelen, vagy nincsenek 
	 * elemek, amik megfeleltek volna a where záradéknak, akkor egy üres tömböt ad.)
	 */
	public function select($where = "1", $logicConn = "AND", $tester = "=", $cols = "//dbelementdef\\"){
		if (isset($this->dbElement) || ($cols != "//dbelementdef\\")){
			$sel = "SELECT ";
			if ($cols == "//dbelementdef\\"){
				$sel .= $this->getKeysAsInsertString();
			}else{
				$sel .= $cols;
			}
			$sel .= " FROM ";
			$sel .= "`".$this->tableName."` ";
			$sel .= "WHERE ";
			if (is_array($where)){
				$whereb = "";
				$logicConn = strtoupper($logicConn);
				if (($logicConn != "AND") && ($logicConn != "OR")){
					return array();
				}
				$tester = strtolower($tester);
				if (($tester != "=") && ($tester != "<") && ($tester != ">") && ($tester != "<>") && ($tester != "like")){
					return array();
				}
				foreach ($where as $k => $v){
					if ($whereb != ""){
						$whereb .= " ".$logicConn." ";
					}
					if ($v === null){
						if ($tester == "="){
							$whereb .= "(`".$k."` IS NULL)";
						}else{
							$whereb .= "(`".$k."` IS NOT NULL)";
						}
					}else{
						$whereb .= "(`".$k."` ".$tester." '".$v."')";
					}
				}
				$sel .= $whereb;
			}elseif (is_object($where) && ($where instanceof Where)){
				$sel .= self::whereToString($where);
			}else{
				$sel .= $where;
			}

			//Logger::log("PEA", Logger::DEBUG, mysql_real_escape_string($sel));
			$res = mysql_query($sel, $this->connID);
			if ($res == false){
				return array();
			}

			$arr = array();
			$i = 0;
			while ($row = mysql_fetch_assoc($res)){
				$arr[$i++] = $row;
			}
			$this->clearMe();
			return $arr;
		}
	}

	/**
	 * A megadott DBField példány alapján határozza meg az SQL utasításban elfogadható 
	 * mazőtípust, és ezt mint stringet adja vissza.
	 *
	 * @param DBField $field A DBField példány, amely alapján a típust meg kell határozni.
	 * @return string A mező típusát meghatározó SQL utasításban felhasználható string.
	 */
	private static function getMySQLType($field){
		$res = "";
		switch ($field->type){
			case "string" : {
				$res = "VARCHAR(".$field->length.")";
				break;
			}
			case "double" : {
				$res = "DOUBLE";
				break;
			}
			case "int" : {
				$res = "INT";
				break;
			}
			case "boolean" : {
				$res = "BOOL";
				break;
			}
			case "date" : {
				$res = "DATE";
				break;
			}
			case "datetime" : {
				$res = "DATETIME";
				break;
			}
		}
		return $res;
	}

	/**
	 * A megadott mezőleírók, illetve elsődleges kulcs alapján létrehozza a táblát az adatbázisban.
	 *
	 * @param array $fields A mezők típusát és nevét definiáló DBField típusú példányokat tartalmazó
	 * tömb.
	 * @param string $id Az elsődleges kulcsot definiáló string.
	 * @return boolean A tábla létrehozásának sikerességétől függően igaz, vagy hamis érték.
	 */
	public function createTable($fields, $id){
		if (isset($this->tableName) && isset($id) && isset($fields)){
			$crea = "CREATE TABLE `".$this->tableName."` (";
			if (is_array($fields)){
				foreach ($fields as $value){
					if ($value instanceof DBField){
						$crea .= "`".$value->name."` ".self::getMySQLType($value)." ".($value->nullable ? "NULL" : "NOT NULL").", ";
					}else{
						return false;
					}
				}
				$crea .= "PRIMARY KEY (`".$id."`) ) ENGINE = MYISAM ;";

				//echo $crea;

				$res = mysql_query($crea, $this->connID);
				if ($res != false){
					$this->clearMe();
					return true;
				}else{
					$this->clearMe();
					return false;
				}
			}else{
				$this->clearMe();
				return false;
			}
		}
		$this->clearMe();
		return false;
	}

	/**
	 * SQL utasítással elindít egy tranzakciót.
	 *
	 * @return boolean A tranzakció indításának sikersességétől függően igaz, vagy hamis érték.
	 */
	public function startTransaction(){
		$res = mysql_query("START TRANSACTION", $this->connID);
		if ($res != false){
			return true;
		}else{
			return false;
		}
	}

	/**
	 * SQL utasítással szabályosan lezárja az aktuális tranzakciót. (COMMIT)
	 *
	 * @return boolean A tranzakció lezásárának sikeressége alapján igaz, vagy hamis érték
	 */
	public function endTransaction(){
		$res = mysql_query("COMMIT;", $this->connID);
		if ($res != false){
			return true;
		}else{
			return false;
		}
	}

	/**
	 * SQL utasítással eldobja az aktuális tranzakciót. (ROLLBACK)
	 *
	 * @return boolean A tranzakció eldobásának sikerességétől függően igaz, vagy hamis érték.
	 */
	public function dropTransaction(){
		$res = mysql_query("ROLLBACK;", $this->connID);
		if ($res != false){
			return true;
		}else{
			return false;
		}
	}

	/**
	 * Leellenőrzi, hogy $tableName attribútumban definiált tábla létezik-e már az adatbázisban, és ettől
	 * függően igaz, vagy hamis értéked ad vissza.
	 *
	 * @return boolean A tábla létezésétől függpen igaz vagy hamis érték
	 */
	public function isTableExists(){
		$res = mysql_query("SHOW TABLES", $this->connID);
		if (($res != false) && (mysql_num_rows($res) != 0)){
			while ($row = mysql_fetch_array($res)){
				if ($row[0] == $this->tableName){
					$this->clearMe();
					return true;
				}
			}
			$this->clearMe();
			return false;
		}else{
			$this->clearMe();
			return false;
		}
	}

	/**
	 * A példányban szereplő attribútumok értékét állítja null-ra, a példány következő felhasználásához,
	 * így nem kell folyton újrapéldányosítani minden művelethez.
	 */
	public function clearMe(){
		$this->dbElement = null;
		$this->dbElementFrom = null;
		$this->dbElementTo = null;
		$this->tableName = null;
	}

	/**
	 * A legutóbbi művelet során előfordult esetleges hibák szövegét kérdezi le.
	 *
	 * @return string A hibák szövege.
	 */
	public function getErrors(){
		return mysql_error();
	}

	/**
	 * A legutóbbi művelet során előfordult esetleges warningok szövegét adja vissza.
	 *
	 * @return string A warningok szövege.
	 */
	public function getWarnings(){
		return mysql_error();
	}

	/**
	 * A megadott adatbáziselem értékeit escapeli ki az SQL utasításban elfogadható formára.
	 *
	 * @return array A kiescapelt adatbáziselem.
	 */
	private function escapeIt($ent){
		$ent2 = array();
		foreach ($ent as $key => $value){
			$ent2[$key] = mysql_escape_string($value);
		}
		return $ent2;
	}

	/**
	 * Az EntityHandler osztály konstruktora, ami beállítja a feltétlenül fontos 
	 * attribútumokat. Erről a konstruktorról bővebben lásd a tutorialt.
	 *
	 * @param DataBaseSerializable $entity Az adatbáziselemre konvertálható példány. (Az Entity példányok
	 * ilyenek.
	 * @param DataBaseSerializable $entityfrom lásd $entity
	 * @param DataBaseSerializable $entityto lásd $entity
	 */
	public function __construct($entity = "", $entityfrom = "", $entityto = ""){
		if ($entity != ""){
			if ($entity instanceof DataBaseSerializable){
				$this->dbElement = $this->escapeIt($entity->toDBElement());
				$this->tableName = $entity->getTableName();
			}else{
				throw new Exception("A megadott egyed nem implementálja a DataBaseSerializable interfészt");
			}
		}else if (($entityfrom != "") && ($entityto != "")){
			if (($entityfrom instanceof DataBaseSerializable) && ($entityto instanceof DataBaseSerializable)){
				$this->dbElementFrom = $this->escapeIt($entityfrom->toDBElement());
				$this->dbElementTo = $this->escapeIt($entityto->toDBElement());
				$this->tableName = $entityfrom->getTableName();
			}else{
				throw new Exception("A megadott egyedek nem implementálják a DataBaseSerializable interfészt");
			}
		}else{
			//throw new Exception("Hibás metódushívás!"); <-- nem, nem mivel mostmár csak az interfész által szolgáltatott adatok léteznek.
		}
	}

}

?>
