<?php
/**
 * This code is licensed under AGPLv3 license or Afterlogic Software License
 * if commercial version of the product was purchased.
 * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
 */

namespace Aurora\System;

/**
 * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
 * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
 * @copyright Copyright (c) 2019, Afterlogic Corp.
 *
 * @package Api
 */
abstract class AbstractContainer
{
	const SESSION_CONTAINER_PREFIX = 'sess_object_container';

	/**
	 * @var bool
	 */
	public $__USE_TRIM_IN_STRINGS__;

	/**
	 * @var string
	 */
	protected $sParentClassName;

	/**
	 * @var string
	 */
	protected $sSessionUniqueProperty;

	/**
	 * @var array
	 */
	protected $aContainer;

	/**
	 * @var array
	 */
	protected $aObsolete;

	/**
	 * @var array
	 */
	protected $aTrimer;

	/**
	 * @var array
	 */
	protected $aLower;

	/**
	 * @var array
	 */
	protected $aUpper;

	/**
	 * @param string $sParentClassName
	 * @param string $sSessionUniqueProperty = ''
	 */
	public function __construct($sParentClassName, $sSessionUniqueProperty = '')
	{
		$this->__USE_TRIM_IN_STRINGS__ = false;
		$this->sParentClassName = $sParentClassName;
		$this->sSessionUniqueProperty = $sSessionUniqueProperty;

		$this->aContainer = array();
		$this->aObsolete = array();
		$this->aTrimer = array();
		$this->aLower = array();
		$this->aUpper = array();
	}

	/**
	 * @param array $aValues
	 * @return void
	 */
	public function SetDefaults($aValues)
	{
		$this->MassSetValues($aValues);
		$this->FlushObsolete();
	}

	/**
	 * @param array $aValues
	 * @return void
	 */
	public function SetTrimer($aValues)
	{
		$this->aTrimer = $aValues;
	}

	/**
	 * @param array $aValues
	 * @return void
	 */
	public function SetLower($aValues)
	{
		$this->aLower = $aValues;
	}

	/**
	 * @param array $aValues
	 * @return void
	 */
	public function SetUpper($aValues)
	{
		$this->aUpper = $aValues;
	}

	/**
	 * @param array $aValues
	 * @return void
	 */
	public function MassSetValues($aValues)
	{
		foreach ($aValues as $sKey => $mValue)
		{
			$this->{$sKey} = $mValue;
		}
	}

	/**
	 * @param stdClass $oRow
	 */
	public function InitByDbRow($oRow)
	{
		$aMap = $this->getMap();
		foreach ($aMap as $sKey => $aTypes)
		{
			if (isset($aTypes[1]) && \property_exists($oRow, $aTypes[1]))
			{
				if ('password' === $aTypes[0])
				{
					$this->{$sKey} = Utils::DecryptValue($oRow->{$aTypes[1]});
				}
				else if ('datetime' === $aTypes[0])
				{
					$iDateTime = 0;
					$aDateTime = Utils::DateParse($oRow->{$aTypes[1]});
					if (\is_array($aDateTime))
					{
						$iDateTime = \gmmktime($aDateTime['hour'], $aDateTime['minute'], $aDateTime['second'],
							$aDateTime['month'], $aDateTime['day'], $aDateTime['year']);

						if (false === $iDateTime || $iDateTime <= 0)
						{
							$iDateTime = 0;
						}
					}

					$this->{$sKey} = $iDateTime;
				}
				else if ('serialize' === $aTypes[0])
				{
					$this->{$sKey} = ('' === $oRow->{$aTypes[1]} || !\is_string($oRow->{$aTypes[1]})) ?
						'' : \unserialize($oRow->{$aTypes[1]});
				}
				else
				{
					$this->{$sKey} = $oRow->{$aTypes[1]};
				}

				$this->FlushObsolete($sKey);
			}
		}
	}

	/**
	 * @param string $sKey
	 * @return mixed
	 */
	public function GetObsoleteValue($sKey)
	{
		if (\key_exists($sKey, $this->aObsolete))
		{
			return $this->aObsolete[$sKey];
		}

		return null;
	}

