<?php

/*
 * This file is part of the FOSHttpCache package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\HttpCache\ProxyClient;

use Http\Discovery\MessageFactoryDiscovery;
use Http\Message\RequestFactory;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Base class for HTTP based caching proxy client.
 *
 * @author David de Boer <david@driebit.nl>
 */
abstract class HttpProxyClient implements ProxyClient
{
    /**
     * Dispatcher for invalidation HTTP requests.
     *
     * @var HttpDispatcher
     */
    private $httpDispatcher;

    /**
     * @var RequestFactory
     */
    private $requestFactory;

    /**
     * The options configured in the constructor argument or default values.
     *
     * @var array The resolved options
     */
    protected $options;

    /**
     * Constructor.
     *
     * The base class has no options.
     *
     * @param Dispatcher          $httpDispatcher Helper to send HTTP requests to caching proxy
     * @param array               $options        Options for this client
     * @param RequestFactory|null $messageFactory Factory for PSR-7 messages. If none supplied,
     *                                            a default one is created
     */
    public function __construct(
        Dispatcher $httpDispatcher,
        array $options = [],
        ?RequestFactory $messageFactory = null
    ) {
        $this->httpDispatcher = $httpDispatcher;
        $this->options = $this->configureOptions()->resolve($options);
        $this->requestFactory = $messageFactory ?: MessageFactoryDiscovery::find();
    }

    public function flush()
    {
        return $this->httpDispatcher->flush();
    }

    /**
     * Get options resolver with default settings.
     *
     * @return OptionsResolver
     */
    protected function configureOptions()
    {
        return new OptionsResolver();
    }

    /**
     * Create a request and queue it with the HTTP dispatcher.
     *
     * @param string                               $method
     * @param string|UriInterface                  $url
     * @param bool                                 $validateHost see Dispatcher::invalidate
     * @param resource|string|StreamInterface|null $body
     */
    protected function queueRequest($method, $url, array $headers, $validateHost = true, $body = null)
    {
        $this->httpDispatcher->invalidate(
            $this->requestFactory->createRequest($method, $url, $headers, $body),
            $validateHost
        );
    }

    /**
     * Make sure that the tags are valid.
     *
     * Reusable function for proxy clients.
     * Escapes `,` and `\n` (newline) characters.
     *
     * Note: This is not a safe escaping function, it can lead to collisions,
     * e.g. between "foo,bar" and "foo_bar". But from the nature of the data,
     * such collisions are unlikely, and from the function of cache tagging,
     * collisions would in the worst case lead to unintended invalidations,
     * which is not a bug.
     *
     * @param array $tags The tags to escape
     *
     * @return array Sane tags
     */
    protected function escapeTags(array $tags)
    {
        array_walk($tags, function (&$tag) {
            // WARNING: changing the list of characters that are escaped is a BC break for existing installations,
            // as existing tags on the cache would not be invalidated anymore if they contain a character that is
            // newly escaped
            $tag = str_replace([',', "\n"], '_', $tag);
        });

        return $tags;
    }

    /**
     * Calculate how many tags fit into the header.
     *
     * This assumes that the tags are separated by one character.
     *
     * @param string[] $escapedTags
     * @param string   $glue        The concatenation string to use
     *
     * @return int Number of tags per tag invalidation request
     */
    protected function determineTagsPerHeader($escapedTags, $glue)
    {
        if (mb_strlen(implode($glue, $escapedTags)) < $this->options['header_length']) {
            return count($escapedTags);
        }

        /*
         * estimate the amount of tags to invalidate by dividing the max
         * header length by the largest tag (minus the glue length)
         */
        $tagsize = max(array_map('mb_strlen', $escapedTags));

        return floor($this->options['header_length'] / ($tagsize + strlen($glue))) ?: 1;
    }
}
