<?php

// TODO: getSerializedProcessedRemarks si getUnserializedProcessedRemarks - ar trebui sa existe intr-o clasa entity helper - ar trebui sa dispara de tot de aici.
// TODO: rename cacheTtl into ALL_CAPS format

namespace App\Entity;

abstract class SelectAbstract implements SelectInterface {

	/**
	 * Indicate how many seconds the external cache is valid. 0 for unlimited, -1 for no cache.
	 * @var Int
	 */
	const cacheTtl=7200;


	/**
	 * The main entity id
	 * @var mixed
	 */
	protected $id;

	/**
	 * The name of the column in the database that represents the primary key of the current entity.
	 * By default it is 'id' but it may be changed for certain entities (country/airport)
	 *
	 * @var String
	 */
	protected $tablePrimaryKey='id';

	/**
	 * The primary key type (by default it is 'numeric' which means Insert_ID() can be used.
	 * If this value is changed in some entities (for eg. Country) Insert_ID will be skipped and the value will be copied from the primary key (manually set during data collection).
	 *
	 * @var String
	 */
	protected $primaryKeyType='numeric';

	/**
	 * Indicates if the populate SELECT operation will add 'FOR UPDATE' to the sql
	 * @var Boolean
	 */
	protected $selectForUpdate=false;

	/**
	 * Array of all the fields of the current entity operation (select/insert/update)
	 *
	 * @var array
	 */
	protected $fields=[];

	/**
	 * Indicate if the current 'select' operation returned an existent record in the database.
	 * @var Boolean
	 */
	protected $recordExists=false;


	/**
	 *
	 * The database table name
	 * @var String
	 */
	protected $tableName;

	/**
	 * Indicates if the current entity operation is 'select', 'insert' or 'update' or 'delete'
	 *
	 * @var String
	 */
	protected $operation=null;


	/**
	 * Constructor for the abstract entity
	 * @param String $operation
	 * @param Integer $id
	 * @param Boolean $forUpdateFlag
	 */
	final protected function __construct($operation, $id, $forUpdateFlag=false){

		// INITIALIZE CLASS ATTRIBUTES ------------------------------------------------------------
		// associate operation
		$this->operation=$operation;
		// associate forUpdateFlag
		if ($forUpdateFlag) {
			$this->selectForUpdate=true;
		}
		// associate entity id
		$this->id=$id;

		// manage DB table - makes sure the table name/group (for MT) is correct
		$this->manageDbTable();

		$this->initialize();

	}

	/**
	 * Makes sure that the DB table name is set.
	 */
	protected function manageDbTable() :void
	{
		// associate table
		$this->setTableName();
		// check tableName
		if (!$this->tableName) {
			throw new \Exception('The table name was NOT set for class `'.get_called_class().'` - the setTableName() method should have filled in the attribute `this->tableName`.');
		}
	}


	/**
	 * Sets the $this->tableName - the database table name used for the current entity. The method is called in the class constructor.
	 */
	abstract protected function setTableName();


	/**
	 * This method is called after the constructor finishes. It will search the db and populate the $this->fields array.
	 */
	protected function initialize() {

		// populates with data from the database
		$this->populate();

	}


	/**
	 * The instantiation for a new selecting entity (simulate singleton by using \App\Entity\Cache class).
	 * The "select" operation will cache the result or will use a previously stored cache.
	 * @param Integer $idEntity
	 * @param Boolean $forUpdateFlag
	 * @return \App\Entity\GeneralAbstract
	 */
	static public function select($idEntity, $forUpdateFlag=false) {

		// REDRECT TO SELECT FOR UPDATE (IF THE FLAG IS SET) --------------------------------------
		if ($forUpdateFlag) {
			return static::selectForUpdate($idEntity);
		}

		// OTHERWISE - SELECT (WITHOUT "FOR UPDATE") ----------------------------------------------

		// the entity class name (stripped of its ns)
		$className=stripClassNamespace(get_called_class());

		// first check the cache; if something is found - return it
		$cachedObject=\App\Entity\Cache::getInstance()->get($className, $idEntity);
		if ($cachedObject) return $cachedObject;

		// no cache found - instantiate the self class (late static binding)
		$entityObj=new static('select', $idEntity);
		// set it so it will be cached when the destructor is executed
		\App\Entity\Cache::getInstance()->set($entityObj);
		// return the current object
		return $entityObj;

	}

	/**
	 * The instantiation for a new selecting (for update) entity (Singleton instantiation).
	 * The "selectForUpdate" operation will ALWAYS exeute the SQL in the current script execution (to lock the row).
	 * @param Integer $idEntity
	 * @return \App\Entity\GeneralAbstract
	 */
	static public function selectForUpdate($idEntity) {

		// redirect to normal select (if not inside a transaction)
		if (!\DB::isInsideTransaction()) {
			return static::select($idEntity);
		}

		$className=stripClassNamespace(get_called_class());

		// first check the cache; if something is found - return it (only the current RAM is checked)
		$cachedObject=\App\Entity\Cache::getInstance()->getInternal($className, $idEntity);
		if ($cachedObject) return $cachedObject;

		// no cache found - instantiate the self class (late static binding)
		$entityObj=new static('select', $idEntity, true);
		// save this in the \App\Entity\Cache internal array (only in RAM, not in external cache)
		\App\Entity\Cache::getInstance()->setInternal($entityObj);
		// return the current object
		return $entityObj;

	}


