<?php
namespace Engine\Dashboard;

abstract class GeneralAbstract
{
	/**
	 * The dashboard id.
	 * @var String
	 */
	protected $id = NULL;

	/**
	 * The dashboard content - an array of containers (the containers can have widgets).
	 * @var array
	 */
	protected $content = [];


	/**
	 * Data-container for the base dashboardContainer options
	 * @var array
	*/
	protected $options = [];

	/**
	 * Temporary options, these never gets saved to the DB
	 * @var array
	*/
	protected $tempOptions = [];

	/**
	 * Internal class version, can be used for upgrading paths from earlier versions.
	 * @var String
	*/
	protected $version = "1.00";

	/**
	 * Stores the dashboard title
	 * @var String
	 */
	protected $title = NULL;

	static protected $index = 1;

	/**
	 * Adds a new container in the dashboard.
	 * @param \Engine\Dashboard\ContainerAbstract $container
	 * @throws Exception
	 * @return self
	 */
	public function addContainer(\Engine\Dashboard\ContainerAbstract $container)
	{
		$this->content[] = $container;

		$container->setParent($this);

		return $this;
	}


	public function insertElement($element, $index)
	{
		if ($index === FALSE) {
			throw new \Engine\Dashboard\Exception("Index invalid");
		}

		array_splice($this->content, $index, 0, [$element]);

		$element->setParent($this);

		return $this;
	}

	public function appendElement($element)
	{
		$this->content[]= $element;
		$element->setParent($this);
		return $this;
	}


	/**
	 * Return element index
	 * @param string $element
	 * @return false|Int
	 */
	public function getElementIndex($element, $dstSubid=NULL)
	{
		return array_search($element, $this->content, true);
	}


	public function removeElement($element)
	{
		// get index
		$index = $this->getElementIndex($element);

		// check if the key is valid
		if ($index !== FALSE){

			// eliminate the reference to the element
			unset($this->content[$index]);

			// if this is a widget - delete its content cache
			if ($element instanceof \Engine\Dashboard\Widgets\WidgetAbstract) {
				$element->__prepareRemove($element);
			}

			return $this;
		}

		throw new \Engine\Dashboard\Exception(__METHOD__.": Element {$element->getId()} not found in parent {$this->getId()}");

	}


	public function moveElement($src, $dst, $afterObj=NULL, $dstSubid=NULL)
	{
		/* @var $src \Engine\Dashboard\Widgets\WidgetAbstract */
		/* @var $dst \Engine\Dashboard\ContainerAbstract */

		if ($afterObj) {
			// get the index of the destination element (that will be moved);
			$index = $dst->getElementIndex($afterObj, $dstSubid);
			if ($index === FALSE) {
				throw new \Engine\Dashboard\Exception(sprintf("[%s] Cannot find target object where to move the src object. this:%s, src:%s, dst:%s, afterObj:%s, dstSubid:%s",
						__METHOD__,
						$this->getId(),
						$src->getId(),
						$dst->getId(),
						($afterObj?$afterObj->getId():'(null)'),
						($dstSubid!==NULL?$dstSubid:'(null)')
				));
			}

			// remove from parent container
			$src->getParent()->removeElement($src);

			// insert element at the requested position
			$dst->insertElement($src, $index, $index);
		}
		else {
			// remove from parent container
			$src->getParent()->removeElement($src);

			// insert element at the requested position
			$dst->insertElement($src, 0, $dstSubid);
		}
	}


	/**
	 * Sets an internal option
	 * @param string $key
	 * @param mixed $value
	 */
	public function setOption($key, $value)
	{
		$this->options[$key] = $value;
	}

	/**
	 * Get an internal option
	 * @param string $key
	 * @return mixed
	 */
	public function getOption($key)
	{
		return $this->options[$key];
	}

	public function getOption_rec($key)
	{
		if($this->getOption($key) === NULL){
			return NULL;
		}else{
			return $this->getOption($key);
		}
	}


	/**
	 * Set an internal temporary option
	 * @param string $key
	 * @param mixed $value
	 */
	public function setTempOption($key, $value)
	{
		$this->tempOptions[$key] = $value;
	}

	/**
	 * Get an internal temporary option
	 * @param String $key
	 * @return mixed
	 */
	public function getTempOption($key)
	{
		return $this->tempOptions[$key];
	}

	/**
	 * Gets the content of the dashboard.
	 * @return Array
	 */
	public function getContentRaw()
	{
		return $this->content;
	}

	/**
	 * Gets the content of the dashboard.
	 * @return Array
	 */
	public function getContentLinear()
	{
		return $this->content;
	}

