<?php

namespace XF\Api\Docs;

use XF\Api\Docs\Annotation\OutLine;
use XF\Api\Docs\Annotation\RouteBlock;
use XF\Api\Docs\Annotation\TypeBlock;
use XF\Mvc\Entity\Entity;

use function strlen;

class ClassParser
{
	/**
	 * @var AnnotationParser
	 */
	protected $annotationParser;

	/**
	 * @var array[]|null
	 */
	protected $actionPrefixes = null;

	public function __construct(AnnotationParser $annotationParser)
	{
		$this->annotationParser = $annotationParser;
	}

	public function parseControllerClass($shortName, $baseRoute = '', $actionPrefix = '')
	{
		$className = \XF::stringToClass($shortName, '%s\%s\Controller\%s', 'Api');

		$reflection = new \ReflectionClass($className);
		if (!$reflection || $reflection->isAbstract())
		{
			return null;
		}

		$defaultGroup = null;

		$classDocBlock = $reflection->getDocComment();
		if ($classDocBlock)
		{
			$classBlock = $this->parseRouteBlock($classDocBlock, $className);
			if ($classBlock->group)
			{
				$defaultGroup = $classBlock->group;
			}
		}

		if (!$defaultGroup)
		{
			$defaultGroup = preg_replace('#/.*$#', '', $baseRoute);
		}

		$routes = [];
		$baseRoute = rtrim($baseRoute, '/');

		foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) AS $method)
		{
			if (!preg_match('#^action(Get|Post|Delete|Put|Patch)(.*)$#i', $method->name, $nameMatch))
			{
				continue;
			}

			$actionSuffix = $nameMatch[2];

			if ($actionPrefix !== '')
			{
				if (strpos($actionSuffix, $actionPrefix) !== 0)
				{
					continue;
				}

				$actionSuffix = substr($actionSuffix, strlen($actionPrefix));
			}
			else if ($actionSuffix !== '' && $this->hasActionPrefixedRoutes($shortName, $actionSuffix))
			{
				continue;
			}

			$docComment = $method->getDocComment();
			if ($docComment)
			{
				$block = $this->parseRouteBlock($docComment, $className);
			}
			else
			{
				// no docs, but we can use the route and action info to at least mention it
				$block = new RouteBlock();
				$block->incomplete = true;
			}

			if (!$block->method)
			{
				$block->method = strtoupper($nameMatch[1]);
			}
			if (!$block->route)
			{
				$block->route = $baseRoute . '/' . $this->convertActionToRoute($actionSuffix);
			}
			if (!$block->group)
			{
				$block->group = $defaultGroup;
			}

			$routes["{$block->method} {$block->route}"] = $block;
		}

		return $routes;
	}

	protected function hasActionPrefixedRoutes($controllerShortName, $actionSuffix)
	{
		if ($this->actionPrefixes === null)
		{
			$this->actionPrefixes = [];

			$prefixes = \XF::db()->fetchAll("
				SELECT controller, action_prefix
				FROM xf_route
				WHERE route_type = 'api'
					AND action_prefix != ''
			");
			foreach ($prefixes AS $row)
			{
				$this->actionPrefixes[$row['controller']][] = $row['action_prefix'];
			}
		}

		$prefixes = $this->actionPrefixes[$controllerShortName] ?? [];
		foreach ($prefixes AS $prefix)
		{
			if (strpos($actionSuffix, $prefix) === 0)
			{
				return true;
			}
		}

		return false;
	}

	protected function convertActionToRoute($string)
	{
		return strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^\-])([A-Z][a-z])/'], '$1-$2', $string));
	}

	public function parseEntityClass($shortName)
	{
		$className = \XF::stringToClass($shortName, '%s\Entity\%s');

		$reflection = new \ReflectionClass($className);
		if (!$reflection || $reflection->isAbstract() || !$reflection->hasMethod('setupApiResultData'))
		{
			return null;
		}

		$method = $reflection->getMethod('setupApiResultData');
		if ($method->class == Entity::class)
		{
			// method hasn't been overridden, so ignore
			return null;
		}

		$docComment = $method->getDocComment();
		if (!$docComment)
		{
			return null;
		}

		$block = $this->parseTypeBlock($docComment, $className);
		$this->addToTypeFromEntityStructure($block, $shortName);

		if (!$block->structure)
		{
			return null;
		}

		if (!$block->type)
		{
			$block->type = $this->getTypeNameFromShortName($shortName);
		}

		return $block;
	}

	protected function addToTypeFromEntityStructure(TypeBlock $block, $entityName)
	{
		$structure = \XF::em()->getEntityStructure($entityName);

		foreach ($structure->columns AS $name => $column)
		{
			if (isset($block->structure[$name]))
			{
				continue;
			}

			if (empty($column['api']) && empty($column['autoIncrement']))
			{
				continue;
			}

			switch ($column['type'])
			{
				case Entity::INT:
				case Entity::UINT:
					$type = 'int';
					break;

				case Entity::FLOAT:
					$type = 'float';
					break;

				case Entity::BOOL:
					$type = 'bool';
					break;

				case Entity::STR:
				case Entity::BINARY:
					$type = 'str';
					break;

				case Entity::SERIALIZED:
				case Entity::JSON:
					$type = 'mixed';
					break;

				case Entity::JSON_ARRAY:
				case Entity::LIST_LINES:
				case Entity::LIST_COMMA:
				case Entity::LIST_ARRAY:
				case Entity::SERIALIZED_ARRAY:
					$type = 'array';
					break;

				default:
					$type = 'mixed';
			}

			$block->structure[$name] = new OutLine($name, '', [$type]);
		}

		foreach ($structure->relations AS $name => $relation)
		{
			if (isset($block->structure[$name]))
			{
				continue;
			}

			if (empty($relation['api']))
			{
				continue;
			}

			$subType = $this->getTypeNameFromShortName($relation['entity']);

			if ($relation['type'] == Entity::TO_MANY)
			{
				$subType .= '[]';
			}

			$block->structure[$name] = new OutLine($name, '', [$subType]);
		}
	}

	protected function getTypeNameFromShortName($shortName)
	{
		$shortName = \XF::classToString($shortName, '%s\Entity\%s');
		$nameParts = explode(':', $shortName, 2);
		if ($nameParts[0] == 'XF')
		{
			// XF is just the entity name
			return $nameParts[1];
		}
		else
		{
			return str_replace('\\', '_', $nameParts[0]) . '_' . $nameParts[1];
		}
	}

	public function parseClassMethod($commentType, $className, $method)
	{
		$reflection = new \ReflectionClass($className);
		if (!$reflection)
		{
			return null;
		}

		if (!$reflection->hasMethod($method))
		{
			return null;
		}

		$methodReflection = $reflection->getMethod($method);
		if (!$methodReflection)
		{
			return null;
		}

		$docComment = $methodReflection->getDocComment();
		if (!$docComment)
		{
			return null;
		}

		switch ($commentType)
		{
			case AnnotationParser::BLOCK_ROUTE:
				return $this->parseRouteBlock($docComment, $className);

			case AnnotationParser::BLOCK_TYPE:
				return $this->parseTypeBlock($docComment, $className);

			default:
				throw new \LogicException("Unknown block type encountered (" . $commentType . ')');
		}
	}

	/**
	 * @param string $comment
	 * @param string|null $className
	 *
	 * @return TypeBlock
	 */
	public function parseTypeBlock($comment, $className = null)
	{
		return $this->annotationParser->parse(AnnotationParser::BLOCK_TYPE, $comment, $className);
	}

	/**
	 * @param string $comment
	 * @param string|null $className
	 *
	 * @return RouteBlock
	 */
	public function parseRouteBlock($comment, $className = null)
	{
		return $this->annotationParser->parse(AnnotationParser::BLOCK_ROUTE, $comment, $className);
	}
}
