<?php

declare(strict_types=1);

namespace App\Framework\Reflection;

use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use ReflectionException;

final class CachedReflectionProvider implements ReflectionProvider
{
    private array $classCache = [];
    private array $methodCache = [];
    private array $parameterCache = [];
    private array $propertyCache = [];
    private array $attributeCache = [];
    private array $parameterInfoCache = [];
    private array $instantiableCache = [];
    private array $interfaceCache = [];
    private array $wrappedClassCache = [];
    private array $wrappedMethodCache = [];

    public function getWrappedClass(string $className): WrappedReflectionClass
    {
        return $this->wrappedClassCache[$className] ??= new WrappedReflectionClass(
            $this->getClass($className)
        );
    }

    public function getWrappedMethod(string $className, string $methodName): WrappedReflectionMethod
    {
        $key = "{$className}::{$methodName}";
        return $this->wrappedMethodCache[$key] ??= new WrappedReflectionMethod(
            $this->getMethod($className, $methodName)
        );
    }

    public function getClass(string $className): ReflectionClass
    {
        return $this->classCache[$className] ??= new ReflectionClass($className);
    }

    public function getMethod(string $className, string $methodName): ReflectionMethod
    {
        $key = "{$className}::{$methodName}";
        return $this->methodCache[$key] ??= $this->getClass($className)->getMethod($methodName);
    }

    public function getMethodParameters(string $className, string $methodName): array
    {
        $key = "{$className}::{$methodName}::params";
        return $this->parameterCache[$key] ??= $this->getMethod($className, $methodName)->getParameters();
    }

    public function getProperties(string $className): array
    {
        $key = "{$className}::properties";
        return $this->propertyCache[$key] ??= $this->getClass($className)->getProperties();
    }

    public function getAttributes(string $className, ?string $attributeClass = null): array
    {
        $key = "{$className}::attributes::" . ($attributeClass ?? 'all');

        return $this->attributeCache[$key] ??= $attributeClass
            ? $this->getClass($className)->getAttributes($attributeClass)
            : $this->getClass($className)->getAttributes();
    }

    public function getMethodAttributes(string $className, string $methodName, ?string $attributeClass = null): array
    {
        $key = "{$className}::{$methodName}::attributes::" . ($attributeClass ?? 'all');

        return $this->attributeCache[$key] ??= $attributeClass
            ? $this->getMethod($className, $methodName)->getAttributes($attributeClass)
            : $this->getMethod($className, $methodName)->getAttributes();
    }

    public function hasAttribute(string $className, string $attributeClass): bool
    {
        return !empty($this->getAttributes($className, $attributeClass));
    }

    public function getParameterInfo(string $className, string $methodName): array
    {
        $key = "{$className}::{$methodName}::paramInfo";

        return $this->parameterInfoCache[$key] ??= $this->buildParameterInfo($className, $methodName);
    }

    public function isInstantiable(string $className): bool
    {
        return $this->instantiableCache[$className] ??= $this->getClass($className)->isInstantiable();
    }

    public function implementsInterface(string $className, string $interfaceName): bool
    {
        $key = "{$className}::implements::{$interfaceName}";

        return $this->interfaceCache[$key] ??= $this->getClass($className)->implementsInterface($interfaceName);
    }

    private function buildParameterInfo(string $className, string $methodName): array
    {
        $parameters = $this->getMethodParameters($className, $methodName);
        $paramInfo = [];

        foreach ($parameters as $param) {
            $type = $param->getType();
            $paramInfo[] = [
                'name' => $param->getName(),
                'type' => $type ? $type->getName() : null,
                'isBuiltin' => $type ? $type->isBuiltin() : true,
                'allowsNull' => $param->allowsNull(),
                'isOptional' => $param->isOptional(),
                'hasDefaultValue' => $param->isDefaultValueAvailable(),
                'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null,
                'position' => $param->getPosition(),
            ];
        }

        return $paramInfo;
    }

    public function forget(string $className): void
    {
        unset($this->classCache[$className]);
        unset($this->instantiableCache[$className]);
        unset($this->wrappedClassCache[$className]);

        // Alle zugehörigen Caches leeren
        $this->clearRelatedCaches($className);
    }

    public function flush(): void
    {
        $this->classCache = [];
        $this->methodCache = [];
        $this->parameterCache = [];
        $this->propertyCache = [];
        $this->attributeCache = [];
        $this->parameterInfoCache = [];
        $this->instantiableCache = [];
        $this->interfaceCache = [];
        $this->wrappedClassCache = [];
        $this->wrappedMethodCache = [];
    }

    private function clearRelatedCaches(string $className): void
    {
        $prefix = $className . '::';

        $caches = [
            &$this->methodCache,
            &$this->parameterCache,
            &$this->propertyCache,
            &$this->attributeCache,
            &$this->parameterInfoCache,
            &$this->interfaceCache,
            &$this->wrappedMethodCache
        ];

        foreach ($caches as &$cache) {
            $cache = array_filter(
                $cache,
                fn($key) => !str_starts_with($key, $prefix),
                ARRAY_FILTER_USE_KEY
            );
        }
    }
}
