<?php

/**
 * League.Uri (https://uri.thephpleague.com)
 *
 * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
declare (strict_types=1);
namespace GDAVSCOPE\League\Uri\Components;

use BackedEnum;
use Deprecated;
use finfo;
use GDAVSCOPE\League\Uri\Contracts\DataPathInterface;
use GDAVSCOPE\League\Uri\Contracts\PathInterface;
use GDAVSCOPE\League\Uri\Contracts\UriInterface;
use GDAVSCOPE\League\Uri\Exceptions\SyntaxError;
use GDAVSCOPE\League\Uri\FeatureDetection;
use GDAVSCOPE\Psr\Http\Message\UriInterface as Psr7UriInterface;
use SplFileObject;
use Stringable;
use Throwable;
use GDAVSCOPE\Uri\Rfc3986\Uri as Rfc3986Uri;
use GDAVSCOPE\Uri\WhatWg\Url as WhatWgUrl;
use function base64_decode;
use function base64_encode;
use function count;
use function explode;
use function file_get_contents;
use function implode;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function sprintf;
use function str_replace;
use function strlen;
use function strtolower;
use const FILEINFO_MIME;
final class DataPath extends Component implements DataPathInterface
{
    /**
     * All ASCII letters sorted by typical frequency of occurrence.
     */
    private const ASCII = " eiasntrolud][cmp'\ng|hv.fb,:=-q10C2*yx)(L9AS/P\"EjMIk3>5T<D4}B{8FwR67UGN;JzV#HOW_&!K?XQ%Y\\\tZ+~^\$@`\x00\x01\x02\x03\x04\x05\x06\x07\x08\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f";
    private const BINARY_PARAMETER = 'base64';
    private const DEFAULT_MIMETYPE = 'text/plain';
    private const DEFAULT_PARAMETER = 'charset=us-ascii';
    private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
    private const REGEXP_DATAPATH = '/^\w+\/[-.\w]+(?:\+[-.\w]+)?;,$/';
    private const REGEXP_DATAPATH_ENCODING = '/[^A-Za-z0-9_\-.~!$&\'()*+,;=%:\/@]+|%(?![A-Fa-f0-9]{2})/x';
    private readonly PathInterface $path;
    private readonly string $mimetype;
    /** @var string[] */
    private readonly array $parameters;
    private readonly bool $isBinaryData;
    private readonly string $document;
    /**
     * New instance.
     */
    private function __construct(BackedEnum|Stringable|string $path)
    {
        /** @var string $path */
        $path = self::filterComponent($path);
        $this->path = Path::new($this->filterPath($path));
        [$mediaType, $this->document] = explode(',', $this->path->toString(), 2) + [1 => ''];
        [$mimetype, $parameters] = explode(';', $mediaType, 2) + [1 => ''];
        $this->mimetype = $this->filterMimeType($mimetype);
        [$this->parameters, $this->isBinaryData] = $this->filterParameters($parameters);
        $this->validateDocument();
    }
    /**
     * Filter the data path.
     *
     * @throws SyntaxError If the path is null
     * @throws SyntaxError If the path is not valid according to RFC2937
     */
    private function filterPath(string $path): string
    {
        if ('' === $path || ',' === $path) {
            return 'text/plain;charset=us-ascii,';
        }
        if (1 === preg_match(self::REGEXP_DATAPATH, $path)) {
            $path = substr($path, 0, -1) . 'charset=us-ascii,';
        }
        if (strlen($path) !== strspn($path, self::ASCII) || !str_contains($path, ',')) {
            throw new SyntaxError(sprintf('The path `%s` is invalid according to RFC2937.', $path));
        }
        return $path;
    }
    /**
     * Filter the mimeType property.
     *
     * @throws SyntaxError If the mimetype is invalid
     */
    private function filterMimeType(string $mimetype): string
    {
        return match (\true) {
            '' === $mimetype => self::DEFAULT_MIMETYPE,
            1 === preg_match(self::REGEXP_MIMETYPE, $mimetype) => $mimetype,
            default => throw new SyntaxError(sprintf('Invalid mimeType, `%s`.', $mimetype)),
        };
    }
    /**
     * Extract and set the binary flag from the parameters if it exists.
     *
     * @throws SyntaxError If the mediatype parameters contain invalid data
     *
     * @return array{0:array<string>, 1:bool}
     */
    private function filterParameters(string $parameters): array
    {
        if ('' === $parameters) {
            return [[self::DEFAULT_PARAMETER], \false];
        }
        $isBinaryData = \false;
        if (1 === preg_match(',(;|^)' . self::BINARY_PARAMETER . '$,', $parameters, $matches)) {
            $parameters = substr($parameters, 0, -strlen($matches[0]));
            $isBinaryData = \true;
        }
        $params = array_filter(explode(';', $parameters), fn(string $param) => '' !== $param);
        if ([] !== array_filter($params, $this->validateParameter(...))) {
            throw new SyntaxError(sprintf('Invalid mediatype parameters, `%s`.', $parameters));
        }
        return [$params, $isBinaryData];
    }
    /**
     * Validate mediatype parameter.
     */
    private function validateParameter(string $parameter): bool
    {
        $properties = explode('=', $parameter);
        return 2 !== count($properties) || self::BINARY_PARAMETER === strtolower($properties[0]);
    }
    /**
     * Validate the path document string representation.
     *
     * @throws SyntaxError If the data is invalid
     */
    private function validateDocument(): void
    {
        if (!$this->isBinaryData) {
            return;
        }
        $res = base64_decode($this->document, \true);
        if (\false === $res || $this->document !== base64_encode($res)) {
            throw new SyntaxError(sprintf('Invalid document, `%s`.', $this->document));
        }
    }
    /**
     * Returns a new instance from a string or a stringable object.
     */
    public static function new(BackedEnum|Stringable|string $value = ''): self
    {
        return new self($value);
    }
    /**
     * Create a new instance from a string.or a stringable structure or returns null on failure.
     */
    public static function tryNew(BackedEnum|Stringable|string $uri = ''): ?self
    {
        try {
            return self::new($uri);
        } catch (Throwable) {
            return null;
        }
    }
    /**
     * Creates a new instance from a file path.
     *
     * @param null|resource $context
     *
     * @throws SyntaxError If the File is not readable
     */
    public static function fromFileContents(string $path, $context = null): self
    {
        FeatureDetection::supportsFileDetection();
        $fileArgs = [$path, \false];
        $mimeArgs = [$path, FILEINFO_MIME];
        if (null !== $context) {
            $fileArgs[] = $context;
            $mimeArgs[] = $context;
        }
        $content = @file_get_contents(...$fileArgs);
        if (\false === $content) {
            throw new SyntaxError(sprintf('`%s` failed to open stream: No such file or directory.', $path));
        }
        $mimetype = (string) (new finfo(FILEINFO_MIME))->file(...$mimeArgs);
        return new self(str_replace(' ', '', $mimetype) . ';base64,' . base64_encode($content));
    }
    /**
     * Create a new instance from a URI object.
     */
    public static function fromUri(WhatWgUrl|Rfc3986Uri|BackedEnum|Stringable|string $uri): self
    {
        return self::new(Path::fromUri($uri)->toString());
    }
    public function value(): ?string
    {
        return $this->path->value();
    }
    public function equals(mixed $value): bool
    {
        return $this->path->equals($value);
    }
    public function getData(): string
    {
        return $this->document;
    }
    public function isBinaryData(): bool
    {
        return $this->isBinaryData;
    }
    public function getMimeType(): string
    {
        return $this->mimetype;
    }
    public function getParameters(): string
    {
        return implode(';', $this->parameters);
    }
    public function getMediaType(): string
    {
        return $this->getMimeType() . ';' . $this->getParameters();
    }
    public function isAbsolute(): bool
    {
        return $this->path->isAbsolute();
    }
    public function hasTrailingSlash(): bool
    {
        return $this->path->hasTrailingSlash();
    }
    public function decoded(): string
    {
        return $this->path->decoded();
    }
    public function normalize(): self
    {
        return new self((string) $this->path->normalize()->value());
    }
    /**
     * @param ?resource $context
     */
    public function save(BackedEnum|Stringable|string $path, string $mode = 'w', $context = null): SplFileObject
    {
        if ($path instanceof BackedEnum) {
            $path = $path->value;
        }
        $data = $this->isBinaryData ? base64_decode($this->document, \true) : rawurldecode($this->document);
        $file = new SplFileObject((string) $path, $mode, context: $context);
        $file->fwrite((string) $data);
        return $file;
    }
    public function toBinary(): DataPathInterface
    {
        if ($this->isBinaryData) {
            return $this;
        }
        return new self($this->formatComponent($this->mimetype, $this->getParameters(), \true, base64_encode(rawurldecode($this->document))));
    }
    /**
     * Format the DataURI string.
     */
    private function formatComponent(string $mimetype, string $parameters, bool $isBinaryData, string $data): string
    {
        if ('' !== $parameters) {
            $parameters = ';' . $parameters;
        }
        if ($isBinaryData) {
            $parameters .= ';base64';
        }
        $path = $mimetype . $parameters . ',' . $data;
        return preg_replace_callback(self::REGEXP_DATAPATH_ENCODING, static fn(array $matches): string => rawurlencode($matches[0]), $path) ?? $path;
    }
    public function toAscii(): DataPathInterface
    {
        return match (\false) {
            $this->isBinaryData => $this,
            default => new self($this->formatComponent($this->mimetype, $this->getParameters(), \false, rawurlencode((string) base64_decode($this->document, \true)))),
        };
    }
    public function withoutDotSegments(): PathInterface
    {
        return $this;
    }
    public function withLeadingSlash(): PathInterface
    {
        return new self($this->path->withLeadingSlash());
    }
    public function withoutLeadingSlash(): PathInterface
    {
        return $this;
    }
    public function withoutTrailingSlash(): PathInterface
    {
        $path = $this->path->withoutTrailingSlash();
        return match ($this->path) {
            $path => $this,
            default => new self($path),
        };
    }
    public function withTrailingSlash(): PathInterface
    {
        $path = $this->path->withTrailingSlash();
        return match ($this->path) {
            $path => $this,
            default => new self($path),
        };
    }
    public function withParameters(BackedEnum|Stringable|string $parameters): DataPathInterface
    {
        if ($parameters instanceof BackedEnum) {
            $parameters = $parameters->value;
        }
        $parameters = (string) $parameters;
        return match ($this->getParameters()) {
            $parameters => $this,
            default => new self($this->formatComponent($this->mimetype, $parameters, $this->isBinaryData, $this->document)),
        };
    }
    /**
     * DEPRECATION WARNING! This method will be removed in the next major point release.
     *
     * @deprecated Since version 7.0.0
     * @see DataPath::new()
     *
     * @codeCoverageIgnore
     *
     * Returns a new instance from a string or a stringable object.
     */
    #[Deprecated(message: 'use League\Uri\Components\DataPath::new() instead', since: 'league/uri-components:7.0.0')]
    public static function createFromString(Stringable|string $path): self
    {
        return self::new($path);
    }
    /**
     * DEPRECATION WARNING! This method will be removed in the next major point release.
     *
     * @deprecated Since version 7.0.0
     * @see DataPath::fromFilePath()
     *
     * @codeCoverageIgnore
     *
     * Creates a new instance from a file path.
     *
     * @param null|resource $context
     *
     * @throws SyntaxError If the File is not readable
     */
    #[Deprecated(message: 'use League\Uri\Components\DataPath::fromFilePath() instead', since: 'league/uri-components:7.0.0')]
    public static function createFromFilePath(string $path, $context = null): self
    {
        return self::fromFileContents($path, $context);
    }
    /**
     * DEPRECATION WARNING! This method will be removed in the next major point release.
     *
     * @deprecated Since version 7.0.0
     * @see DataPath::fromUri()
     *
     * @codeCoverageIgnore
     *
     * Create a new instance from a URI object.
     */
    #[Deprecated(message: 'use League\Uri\Components\DataPath::fromUri() instead', since: 'league/uri-components:7.0.0')]
    public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
    {
        return self::fromUri($uri);
    }
}