	/**
	 * Get the internal, numeric id
	 */
	public function getInternalId()
	{
		if (!$this->id) {
			$this->id = \Engine\Dashboard\GeneralAbstract::nextIndex();
		}
		return $this->id;
	}

	/**
	 * Generate & get the id for a certain element in this tree
	 * @return string
	 */
	public function getId()
	{
		return sprintf("_d_%d", $this->getInternalId());
	}

	/**
	 * Check if the dashboard is locked
	 * @return bool
	 */
	public function isLocked()
	{
		return (bool)$this->getOption('locked');
	}

	/**
	 * Lock dashboard
	 */
	public function lock()
	{
		$this->setOption('locked', true);
	}

	/**
	 * Unlock dashboard
	 */
	public function unlock()
	{
		$this->setOption('locked', false);
	}


	/**
	 * Loop trough all childs and populate their id's
	 */
	public function prepareRenderer()
	{
		$stats = [
			'containers' =>	0,
			'widgets' =>	0,
			'id' =>	[
				'min' => NULL,
				'max' => NULL,
			],
		];

		$id = $this->getInternalId();

		// gather min/max id's
		$stats['id']['min'] = ($stats['id']['min']===NULL?$id:min($stats['id']['min'], $id));
		$stats['id']['max'] = max($stats['id']['max'], $id);

		if($stats['id']['list'][$id]){
			throw new \Engine\Dashboard\Exception(__METHOD__.": id {$this->getId()} seems to repeat itself. Something is broken in the internal tree representation");
		}
		$stats['id']['list'][$id]++;

		// loop through children and do the same thing
		foreach($this->content as $container){
			// validate parent
			if($container->getParent()!==$this){
				throw new \Engine\Dashboard\Exception(__METHOD__.": invalid link between parents->child for {$this->getId()} -> {$container->getId()}");
			}
			$this->prepareRenderer_rec($container, $stats);
		}

		return $stats;
	}

	protected function prepareRenderer_rec($child, &$stats)
	{
		if ($child instanceof \Engine\Dashboard\ContainerAbstract) {
			$stats['containers']++;

			$id = $child->getInternalId();

			// gather min/max id's
			$stats['id']['min'] = ($stats['id']['min']===null?$id:min($stats['id']['min'], $id));
			$stats['id']['max'] = max($stats['id']['max'], $id);
			if($stats['id']['list'][$id]){
				throw new \Engine\Dashboard\Exception(__METHOD__.": id {$child->getId()} seems to repeat itself. Something is broken in the internal tree representation");
			}
			$stats['id']['list'][$id]++;


			// loop through children and do the same thing
			$content = $child->getContentLinear();
			foreach($content as $container){
				// validate parent
				if($container->getParent()!==$child){
					throw new \Engine\Dashboard\Exception(__METHOD__.": invalid link between parents->child for {$child->getId()} -> {$container->getId()}");
				}

				$this->prepareRenderer_rec($container, $stats);
			}
		}
		elseif ($child instanceof \Engine\Dashboard\Widgets\WidgetAbstract) {
			$stats['widgets']++;

			$id = $child->getInternalId();

			// gather min/max id's
			$stats['id']['min'] = ($stats['id']['min']===null?$id:min($stats['id']['min'], $id));
			$stats['id']['max'] = max($stats['id']['max'], $id);
		}
		else {
			throw new \Engine\Dashboard\Exception("Invalid item, unreconized class");
		}
	}

	public function getElementById($id)
	{
		if ($id == $this->getId()) {
			return $this;
		}
		else {
			foreach($this->content as $container){
				if($id == $container->getId()){
					return $container;
				}
				$s = $this->getElementById_rec($id, $container);
				if($s){
					return $s;
				}
			}
		}

		return NULL;
	}


	public function getElementById_rec($id, $child)
	{
		if($child instanceof \Engine\Dashboard\ContainerAbstract){
			if($id == $child->getId()){
				return $child;
			}

			$content = $child->getContentLinear();
			foreach($content as $container){
				$s = $this->getElementById_rec($id, $container);
				if($s){
					return $s;
				}
			}
		}elseif($child instanceof \Engine\Dashboard\Widgets\WidgetAbstract){
			if($id == $child->getId()){
				return $child;
			}
		}else{
			throw new \Engine\Dashboard\Exception("Invalid item, unreconized class");
		}

		return NULL;
	}

	/**
	 * Set dashboard title
	 * @param String $title
	 */
	public function setTitle($title)
	{
		$this->title = $title;
		return $this;
	}

