<?php
namespace ModulesAbstract\PluginReports\Report\Model\Report;

class SystemResources
{
	/**
	 * Internal state
	 * @var array
	 */
	protected $sysResources = [
		'wait' => 			1,
		'lastMemFlush' => 	null,	// will get to contain a timestamp
		'lastMemOp' => 		null,	// should contain an internal id describing what was clean on the last cleanup
	];

	/**
	 * System state when the report started
	*/
	protected $statsStart = [
		'load' => 	null,
		'mem' => 	null,
		'time' => 	0,
		'user' => 	0,
	];

	protected $statsEnd = [
		'load' => 	null,
		'mem' => 	null,
		'time' => 	0,
	];

	protected $limits = [
		'sysMem' => 	null,
		'time' => 		null,
		'maxSysMem' => 	null,
	];

	/**
	 * Main report attributes, and general running parameters
	 * @var array
	 */
	protected $reportAttributes = [
		'sysCheckInterval:rows' => 	100,

		/**
		 * Maximum system load accepted to run this. If the load is too high, then the report will sleep() more and more until the load is acceptable again
		*/
		'systemLoad' =>  4.0,

		/**
		 * Maximum occupied memory (in Mb). The larger the better for the caching system (system cache & entity cache, as they are handled separately)
		 * NOTE: do not set this too low(<50) because it seems to cause problems and will try to free memory continuosly
		 */
		'sysMem' => 	50,

		'maxSysMem' => 	447,	// max is set at 250Mb, because the 'engine' has about 10-20Mb already occupied, and the memory_limit is set at 512Mb by default, and we try to stay within limits

		/**
		 * Maximum time allowed to run this report (in seconds). Used in set_time_limit(), additional checks and decisions may be taken depending on this value
		*/
		'allowedTime' => 150, // 2.5 minutes

		'cache:use' => 		'custom',		// default, custom, none
		'cache:maxItems' => 	5000,		// 1200 is default
	];

	/**
	 * Stores logs
	 */
	protected $logs= [];

	protected $index = 0;

	/**
	 * The local cache obj
	 * @var \Engine\Cache\LocalMemory
	 */
	protected $cacheObj = null;

	/**
	 * Initialize system resources
	 */
	public function initialize()
	{
		// reset the waiting period
		$this->sysResources['wait'] = 1;
		$this->index = 0;

		$this->statsStart['load'] = 	$this->getSysLoad();
		$this->statsStart['time'] = 	microtime(true);
		$this->statsStart['user'] = 	CURRENT_ID_USER;
		$this->statsStart['mem'] = 		memory_get_usage(true);

		$this->limits['sysMem'] = 		$this->statsStart['mem'] + ($this->reportAttributes['sysMem'] * 1024 * 1024);
		$this->limits['maxSysMem'] = 	$this->statsStart['mem'] + ($this->reportAttributes['maxSysMem'] * 1024 * 1024);
		$this->limits['time'] = 		$this->statsStart['time'] + ($this->reportAttributes['allowedTime']*0.95);	// set the backend timelimit to 95% of the requested value, to allow for graceful handling

		set_time_limit($this->reportAttributes['allowedTime']);

		switch ($this->reportAttributes['cache:use']) {
			case 'default':
				$this->cacheObj = \App\Entity\Cache::getInstance();
			break;

			case 'none':
				// replace the entity cache during the report generation
				\App\Entity\Cache::getInstance()->flush();
				\App\Entity\Cache::getInstance()->flushInternal();

				$cacheObj = new \Engine\Cache\Dummy();
				\App\Entity\Cache::getInstance()->setCacheObject($cacheObj);

				$this->cacheObj = $cacheObj;
			break;

			case 'custom':
				// replace the entity cache during the report generation
				\App\Entity\Cache::getInstance()->flush();
				\App\Entity\Cache::getInstance()->flushInternal();

				$cacheObj = new \Engine\Cache\LocalMemory();
				// override the max number of entities in the cache, but the cache will get disabled by the generator anyway and gc() will be manually called after each row, so it might get above this limit from time to time
				$cacheObj->setStorageLimits($this->reportAttributes['cache:maxItems']);
				\App\Entity\Cache::getInstance()->setCacheObject($cacheObj);

				$this->cacheObj = $cacheObj;
			break;
		}

	}

	public function destroy()
	{
		// do nothing
	}

	public function cacheSetAutoGcEnabled($gc_enabled)
	{
		switch ($this->reportAttributes['cache:use']) {
			case 'custom':
				$this->cacheObj->setAutoGcEnabled($gc_enabled);
				$this->cacheObj->autoGc(microtime(true));
			break;
		}

		return $this;
	}

	public function cacheAutoGc($ts)
	{
		switch ($this->reportAttributes['cache:use']) {
			case 'custom':
				$this->cacheObj->autoGc($ts);
			break;
		}

		return $this;
	}

	public function setAttribute($key, $value)
	{
		$this->reportAttributes[$key] = $value;
		return $this;
	}

