<?php

/**
 *
 * @author Peter Csubak (chuby)
 * @copyright Peter Csubak (chuby)
 */
class PersistenceHandler{

	private static $defaultHandler = null; // Mindig kell hogy legyen egy ilyen
	private static $handlers = array(); // lehetnek ilyenek is, hogy a komplexebb feladatok is megoldhatóak legyenek.

	private $dbConnector;
	private $entityHandler;

	/**
	 * A kibővített singletonnak megfelelően működő osztály szintű metódus.
	 * A kibővítettséget itt az jelenti, hogy a metódus, több példányt is képes vissza adni, a 
	 * megadott névnek megfelelően. Ez gyakorlatilag olyan, mintha a példányok neve lenne, viszont
	 * így nem kell egy külön osztály amiben a példányokat tároljuk. Illetve ebben az esetben nem
	 * kell minden entitásnak odaadni a megfelelő perzisztencia kezelőt, hanem az entitásban elég
	 * előírni, a perzisztencia kezelő nevét, és innen elérhetővé vállnak. 
	 * 
	 * A metódus ha nem adjuk meg melyik kezelőt kérjük tőle, akkor az alapértelmezettet adja vissza.
	 * Ha már létre van hozva, akkor azt, ha pedig még nincs, akkor létrehozza és azt az új példányt
	 * adja vissza. Ha pedig megadjuk a kezelő nevét, akkor a megnevezett kezelőt adja vissza, ha már
	 * példányosítva van, ha nincs, akkor példányosítja és azt adja vissza.
	 * 
	 * @param string $handlerName ezzel a paraméterrel lehet megnevezni, hogy melyik perzisztencia 
	 * kezelő példányra van szükség. Opcionális paraméter, ha nem adjuk meg, akkor az alapértelmezett
	 * perzisztenica kezelőt fogja jelenteni. Akárcsak akkor, ha a "default" stringet adjuk át benne.
	 * @return PersistenceHandler a megnevezett, vagy az alapértelmezett perzisztencia kezelő példány. 
	 */
	public static function getInstance($handlerName = "default"){
		if ($handlerName == "default"){
			if (self::$defaultHandler == null){
				self::$defaultHandler = new PersistenceHandler();
			}
			return self::$defaultHandler;
		}else{
			if (!isset(self::$handlers[$handlerName])){
				self::$handlers[$handlerName] = new PersistenceHandler();
			}
			return self::$handlers[$handlerName];
		}
	}

	/**
	 * A kibővített singleton miatt kell privátnak lennie, és így nem is hívható máshonnan, mint a 
	 * getInstance metódusból.
	 */
	private function __construct(){
	}

	/**
	 * Beállítja az adatbázis connectorokat, azaz pontosabban az adatbázis connectort, és az entity kezelőt.
	 * Ez mindkettő konkrét szinten kapcsolódik az adatbázisrendszerhez, amiben tárolni szeretnénk az 
	 * entitásokat. Egyelőre egy ilyen kapcsoló csomag van, mégpedig a MySQL adatbázisrendszerhez.
	 * 
	 * @param DBCInterface $dbConnector Az adatbázis kapcsoló példány. (Kötelezően implementálnia kell a
	 * DBCInterface interfészt!)
	 * @param EHInterface $entityHandler Az entitiás kezelő példány. (Kötelezően implementálnia kell az
	 * EHInterface interfészt!)
	 * @throws PersistenceException kibásan megadott kapcsolók esetén a 100-as hibakódú kivételt válthatja ki.
	 */
	public function setConnector($dbConnector, $entityHandler){
		if (($dbConnector instanceof DBCInterface) && ($entityHandler instanceof EHInterface)){
			if ($dbConnector->isConnected()){
				$this->dbConnector = $dbConnector;
			}else{
				$dbConnector->connect();
				$this->dbConnector = $dbConnector;
			}
			$entityHandler->setConnectionId($dbConnector->getConnectionID());
			$this->entityHandler = $entityHandler;
		}else{
			throw new PersistenceException("Az adatbáziskapcsolók valamelyike, vagy mindkettő hibás. (Nem implementálja a megfelelő interfészeket.)", null, 100);
		}
	}

	/**
	 * Lekérdezi az entitás kezelőt.
	 * 
	 * @return EHInterface A beállított entitáskezelő példány.
	 * @throws PersistenceException Ha nem áll fenn aktív kapcsolat az adatbázissal, akkor a 101-es hibakódú 
	 * kivétel váltódik ki.
	 */
	public function getEntityHandler(){
		if ($this->isConnected()){
			return $this->entityHandler;
		}else{
			throw new PersistenceException("Nincs aktív adatbázis kapcsolat!", null, 101);
		}
	}

