<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\Exception\InvalidMetadataException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

final class EnumHandler implements SubscribingHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public static function getSubscribingMethods()
    {
        $methods = [];

        foreach (['json', 'xml'] as $format) {
            $methods[] = [
                'type' => 'enum',
                'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                'format' => $format,
                'method' => 'deserializeEnum',
            ];
            $methods[] = [
                'type' => 'enum',
                'format' => $format,
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'method' => 'serializeEnum',
            ];
        }

        return $methods;
    }

    public function serializeEnum(
        SerializationVisitorInterface $visitor,
        \UnitEnum $enum,
        array $type,
        SerializationContext $context
    ) {
        if ((isset($type['params'][1]) && 'value' === $type['params'][1]) || (!isset($type['params'][1]) && $enum instanceof \BackedEnum)) {
            if (!$enum instanceof \BackedEnum) {
                throw new InvalidMetadataException(sprintf('The type "%s" is not a backed enum, thus you can not use "value" as serialization mode for its value.', get_class($enum)));
            }

            $valueType = isset($type['params'][2]) ? ['name' => $type['params'][2]] : null;

            return $context->getNavigator()->accept($enum->value, $valueType);
        } else {
            return $context->getNavigator()->accept($enum->name);
        }
    }

    /**
     * @param int|string|\SimpleXMLElement $data
     * @param array $type
     */
    public function deserializeEnum(DeserializationVisitorInterface $visitor, $data, array $type): ?\UnitEnum
    {
        $enumType = $type['params'][0];
        $caseValue = (string) $data;

        $ref = new \ReflectionEnum($enumType);
        if (isset($type['params'][1]) && 'value' === $type['params'][1] || (!isset($type['params'][1]) && is_a($enumType, \BackedEnum::class, true))) {
            if (!is_a($enumType, \BackedEnum::class, true)) {
                throw new InvalidMetadataException(sprintf('The type "%s" is not a backed enum, thus you can not use "value" as serialization mode for its value.', $enumType));
            }

            if ('int' === $ref->getBackingType()->getName()) {
                if (!is_numeric($caseValue)) {
                    throw new RuntimeException(sprintf('"%s" is not a valid backing value for enum "%s"', $caseValue, $enumType));
                }

                $caseValue = (int) $caseValue;
            }

            return $enumType::from($caseValue);
        } else {
            if (!$ref->hasCase($caseValue)) {
                throw new InvalidMetadataException(sprintf('The type "%s" does not have the case "%s"', $ref->getName(), $caseValue));
            }

            return $ref->getCase($caseValue)->getValue();
        }
    }
}