	/**
	 * @param string $sKey = null
	 * @return void
	 */
	public function FlushObsolete($nsKey = null)
	{
		if (null === $nsKey)
		{
			$this->aObsolete = array();
		}
		else
		{
			if (\key_exists($nsKey, $this->aObsolete))
			{
				unset($this->aObsolete[$nsKey]);
			}
		}
	}

	/**
	 * @param string $sKey
	 * @param mixed $mDefault
	 * @return mixed
	 */
	public function GetSessionValue($sKey, $mDefault = null)
	{
		$aValues = Session::get($this->getSessionUniqueKey(), null);

		return (\is_array($aValues) && \array_key_exists($sKey, $aValues)) ? $aValues[$sKey] : $mDefault;
	}

	/**
	 * @param string $sKey
	 * @param mixed $mValue
	 */
	public function SetSessionValue($sKey, $mValue)
	{
		$sUniqueKey = $this->getSessionUniqueKey();
		$aValues = Session::get($sUniqueKey, array());
		if (!\is_array($aValues))
		{
			$aValues = array();
		}

		$aValues[$sKey] = $mValue;
		Session::Set($sUniqueKey, $aValues);
	}

	/**
	 * @return string
	 */
	protected function getSessionUniqueKey()
	{
		$sUniqueKey = (0 === \strlen($this->sSessionUniqueProperty)) ? '' : $this->{$this->sSessionUniqueProperty};
		return AbstractContainer::SESSION_CONTAINER_PREFIX.$this->sParentClassName.$sUniqueKey;
	}

	/**
	 * @return string
	 */
	public function SessionUniqueProperty()
	{
		return $this->sSessionUniqueProperty;
	}

	/**
	 * @return string
	 */
	public function SessionUniquePropertyValue()
	{
		return (0 === \strlen($this->sSessionUniqueProperty)) ? '' : $this->{$this->sSessionUniqueProperty};
	}

	/**
	 * @param string $sPropertyName
	 * @param mixed $mValue
	 * @return bool
	 */
	public function IsProperty($sPropertyName)
	{
		$aMap = $this->getMap();
		return isset($aMap[$sPropertyName]);
	}

	/**
	 * @param string $sPropertyName
	 * @return bool
	 */
	public function __isset($sPropertyName)
	{
		return $this->IsProperty($sPropertyName);
	}

	/**
	 * @param string $sKey
	 * @param mixed $mValue
	 * @return void
	 */
	public function __set($sKey, $mValue)
	{
		$aMap = $this->getMap();
		if (isset($aMap[$sKey]))
		{
			$this->setType($mValue, $aMap[$sKey][0]);

			if (\key_exists($sKey, $this->aContainer))
			{
				$this->aObsolete[$sKey] = $this->aContainer[$sKey];
			}

			if (($this->__USE_TRIM_IN_STRINGS__ && 0 === \strpos($aMap[$sKey][0], 'string')) ||
				(\in_array($sKey, $this->aTrimer) && \is_string($mValue)))
			{
				$mValue = trim($mValue);
			}

			if (\is_string($mValue))
			{
				if (\in_array($sKey, $this->aLower))
				{
					$mValue = \strtolower($mValue);
				}
				else if (\in_array($sKey, $this->aUpper))
				{
					$mValue = \strtoupper($mValue);
				}
			}

			$this->aContainer[$sKey] = $mValue;
		}
		else
		{
			throw new Exceptions\BaseException(Exceptions\Errs::Container_UndefinedProperty, null, array('{{PropertyName}}' => $sKey));
		}
	}

	/**
	 * @param string $sKey
	 *
	 * @throws Exception
	 *
	 * @return mixed
	 */
	public function __get($sKey)
	{
		$mReturn = null;
		if (\array_key_exists($sKey, $this->aContainer))
		{
			$mReturn = $this->aContainer[$sKey];
		}
		else
		{
			throw new Exceptions\Exception('Undefined property '.$sKey);
		}

		return $mReturn;
	}

