<?php declare(strict_types=1);

namespace Newland\Toubiz\Sync\Neos\ErrorHandling;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\ORM\EntityManagerInterface;
use Neos\Error\Messages\Result;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Persistence\Doctrine\Mapping\ClassMetadata;
use Neos\Flow\Reflection\ReflectionService;
use Neos\Flow\Validation\Validator\GenericObjectValidator;
use Neos\Flow\Validation\Validator\PolyTypeObjectValidatorInterface;
use Neos\Flow\Validation\Validator\ValidatorInterface;
use Newland\Toubiz\Sync\Neos\ErrorHandling\Validator\FloatValidator;
use Newland\Toubiz\Sync\Neos\ErrorHandling\Validator\IntegerValidator;
use Newland\Toubiz\Sync\Neos\ErrorHandling\Validator\NullableValidator;
use Newland\Toubiz\Sync\Neos\ErrorHandling\Validator\StringLengthValidator;
use Newland\Toubiz\Sync\Neos\ErrorHandling\Validator\StringValidator;

/**
 * Validator that applies for all models in this package. It converts doctrine db mapping information
 * into basic validators (e.g. `@var string` becomes `StringLength(maximum=255)`).
 */
class DoctrineAnnotationBasedValidator implements PolyTypeObjectValidatorInterface
{

    /**
     * @var ReflectionService
     * @Flow\Inject()
     */
    protected $reflectionService;

    /**
     * @var EntityManagerInterface
     * @Flow\Inject()
     */
    protected $entityManager;

    /**
     * @var \SplObjectStorage
     */
    protected $validatedInstancesContainer;

    public function __construct()
    {
        $this->validatedInstancesContainer = new \SplObjectStorage();
    }

    public function setValidatedInstancesContainer(\SplObjectStorage $validatedInstancesContainer)
    {
        $this->validatedInstancesContainer = $validatedInstancesContainer;
    }

    public function canValidate($target): bool
    {
        if ($target === null) {
            return false;
        }
        $className = \is_object($target) ? \get_class($target) : $target;
        return strpos($className, 'Newland\\Toubiz\\Sync\\Neos\\Domain\\Model') !== false;
    }

    public function getPriority(): int
    {
        return 0;
    }

    public function validate($value): Result
    {
        // This validator only deals with fully existing models.
        // Relationships that are set to null are ignored
        if ($value === null || $this->validatedInstancesContainer->contains($value)) {
            return new Result();
        }

        $className = \get_class($value);

        return $this->buildObjectValidator($className)->validate($value);
    }

    private function buildObjectValidator(string $className): GenericObjectValidator
    {
        $metadata = $this->getMetadataForClassName($className);

        $objectValidator = new GenericObjectValidator();
        foreach ($metadata->fieldMappings as $fieldMapping) {
            if ($this->isValidationIgnored($className, $fieldMapping['fieldName'])) {
                continue;
            }

            $fieldValidator = $this->buildPropertyValidator($fieldMapping);
            if ($fieldValidator !== null) {
                $objectValidator->addPropertyValidator($fieldMapping['fieldName'], $fieldValidator);
            }
        }

        $objectValidator->setValidatedInstancesContainer($this->validatedInstancesContainer);
        return $objectValidator;
    }

    private function isValidationIgnored(string $className, string $propertyName): bool
    {
        $property = $this->getReflectionProperty($className, $propertyName);
        $annotation = (new AnnotationReader())
            ->getPropertyAnnotation($property, Flow\IgnoreValidation::class);
        return $annotation !== null;
    }

    private function buildPropertyValidator(array $mapping): ?ValidatorInterface
    {
        $validator = null;

        switch ($mapping['type']) {
            case 'string':
                $length = ($mapping['length'] ?? 255) ?: 255;
                $validator = new StringLengthValidator([ 'maximum' => $length ]);
                break;
            case 'text':
                $validator = new StringValidator();
                break;
            case 'float':
                $validator = new FloatValidator();
                break;
            case 'integer':
                $validator = new IntegerValidator();
                break;
        }

        if ($validator === null) {
            return null;
        }

        if ($mapping['nullable']) {
            return new NullableValidator($validator);
        }

        return $validator;
    }

    public function getOptions(): array
    {
        return [];
    }

    /** @var array */
    protected static $reflectionInformation = [];
    protected function getMetadataForClassName(string $className): ClassMetadata
    {
        if ((static::$reflectionInformation[$className]['__metadata'] ?? null) === null) {
            static::$reflectionInformation[$className] = static::$reflectionInformation[$className] ?? [];
            static::$reflectionInformation[$className]['__metadata'] =
                $this->entityManager->getClassMetadata($className);
        }
        return static::$reflectionInformation[$className]['__metadata'];
    }

    protected function getReflectionProperty(string $className, string $propertyName): \ReflectionProperty
    {
        if ((static::$reflectionInformation[$className][$propertyName] ?? null) === null) {
            static::$reflectionInformation[$className] = static::$reflectionInformation[$className] ?? [];
            static::$reflectionInformation[$className][$propertyName] =
                new \ReflectionProperty($className, $propertyName);
        }
        return static::$reflectionInformation[$className][$propertyName];
    }
}
