<?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\HttpClient\Response;

use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\HttpClient\Exception\RedirectionException;
use Symfony\Component\HttpClient\Exception\ServerException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

/**
 * Implements common logic for response classes.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
trait CommonResponseTrait
{
    /**
     * @var callable|null A callback that tells whether we're waiting for response headers
     */
    private $initializer;
    /** @var bool|\Closure|resource|null */
    private $shouldBuffer;
    /** @var resource|null */
    private $content;
    private int $offset = 0;
    private ?array $jsonData = null;

    public function getContent(bool $throw = true): string
    {
        if ($this->initializer) {
            self::initialize($this);
        }

        if ($throw) {
            $this->checkStatusCode();
        }

        if (null === $this->content) {
            $content = null;

            foreach (self::stream([$this]) as $chunk) {
                if (!$chunk->isLast()) {
                    $content .= $chunk->getContent();
                }
            }

            if (null !== $content) {
                return $content;
            }

            if (null === $this->content) {
                throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
            }
        } else {
            foreach (self::stream([$this]) as $chunk) {
                // Chunks are buffered in $this->content already
            }
        }

        rewind($this->content);

        return stream_get_contents($this->content);
    }

    public function toArray(bool $throw = true): array
    {
        if ('' === $content = $this->getContent($throw)) {
            throw new JsonException('Response body is empty.');
        }

        if (null !== $this->jsonData) {
            return $this->jsonData;
        }

        try {
            $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
        } catch (\JsonException $e) {
            throw new JsonException($e->getMessage().\sprintf(' for "%s".', $this->getInfo('url')), $e->getCode());
        }

        if (!\is_array($content)) {
            throw new JsonException(\sprintf('JSON content was expected to decode to an array, "%s" returned for "%s".', get_debug_type($content), $this->getInfo('url')));
        }

        if (null !== $this->content) {
            // Option "buffer" is true
            return $this->jsonData = $content;
        }

        return $content;
    }

    /**
     * @return resource
     */
    public function toStream(bool $throw = true)
    {
        if ($throw) {
            // Ensure headers arrived
            $this->getHeaders($throw);
        }

        $stream = StreamWrapper::createResource($this);
        stream_get_meta_data($stream)['wrapper_data']
            ->bindHandles($this->handle, $this->content);

        return $stream;
    }

    public function __serialize(): array
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __unserialize(array $data): void
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    /**
     * Closes the response and all its network handles.
     */
    abstract protected function close(): void;

    private static function initialize(self $response): void
    {
        if (null !== $response->getInfo('error')) {
            throw new TransportException($response->getInfo('error'));
        }

        try {
            if (($response->initializer)($response, -0.0)) {
                foreach (self::stream([$response], -0.0) as $chunk) {
                    if ($chunk->isFirst()) {
                        break;
                    }
                }
            }
        } catch (\Throwable $e) {
            // Persist timeouts thrown during initialization
            $response->info['error'] = $e->getMessage();
            $response->close();
            throw $e;
        }

        $response->initializer = null;
    }

    private function checkStatusCode(): void
    {
        $code = $this->getInfo('http_code');

        if (300 > $code) {
            return;
        }

        $getErrorBody = $this->getErrorBody(...);

        if (500 <= $code) {
            throw new ServerException($this, $getErrorBody);
        }

        if (400 <= $code) {
            throw new ClientException($this, $getErrorBody);
        }

        throw new RedirectionException($this, $getErrorBody);
    }

    /**
     * Returns the head of the body, where API error formats put the fields an error message is made of.
     *
     * The body can be arbitrarily large, and it is decompressed on the way in, so keeping it whole in
     * memory is not an option. The rest of it is still streamed, so that the response is left in the
     * state it would be in if this had not run. Read errors are ignored because this only makes the
     * message nicer: they are reported again when the body itself is read.
     */
    private function getErrorBody(): string
    {
        $body = '';

        try {
            foreach (self::stream([$this]) as $chunk) {
                if (!$chunk->isLast() && 32768 > \strlen($body)) {
                    $body .= substr($chunk->getContent(), 0, 32768 - \strlen($body));
                }

                // Free the current chunk before the next one is inflated
                $chunk = null;
            }
        } catch (TransportExceptionInterface) {
        }

        return $body;
    }
}
