<?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\JsonStreamer\Mapping\Write;

use Psr\Container\ContainerInterface;
use Symfony\Component\JsonStreamer\Attribute\StreamedName;
use Symfony\Component\JsonStreamer\Attribute\ValueTransformer;
use Symfony\Component\JsonStreamer\Exception\InvalidArgumentException;
use Symfony\Component\JsonStreamer\Exception\RuntimeException;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
use Symfony\Component\JsonStreamer\Transformer\PropertyValueTransformerInterface;
use Symfony\Component\JsonStreamer\Transformer\ValueObjectTransformerInterface;
use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;

/**
 * Enhances properties stream writing metadata based on properties' attributes.
 *
 * @author Mathias Arlaud <mathias.arlaud@gmail.com>
 *
 * @internal
 */
final class AttributePropertyMetadataLoader implements PropertyMetadataLoaderInterface
{
    public function __construct(
        private PropertyMetadataLoaderInterface $decorated,
        private ContainerInterface $transformers,
        private TypeResolverInterface $typeResolver,
    ) {
    }

    public function load(string $className, array $options = [], array $context = []): array
    {
        $initialResult = $this->decorated->load($className, $options, $context);
        $result = [];

        foreach ($initialResult as $initialStreamedName => $initialMetadata) {
            if (!$initialName = $initialMetadata->getName()) {
                continue;
            }

            try {
                $propertyReflection = new \ReflectionProperty($className, $initialName);
            } catch (\ReflectionException $e) {
                throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
            }

            $attributesMetadata = $this->getPropertyAttributesMetadata($propertyReflection);
            $streamedName = $attributesMetadata['name'] ?? $initialStreamedName;

            if (null === $valueTransformer = $attributesMetadata['nativeToStreamValueTransformer'] ?? null) {
                $result[$streamedName] = $initialMetadata;

                continue;
            }

            if (\is_string($valueTransformer)) {
                $valueTransformerService = $this->getAndValidatePropertyValueTransformerService($valueTransformer);

                $result[$streamedName] = $initialMetadata
                    ->withType($valueTransformerService::getStreamValueType())
                    ->withAdditionalValueTransformer($valueTransformer);

                continue;
            }

            try {
                $valueTransformerReflection = new \ReflectionFunction($valueTransformer);
            } catch (\ReflectionException $e) {
                throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
            }

            $result[$streamedName] = $initialMetadata
                ->withType($this->typeResolver->resolve($valueTransformerReflection))
                ->withAdditionalValueTransformer($valueTransformer);
        }

        return $result;
    }

    /**
     * @return array{name?: string, nativeToStreamValueTransformer?: string|\Closure}
     */
    private function getPropertyAttributesMetadata(\ReflectionProperty $reflectionProperty): array
    {
        $metadata = [];

        $reflectionAttribute = $reflectionProperty->getAttributes(StreamedName::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
        if (null !== $reflectionAttribute) {
            $metadata['name'] = $reflectionAttribute->newInstance()->getName();
        }

        $reflectionAttribute = $reflectionProperty->getAttributes(ValueTransformer::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
        if (null !== $reflectionAttribute) {
            $metadata['nativeToStreamValueTransformer'] = $reflectionAttribute->newInstance()->getNativeToStream();
        }

        return $metadata;
    }

    private function getAndValidatePropertyValueTransformerService(string $id): PropertyValueTransformerInterface
    {
        if (!$this->transformers->has($id)) {
            throw new InvalidArgumentException(\sprintf('You have requested a non-existent property value transformer service "%s". Did you implement "%s"?', $id, PropertyValueTransformerInterface::class));
        }

        $transformer = $this->transformers->get($id);
        if ($transformer instanceof ValueObjectTransformerInterface) {
            throw new InvalidArgumentException(\sprintf('"%s" is a "%s" and must not be specified as a property value transformer.', $id, ValueObjectTransformerInterface::class));
        }

        if (!$transformer instanceof PropertyValueTransformerInterface) {
            throw new InvalidArgumentException(\sprintf('The "%s" property value transformer service does not implement "%s".', $id, PropertyValueTransformerInterface::class));
        }

        return $transformer;
    }
}
