<?php

// TODO: unset the $fullBalance after balance is rewritten

// TODO: take into account toleration - see old way

namespace App\PartnerBalance;

abstract class GeneralAbstract
{

	/**
	 * The entity id.
	 * @var Int
	 */
	protected $idEntity;

	/**
	 * The currency code.
	 * @var String
	 */
	protected $currency;

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

	/**
	 * The db table column name for the entity id.
	 * @var String
	 */
	protected $tableColumnIdEntity;

	/**
	 * The entity balance values (associative array of currency=>value).
	 * @var array - if set to null, no cache was executed
	 */
	protected $fullBalance = null;


	protected $tolerance = 0.0001;

	/**
	 * The singleton instances.
	 * @var array
	 */
	static protected $instance = [];



	/**
	 * Class constructor - sets the entity and the currency of the invoice to be processed.
	 * @param Int $idEntity
	 * @throws \Exception - if input is invalid
	 */
	final protected function __construct($idEntity)
	{

		// check customer id
		if (!is_numeric($idEntity) and $idEntity<=0) {
			throw new \Exception('The entity id is not set for class `'.get_called_class().'`.');
		}

		// set db table details
		$this->setTableDetails();
		// chek db table details
		if (!$this->tableName) {
			throw new \Exception('The table name for class `'.get_called_class().'` is not set.');
		}
		if (!$this->tableColumnIdEntity) {
			throw new \Exception('The entity id table column for class `'.get_called_class().'` is not set.');
		}

		// set the entity id
		$this->idEntity = $idEntity;

	}



	/**
	 * Multiton instantiation for balance stuff.
	 * @param String $idEntity
	 * @return static
	 */
	static public function getInstance($idEntity)
	{
		$currClass = get_called_class();
		if (!static::$instance[$currClass][$idEntity]) {
			static::$instance[$currClass][$idEntity] = new static($idEntity);
		}
		return static::$instance[$currClass][$idEntity];
	}



	/**
	 * Sets the details of the db table (table name and column name)
	 */
	abstract protected function setTableDetails();



	/**
	 * Returns the full balance array  for the current entity (assoc array); it also caches the result.
	 * @return multitype:
	 */
	public function getFullBalance()
	{
		// if balances are not cached - get them from db (and cache them)
		if ($this->fullBalance===null) {
			$this->fullBalance = $this->getFullBalanceFromDb(false);
		}

		return $this->fullBalance;

	}

	public function getEquivBalance($currency=null, $currencyDate=TODAY_ISO, $currencyType=null)
	{

		// get and check current balance - if no balance set, return 0
		$balDet = $this->getFullBalance();
		if (!$balDet) {
			return 0;
		}

		// set currency
		if (!$currency) {
			$currency = \App\Config\Currencies::getInstance()->getDefaultCurrency();
		}

		// set currency type
		if (!$currencyType) {
			$currencyType = \App\Config\CurrencyTypes::getInstance()->getDefaultCurrencyType();
		}

		// calculate total balance (from all currencies)
		$ttl = 0;
		foreach($balDet as $curr=>$val) {
			$ttl += \App\CurrencyExchange\Rates::getInstance($currencyDate, $currencyType)->convert($val, $curr, $currency);
		}

		return round($ttl, 3); // clean floating point issues

	}


	/**
	 * Gets the balances from db for all currencies.
	 * @param Boolean $selectForUpdate - indicates if the select is done "FOR UPDATE".
	 */
	protected function getFullBalanceFromDb($selectForUpdate)
	{

		// get entity balance from db
		$db = \DB::getInstance();
		$getBalancesSQL = "SELECT `currency`, `value` FROM `{$this->tableName}` WHERE `{$this->tableColumnIdEntity}`=?";
		if ($selectForUpdate) {
			$getBalancesSQL .= " FOR UPDATE";
		}
		$getBalancesEx = $db->Execute($getBalancesSQL, [$this->idEntity]);
		if (!$getBalancesEx) {
			throw new \DBException('Could not get entity balance for id = '.$this->idEntity.' ; class = '.get_called_class());
		}

		// create balances output
		$return = [];
		while ($row=$getBalancesEx->FetchRow()) {
			$return[$row['currency']] = (Float)$row['value'];
		}
		return $return;

	}



	/**
	 * Returns the balance for a certain currency - forces it to 0 if the balance does not exist at all.
	 * @param String $currency
	 * @return Float - the entity balance.
	 */
	public function getCurrencyBalance($currency)
	{
		$bals = $this->getFullBalance();
		return (Float)$bals[$currency];
	}


	protected function updateBalance($currency, $value)
	{

		// if no value - do not do anything
		if (!$value) {
			return true;
		}

		// get current balances
		$bals = $this->getFullBalanceFromDb(true);

		// determine the initial balance
		if ($bals[$currency]) {
			$initialBalance = $bals[$currency];
		} else {
			$initialBalance = 0;
		}
		// calculate final balance
		$finalBalance = $initialBalance + $value;
		// apply tolerance
		if (abs($finalBalance)<$this->tolerance) {
			$finalBalance = 0;
		}

		// WRITE THE FINAL BALANCE IN THE DB ------------------------------------------------------

		if (!$finalBalance) {
			$updateBalanceSQL = "DELETE FROM `{$this->tableName}` WHERE `{$this->tableColumnIdEntity}`=? AND currency=?";
			$prepVals = [$this->idEntity, $currency];
		} elseif ($initialBalance) {
			$updateBalanceSQL = "UPDATE `{$this->tableName}` SET value = ? WHERE `{$this->tableColumnIdEntity}`=? AND currency=?";
			$prepVals = [$finalBalance, $this->idEntity, $currency];
		} else {
			$updateBalanceSQL = "INSERT INTO `{$this->tableName}` (`{$this->tableColumnIdEntity}`, `currency`, `value`) VALUES (?, ?, ?)";
			$prepVals = [$this->idEntity, $currency, $finalBalance];
		}

		$db = \DB::getInstance();
		$updateBalanceEx = $db->Execute($updateBalanceSQL, $prepVals);
		if (!$updateBalanceEx) {
			throw new \DBException('Failed to update entity balance for '.get_called_class());
		}

		$this->fullBalance = null;

	}


}

