<?php

namespace WP_Ultimo\Dependencies\Amp\Http\Client\Body;

use WP_Ultimo\Dependencies\Amp\ByteStream\InMemoryStream;
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
use WP_Ultimo\Dependencies\Amp\ByteStream\IteratorStream;
use WP_Ultimo\Dependencies\Amp\Http\Client\RequestBody;
use WP_Ultimo\Dependencies\Amp\Producer;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Success;
use function WP_Ultimo\Dependencies\Amp\call;
final class FormBody implements RequestBody
{
    /** @var (array{0: string, 1: string, 2: string, 3: null}|array{0: string, 1: FileBody, 2: string, 3: string})[] */
    private $fields = [];
    /** @var string */
    private $boundary;
    /** @var bool */
    private $isMultipart = \false;
    /** @var string|null */
    private $cachedBody;
    /** @var Promise<int>|null */
    private $cachedLength;
    /** @var list<string|FileBody>|null */
    private $cachedFields;
    /**
     * @param string $boundary An optional multipart boundary string
     */
    public function __construct(string $boundary = null)
    {
        /** @noinspection PhpUnhandledExceptionInspection */
        $this->boundary = $boundary ?? \bin2hex(\random_bytes(16));
    }
    /**
     * Add a data field to the form entity body.
     *
     * @param string $name
     * @param string $value
     * @param string $contentType
     */
    public function addField(string $name, string $value, string $contentType = 'text/plain') : void
    {
        $this->fields[] = [$name, $value, $contentType, null];
        $this->resetCache();
    }
    /**
     * Add each element of a associative array as a data field to the form entity body.
     *
     * @param array  $data
     * @param string $contentType
     */
    public function addFields(array $data, string $contentType = 'text/plain') : void
    {
        foreach ($data as $key => $value) {
            $this->addField($key, $value, $contentType);
        }
    }
    /**
     * Add a file field to the form entity body.
     *
     * @param string $name
     * @param string $filePath
     * @param string $contentType
     */
    public function addFile(string $name, string $filePath, string $contentType = 'application/octet-stream') : void
    {
        $fileName = \basename($filePath);
        $this->fields[] = [$name, new FileBody($filePath), $contentType, $fileName];
        $this->isMultipart = \true;
        $this->resetCache();
    }
    /**
     * Add each element of a associative array as a file field to the form entity body.
     *
     * @param array  $data
     * @param string $contentType
     */
    public function addFiles(array $data, string $contentType = 'application/octet-stream') : void
    {
        foreach ($data as $key => $value) {
            $this->addFile($key, $value, $contentType);
        }
    }
    /**
     * Add a file field to the form from a string.
     *
     * @param string $name
     * @param string $fileContent
     * @param string $fileName
     * @param string $contentType
     */
    public function addFileFromString(string $name, string $fileContent, string $fileName, string $contentType = 'application/octet-stream') : void
    {
        $this->fields[] = [$name, $fileContent, $contentType, $fileName];
        $this->isMultipart = \true;
        $this->resetCache();
    }
    /**
     * Returns an array of fields, each being an array of [name, value, content-type, file-name|null].
     * Both fields and files are returned in the array. Files use a FileBody object as the value. The file-name is
     * always null for fields.
     *
     * @return array
     */
    public function getFields() : array
    {
        return $this->fields;
    }
    private function resetCache() : void
    {
        $this->cachedBody = null;
        $this->cachedLength = null;
        $this->cachedFields = null;
    }
    public function createBodyStream() : InputStream
    {
        if ($this->isMultipart) {
            return $this->generateMultipartStreamFromFields($this->getMultipartFieldArray());
        }
        return new InMemoryStream($this->getFormEncodedBodyString());
    }
    private function getMultipartFieldArray() : array
    {
        if (isset($this->cachedFields)) {
            return $this->cachedFields;
        }
        $fields = [];
        foreach ($this->fields as $fieldArr) {
            [$name, $field, $contentType, $fileName] = $fieldArr;
            $fields[] = "--{$this->boundary}\r\n";
            /** @psalm-suppress PossiblyNullArgument */
            $fields[] = $fileName !== null ? $this->generateMultipartFileHeader($name, $fileName, $contentType) : $this->generateMultipartFieldHeader($name, $contentType);
            $fields[] = $field;
            $fields[] = "\r\n";
        }
        $fields[] = "--{$this->boundary}--\r\n";
        return $this->cachedFields = $fields;
    }
    private function generateMultipartFileHeader(string $name, string $fileName, string $contentType) : string
    {
        $header = "Content-Disposition: form-data; name=\"{$name}\"; filename=\"{$fileName}\"\r\n";
        $header .= "Content-Type: {$contentType}\r\n";
        $header .= "Content-Transfer-Encoding: binary\r\n\r\n";
        return $header;
    }
    private function generateMultipartFieldHeader(string $name, string $contentType) : string
    {
        $header = "Content-Disposition: form-data; name=\"{$name}\"\r\n";
        if ($contentType !== "") {
            $header .= "Content-Type: {$contentType}\r\n\r\n";
        } else {
            $header .= "\r\n";
        }
        return $header;
    }
    private function generateMultipartStreamFromFields(array $fields) : InputStream
    {
        foreach ($fields as $key => $field) {
            $fields[$key] = $field instanceof FileBody ? $field->createBodyStream() : new InMemoryStream($field);
        }
        return new IteratorStream(new Producer(static function (callable $emit) use($fields) {
            /** @psalm-var callable(string) $emit */
            foreach ($fields as $stream) {
                /** @var InputStream $stream */
                while (null !== ($chunk = (yield $stream->read()))) {
                    (yield $emit($chunk));
                }
            }
        }));
    }
    private function getFormEncodedBodyString() : string
    {
        if ($this->cachedBody) {
            return $this->cachedBody;
        }
        $fields = [];
        foreach ($this->fields as $fieldArr) {
            [$name, $value] = $fieldArr;
            $fields[$name][] = $value;
        }
        foreach ($fields as $key => $value) {
            $fields[$key] = isset($value[1]) ? $value : $value[0];
        }
        return $this->cachedBody = \http_build_query($fields);
    }
    public function getHeaders() : Promise
    {
        return new Success(['Content-Type' => $this->determineContentType()]);
    }
    private function determineContentType() : string
    {
        return $this->isMultipart ? "multipart/form-data; boundary={$this->boundary}" : 'application/x-www-form-urlencoded';
    }
    public function getBodyLength() : Promise
    {
        if ($this->cachedLength) {
            return $this->cachedLength;
        }
        if (!$this->isMultipart) {
            return $this->cachedLength = new Success(\strlen($this->getFormEncodedBodyString()));
        }
        /** @var Promise<int> $lengthPromise */
        $lengthPromise = call(function () : \Generator {
            $fields = $this->getMultipartFieldArray();
            $length = 0;
            foreach ($fields as $field) {
                if (\is_string($field)) {
                    $length += \strlen($field);
                } else {
                    $length += (yield $field->getBodyLength());
                }
            }
            return $length;
        });
        return $this->cachedLength = $lengthPromise;
    }
}
