<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @final
 */
class ReflectionClassResource implements SelfCheckingResourceInterface
{
    private array $files = [];
    private string $className;
    private array $excludedVendors = [];
    private string $hash;

    public function __construct(
        private \ReflectionClass $classReflector,
        array $excludedVendors = [],
    ) {
        $this->className = $classReflector->name;
        $this->excludedVendors = $excludedVendors;
    }

    public function isFresh(int $timestamp): bool
    {
        if (!isset($this->hash)) {
            $this->hash = $this->computeHash();
            $this->loadFiles($this->classReflector);
        }

        foreach ($this->files as $file => $v) {
            if (false === $filemtime = @filemtime($file)) {
                return false;
            }

            if ($filemtime > $timestamp) {
                return $this->hash === $this->computeHash();
            }
        }

        return true;
    }

    public function __toString(): string
    {
        return 'reflection.'.$this->className;
    }

    public function __serialize(): array
    {
        if (!isset($this->hash)) {
            $this->hash = $this->computeHash();
            $this->loadFiles($this->classReflector);
        }

        return [
            'files' => $this->files,
            'className' => $this->className,
            'excludedVendors' => $this->excludedVendors,
            'hash' => $this->hash,
        ];
    }

    private function loadFiles(\ReflectionClass $class): void
    {
        foreach ($class->getInterfaces() as $v) {
            $this->loadFiles($v);
        }
        do {
            $file = $class->getFileName();
            if (false !== $file && is_file($file)) {
                foreach ($this->excludedVendors as $vendor) {
                    if (\in_array($file[\strlen($vendor)] ?? '', ['/', \DIRECTORY_SEPARATOR], true) && str_starts_with($file, $vendor)) {
                        $file = false;
                        break;
                    }
                }
                if ($file) {
                    $this->files[$file] = null;
                }
            }
            foreach ($class->getTraits() as $v) {
                $this->loadFiles($v);
            }
        } while ($class = $class->getParentClass());
    }

    private function computeHash(): string
    {
        try {
            $this->classReflector ??= new \ReflectionClass($this->className);
        } catch (\ReflectionException) {
            // the class does not exist anymore
            return false;
        }
        $hash = hash_init('xxh128');

        foreach ($this->generateSignature($this->classReflector) as $info) {
            hash_update($hash, $info);
        }

        return hash_final($hash);
    }

    private function generateSignature(\ReflectionClass $class): iterable
    {
        $attributes = [];
        foreach ($class->getAttributes() as $a) {
            $attributes[] = [$a->getName(), (string) $a];
        }
        yield print_r($attributes, true);
        $attributes = [];

        yield $class->getDocComment() ?: '';
        yield (int) $class->isFinal();
        yield (int) $class->isAbstract();

        if ($class->isTrait()) {
            yield print_r(class_uses($class->name), true);
        } else {
            yield print_r(class_parents($class->name), true);
            yield print_r(class_implements($class->name), true);
            yield print_r($class->getConstants(), true);
        }

        foreach ($class->getReflectionConstants() as $constant) {
            foreach ($constant->getAttributes() as $a) {
                $attributes[] = [$a->getName(), (string) $a];
            }
            yield $constant->name.print_r($attributes, true);
            $attributes = [];
        }

        if (!$class->isInterface()) {
            $defaults = $class->getDefaultProperties();

            foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $p) {
                foreach ($p->getAttributes() as $a) {
                    $attributes[] = [$a->getName(), (string) $a];
                }
                yield print_r($attributes, true);
                $attributes = [];

                yield $p->getDocComment() ?: '';
                yield $p->isDefault() ? '<default>' : '';
                yield $p->isPublic() ? 'public' : 'protected';
                yield $p->isStatic() ? 'static' : '';
                yield '$'.$p->name;
                yield print_r(isset($defaults[$p->name]) && !\is_object($defaults[$p->name]) ? $defaults[$p->name] : null, true);
            }
        }

        foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) {
            foreach ($this->excludedVendors as $vendor) {
                $file = $m->getFileName();
                if (\in_array($file[\strlen($vendor)] ?? '', ['/', \DIRECTORY_SEPARATOR], true) && str_starts_with($file, $vendor)) {
                    continue 2;
                }
            }

            foreach ($m->getAttributes() as $a) {
                $attributes[] = [$a->getName(), (string) $a];
            }
            yield print_r($attributes, true);
            $attributes = [];

            $defaults = [];
            foreach ($m->getParameters() as $p) {
                foreach ($p->getAttributes() as $a) {
                    $attributes[] = [$a->getName(), (string) $a];
                }
                yield print_r($attributes, true);
                $attributes = [];

                if (!$p->isDefaultValueAvailable()) {
                    $defaults[$p->name] = null;

                    continue;
                }

                $defaults[$p->name] = (string) $p;
            }

            yield preg_replace('/^  @@.*/m', '', $m);
            yield print_r($defaults, true);
        }

        if ($class->isAbstract() || $class->isInterface() || $class->isTrait()) {
            return;
        }

        if (interface_exists(EventSubscriberInterface::class, false) && $class->isSubclassOf(EventSubscriberInterface::class)) {
            yield EventSubscriberInterface::class;
            yield print_r($class->name::getSubscribedEvents(), true);
        }

        if (interface_exists(ServiceSubscriberInterface::class, false) && $class->isSubclassOf(ServiceSubscriberInterface::class)) {
            yield ServiceSubscriberInterface::class;
            yield print_r($class->name::getSubscribedServices(), true);
        }

        if (interface_exists(FormTypeExtensionInterface::class, false) && $class->isSubclassOf(FormTypeExtensionInterface::class)) {
            yield FormTypeExtensionInterface::class;
            foreach ($class->name::getExtendedTypes() as $key => $value) {
                yield $key.print_r($value, true);
            }
        }
    }
}
