<?php

namespace XF\Entity;

use XF\Api\Result\EntityResult;
use XF\Mvc\Entity\AbstractCollection;
use XF\Mvc\Entity\Entity;
use XF\Mvc\Entity\Structure;
use XF\Mvc\Router;
use XF\NodeType\AbstractHandler;
use XF\Repository\NodeRepository;

use function intval, is_int, strval;

/**
 * COLUMNS
 * @property int|null $node_id
 * @property string $title
 * @property string|null $node_name
 * @property string $description
 * @property string $node_type_id
 * @property int $parent_node_id
 * @property int $display_order
 * @property int $lft
 * @property int $rgt
 * @property int $depth
 * @property int $style_id
 * @property int $effective_style_id
 * @property bool $display_in_list
 * @property array|null $breadcrumb_data
 * @property string $navigation_id
 * @property string $effective_navigation_id
 *
 * GETTERS
 * @property-read AbstractNode|null $Data
 * @property-read array|null $node_type_info
 * @property-read AbstractHandler|null $handler
 *
 * RELATIONS
 * @property-read Node|null $Parent
 * @property-read NodeType|null $NodeType
 * @property-read AbstractCollection<PermissionCacheContent> $Permissions
 * @property-read AbstractCollection<ModeratorContent> $Moderators
 * @property-read AbstractCollection<BookmarkItem> $Bookmarks
 */
class Node extends Entity implements LinkableInterface, ViewableInterface
{
	use BookmarkTrait;

	public function canView(&$error = null)
	{
		/** @var AbstractNode $data */
		$data = $this->Data;
		if (!$data)
		{
			return false;
		}

		return $data->canView($error);
	}

	protected function canBookmarkContent(&$error = null)
	{
		return ($this->node_type_id == 'Page' && $this->canView($error));
	}

	public function isSearchEngineIndexable(): bool
	{
		/** @var AbstractNode $data */
		$data = $this->Data;
		if (!$data)
		{
			return false;
		}

		return $data->isSearchEngineIndexable();
	}

	public function hasChildren()
	{
		return ($this->rgt - $this->lft) > 1;
	}

	public function getNodeListExtras()
	{
		/** @var AbstractNode $data */
		$data = $this->Data;
		if (!$data)
		{
			return [];
		}

		return $data->getNodeListExtras();
	}

	public function getRoute($linkType = 'public')
	{
		$nodeType = $this->node_type_info;
		if (!$nodeType)
		{
			return '';
		}

		return $linkType == 'public' ? $nodeType['public_route'] : $nodeType['admin_route'];
	}

	public function getBreadcrumbs($includeSelf = true, $linkType = 'public')
	{
		/** @var Router $router */
		$router = $this->app()->container('router.' . $linkType);
		$nodeTypes = $this->app()->container('nodeTypes');

		$output = [];
		if ($this->breadcrumb_data)
		{
			foreach ($this->breadcrumb_data AS $crumb)
			{
				if (!isset($nodeTypes[$crumb['node_type_id']]))
				{
					continue;
				}

				$nodeType = $nodeTypes[$crumb['node_type_id']];
				$route = $linkType == 'public' ? $nodeType['public_route'] : $nodeType['admin_route'];

				$output[] = [
					'value' => $crumb['title'],
					'href' => $router->buildLink($route, $crumb),
					'node_id' => $crumb['node_id'],
				];
			}
		}

		$nodeType = $this->node_type_info;

		if ($includeSelf && $nodeType)
		{
			$route = $linkType == 'public' ? $nodeType['public_route'] : $nodeType['admin_route'];

			$output[] = [
				'value' => $this->title,
				'href' => $router->buildLink($route, $this),
				'node_id' => $this->node_id,
			];
		}

		return $output;
	}

	public function getNodeTemplateRenderer($depth)
	{
		/** @var AbstractNode $data */
		$data = $this->Data;
		if (!$data)
		{
			return null;
		}

		return $data->getNodeTemplateRenderer($depth);
	}

	/**
	 * @return AbstractNode|null
	 */
	public function getData()
	{
		if (!$this->node_id)
		{
			return null;
		}

		$dataEntity = $this->getDataEntityName();
		if (!$dataEntity)
		{
			return null; // no node type record
		}

		return $this->_em->find($dataEntity, $this->node_id);
	}

	public function getDataEntityName()
	{
		$nodeType = $this->node_type_info;
		return $nodeType ? $nodeType['entity_identifier'] : null;
	}