	/**
	 * Returns the dashboard title
	 * @return string
	 */
	public function getTitle()
	{
		return $this->title;
	}


	/**
	 * Get the output TPL file path
	 * @param String $templateClass (optional)
	 * @return String
	 */
	abstract public function getTemplatePath();

	/**
	 * Get the output TPL file path
	 * @return String
	 */
	public function determineTemplatePath($templateClass)
	{

		$templateFile = "";

		if (is_file(APP_REAL_PATH."/_templates/application/dashboard/dashboard/{$templateClass}.tpl")) {
			$templateFile = APP_REAL_PATH."/_templates/application/dashboard/dashboard/{$templateClass}.tpl";
		}
		elseif (is_file(APP_REAL_PATH."/_templates/engine/dashboard/dashboard/{$templateClass}.tpl")) {
			$templateFile = APP_REAL_PATH."/_templates/engine/dashboard/dashboard/{$templateClass}.tpl";
		}

		if(!$templateFile){
			throw new \Engine\Dashboard\Exception("Cannot find template '{$templateClass}'");
		}

		return $templateFile;
	}

	/**
	 * Get the parameters that will be sent down to the TPL file
	 * @return Array
	 */
	public function getTemplateParams()
	{
		// the element ID that will be used in the TPL file
		return [
			'title' => 		$this->title,
			'id' => 		$this->getId(),
			'methods' => 	$this,
			'options' => 	$this->options,
			'childs' => 	$this->getContentRaw(),
		];
	}


	/**
	 * Return the rendered content for this element
	 * @return string
	 */
	public function render()
	{
		$smarty = \Engine\Smarty\SmartyMultiton::getInstance();
		$_LANG = \App\Loader_Lang::getInstance()->getGlobal();

		$oldElement = $smarty->getTemplateVars('container');

		$smarty->assign('container', $this->getTemplateParams());
		$smarty->assign('generalLang', $_LANG['dashboard']['widgetsList']['_GENERAL_']);
		$return = $smarty->fetch($this->getTemplatePath(get_class($this)));
		$smarty->clearAssign('container');

		if($oldElement){
			$smarty->assign('container', $oldElement);
		}

		return $return;
	}

	/**
	 * Used for AJAX-ex to call a certain method of the current widget.
	 */
	public function __callMethod($method, $params, $JSONResponse)
	{
		throw new \Exception('Invalid method call.');
	}


	public function dumpTree($container)
	{
		echo "<div style='margin-left: 2em;'>";
		if ($container instanceof \Engine\Dashboard\ContainerAbstract || $container instanceof \Engine\Dashboard\GeneralAbstract) {
			echo "<h1>{$container->getId()}</h1>";
			$childs = $container->getContentLinear();
			foreach ($childs as $c) {
				$this->dumpTree($c);
			}
		}
		else {
			echo "<h2>{$container->getId()}</h2>";
		}

		echo "</div>";
	}

	private function getPossibleWidgets()
	{
		$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();
		$ret = [];
		$dashboardClass =  get_called_class();

		foreach ($CFG['dashboardWidgets'] as $widgetName=>$settings){
			if ($settings['active'] && in_array($dashboardClass, $settings['possibleDashboards'])) {
				if ($settings['category'] == 'qr' && in_array("Modules\Home\Dashboard", $settings['possibleDashboards']) && !\App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToOption('Modules/Home_category_qr')) {
					continue;
				}

				$ret[] = new $widgetName();
			}
		}

		return $ret;
	}

	private function getPossibleContainers()
	{
		$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();
		$ret = [];
		$dashboardClass = get_called_class();
		foreach ($CFG['dashboardContainers'] as $containerName=>$settings) {
			if ($settings['active'] && in_array($dashboardClass, $settings['possibleDashboards'])) {
				$ret[] = new $containerName();
			}
		}
		return $ret;
	}

