<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\AdminBundle\Form\ChoiceList;

use Sonata\AdminBundle\Model\ModelManagerInterface;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

/**
 * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
 */
final class ModelChoiceLoader implements ChoiceLoaderInterface
{
    /**
     * @var ModelManagerInterface<object>
     */
    private ModelManagerInterface $modelManager;

    private PropertyAccessorInterface $propertyAccessor;

    /**
     * @phpstan-var class-string
     */
    private string $class;

    private ?string $property = null;

    private ?object $query = null;

    /**
     * @var object[]|null
     */
    private ?array $choices = null;

    private ?ChoiceListInterface $choiceList = null;

    /**
     * @param ModelManagerInterface<object> $modelManager
     * @param object[]|null                 $choices
     *
     * @phpstan-param class-string $class
     */
    public function __construct(
        ModelManagerInterface $modelManager,
        PropertyAccessorInterface $propertyAccessor,
        string $class,
        ?string $property = null,
        ?object $query = null,
        ?array $choices = null
    ) {
        $this->modelManager = $modelManager;
        $this->propertyAccessor = $propertyAccessor;
        $this->class = $class;
        $this->property = $property;
        $this->choices = $choices;

        if (null !== $query) {
            if (!$this->modelManager->supportsQuery($query)) {
                throw new InvalidArgumentException('The model manager does not support the query.');
            }

            $this->query = $query;
        }
    }

    public function loadChoiceList($value = null): ChoiceListInterface
    {
        if (null === $this->choiceList) {
            if (null !== $this->query) {
                $entities = $this->modelManager->executeQuery($this->query);
            } elseif (\is_array($this->choices)) {
                $entities = $this->choices;
            } else {
                $entities = $this->modelManager->findBy($this->class);
            }

            $choices = [];
            foreach ($entities as $model) {
                if (null !== $this->property) {
                    // If the property option was given, use it
                    $valueObject = $this->propertyAccessor->getValue($model, $this->property);
                } elseif (method_exists($model, '__toString')) {
                    // Otherwise expect a __toString() method in the entity
                    $valueObject = (string) $model;
                } else {
                    throw new TransformationFailedException(sprintf(
                        'Unable to convert the model "%s" to string, provide "property" option'
                        .' or implement "__toString()" method in your model.',
                        $this->class
                    ));
                }

                if (!\array_key_exists($valueObject, $choices)) {
                    $choices[$valueObject] = [];
                }

                $identifier = $this->modelManager->getNormalizedIdentifier($model);
                if (null === $identifier) {
                    throw new TransformationFailedException(sprintf(
                        'No identifier was found for the model "%s".',
                        $this->class
                    ));
                }

                $choices[$valueObject][] = $identifier;
            }

            $finalChoices = [];
            foreach ($choices as $valueObject => $idx) {
                if (\count($idx) > 1) { // avoid issue with identical values ...
                    foreach ($idx as $id) {
                        $finalChoices[sprintf('%s (id: %s)', $valueObject, $id)] = $id;
                    }
                } else {
                    $finalChoices[$valueObject] = current($idx);
                }
            }

            $this->choiceList = new ArrayChoiceList($finalChoices, $value);
        }

        return $this->choiceList;
    }

    public function loadChoicesForValues(array $values, $value = null): array
    {
        return $this->loadChoiceList($value)->getChoicesForValues($values);
    }

    public function loadValuesForChoices(array $choices, $value = null): array
    {
        return $this->loadChoiceList($value)->getValuesForChoices($choices);
    }
}
