<?php

/**
 * @package   bdk/debug
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2025 Brad Kent
 * @since     3.3
 */

namespace bdk\Debug\Utility\PhpDoc;

use bdk\Container\ObjectBuilder as ContainerObjectBuilder;
use bdk\Container\UseStatements;
use bdk\Debug\Utility\PhpDoc;
use bdk\Debug\Utility\Reflection;

/**
 * PhpDoc parsing helper methods
 */
class Type
{
    /** @var list<string> */
    public $types = [];

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->types = ContainerObjectBuilder::$phpDocTypes;
    }

    /**
     * Convert "self[]|null" to array
     *
     * @param string $type             type hint
     * @param string $className        Classname where element is defined
     * @param int    $fullyQualifyType Whether to fully qualify type(s)
     *                                   Bitmask of FULLY_QUALIFY* constants
     *
     * @return string|null
     */
    public function normalize($type, $className, $fullyQualifyType = 0)
    {
        if (\in_array($type, ['', null], true)) {
            return null;
        }
        if (\preg_match('/array[<([{]/', $type)) {
            // type contains "complex" array type... don't deal with parsing
            return $type;
        }
        $types = \preg_split('#\s*\|\s*#', $type);
        foreach ($types as $i => $type) {
            $types[$i] = $this->normalizeSingle($type, $className, $fullyQualifyType);
        }
        return \implode('|', $types);
    }

    /**
     * Normalize individual part of type
     *
     * @param string $type             type hint
     * @param string $className        Classname where element is defined
     * @param int    $fullyQualifyType Whether to fully qualify type(s)
     *                                   Bitmask of FULLY_QUALIFY* constants
     *
     * @return string
     */
    private function normalizeSingle($type, $className, $fullyQualifyType = 0)
    {
        if (\strpos($type, '\\') === 0) {
            return \substr($type, 1);
        }
        $isArray = false;
        if (\substr($type, -2) === '[]') {
            $isArray = true;
            $type = \substr($type, 0, -2);
        }
        $translate = array(
            'boolean' => 'bool',
            'integer' => 'int',
            'self' => $className,
        );
        if (isset($translate[$type])) {
            $type = $translate[$type];
        } elseif ($fullyQualifyType && \in_array($type, $this->types, true) === false) {
            $type = $this->resolveTypeClass($type, $className, $fullyQualifyType);
        }
        if ($isArray) {
            $type .= '[]';
        }
        return $type;
    }

    /**
     * Check type-hint in use statements, and whether relative or absolute
     *
     * @param string $type             Type-hint
     * @param string $className        Classname where element is defined
     * @param int    $fullyQualifyType Whether to fully qualify type(s)
     *                                   Bitmask of FULLY_QUALIFY* constants
     *
     * @return string
     */
    private function resolveTypeClass($type, $className, $fullyQualifyType = 0)
    {
        $className = $className ?: '';

        $resolvedFromUse = $this->resolveFromUseStatements($type, $className);
        if ($resolvedFromUse) {
            return $resolvedFromUse;
        }

        $namespace = \substr($className, 0, \strrpos($className, '\\') ?: 0);
        if (!$namespace) {
            return $type;
        }

        /*
            Truly relative?  Or, does PhpDoc omit '\' ?
            Not 100% accurate, but check if assumed namespace'd class exists
        */
        $autoload = ($fullyQualifyType & PhpDoc::FULLY_QUALIFY_AUTOLOAD) === PhpDoc::FULLY_QUALIFY_AUTOLOAD;
        return \class_exists($namespace . '\\' . $type, $autoload)
            ? $namespace . '\\' . $type
            : $type;
    }

    /**
     * Check if type-hint is defined in use statements
     *
     * @param string $type      Type-hint
     * @param string $className Classname where element is defined
     *
     * @return string|false
     */
    private function resolveFromUseStatements($type, $className)
    {
        $first = \substr($type, 0, \strpos($type, '\\') ?: 0) ?: $type;
        $classReflector = Reflection::getReflector($className, true);
        $useStatements = $classReflector
            ? UseStatements::getUseStatements($classReflector)['class']
            : array();
        return isset($useStatements[$first])
            ? $useStatements[$first] . \substr($type, \strlen($first))
            : false;
    }
}
