<?php

namespace Engine\Menu\Structure;

class Section {


	/**
	 * The unique id of the current section.
	 * @var String
	 */
	protected $id='';

	/**
	 * The path of the current section.
	 * @var String
	 */
	protected $path='';


	/**
	 * The value indicates the access level per section. It can be:
	 * - 'profile' (the section is accessible if the user has access in his profile);
	 * - 'hidden' (the section is accessible by anyone but it is not displayed in the menu);
	 * - 'always' (the section is always accessible);
	 * - 'inactive' (the section is never accessible).
	 * @var String
	 */
	protected $accessLevel='profile';

	/**
	 * An associative array of sections (id=>object) which represents the lisf ot children sections available for the current section.
	 * @var \Engine\Menu\Structure\Section[]
	 */
	protected $children=[];

	/**
	 * Internal cache for storing recursive calls results
	 * @var array
	 */
	private $cache;

	/**
	 * Describe type of menu:default, userMenu
	 * @var String
	 */
	protected $type='menuItem';

	/**
	 * Set javascript action
	 * @var String
	 */
	protected $jsAction=NULL;

	/**
	 * The class constructor - creates a new section object.
	 * @param String $id - the unique section id.
	 * @param String $path - the path to this section.
	 * @param String $accessLevel - the default value for accessLevel
	 * @throws \Exception - if the id or the path are not set.
	 */
	public function __construct($id, $path, $accessLevel=null)
	{
		$this->id=$id;
		$this->path=$path;
		if ($accessLevel!==null) 	$this->setAccessLevel($accessLevel);
	}


	/**
	 * Returns the id of the current section.
	 * @return String
	 */
	public function getId() {
		return $this->id;
	}


	public function getPath() {
		return rtrim((string)$this->path, "/");
	}
	public function setPath($path) {
		$this->path=$path;
	}


	public function getAccessLevel() {
		return $this->accessLevel;
	}
	public function isInactive() {
		return ($this->accessLevel=='inactive');
	}
	public function isHidden() {
		return ($this->accessLevel=='hidden');
	}
	public function isAlways() {
		return ($this->accessLevel=='always');
	}
	public function setAccessLevel($accessLevel) {
		// check the access level
		if ($accessLevel!='profile' and $accessLevel!='hidden' and $accessLevel!='always' and $accessLevel!='inactive') {
			throw new \Exception('The access level `'.$accessLevel.'` is not valid for the menu.');
		}
		// set the access level
		$this->accessLevel=$accessLevel;
	}

	public function getType()
	{
		return $this->type;
	}

	public function setType($type)
	{
		$this->type = $type;
	}

	public function getJsAction()
	{
		return $this->jsAction;
	}

	public function setJsAction($jsAction)
	{
		$this->jsAction = $jsAction;
	}

	/**
	 * Returns an associative array of section objects
	 * @return \Engine\Menu\Structure\Section[]
	 */
	public function getChildren($type) {
		$children = [];
		if ($type=='default') {
			$types = ['menuItem', 'group', 'separator', 'title'];
		}
		elseif ($type=='functional') {
			$types = ['menuItem', 'group', 'userMenu',];
		}
		else {
			$types = [$type];
		}

		foreach ($this->children as $idSection => $sectionObj) {
			if (in_array($sectionObj->getType(), $types)) {
				$children[$idSection] = $sectionObj;
			}
		}
		return $children;
	}


	/**
	 * Get section label from langs
	 */
	public function getLabel()
	{
		$menuLang = \App\Loader_Lang::getInstance()->getSection('menu');
		$section = \Engine\Mvc\Dispatcher::getInstance()->getRouter()->matchAlias($this->getId())->getPath();
		return $menuLang[$section];
	}


	/**
	 * The method is used for adding a new child section on the current section.
	 * @param \Engine\Menu\Structure\Section $section - the new section to be added
	 * @param \Engine\Menu\Structure\Section $beforeIdSection - the new section will be added before this section (if not specified, it will be added in the end).
	 * @throws \Exception - if input is incorrect
	 */
	public function addChildSection(\Engine\Menu\Structure\Section $section, ?\Engine\Menu\Structure\Section $beforeSection=null) {

		// get the adding section id
		$idSection=$section->getId();

		// gets the id of the "before" section
		if ($beforeSection) $beforeIdSection=$beforeSection->getId();
		else $beforeIdSection=null;

		// check if the section already exists
		if (!empty($this->children[$idSection])) throw new \Exception("Cannot add the section `{$idSection}` twice in the menu.");

		// check if the "before" section is on the same level and has the same parent
		if ($beforeIdSection and !$this->children[$beforeIdSection]) throw new \Exception("Cannot add the application menu section `{$section->getId()}` after `{$beforeIdSection}` as the section `{$beforeIdSection}` does not exist for the parent section `{$this->id}`.");

		// attach this section - at the end of the children list
		if (!$beforeIdSection) {
			$this->children[$idSection]=$section;
		}
		// or attach it "before" a certain section
		else {
			// scan sections of this parent and inject the new one before the requested section
			$newChildrenList=[];
			foreach($this->children as $id=>$child) {
				if ($id==$beforeIdSection) $newChildrenList[$idSection]=$section;
				$newChildrenList[$id]=$child;
			}
			$this->children=$newChildrenList;
		}

		// return current object
		return $this;

	}