	/**
	 * @param mixed $mValue
	 * @param string $sType
	 */
	protected function setType(&$mValue, $sType)
	{
		$sType = \strtolower($sType);
		if (\in_array($sType, array('string', 'int', 'bool', 'array')))
		{
			\settype($mValue, $sType);
		}
		else if (\in_array($sType, array('datetime')))
		{
			\settype($mValue, 'int');
		}
		else if (\in_array($sType, array('password')))
		{
			\settype($mValue, 'string');
		}
		else if (0 === \strpos($sType, 'string('))
		{
			\settype($mValue, 'string');
			if (0 < \strlen($mValue))
			{
				$iSize = \substr($sType, 7, -1);
				if (\is_numeric($iSize) && (int) $iSize < \strlen($mValue))
				{
					// $mValue = substr($mValue, 0, (int) $iSize);
					$mValue = Utils::Utf8Truncate($mValue, (int) $iSize);
				}
			}
		}
	}

	/**
	 * @return bool
	 */
	public function initBeforeChange()
	{
		return true;
	}

	/**
	 * @return bool
	 */
	public function validate()
	{
		return true;
	}

	/**
	 * @return array
	 */
	public function getMap()
	{
		return array();
	}

	/**
	 * @return array
	 */
	public static function getStaticMap()
	{
		return array();
	}

	/**
	 * @param array $aMap
	 *
	 * @return array
	 */
	public static function DbReadKeys($aMap)
	{
		$aResult = array();
		foreach ($aMap as $aTypes)
		{
			if (isset($aTypes[1]))
			{
				$aResult[] = $aTypes[1];
			}
		}
		return $aResult;
	}

	/**
	 * @param array $aMap
	 * @param bool $bInsert
	 *
	 * @return array
	 */
	public static function DbWriteKeys($aMap, $bInsert)
	{
		$aResult = array();
		foreach ($aMap as $sKey => $aTypes)
		{
			if (isset($aTypes[1]))
			{
				$bUseInInsert = $bUseInUpdate = true;

				if (isset($aTypes[2]) && !$aTypes[2])
				{
					$bUseInInsert = false;
				}

				if (isset($aTypes[3]) && !$aTypes[3])
				{
					$bUseInUpdate = false;
				}

				if (($bInsert && $bUseInInsert) || (!$bInsert && $bUseInUpdate))
				{
					$aResult[$aTypes[1]] = $sKey;
				}
			}
		}
		return $aResult;
	}

	/**
	 * @param object $oObject
	 * @param object $oHelper
	 * @param array $aExclude
	 *
	 * @return array
	 */
	public static function DbUpdateArray($oObject, $oHelper, $aExclude = array())
	{
		$aResult = array();
		$aExclude = \is_array($aExclude) && 0 < \count($aExclude) ? $aExclude : array();

		$sQueryParams = '';
		$oSettings =& Api::GetSettings();
		$bUseLogQueryParams = (bool) $oSettings->GetValue('DBLogQueryParams', false);

		$oObject->initBeforeChange();

		$aStaticMap = $oObject->getMap();
		$aMap = AbstractContainer::DbWriteKeys($aStaticMap, false);

		foreach ($aMap as $sDbKey => $sObjectKey)
		{
			if (\in_array($sDbKey, $aExclude))
			{
				continue;
			}

			$mValue = $oObject->{$sObjectKey};
			if (isset($aStaticMap[$sObjectKey][0]))
			{
				if ('password' === $aStaticMap[$sObjectKey][0])
				{
					$mValue = Utils::EncryptValue($mValue);
				}
				else if ('datetime' === $aStaticMap[$sObjectKey][0])
				{
					$mValue = $oHelper->TimeStampToDateFormat($mValue);
				}
				else if ('serialize' === $aStaticMap[$sObjectKey][0])
				{
					$mValue = '' === $mValue ? '' : \serialize($mValue);
				}
			}

			$aResult[] = $oHelper->EscapeColumn($sDbKey).' = '.
				(\is_string($mValue) ? $oHelper->EscapeString($mValue) : (int) $mValue);

			if ($bUseLogQueryParams)
			{
				$sQueryParams .=
					AU_API_CRLF.AU_API_TAB.$sDbKey.' = '.(
						\is_string($mValue) ? $oHelper->EscapeString($mValue) : (int) $mValue);
			}
		}

		if ($bUseLogQueryParams)
		{
			Api::Log($sQueryParams);
		}

		return $aResult;
	}

