<?php
namespace Payum\Core;

use Http\Adapter\Guzzle7\Client as HttpGuzzle7Client;
use Http\Adapter\Guzzle6\Client as HttpGuzzle6Client;
use Http\Adapter\Guzzle5\Client as HttpGuzzle5Client;
use Http\Adapter\Buzz\Client as HttpBuzzClient;
use Http\Client\Curl\Client as HttpCurlClient;
use Http\Client\Socket\Client as HttpSocketClient;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\StreamFactoryDiscovery;
use Http\Message\MessageFactory\DiactorosMessageFactory;
use Http\Message\MessageFactory\GuzzleMessageFactory;
use Http\Message\StreamFactory\DiactorosStreamFactory;
use Http\Message\StreamFactory\GuzzleStreamFactory;
use Nyholm\Psr7\Factory\HttplugFactory;
use Payum\Core\Action\AuthorizePaymentAction;
use Payum\Core\Action\CapturePaymentAction;
use Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction;
use Payum\Core\Action\GetCurrencyAction;
use Payum\Core\Action\GetTokenAction;
use Payum\Core\Action\PayoutPayoutAction;
use Payum\Core\Bridge\Httplug\HttplugClient;
use Payum\Core\Bridge\PlainPhp\Action\GetHttpRequestAction;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Bridge\Twig\Action\RenderTemplateAction;
use Payum\Core\Bridge\Twig\TwigUtil;
use Payum\Core\Extension\EndlessCycleDetectorExtension;
use Symfony\Component\HttpClient\HttplugClient as SymfonyHttplugClient;
use Twig\Environment;
use Twig\Loader\ChainLoader;

class CoreGatewayFactory implements GatewayFactoryInterface
{
    /**
     * @var array
     */
    protected $defaultConfig;

    /**
     * @param array $defaultConfig
     */
    public function __construct(array $defaultConfig = [])
    {
        $this->defaultConfig = $defaultConfig;
    }

    /**
     * {@inheritDoc}
     */
    public function create(array $config = [])
    {
        $config = ArrayObject::ensureArrayObject($config);
        $config->defaults($this->createConfig());

        $gateway = new Gateway();

        $this->buildClosures($config);

        $this->buildActions($gateway, $config);
        $this->buildApis($gateway, $config);
        $this->buildExtensions($gateway, $config);

        return $gateway;
    }