	/**
	 * Populates the $this->fields array with every field from the database table
	 */
	protected function populate() {

		// CHECK THE ID ---------------------------------------------------------------------------
		// check id (numeric)
		if ($this->primaryKeyType=='numeric' and (!is_numeric($this->id) or $this->id<=0) ) {
			return ;
		}
		// check id (string)
		if ($this->primaryKeyType=='string' and !strlen((string)$this->id) ) {
			return ;
		}

		$db=\DB::getInstance();

		// GET THE DETAILS FROM THE DATABASE ------------------------------------------------------
		// get data from database
		$entityDetSQL = "SELECT * FROM `{$this->tableName}` WHERE `{$this->tablePrimaryKey}`=?";
		if ($this->selectForUpdate) $entityDetSQL .= " FOR UPDATE";
		$entityDetEx = $db->Execute( $entityDetSQL, [$this->id]);
		// check sql execution
		if (!$entityDetEx) {
			\Log::getInstance()->error("Could not get entity details for table `{$this->tableName}`; '{$this->id}' Error: {$db->ErrorMsg()} [{$db->ErrorNo()}].");
			throw new \DBException("Could not get entity details for table `{$this->tableName}`; '{$this->id}'.");
		}
		// get data from db
		$entityDet = $entityDetEx->FetchRow();
		// if nothing found - return an empty array
		if (!$entityDet) $entityDet = [];
		// if record exists in the database - set the attribute in the class
		if ($entityDet) $this->recordExists=true;
		// store the "select"-ed fields
		$this->fields = $entityDet;

	}


	/**
	 * Returns the entity fields collected via SQL (select) or manually set (insert/update)
	 * @return Array
	 */
	public function getFields() {
		return $this->fields;
	}


	/**
	 * Gets the current id of the entity
	 */
	public function getId(){
		return $this->id;
	}


	/**
	 * Returns the current entity table name.
	 * @return String
	 */
	public function getTableName() {
		return $this->tableName;
	}

	/**
	 * Returns true/false if the selected record exists in the database
	 * @return Boolean
	 */
	public function recordExists() {
		if ($this->operation!='select') throw new \Exception("You can check if a record exists only for `select` operation. Called for Entity - `{$this->tableName}`");
		return $this->recordExists;
	}


	/**
	 * Overload __get (get the corresponding key from $this->fields)
	 */
	public function __get($var) {

		// check operation
		if ($this->operation!='select') {
			throw new \Exception('You can call `'.__METHOD__.'` only for `select` operations. You used it for `'.$this->operation.'` on `'.get_called_class().'` class on `'.$var.'` attribute.');
		}

		// apply the getAttribute from outside extensions for the current entity type
		$className = stripClassNamespace(get_class($this));
		foreach(\App\Entity\Utils\ExtensionsManager::getInstance($className)->getExtensions('\App\Entity\Utils\Extension_GetAttributeAbstract') as $extClass) {
			$extObj = new $extClass($this);
			$value = $extObj->getAttribute($var);
			$extObj->destroy();
			if ($value!==null) {
				return $value;
			}
		}

		return $this->getAttribute($var);

	}

	/**
	 * The standard getter for class attributes - it searches the value in the $this->fields array.
	 * The method shouold be rewritten in the concrete classes and it should cover special fields - for
	 * the other simple (straightforward) fields - parent::getAttribute($var) may be called
	 * @param String $var
	 * @throws Exception - if the var name does not exist (as a key in the array).
	 * @return Mixed; null for entities without recordExists.
	 */
	protected function getAttribute($var) {

		// if the key exists in the $this->fields array
		if (array_key_exists($var, $this->fields)) {
			return $this->fields[$var];
		}
		// the key does not exist in the $this->fields array
		else {
			// if the current record exists - the selected attribute is invalid
			if ($this->recordExists) {
				throw new \Exception(sprintf("The field '%s' doesn't exist in entity %s. Please check if this attribute is actually defined in the DB (it its a real attribute) or in the entity (if its a virtual attribute)", $var, get_called_class()));
			}
			// if the current record does not exist - return null
			else {
				return null;
			}
		}

	}



	/**
	 * Transforms the processedRemarks from array to string (array2string, and if the array is empty it returns blank.
	 * @return String
	 */
	protected function getSerializedProcessedRemarks() {
		if (is_array($this->fields['processedRemarks']) and $this->fields['processedRemarks']) {
			return array2string($this->fields['processedRemarks']);
		} else {
			return '';
		}
	}

	/**
	 * Transforms the processedRemarks (from fields['processedRemarks']) from string (db format) to array (string2array, and if no remarks set, it returns an empty array.
	 * @return Array
	 */
	protected function getUnserializedProcessedRemarks() {
		if ($this->fields['processedRemarks']) {
			return string2array($this->fields['processedRemarks']);
		} else {
			return [];
		}
	}

	/**
	 * Returns the value of the flag (attribute) "selectForUpdate".
	 * @return boolean
	 */
	public function getSelectForUpdateFlag()
	{
		return $this->selectForUpdate;
	}

}