	/**
	 * @param bool $cascadeSave
	 *
	 * @return AbstractNode
	 */
	public function getDataRelationOrDefault($cascadeSave = true)
	{
		$data = $this->getData();
		if (!$data)
		{
			$dataEntity = $this->getDataEntityName();
			if (!$dataEntity)
			{
				throw new \LogicException("No node type for '$this->node_type_id' could be found");
			}

			$data = $this->_em->create($dataEntity);
			$data->node_id = $this->_em->getDeferredValue(
				function () { return $this->getValue('node_id'); },
				'save'
			);

			$this->_getterCache['Data'] = $data;
		}

		if ($cascadeSave)
		{
			$this->addCascadedSave($data);
		}

		return $data;
	}

	/**
	 * @return AbstractHandler|null
	 */
	public function getHandler()
	{
		$nodeType = $this->node_type_info;
		if (!$nodeType || !$nodeType['handler_class'])
		{
			return null;
		}

		$class = \XF::stringToClass($nodeType['handler_class'], '%s\NodeType\%s');
		$class = \XF::extendClass($class);

		return new $class($this->node_type_id, $nodeType);
	}

	/**
	 * @return array|null
	 */
	public function getNodeTypeInfo()
	{
		$nodeTypes = $this->app()->container('nodeTypes');
		$nodeTypeId = $this->node_type_id;

		return $nodeTypes[$nodeTypeId] ?? null;
	}

	// *********** VERIFIERS *************

	protected function verifyNodeName(&$name)
	{
		if (!$name)
		{
			if ($this->node_type_id == 'Page')
			{
				$this->error(\XF::phrase('please_enter_valid_url_portion'));
				return false;
			}

			$name = null;
			return true;
		}

		if ($name === strval(intval($name)) || $name == '-')
		{
			$this->error(\XF::phrase('node_names_contain_more_numbers_hyphen'), 'node_name');
			return false;
		}

		return true;
	}

	protected function verifyNodeTypeId(&$nodeTypeId)
	{
		$nodeTypes = $this->app()->container('nodeTypes');
		if (!isset($nodeTypes[$nodeTypeId]))
		{
			$this->error(\XF::phrase('please_choose_valid_node_type'), 'node_type_id');
			return false;
		}

		return true;
	}

	// *********** LIFE CYCLE **************

	protected function _preSave()
	{
		if ($this->isUpdate() && $this->isChanged('node_type_id'))
		{
			throw new \LogicException("Node types can't be changed");
		}
	}

	protected function _preDelete()
	{
		/** @var Entity $data */
		$data = $this->Data;
		if ($data)
		{
			if (!$data->preDelete())
			{
				foreach ($data->getErrors() AS $key => $error)
				{
					$this->error($error, is_int($key) ? null : $key, false);
				}
			}
		}
	}

	protected function _postDelete()
	{
		/** @var Entity $data */
		$data = $this->Data;
		if ($data)
		{
			$data->delete();
		}

		if ($this->Moderators)
		{
			/** @var ModeratorContent $moderator */
			foreach ($this->Moderators AS $moderator)
			{
				$moderator->delete();
			}
		}
	}

	/**
	 * @param EntityResult $result
	 * @param int $verbosity
	 * @param array $options
	 *
	 * @api-out array $breadcrumbs A list of breadcrumbs for this node, including the node_id, title, and node_type_id
	 * @api-out object $type_data Data related to the specific node type this represents. Contents will vary significantly.
	 * @api-out string $view_url
	 */
	protected function setupApiResultData(
		EntityResult $result,
		$verbosity = self::VERBOSITY_NORMAL,
		array $options = []
	)
	{
		if ($result->getResultType() === EntityResult::TYPE_API)
		{
			$breadcrumbs = [];
			if ($this->breadcrumb_data)
			{
				foreach ($this->breadcrumb_data AS $breadcrumb)
				{
					$breadcrumbs[] = [
						'node_id' => $breadcrumb['node_id'],
						'title' => $breadcrumb['title'],
						'node_type_id' => $breadcrumb['node_type_id'],
					];
				}
			}

			$result->breadcrumbs = $breadcrumbs;
		}

		$typeData = $this->Data ? $this->Data->getNodeTypeApiData($verbosity, $options) : (object) [];
		$result->type_data = $typeData;

		$viewUrl = $this->getContentUrl(true);
		if ($viewUrl)
		{
			$result->view_url = $viewUrl;
		}
	}