    /**
     * {@inheritDoc}
     */
    public function createConfig(array $config = [])
    {
        $config = ArrayObject::ensureArrayObject($config);
        $config->defaults($this->defaultConfig);

        $config->defaults([
            'httplug.message_factory'=>function (ArrayObject $config) {
                if (class_exists(MessageFactoryDiscovery::class)) {
                    return MessageFactoryDiscovery::find();
                }

                if (class_exists(\GuzzleHttp\Psr7\Request::class)) {
                    return new GuzzleMessageFactory();
                }

                if (class_exists(\Laminas\Diactoros\Request::class) || class_exists(\Zend\Diactoros\Request::class)) {
                    return new DiactorosMessageFactory();
                }

                if (class_exists(\Nyholm\Psr7\Request::class)) {
                    return new HttplugFactory();
                }

                throw new \LogicException('The httplug.message_factory could not be guessed. Install one of the following packages: php-http/guzzle6-adapter, zendframework/zend-diactoros. You can also overwrite the config option with your implementation.');
            },
            'httplug.stream_factory'=>function (ArrayObject $config) {
                if (class_exists(StreamFactoryDiscovery::class)) {
                    return StreamFactoryDiscovery::find();
                }

                if (class_exists(\GuzzleHttp\Psr7\Request::class)) {
                    return new GuzzleStreamFactory();
                }

                if (class_exists(\Zend\Diactoros\Request::class)) {
                    return new DiactorosStreamFactory();
                }

                if (class_exists(\Nyholm\Psr7\Request::class)) {
                    return new HttplugFactory();
                }

                throw new \LogicException('The httplug.stream_factory could not be guessed. Install one of the following packages: php-http/guzzle6-adapter, zendframework/zend-diactoros. You can also overwrite the config option with your implementation.');
            },
            'httplug.client'=>function (ArrayObject $config) {
                if (class_exists(HttpClientDiscovery::class)) {
                    return HttpClientDiscovery::find();
                }

                if (class_exists(HttpGuzzle7Client::class)) {
                    return new HttpGuzzle7Client();
                }

                if (class_exists(HttpGuzzle6Client::class)) {
                    return new HttpGuzzle6Client();
                }

                if (class_exists(HttpGuzzle5Client::class)) {
                    return new HttpGuzzle5Client();
                }

                if (class_exists(SymfonyHttplugClient::class)) {
                    return new SymfonyHttplugClient();
                }

                if (class_exists(HttpSocketClient::class)) {
                    return new HttpSocketClient();
                }

                if (class_exists(HttpCurlClient::class)) {
                    return new HttpCurlClient($config['httplug.message_factory'], $config['httplug.stream_factory']);
                }

                if (class_exists(HttpBuzzClient::class)) {
                    return new HttpBuzzClient();
                }

                throw new \LogicException('The httplug.client could not be guessed. Install one of the following packages: php-http/guzzle7-adapter, php-http/guzzle6-adapter. You can also overwrite the config option with your implementation.');
            },
            'payum.http_client'=>function (ArrayObject $config) {
                return new HttplugClient($config['httplug.client']);
            },
            'payum.template.layout' => '@PayumCore/layout.html.twig',

            'twig.env' => function () {
                return new Environment(new ChainLoader());
            },
            'twig.register_paths' => function (ArrayObject $config) {
                $twig = $config['twig.env'];
                if (false == $twig instanceof Environment) {
                    throw new \LogicException(sprintf(
                        'The `twig.env config option must contains instance of Twig\Environment but got %s`',
                        is_object($twig) ? get_class($twig) : gettype($twig)
                    ));
                }

                TwigUtil::registerPaths($twig, $config['payum.paths']);

                return null;
            },
            'payum.action.get_http_request' => new GetHttpRequestAction(),
            'payum.action.capture_payment' => new CapturePaymentAction(),
            'payum.action.authorize_payment' => new AuthorizePaymentAction(),
            'payum.action.payout_payout' => new PayoutPayoutAction(),
            'payum.action.execute_same_request_with_model_details' => new ExecuteSameRequestWithModelDetailsAction(),
            'payum.action.render_template' => function (ArrayObject $config) {
                return new RenderTemplateAction($config['twig.env'], $config['payum.template.layout']);
            },
            'payum.extension.endless_cycle_detector' => new EndlessCycleDetectorExtension(),
            'payum.action.get_currency' => function (ArrayObject $config) {
                return new GetCurrencyAction($config['payum.iso4217']);
            },
            'payum.prepend_actions' => [],
            'payum.prepend_extensions' => [],
            'payum.prepend_apis' => [],
            'payum.default_options' => [],
            'payum.required_options' => [],

            'payum.api.http_client' => function (ArrayObject $config) {
                return $config['payum.http_client'];
            },

            'payum.security.token_storage' => null,
        ]);

        if ($config['payum.security.token_storage']) {
            $config['payum.action.get_token'] = function (ArrayObject $config) {
                return new GetTokenAction($config['payum.security.token_storage']);
            };
        }

        $config['payum.paths'] = array_replace([
            'PayumCore' => __DIR__.'/Resources/views',
        ], $config['payum.paths'] ?: []);

        return (array) $config;
    }

    /**
     * @param ArrayObject $config
     */
    protected function buildClosures(ArrayObject $config)
    {
        // with higher priority
        foreach (['httplug.message_factory', 'httplug.stream_factory', 'httplug.client', 'payum.http_client', 'payum.paths', 'twig.env', 'twig.register_paths'] as $name) {
            $value = $config[$name];
            if (is_callable($value)) {
                $config[$name] = call_user_func($value, $config);
            }
        }

        foreach ($config as $name => $value) {
            if (is_callable($value) && !(is_string($value) && function_exists('\\'.$value))) {
                $config[$name] = call_user_func($value, $config);
            }
        }
    }

    /**
     * @param Gateway     $gateway
     * @param ArrayObject $config
     */
    protected function buildActions(Gateway $gateway, ArrayObject $config)
    {
        foreach ($config as $name => $value) {
            if (0 === strpos($name, 'payum.action')) {
                $prepend = in_array($name, $config['payum.prepend_actions']);

                $gateway->addAction($value, $prepend);
            }
        }
    }

    /**
     * @param Gateway     $gateway
     * @param ArrayObject $config
     */
    protected function buildApis(Gateway $gateway, ArrayObject $config)
    {
        foreach ($config as $name => $value) {
            if (0 === strpos($name, 'payum.api')) {
                $prepend = in_array($name, $config['payum.prepend_apis']);

                $gateway->addApi($value, $prepend);
            }
        }
    }

    /**
     * @param Gateway     $gateway
     * @param ArrayObject $config
     */
    protected function buildExtensions(Gateway $gateway, ArrayObject $config)
    {
        foreach ($config as $name => $value) {
            if (0 === strpos($name, 'payum.extension')) {
                $prepend = in_array($name, $config['payum.prepend_extensions']);

                $gateway->addExtension($value, $prepend);
            }
        }
    }
}