	/**
	 * Leellenőrzi, hogy van-e aktív adatbázis kapcsolat megnyitva a konkrét perzisztencia kezelőhöz.
	 * 
	 * @return boolean Igaz érték, ha az adatbázis kapcsoló, és az entitás kezelő be van állítva, és 
	 * az adatbázis kapcsoló isConnected metódusa igaz. Minden más esetben hamis érték.
	 */
	public function isConnected(){
		if (isset($this->dbConnector) && isset($this->entityHandler)){
			return $this->dbConnector->isConnected();
		}else{
			return false;
		}
	}

	/**
	 * Az adatbázisban használandó prefixumot adja vissza, amire azért lehet szükség, hogy elkerüljük az
	 * esetleges névütközéseket. A prefixumot az adatbázis kapcsoló getPrefix metódusa kell, hogy megmondja.
	 * 
	 * @return string Az adatbázsi kapcsoló által megadott prefixum.
	 * @throws PersistenceException ha nincs beállíva még az adatbázis kapcsoló, akkor 105-ös hibakódú kivételt 
	 * vált ki.
	 */
	public function getPrefix(){
		if (isset($this->dbConnector)){
			return $this->dbConnector->getPrefix();
		}else{
			throw new PersistenceException("A tábla prefix nem lekérdezhető, mert nincs beállítva az adatbázis kapcsoló.", null, 105);
		}
	}

	/**
	 * Lekérdezést végez az adatbázisból, úgy, hogy a visszatérésben konkrét példányosított entitások 
	 * szerepeljenek. A használata úgy történik, hogy, meg kell adni a példányosítandó entitás osztálynevet,
	 * és a where záradékot. Ez utóbbi számos módon megtehető, de talán a legjobban használható a Where 
	 * osztály megfelelő példányának megadásával. Ez hatékonyabb, és könnyebben érthető mód, mint a tömb
	 * alapú where záradék megadása. (Ez csak a kompatibilitási gondok elkerülése okán marad meg.)
	 * A natív sql where utasítás is hatékony, de nem elég magasszintű megoldás, ráadásul nem oldja meg az
	 * adatbázisrendszer cserélhetőségét a rendszer alatt, míg ezt a Where példányok maximálisan támogatják. 
	 * 
	 * @param string $entityClass Az eredményben példányosítandó entitások osztályneve. (Kötelezően string
	 * formában!)
	 * @param mixed $where A lekérdezés where záradéka. Lehet string, ebben az esetben ez a string kerül 
	 * közvetlenül az adatbázisnak küldött select utasításban a where szó után. Lehet asszociatív tömb 
	 * típusú is. Ebben az esetben a tömb a $tester paraméterben megadott hasoonlító operátort használva
	 * és a $logicConn logikai operátort használva fog értelmeződni, mégpedig úgy, hogy a tömb kulcsai 
	 * és értékei közt a hasonlító operátorok lesznek felhasználva, és az egyes kulcs-érték párok közt 
	 * pedig a logikai operátorok lesznek használva. Ezen kívül már lehet Where típusú példány is. 
	 * Ennek működéséről lásd a Where osztályt.
	 * @param string $logicConn A tesztelésben felhasználandó kifejezések közt használandó logikai 
	 * operátor/kapcsoló. Ez természetesen csak abban az esetben lesz értelmezve, ha a $where paraméter 
	 * egy asszociatív tömb.
	 * @param string $tester A tesztelésben felhasználandó kifejezésekben használandó hasonlító operátor.
	 * @return array Üres tömb, amennyiben a lekérdezésre adott válasz nem tartalmazott egy sort sem. 
	 * Egyébként pedig a lekérdezésre kapott válaszból képzett entitásokat tartalmazó tömb.
	 * @throws PersistenceException a lekérdezés közben felmerülő hibák esetén 101, 102, 103, 104-es hibakódú
	 * kivételeket válthat ki.
	 */
	public function selectEntities($entityClass, $where, $logicConn = "AND", $tester = "="){
		if ($this->isConnected()){
			$ref = new ReflectionAnnotatedClass($entityClass);
			if ($ref->hasAnnotation("Table")){
				if ($ref->hasAnnotation("DisableAutoLoad")){
					throw new PersistenceException("Hibás egyedalapú lekérdezés, a hivatkozott egyedtípuson le van tíltva az automatikus betöltés.", null, 102); 
				}
				$tab = $ref->getAnnotation("Table");
				$tablename = "";
				if ($tab->name == "default"){
					//egyezik az osztály nevével csak minden betű kicsi és van a végén egy s mint a többesszám jele
					$tablename = strtolower($entityClass)."s";
				}else{
					$tablename = $tab->name;
				}

				//megkeressük az id-t
				$idname = "";
				$fields = $ref->getProperties();
				for ($i = 0; $i < count($fields); $i++){
					$f = $fields[$i];
					$afield = new ReflectionAnnotatedProperty($entityClass, $f->getName());
					if ($afield->hasAnnotation("Id")){
						if ($afield->hasAnnotation("Attribute")){
							$attrib = $afield->getAnnotation("Attribute");
							if ($attrib->name == "default"){
								$idname = strtoupper(substr($afield->getName(), 0, 1)).substr($afield->getName(), 1);
								break;
							}else{
								$idname = $attrib->name;
								break;
							}
						}else{
							throw new PersistenceException("Hibás entitás leírás: Az azonosítónak mindig tárolt mezőnek kell lennie, viszont a <<type>> nevű entitásban ez nem így van.", array("type" => $ann->type), 103);
						}
					}
				}

				$dbelement = array($idname => "");
				$this->entityHandler->setDBElement($dbelement);
				$this->entityHandler->setTableName($tablename);
				$selres = $this->entityHandler->select($where, $logicConn, $tester);
				$res = array();
				$i = 0;
				while ($row = $selres[$i]){
					$ent = $ref->newInstance();
					$ent->autoLoad($row[$idname]);
					$res[$i++] = $ent;
				}
				return $res;
			}else{
				throw new PersistenceException("Hibás egyedtípusú lekérdezés: A lekérdezés olyan osztályt hivatkozik, ami nem tárolt entitás, így a lekérdezés sem lehetséges.", null, 104);
			}
		}else{
			throw new PersistenceException("Nincs aktív adatbázis kapcsolat!", null, 101);
		}
	}

