<?php

namespace XF\Api\Result;

use XF\Entity\ResultInterface;
use XF\Mvc\Entity\AbstractCollection;
use XF\Mvc\Entity\Entity;
use XF\Mvc\Entity\Structure;
use XF\Phrase;
use XF\PreEscaped;
use XF\PreEscapedInterface;

use function is_array;

class EntityResult implements EntityResultInterface
{
	/**
	 * @var Entity
	 */
	protected $entity;

	protected $resultType;

	protected $skipColumns = [];

	protected $includeColumns = [];

	protected $includeGetters = [];

	protected $skipRelations = [];

	protected $includeRelations = [];

	protected $extra = [];

	/**
	 * @var callable[]
	 */
	protected $callbacks = [];

	public function __construct(Entity $entity, $resultType = self::TYPE_API)
	{
		$this->entity = $entity;
		$this->resultType = $resultType;
	}

	public function getResultType(): string
	{
		return $this->resultType;
	}

	public function skipColumn($column)
	{
		foreach ((array) $column AS $k)
		{
			$this->skipColumns[$k] = true;
		}

		return $this;
	}

	public function includeColumn($column)
	{
		foreach ((array) $column AS $k)
		{
			$this->includeColumns[$k] = true;
		}

		return $this;
	}

	public function includeGetter($getter)
	{
		foreach ((array) $getter AS $k)
		{
			$this->includeGetters[$k] = true;
		}

		return $this;
	}

	public function skipRelation($relation)
	{
		foreach ((array) $relation AS $k)
		{
			$this->skipRelations[$k] = true;
		}
	}

	public function includeRelation($relation, $verbosity = Entity::VERBOSITY_NORMAL, array $options = [])
	{
		foreach ((array) $relation AS $k)
		{
			$this->includeRelations[$k] = [$verbosity, $options];
		}

		return $this;
	}

	public function includeExtra($k, $v = null)
	{
		if (is_array($k))
		{
			$pairs = $k;
		}
		else
		{
			$pairs = [$k => $v];
		}

		foreach ($pairs AS $k => $v)
		{
			$this->extra[$k] = $v;
		}

		return $this;
	}

	public function addCallback(callable $c)
	{
		$this->callbacks[] = $c;

		return $this;
	}

	public function __set($k, $v)
	{
		$this->extra[$k] = $v;
	}

	public function getEntity()
	{
		return $this->entity;
	}

	public function render()
	{
		$result = [];

		$entity = $this->entity;
		$structure = $entity->structure();
		foreach ($structure->columns AS $key => $column)
		{
			if ($this->isColumnIncluded($key, $column, $structure))
			{
				$result[$key] = $entity->getValue($key);
			}
		}

		foreach ($this->includeGetters AS $getter => $null)
		{
			$result[$getter] = $this->castToFinalValue($entity->get($getter));
		}

		foreach ($structure->relations AS $key => $relation)
		{
			if (!$this->isRelationIncluded($key, $relation, $structure))
			{
				continue;
			}

			if (isset($this->includeRelations[$key]))
			{
				$relationVerbosity = $this->includeRelations[$key][0];
				$relationOptions = $this->includeRelations[$key][1];
			}
			else
			{
				$relationVerbosity = Entity::VERBOSITY_NORMAL;
				$relationOptions = [];
			}

			$relationResult = $entity->getRelation($key);
			$result[$key] = $this->castToFinalValue($relationResult, $relationVerbosity, $relationOptions);
		}

		foreach ($this->extra AS $k => $v)
		{
			$result[$k] = $this->castToFinalValue($v);
		}

		foreach ($this->callbacks AS $c)
		{
			$pairs = $c($entity);
			if (is_array($pairs))
			{
				foreach ($pairs AS $k => $v)
				{
					$result[$k] = $this->castToFinalValue($v);
				}
			}
		}

		// sort results with lower-case keys first
		uksort($result, function (string $left, string $right): int
		{
			$leftGroup  = ctype_lower($left[0]) ? 0 : 1;
			$rightGroup = ctype_lower($right[0]) ? 0 : 1;

			if ($leftGroup !== $rightGroup)
			{
				return $leftGroup <=> $rightGroup;
			}

			$caseInsensitiveOrder = strcasecmp($left, $right);
			return $caseInsensitiveOrder !== 0 ? $caseInsensitiveOrder : ($left <=> $right);
		});

		if ($this->getResultType() === self::TYPE_WEBHOOK)
		{
			return $result;
		}

		// casting to an object for the rare case of [] being returned here
		return (object) $result;
	}

	protected function castToFinalValue($value, $verbosity = Entity::VERBOSITY_NORMAL, array $options = [])
	{
		if ($value instanceof Entity)
		{
			if ($this->getResultType() === self::TYPE_API)
			{
				$value = $value->toApiResult($verbosity, $options);
			}
			else
			{
				$value = $value->toWebhookResult($verbosity, $options);
			}
		}
		else if ($value instanceof AbstractCollection)
		{
			if ($this->getResultType() === self::TYPE_API)
			{
				$value = $value->toApiResults($verbosity, $options);
			}
			else
			{
				$value = $value->toWebhookResults($verbosity, $options);
			}
		}

		if ($value instanceof ResultInterface
			|| $value instanceof Phrase
			|| $value instanceof PreEscaped
			|| $value instanceof PreEscapedInterface
		)
		{
			$value = $value->render();
		}

		return $value;
	}

	protected function isColumnIncluded($key, array $column, Structure $structure)
	{
		if (!empty($this->skipColumns[$key]))
		{
			// skip has highest priority
			return false;
		}

		if (!empty($this->includeColumns[$key]) || !empty($column['api']) || !empty($column['autoIncrement']))
		{
			// specifically included in this request, named as api column, or an autoInc column means included
			return true;
		}

		// otherwise, default to false
		return false;
	}

	protected function isRelationIncluded($key, array $relation, Structure $structure)
	{
		if (!empty($this->skipRelations[$key]))
		{
			return false;
		}

		if (!empty($this->includeRelations[$key]) || !empty($relation['api']))
		{
			return true;
		}

		return false;
	}
}
