<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.5.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\View\Form;

use Cake\Collection\Collection;
use Cake\Datasource\EntityInterface;
use Cake\Form\Form;
use Cake\Http\ServerRequest;
use RuntimeException;
use Traversable;

/**
 * Factory for getting form context instance based on provided data.
 */
class ContextFactory
{
    /**
     * Context providers.
     *
     * @var array
     */
    protected $providers = [];

    /**
     * Constructor.
     *
     * @param array $providers Array of provider callables. Each element should
     *   be of form `['type' => 'a-string', 'callable' => ..]`
     */
    public function __construct(array $providers = [])
    {
        foreach ($providers as $provider) {
            $this->addProvider($provider['type'], $provider['callable']);
        }
    }

    /**
     * Create factory instance with providers "array", "form" and "orm".
     *
     * @param array $providers Array of provider callables. Each element should
     *   be of form `['type' => 'a-string', 'callable' => ..]`
     * @return \Cake\View\Form\ContextFactory
     */
    public static function createWithDefaults(array $providers = [])
    {
        $providers = [
            [
                'type' => 'orm',
                'callable' => function ($request, $data) {
                    if (is_array($data['entity']) || $data['entity'] instanceof Traversable) {
                        $pass = (new Collection($data['entity']))->first() !== null;
                        if ($pass) {
                            return new EntityContext($request, $data);
                        }
                    }
                    if ($data['entity'] instanceof EntityInterface) {
                        return new EntityContext($request, $data);
                    }
                    if (is_array($data['entity']) && empty($data['entity']['schema'])) {
                        return new EntityContext($request, $data);
                    }
                },
            ],
            [
                'type' => 'array',
                'callable' => function ($request, $data) {
                    if (is_array($data['entity']) && isset($data['entity']['schema'])) {
                        return new ArrayContext($request, $data['entity']);
                    }
                },
            ],
            [
                'type' => 'form',
                'callable' => function ($request, $data) {
                    if ($data['entity'] instanceof Form) {
                        return new FormContext($request, $data);
                    }
                },
            ],
        ] + $providers;

        return new static($providers);
    }

    /**
     * Add a new context type.
     *
     * Form context types allow FormHelper to interact with
     * data providers that come from outside CakePHP. For example
     * if you wanted to use an alternative ORM like Doctrine you could
     * create and connect a new context class to allow FormHelper to
     * read metadata from doctrine.
     *
     * @param string $type The type of context. This key
     *   can be used to overwrite existing providers.
     * @param callable $check A callable that returns an object
     *   when the form context is the correct type.
     * @return $this
     */
    public function addProvider($type, callable $check)
    {
        $this->providers = [$type => ['type' => $type, 'callable' => $check]]
            + $this->providers;

        return $this;
    }

    /**
     * Find the matching context for the data.
     *
     * If no type can be matched a NullContext will be returned.
     *
     * @param \Cake\Http\ServerRequest $request Request instance.
     * @param array $data The data to get a context provider for.
     * @return \Cake\View\Form\ContextInterface Context provider.
     * @throws \RuntimeException when the context class does not implement the
     *   ContextInterface.
     */
    public function get(ServerRequest $request, array $data = [])
    {
        $data += ['entity' => null];

        foreach ($this->providers as $provider) {
            $check = $provider['callable'];
            $context = $check($request, $data);
            if ($context) {
                break;
            }
        }
        if (!isset($context)) {
            $context = new NullContext($request, $data);
        }
        if (!($context instanceof ContextInterface)) {
            throw new RuntimeException(sprintf(
                'Context providers must return object implementing %s. Got "%s" instead.',
                ContextInterface::class,
                getTypeName($context)
            ));
        }

        return $context;
    }
}
