<?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\Form\ChoiceList\Factory;

use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Contracts\Service\ResetInterface;

/**
 * Caches the choice lists created by the decorated factory.
 *
 * To cache a list based on its options, arguments must be decorated
 * by a {@see Cache\AbstractStaticOption} implementation.
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Jules Pietri <jules@heahprod.com>
 */
class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface
{
    private $decoratedFactory;

    /**
     * @var ChoiceListInterface[]
     */
    private $lists = [];

    /**
     * @var ChoiceListView[]
     */
    private $views = [];

    /**
     * Generates a SHA-256 hash for the given value.
     *
     * Optionally, a namespace string can be passed. Calling this method will
     * the same values, but different namespaces, will return different hashes.
     *
     * @param mixed $value The value to hash
     *
     * @return string The SHA-256 hash
     *
     * @internal
     */
    public static function generateHash($value, string $namespace = ''): string
    {
        if (\is_object($value)) {
            $value = spl_object_hash($value);
        } elseif (\is_array($value)) {
            array_walk_recursive($value, function (&$v) {
                if (\is_object($v)) {
                    $v = spl_object_hash($v);
                }
            });
        }

        return hash('sha256', $namespace.':'.serialize($value));
    }

    public function __construct(ChoiceListFactoryInterface $decoratedFactory)
    {
        $this->decoratedFactory = $decoratedFactory;
    }

    /**
     * Returns the decorated factory.
     *
     * @return ChoiceListFactoryInterface The decorated factory
     */
    public function getDecoratedFactory()
    {
        return $this->decoratedFactory;
    }

    /**
     * {@inheritdoc}
     *
     * @param callable|Cache\ChoiceValue|null  $value  The callable or static option for
     *                                                 generating the choice values
     * @param callable|Cache\ChoiceFilter|null $filter The callable or static option for
     *                                                 filtering the choices
     */
    public function createListFromChoices(iterable $choices, $value = null/*, $filter = null*/)
    {
        $filter = \func_num_args() > 2 ? func_get_arg(2) : null;

        if ($choices instanceof \Traversable) {
            $choices = iterator_to_array($choices);
        }

        $cache = true;
        // Only cache per value and filter when needed. The value is not validated on purpose.
        // The decorated factory may decide which values to accept and which not.
        if ($value instanceof Cache\ChoiceValue) {
            $value = $value->getOption();
        } elseif ($value) {
            $cache = false;
        }
        if ($filter instanceof Cache\ChoiceFilter) {
            $filter = $filter->getOption();
        } elseif ($filter) {
            $cache = false;
        }

        if (!$cache) {
            return $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
        }

        $hash = self::generateHash([$choices, $value, $filter], 'fromChoices');

        if (!isset($this->lists[$hash])) {
            $this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
        }

        return $this->lists[$hash];
    }

    /**
     * {@inheritdoc}
     *
     * @param ChoiceLoaderInterface|Cache\ChoiceLoader $loader The loader or static loader to load
     *                                                         the choices lazily
     * @param callable|Cache\ChoiceValue|null          $value  The callable or static option for
     *                                                         generating the choice values
     * @param callable|Cache\ChoiceFilter|null         $filter The callable or static option for
     *                                                         filtering the choices
     */
    public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null/*, $filter = null*/)
    {
        $filter = \func_num_args() > 2 ? func_get_arg(2) : null;

        $cache = true;

        if ($loader instanceof Cache\ChoiceLoader) {
            $loader = $loader->getOption();
        } else {
            $cache = false;
        }

        if ($value instanceof Cache\ChoiceValue) {
            $value = $value->getOption();
        } elseif ($value) {
            $cache = false;
        }

        if ($filter instanceof Cache\ChoiceFilter) {
            $filter = $filter->getOption();
        } elseif ($filter) {
            $cache = false;
        }

        if (!$cache) {
            return $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
        }

        $hash = self::generateHash([$loader, $value, $filter], 'fromLoader');

        if (!isset($this->lists[$hash])) {
            $this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
        }

        return $this->lists[$hash];
    }

    /**
     * {@inheritdoc}
     *
     * @param array|callable|Cache\PreferredChoice|null $preferredChoices The preferred choices
     * @param callable|false|Cache\ChoiceLabel|null     $label            The option or static option generating the choice labels
     * @param callable|Cache\ChoiceFieldName|null       $index            The option or static option generating the view indices
     * @param callable|Cache\GroupBy|null               $groupBy          The option or static option generating the group names
     * @param array|callable|Cache\ChoiceAttr|null      $attr             The option or static option generating the HTML attributes
     */
    public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
    {
        $cache = true;

        if ($preferredChoices instanceof Cache\PreferredChoice) {
            $preferredChoices = $preferredChoices->getOption();
        } elseif ($preferredChoices) {
            $cache = false;
        }

        if ($label instanceof Cache\ChoiceLabel) {
            $label = $label->getOption();
        } elseif (null !== $label) {
            $cache = false;
        }

        if ($index instanceof Cache\ChoiceFieldName) {
            $index = $index->getOption();
        } elseif ($index) {
            $cache = false;
        }

        if ($groupBy instanceof Cache\GroupBy) {
            $groupBy = $groupBy->getOption();
        } elseif ($groupBy) {
            $cache = false;
        }

        if ($attr instanceof Cache\ChoiceAttr) {
            $attr = $attr->getOption();
        } elseif ($attr) {
            $cache = false;
        }

        if (!$cache) {
            return $this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr);
        }

        $hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr]);

        if (!isset($this->views[$hash])) {
            $this->views[$hash] = $this->decoratedFactory->createView(
                $list,
                $preferredChoices,
                $label,
                $index,
                $groupBy,
                $attr
            );
        }

        return $this->views[$hash];
    }

    public function reset()
    {
        $this->lists = [];
        $this->views = [];
        Cache\AbstractStaticOption::reset();
    }
}