	/**
	* Possible content/validation management
	*/
	public function getPossibleElements()
	{
		$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();

		$widgetsArr = [];
		$containersArr = [];

		$widgets = $this->getPossibleWidgets();

		foreach ($widgets as $widgetObj) {
			$details = $widgetObj->getDetails();
			$details['_name'] = get_class($widgetObj);
			$key1 = sprintf("_%04d_%s", $CFG['dashboardCategories']['order'][$details['category']]['order'], $details['category']);
			$key2 = sprintf("w_%04d_%s", $details['order'], get_class($widgetObj));
			$widgetsArr[$key1][$key2] = $details;
		}

		$containers = $this->getPossibleContainers();
		foreach ($containers as $containerObj) {
			$details = $containerObj->getDetails();
			$details['_name'] = get_class($containerObj);
			$key1 = sprintf("_%04d_%s", $CFG['dashboardCategories']['order'][$details['category']]['order'], $details['category']);
			$key2 = sprintf("c_%04d_%s",$details['order'], get_class($containerObj));
			$containersArr[$key1][$key2] = $details;
		}

		ksort($widgetsArr);
		foreach ($widgetsArr as $category=>$widgets) {
			ksort($widgetsArr[$category]);
		}

		ksort($containersArr);
		foreach ($containersArr as $category=>$widgets) {
			ksort($containersArr[$category]);
		}

		return [
			'widgets' =>	$widgetsArr,
			'containers' => $containersArr,
		];
	}

	public function getPossibleTemplates()
	{
		$dashboardClass = get_called_class();

		// cache this result according to user id and permission
		$cacheObj = new \Engine\Cache\Disk();
		$key = sprintf("%s-%s-%s-Templates", $dashboardClass, CURRENT_ID_USER, (int)\App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToOption('Modules/Home_category_qr'));

 		$accessibleTemplatesArr = unserialize((string)$cacheObj->get($key));
		if ($accessibleTemplatesArr) {
			return $accessibleTemplatesArr;
		}

		\Engine\Loader_Cfg::getInstance()->loadCustomFile("dashboard/templates/templates.cfg.php");
		$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();

		$rawTemplatesArr = [];
		foreach ($CFG['dashboardTemplates'][$dashboardClass] as $settings) {
			if ($settings['active']) {
				$rawTemplatesArr[] = [
					'class'  => $settings['class'],
					'widgets'=> [],
				];
			}
		}

		// check permissions on financial widgets
		$accessibleTemplatesArr = [];
		foreach ($rawTemplatesArr as $template) {
			$dashboard = new $dashboardClass();
			$templateObj = $template['class']::getInstance($dashboard);
			$templateObj->configureDashboard();

			// please note the negated array_filter() call
			$containedWidgets = [];
			$templateIsAccesible = !array_filter($this->traverseTree($dashboard, function($child, $container) use (&$containedWidgets){
				if ($child instanceof \Engine\Dashboard\Widgets\WidgetAbstract) {
					$containedWidgets[] = get_class($child);
					return !$child->isAccesibleByUser(CURRENT_ID_USER);
				}

				return false;	// always return false, as these got put into $templateIsAccesible and containers are always accesible
			}));

			if ($templateIsAccesible) {
				$template['widgets'] = $containedWidgets;

				$accessibleTemplatesArr[] = $template;
			}
		}

		$cacheObj->set($key, serialize($accessibleTemplatesArr));

		return $accessibleTemplatesArr;
	}


	/**
	 * Traverse the internal widget & container tree and call the speciefied callback
	 * @return array
	 */
	public function traverseTree($container, $callback)
	{
		$ret = [];
		foreach ($container->getContentLinear() as $child) {
			$ret[]= $callback($child, $container);

			if ($child instanceof \Engine\Dashboard\ContainerAbstract) {
				$ret2 = $this->traverseTree($child, $callback);

				foreach ($ret2 as $r) {
					$ret[]= $r;
				}
			}
		}

		return $ret;
	}

	// save/restore callbacks ///////////////////////////////////////////////////////////////////////////

	/**
	* This method is executed before the object serialization - it can be used to clean some class attributes
	*/
	public function __prepareSleep()
	{
		$this->tempOptions = null;

		foreach($this->content as $container) {
			$container->__prepareSleep();
		}
	}

	public function __prepareWakeup()
	{
		foreach ($this->content as $container) {
			// check for undefined classes
			if (get_class($container)=='__PHP_Incomplete_Class') {
				// $container->__PHP_Incomplete_Class_Name return null, I don't understand why. The workaround is to typecast to array and use that
				$convertedContainer = (array)$container;
				\Log::getInstance()->error(sprintf("Unknown class for the content found in dashboard %s. Ignoring it.", get_class($this)), [
					'className' => new \Engine\Logger\Context\StringRaw($convertedContainer['__PHP_Incomplete_Class_Name']),
				]);

				$this->removeElement($container);
				// this will trigger an auto-save in index.php
				$this->setOption("contentDirty", true);
					continue;
			}

			$container->__prepareWakeup();
		}
	}

	static public function setCurrentIndex($index)
	{
		self::$index = $index;
	}
	static public function nextIndex()
	{
		return ++self::$index;
	}
}