	public function getContentUrl(bool $canonical = false, array $extraParams = [], $hash = null)
	{
		$nodeTypeInfo = $this->getNodeTypeInfo();
		if (!$nodeTypeInfo)
		{
			return '';
		}

		$route = ($canonical ? 'canonical:' : '') . $nodeTypeInfo['public_route'];
		return $this->app()->router('public')->buildLink($route, $this, $extraParams, $hash);
	}

	public function getContentPublicRoute()
	{
		$nodeTypeInfo = $this->getNodeTypeInfo();
		if (!$nodeTypeInfo)
		{
			return null;
		}

		return $nodeTypeInfo['public_route'];
	}

	public function getContentTitle(string $context = '')
	{
		return $this->title;
	}

	public static function getStructure(Structure $structure)
	{
		$structure->table = 'xf_node';
		$structure->shortName = 'XF:Node';
		$structure->primaryKey = 'node_id';
		$structure->contentType = 'node';
		$structure->columns = [
			'node_id' => ['type' => self::UINT, 'autoIncrement' => true, 'nullable' => true],
			'title' => ['type' => self::STR, 'maxLength' => 50,
				'required' => 'please_enter_valid_title', 'api' => true,
			],
			'node_name' => ['type' => self::STR, 'maxLength' => 50, 'nullable' => true, 'default' => null,
				'unique' => 'node_names_must_be_unique',
				'match' => self::MATCH_ALPHANUMERIC_HYPHEN,
				'api' => true,
			],
			'description' => ['type' => self::STR, 'default' => '', 'api' => true],
			'node_type_id' => ['type' => self::BINARY, 'maxLength' => 25, 'required' => true, 'api' => true],
			'parent_node_id' => ['type' => self::UINT, 'required' => true, 'default' => 0, 'api' => true],
			'display_order' => ['type' => self::UINT, 'default' => 1, 'api' => true],
			'lft' => ['type' => self::UINT],
			'rgt' => ['type' => self::UINT],
			'depth' => ['type' => self::UINT],
			'style_id' => ['type' => self::UINT, 'default' => 0],
			'effective_style_id' => ['type' => self::UINT, 'default' => 0],
			'display_in_list' => ['type' => self::BOOL, 'default' => true, 'api' => true],
			'breadcrumb_data' => ['type' => self::JSON_ARRAY, 'default' => []],
			'navigation_id' => ['type' => self::STR, 'maxLength' => 50, 'default' => ''],
			'effective_navigation_id' => ['type' => self::STR, 'maxLength' => 50, 'default' => ''],
		];
		$structure->getters = [
			'Data' => true,
			'node_type_info' => true,
			'handler' => true,
		];
		$structure->behaviors = [
			'XF:TreeStructured' => [
				'parentField' => 'parent_node_id',
				'permissionContentType' => 'node',
				'rebuildExtraFields' => ['style_id', 'navigation_id', 'node_name'],
				'rebuildService' => 'XF:Node\RebuildNestedSet',
			],
		];
		$structure->relations = [
			'Parent' => [
				'entity' => 'XF:Node',
				'type' => self::TO_ONE,
				'conditions' => [
					['node_id', '=', '$parent_node_id'],
				],
				'primary' => true,
			],
			'NodeType' => [
				'entity' => 'XF:NodeType',
				'type' => self::TO_ONE,
				'conditions' => 'node_type_id',
				'primary' => true,
			],
			'Permissions' => [
				'entity' => 'XF:PermissionCacheContent',
				'type' => self::TO_MANY,
				'conditions' => [
					['content_type', '=', 'node'],
					['content_id', '=', '$node_id'],
				],
				'key' => 'permission_combination_id',
				'proxy' => true,
			],
			'Moderators' => [
				'entity' => 'XF:ModeratorContent',
				'type' => self::TO_MANY,
				'conditions' => [
					['content_type', '=', 'node'],
					['content_id', '=', '$node_id'],
				],
			],
		];
		$structure->withAliases = [
			'api' => [
				function ()
				{
					return 'Permissions|' . \XF::visitor()->permission_combination_id;
				},
			],
		];

		static::addBookmarkableStructureElements($structure);

		return $structure;
	}

	/**
	 * @return NodeRepository
	 */
	protected function getNodeRepo()
	{
		return $this->repository(NodeRepository::class);
	}
}