	/**
	 * @param string $sWhere
	 * @param string $sTableName
	 * @param array $aStaticMap
	 * @param object $oHelper
	 *
	 * @return string
	 */
	public static function DbGetObjectSqlString($sWhere, $sTableName, $aStaticMap, $oHelper)
	{
		$aMap = AbstractContainer::DbReadKeys($aStaticMap);
		$aMap = \array_map(array($oHelper, 'EscapeColumn'), $aMap);

		$sSql = 'SELECT %s FROM %s WHERE %s';

		return \sprintf($sSql, \implode(', ', $aMap), $sTableName, $sWhere);
	}

	/**
	 * @param string $sTableName
	 * @param object $oObject
	 * @param object $oHelper
	 *
	 * @return string
	 */
	public static function DbCreateObjectSqlString($sTableName, $oObject, $oHelper)
	{
		$sSql = '';
		$aResults = self::DbInsertArrays($oObject, $oHelper);

		if ($aResults[0] && $aResults[1])
		{
			$sSql = 'INSERT INTO %s ( %s ) VALUES ( %s )';
			$sSql = \sprintf($sSql, $sTableName, \implode(', ', $aResults[0]), \implode(', ', $aResults[1]));
		}

		return $sSql;
	}

	/**
	 * @param string $sTableName
	 * @param object $oObject
	 * @param object $oHelper
	 *
	 * @return string
	 */
	public static function DbUpdateObjectSqlString($sTableName, $oObject, $oHelper)
	{
		$aResult = self::DbUpdateArray($oObject, $oHelper);
		$mValue = $oObject->SessionUniquePropertyValue();

		$sSql = 'UPDATE %s SET %s WHERE %s = %s';
		return \sprintf($sSql, $sTableName, \implode(', ', $aResult),
			$oHelper->EscapeColumn($oObject->SessionUniqueProperty()),
			(\is_string($mValue) ? $oHelper->EscapeString($mValue) : (int) $mValue)
		);
	}

	/**
	 * @param object $oObject
	 * @param object $oHelper
	 * @return array
	 */
	public static function DbInsertArrays($oObject, $oHelper)
	{
		$aResult = array(false, false);

		$sQueryParams = '';
		$oSettings =& Api::GetSettings();
		$bUseLogQueryParams = (bool) $oSettings->GetValue('DBLogQueryParams', false);

		$oObject->initBeforeChange();

		$aStaticMap = $oObject->getMap();
		$aMap = AbstractContainer::DbWriteKeys($aStaticMap, true);

		$aDbKeys = \array_keys($aMap);
		$aResult[0] = \array_map(array(&$oHelper, 'EscapeColumn'), $aDbKeys);

		$aDbValues = \array_values($aMap);
		foreach ($aDbValues as $iIndex => $sKey)
		{
			$mValue = $oObject->{$sKey};
			if (isset($aStaticMap[$sKey][0]))
			{
				if ('password' === $aStaticMap[$sKey][0])
				{
					$mValue = Utils::EncryptValue($mValue);
				}
				else if ('datetime' === $aStaticMap[$sKey][0])
				{
					$mValue = $oHelper->TimeStampToDateFormat($mValue);
				}
				else if ('serialize' === $aStaticMap[$sKey][0])
				{
					$mValue = '' === $mValue ? '' : serialize($mValue);
				}
			}

			$aDbValues[$iIndex] = \is_string($mValue)
				? $oHelper->EscapeString($mValue) : (int) $mValue;

			if ($bUseLogQueryParams)
			{
				$sDbKey = isset($aDbKeys[$iIndex]) ? $aDbKeys[$iIndex] : '!unknown!';
				$sQueryParams .= AU_API_CRLF.AU_API_TAB.$sDbKey.' = '.$aDbValues[$iIndex];
			}
		}

		$aResult[1] = $aDbValues;

		if ($bUseLogQueryParams)
		{
			Api::Log($sQueryParams);
		}

		return $aResult;
	}
}