	public function setChildren(array $sections)
	{
		$this->children = [];

		foreach ($sections as $section) {
			$this->addChildSection($section);
		}

		// return current object
		return $this;

	}


	/**
	 * Find a child section by id recursively
	 * @param string $idSection
	 * @return \Engine\Menu\Structure\Section
	 */
	public function getChildById($idSection)
	{
		$key = sprintf("%s-%s", __METHOD__, $idSection);
		if (!empty($this->cache[$key])) {
			return $this->cache[$key];
		}

		$results = $this->filterChilds(function($section) use($idSection) {
			return $section->getId()==$idSection;
		}, ['maxDepth' => PHP_INT_MAX, 'depth'=>0,]);
		if (isset($results[0])) {
			$this->cache[$key] = $results[0];
			return $results[0];
		} else {
			$this->cache[$key] = null;
			return null;
		}
	}


	/**
	 * Recuresively find a child section Id starting from its path
	 * @param String $path
	 * @return \Engine\Menu\Structure\Section
	 */
	public function getChildByPath($path)
	{
		$searchPath = rtrim($path, "/");
		$results = $this->filterChilds(function($section) use($searchPath) {
			return rtrim($section->getPath(), "/")==$searchPath;
		}, ['maxDepth' => PHP_INT_MAX, 'depth'=>0,]);

		if (isset($results[0])) {
			return $results[0];
		} else {
			return null;
		}
	}

	/**
	 * Get the closest parent to a path
	 * @param String $path
	 * @return \Engine\Menu\Structure\Section
	 */
	public function getClosestParentFromPath($path)
	{
		$searchPath = rtrim($path, "/");

		$searchPathArr = explode("/", $searchPath);

		$lastValidParent = $this;
		$fullPathArr = [];
		foreach($searchPathArr as $p) {
			$fullPathArr[] = $p;

			$results = $lastValidParent->filterChilds(function($section) use($fullPathArr) {
				return count(array_intersect(explode("/", $section->getPath()), $fullPathArr))>=count($fullPathArr);
			}, ['maxDepth' => 0, 'depth'=>0, 'maxResults'=>null]);

			if ($results) {
				$lastValidParent = $results[0];
			}
			else {
				break;
			}
		}

		return $lastValidParent;
	}


	/**
	 * Filter children with a callback function
	 */
	public function filterChilds($callback, $options=[])
	{
		$ret = [];

		if ($options['depth']==0) {
			$sectionObj = $this;
			if ($callback($sectionObj)) {
				$ret[] = $sectionObj;
			}
		}

		foreach ($this->children as $sectionObj) {
			if ($callback($sectionObj)) {
				$ret[] = $sectionObj;
			}

			// stop if maxResults results were found
			if (!empty($options['maxResults']) && count($ret)>=$options['maxResults']) {
				break;
			}

			// continue recursively until maxDepth is reached
			if ($options['depth'] < $options['maxDepth']) {
				$o = $options;
				$o['depth'] = $options['depth']+1;
				foreach ($sectionObj->filterChilds($callback, $o) as $sect) {
					$ret[] = $sect;
				}
			}
		}

		return $ret;
	}

	/**
	 * Map children with a callback function
	 */
	public function each($callback, $options=[])
	{
		if ($options['depth']==0) {
			$sectionObj = $this;
			$o = $options;
			$o['parents'][]= $sectionObj;
			$callback($sectionObj, $o);
		}

		foreach ($this->children as $sectionObj) {
			$callback($sectionObj, $options);

			// continue recursively until maxDepth is reached
			$o = $options;
			$o['parents'][]= $sectionObj;
			$o['depth'] = $options['depth']+1;
			$sectionObj->each($callback, $o);
		}

		return $this;
	}


	/**
	 * Get a list of the parent section ids
	 * @param string $idSection
	 * @return Array
	 */
	public function getParentsIdsOfSectionId($idSection)
	{
		$key = sprintf("%s-%s", __METHOD__, $idSection);
		if ($this->cache[$key]??null) {
			return $this->cache[$key];
		}

		$found = $this->getParentsIdsOfSectionId_rec($idSection, []);
		$this->cache[$key] = $found;
		return $found;
	}

	private function getParentsIdsOfSectionId_rec($idSection, $parents)
	{
		foreach ($this->children as $id=>$sectionObj) {
			if ($id==$idSection) {
				return $parents;
			}

			$p = $parents;
			$p[] = $sectionObj->getId();
			$res = $sectionObj->getParentsIdsOfSectionId_rec($idSection, $p);
			if ($res) {
				return $res;
			}
		}

		return null;
	}
}