	public function finalizeReport()
	{
		$this->statsEnd['load'] = 	$this->getSysLoad();
		$this->statsEnd['mem'] = 	memory_get_usage(true);
		$this->statsEnd['time'] = 	microtime(true);

		$this->statsEnd['cache'] = [];

		switch ($this->reportAttributes['cache:use']) {
			case 'custom':
				$this->statsEnd['cache'] = $this->cacheObj->stats();
			break;
		}

		$this->statsEnd['cache']['type'] = $this->reportAttributes['cache:use'];

		$this->addLog(sprintf("reporting finalized"));
		$this->addLog(sprintf("duration: %.2fs", $this->statsEnd['time'] - $this->statsStart['time']));
		$this->addLog(sprintf("used memory: %.2fMb", ($this->statsEnd['mem'] - $this->statsStart['mem'])/(1024*1024)));
		$this->addLog(sprintf("server load at the report finish %.2f (%s%.2f)", $this->statsEnd['load'], ($this->statsEnd['load']-$this->statsStart['load']>=0?'+':'-'), abs($this->statsEnd['load'] - $this->statsStart['load'])));
	}

	/**
	 * Current system load
	 * @return float
	 */
	// TODO: ar trebui sa fie in Engine/OS
	protected function getSysLoad()
	{
		$load = sys_getloadavg();
		return (float)$load[0];
	}

	public function checkAndFreeMemory()
	{
		if ($this->index % $this->reportAttributes['sysCheckInterval:rows'] == 0) {
			if ($this->checkMemory()) {
				$this->freeMemory();
			}
		}
		$this->index++;
	}

	/**
	 * Check if resource 'memory' needs to be free
	 * @return boolean
	 */
	protected function checkMemory()
	{
		if (memory_get_usage(false) > $this->limits['sysMem']) {
			return true;
		}
		return false;
	}

	protected function freeMemory()
	{
		$timestamp = microtime(true);
		// don't flush memory more than once every 0.25 seconds
		if ($timestamp - $this->sysResources['lastMemFlush'] > 0.25) {
			$memInit = memory_get_usage(false);

			$this->sysResources['lastMemFlush'] = $timestamp;

			// clear cache entity
			\App\Entity\Cache::getInstance()->flush();
			switch ($this->reportAttributes['cache:use']) {
				case 'custom':
					$this->cacheObj->flush();
				break;
			}

			gc_collect_cycles();

			$memAfter = memory_get_usage(false);
			$this->addLog(sprintf("[free 'mem'] %.2fMb -> %.2f", $memInit/(1024*1024), $memAfter/(1024*1024)));

			$memEnd = memory_get_usage(false);
			$this->addLog(sprintf("[free 'mem'] (%.2f accepted) script occupies %.2fMb. After freeing data: %.2f.", $this->limits['sysMem'] / ( 1024 * 1024 ), $memInit / ( 1024 * 1024 ), $memEnd / ( 1024 * 1024 )));

			// change the memory limit, if we're still out of limits
			if ($memEnd > $this->limits['sysMem']) {
				if ($this->limits['sysMem'] * 1.250 <= $this->limits['maxSysMem']) {
					$this->limits['sysMem'] = (int)($this->limits['sysMem'] * 1.250);
					$this->addLog(sprintf("[free 'mem'] called and no usable resources were freed, increasing accepted memory to %.2fMb", $this->limits['sysMem']/(1024*1024)));
				}
				else {
					throw new \Exception("Trying to free more memory, but the caches are already empty and the max memory limit was reached. You should expect module crash");
				}
			}
		}
	}

	/**
	 * Check and wait resource load
	 */
	public function checkAndWaitLoad()
	{
		if ($this->index % $this->reportAttributes['sysCheckInterval:rows'] == 0) {
			if ($this->checkLoad()) {
				$this->waitLoad();
			}
		}

		$this->index++;
	}

	/**
	 * Check if load need to be analyze
	 * @return boolean
	 */
	protected function checkLoad()
	{
		// check for the system load
		$sysLoad = $this->getSysLoad();

		if ($sysLoad > $this->reportAttributes['systemLoad']) {
			return true;
		}

		return false;
	}

	/**
	 * Wait load
	 */
	protected function waitLoad()
	{
		// sleep for the computed waiting period
		usleep($this->sysResources['wait'] * 1000);
		$this->addLog(sprintf("[waiting resources '%s'] for %sms", 'load', $this->sysResources['wait']));

		$wait = $this->sysResources['wait'] * ($this->sysResources['wait'] + 1);
		if ($wait < 2500) {
			$this->sysResources['wait'] = $wait;
		}
	}

	public function getRawLogs()
	{
		return $this->logs;
	}

	public function addLog($message)
	{
		return $this->logs[] = [
			'time' => 		microtime(true),
			'memory' => 	memory_get_usage(false),
			'message' => 	$message,
		];
	}

	public function getInternalStatsStart()
	{
		return $this->statsStart;
	}

	public function getInternalStatsEnd()
	{
		return $this->statsEnd;
	}

	public function getInternalLimits()
	{
		return $this->limits;
	}
}