	/**
	 * Lekérdezést végez az adatbázisból, úgy, hogy a visszatérési értékben értelmezés nélküli asszociatív
	 * tömbök álljanak. (Jó módszer lehet az olyan lekérdezéseknél, ahhol csoportosító utasításokat kell 
	 * alkalmazni.) A használata úgy történik, hogy, meg kell adni a példányosítandó entitás osztálynevet,
	 * és a where záradékot. Ez utóbbi számos módon megtehető, de talán a legjobban használható a Where 
	 * osztály megfelelő példányának megadásával. Ez hatékonyabb, és könnyebben érthető mód, mint a tömb
	 * alapú where záradék megadása. (Ez csak a kompatibilitási gondok elkerülése okán marad meg.)
	 * A natív sql where utasítás is hatékony, de nem elég magasszintű megoldás, ráadásul nem oldja meg az
	 * adatbázisrendszer cserélhetőségét a rendszer alatt, míg ezt a Where példányok maximálisan támogatják. 
	 * 
	 * 
	 * @param string $table Azt az adatbázisbeli táblanevet kell átadni ebben a paraméterben, amit a 
	 * lekérdezés érinteni fog.
	 * @param mixed $cols A lekérdezendő oszlopok felsorolása. Lehet string, illetve tömb, amennyiben string
	 * akkor nyilván az adatbázis vezérlő nyelven értelmezhető stringnek kell lennie. Amennyiben tömb
	 * akkor nyílván nem számít a kompatibilitás, ezért érdemesebb ezt a módszert alkalmazni. (A stringes 
	 * megoldás csak azért létezik, hogy kompatibilis maradjon a korábbi verziókkal, illetve, hogy a 
	 * komplexebb lekérdezések is megvalósíthatóak legyenek.)
	 * @param mixed $where A lekérdezés where záradéka. Lehet string, ebben az esetben ez a string kerül 
	 * közvetlenül az adatbázisnak küldött select utasításban a where szó után. Lehet asszociatív tömb 
	 * típusú is. Ebben az esetben a tömb a $tester paraméterben megadott hasoonlító operátort használva
	 * és a $logicConn logikai operátort használva fog értelmeződni, mégpedig úgy, hogy a tömb kulcsai 
	 * és értékei közt a hasonlító operátorok lesznek felhasználva, és az egyes kulcs-érték párok közt 
	 * pedig a logikai operátorok lesznek használva. Ezen kívül már lehet Where típusú példány is. 
	 * Ennek működéséről lásd a Where osztályt.
	 * @param string $logicConn A tesztelésben felhasználandó kifejezések közt használandó logikai 
	 * operátor/kapcsoló. Ez természetesen csak abban az esetben lesz értelmezve, ha a $where paraméter 
	 * egy asszociatív tömb.
	 * @param string $tester A tesztelésben felhasználandó kifejezésekben használandó hasonlító operátor.
	 * @return array A lekérdezés eredménye kerül bele, mégpedig asszociatív tömbök formájában
	 * @throws PersistenceExcepton adatbázis kapcsolat hiánya, illetve hiányos adatközlés esetén
	 * 101, illetve 106-as hibakódú kivételt vált ki.
	 */
	public function selectArray($table, $cols, $where, $logicConn = "AND", $tester = "="){
		if ($this->isConnected()){
			if (isset($table) && isset($cols)){
				if (is_string($cols)){
					$this->entityHandler->setTableName($table);
					$res = $this->entityHandler->select($where, $logicConn, $tester, $cols);
				}elseif (is_array($cols)){
					$dbelement = array();
					for ($i = 0; $i < count($cols); $i++){
						$dbelement[$cols[$i]] = "";
					}
					$this->entityHandler->setTableName($table);
					$this->entityHandler->setDBElement($dbelement);
					$res = $this->entityHandler->select($where, $logicConn, $tester, $cols);
				}
				return $res;
			}else{
				throw new PersistenceException("Hibás tömb alapú lekérdezés, a táblanév, vagy a mezők neve nincs beállítva.", null, 106);
			}
		}else{
			throw new PersistenceException("Nincs aktív adatbázis kapcsolat!", null, 101);
		}
	}

	/**
	 * Elindít egy szabályos adatbázis tranzakciót. Ez a tranzakció elindítása ettől függetlenül nem hat ki
	 * az entitások működésére, ez pusztán adatbázis szintű tranzakció. Természetesen tervbe van véve a 
	 * magasszintű entitásokra is kiható tranzakciókezelő írása is, de addig is az adatbázisszintű védelemmel
	 * az adatok konzisztenciája megóvható, még ha a tranzakciók az entitásokra nem is hatnak ki.
	 * 
	 * 
	 * @throws PersistenceException Tranzakciós hiba esetén, illeve adatbázis kapcsolat hiánya esetén 
	 * 400, illetve 101-es hibát válthat ki.
	 */
	public function startTransaction(){
		if ($this->isConnected()){
			$res = $this->entityHandler->startTransaction();
			if ($res == false){
				throw new PersistenceException("Tranzakció hiba: nem lehet tranzakciót indítani, a hibaüzenet: <<error>>", array("error" => $this->entityHandler->getErrors()), 400);
			}
		}else{
			throw new PersistenceException("Nincs aktív adatbázis kapcsolat!", null, 101);
		}
	}

	/**
	 * Lezárja a megnyitott adatbázis tranzakciót. Szabályos adatbázisszintű tranzakció lezárást végez, 
	 * tehát tulajdonképpen végrehajt egy commit utasítást.
	 * 
	 * @throws PersistenceException Tranzakciós hiba esetén, illetve adatbázis kapcsolat hiánya esetén
	 * 401, illetve 101-es hibát válthat ki.
	 */
	public function endTransaction(){
		if ($this->isConnected()){
			$res = $this->entityHandler->endTransaction();
			if ($res == false){
				throw new PersistenceException("Tranzakció hiba: nem lehet tranzakciót befejezni, a hibaüzenet: <<error>>", array("error" => $this->entityHandler->getErrors()), 401);
				// Nah kb ez fog soha bekövetkezni, mert a commit mindig átmegy akármi van, de mindegy maradjon, ártani nem árt :-)
			}
		}else{
			throw new PersistenceException("Nincs aktív adatbázis kapcsolat!", null, 101);
		}
	}

	/**
	 * Eldob egy megnyitott tranzakciót. Azaz eldobja az adatbázisra értelmezett változáspkat.
	 * Egyelőre a tranzakciók az entitásokra nem hatnak ki, tehát ez az eldobás nem jelenti az 
	 * entitások állapotának visszaállítását.
	 * TODO A trenzakciók határának kitejesztése az entitásokra közvetlenül.
	 * 
	 * @throws PersistenceException Tranzakciós hiba, illetve adatbázis kapcsolat hiánya esetén 
	 * 402, illetve 101-es hibakódú kivételt válthat ki.
	 */
	public function dropTransaction(){
		if ($this->isConnected()){
			$res = $this->entityHandler->dropTransaction();
			if ($res == false){
				throw new PersistenceException("Tranzakció hiba: nem lehet tranzakciót eldobni, a hibaüzenet: <<error>>", array("error" => $this->entityHandler->getErrors()), 402);
			}
		}else{
			throw new PersistenceException("Nincs aktív adatbázis kapcsolat!", null, 101);
		}
	}

}
?>