<?php

/**
 * Pure-PHP CMS / CompressedData Parser
 *
 * PHP version 8.1+
 *
 * Encode and decode CMS / CompressedData files.
 *
 * @author    Jim Wigginton <terrafrost@php.net>
 * @copyright 2026 Jim Wigginton
 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
 * @link      https://phpseclib.com/
 */

declare(strict_types=1);

namespace phpseclib4\File\CMS;

use phpseclib4\Common\Functions\Strings;
use phpseclib4\Exception\{BadConfigurationException, UnsupportedValueException};
use phpseclib4\File\ASN1\{Constructed, Element, Maps};
use phpseclib4\File\{ASN1, CMS};

/**
 * Pure-PHP CMS / CompressedData Parser
 *
 * @author  Jim Wigginton <terrafrost@php.net>
 * @implements \ArrayAccess<string, mixed>
 * @implements \Iterator<string, mixed>
 */
class CompressedData implements \ArrayAccess, \Countable, \Iterator
{
    private Constructed|array $cms;
    private string $decompressed;

    /**
     * @param string $data
     */
    public function __construct(string $data)
    {
        if (!function_exists('zlib_encode')) {
            throw new BadConfigurationException('zlib_encode() is not available');
        }
        $this->decompressed = $data;
        $this->cms = [
            'contentType' => 'id-ct-compressedData',
            'content' => [
                'version' => 'v0',
                'compressionAlgorithm' => ['algorithm' => 'id-alg-zlibCompress'],
                'encapContentInfo' => [
                    'eContentType' => 'id-data',
                    'eContent' => zlib_encode($data, ZLIB_ENCODING_DEFLATE),
                ],
            ]
        ];
    }

    // CMS::load() takes care of the PEM / DER encoding toggling
    // if you want to load an array or Constructed as a SignedData instance you'll
    // need to call CMS\SignedData::load()
    public static function load(string|array|Constructed $encoded): self
    {
        $r = new \ReflectionClass(__CLASS__);
        $cms = $r->newInstanceWithoutConstructor();
        $cms->cms = is_string($encoded) ? self::loadString($encoded) : $encoded;
        return $cms;
    }

    private static function loadString(string $encoded): Constructed
    {
        $decoded = ASN1::decodeBER($encoded);
        $cms = ASN1::map($decoded, Maps\ContentInfo::MAP);
        $decoded = ASN1::decodeBER($cms['content']->value);
        $cms['content'] = ASN1::map($decoded, Maps\CompressedData::MAP);
        $cms['content']->parent = $cms;
        $cms['content']->key = 'content';
        return $cms;
    }

    public function &offsetGet(mixed $offset): mixed
    {
        $this->compile();
        /** @psalm-suppress NonVariableReferenceReturn */
        return $this->cms[$offset];
    }

    public function offsetExists(mixed $offset): bool
    {
        return isset($this->cms[$offset]);
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        $this->cms[$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->cms[$offset]);
    }

    public function count(): int
    {
        return is_array($this->cms) ? count($this->cms) : $this->cms->count();
    }

    public function rewind(): void
    {
        $this->compile();
        $this->cms->rewind();
    }

    public function current(): mixed
    {
        $this->compile();
        return $this->cms->current();
    }

    public function key(): mixed
    {
        $this->compile();
        return $this->cms->key();
    }

    public function next(): void
    {
        $this->compile();
        $this->cms->next();
    }

    public function valid(): bool
    {
        $this->compile();
        return $this->cms->valid();
    }

    public function toString(array $options = []): string
    {
        if ($this->cms instanceof Constructed) {
            ASN1::encodeDER($this->cms['content'], Maps\CompressedData::MAP);
            $cms = ASN1::encodeDER($this->cms, Maps\ContentInfo::MAP);
        } else {
            $temp = [
                'contentType' => $this->cms['contentType'], // 99% of the time this'll be 'id-compressedData'
                'content' => new Element(ASN1::encodeDER($this->cms['content'], Maps\CompressedData::MAP)),
            ];
            $cms = ASN1::encodeDER($temp, Maps\ContentInfo::MAP);
            $this->cms = self::load($cms)->cms;
        }

        if ($options['binary'] ?? CMS::$binary) {
            return $cms;
        }

        return "-----BEGIN CMS-----\r\n" . chunk_split(Strings::base64_encode($cms), 64) . '-----END CMS-----';
    }

    public function __toString(): string
    {
        return $this->toString();
    }

    public function compile(): void
    {
        if (!$this->cms instanceof Constructed || !$this->cms->hasEncoded()) {
            $temp = self::load($this->toString(['binary' => true]));
            $this->cms = $temp->cms;
        }
    }

    public function __debugInfo(): array
    {
        $this->compile();
        return $this->cms->__debugInfo();
    }

    public function getContent(): string
    {
        if (!isset($this->decompressed)) {
            if (!function_exists('zlib_decode')) {
                throw new BadConfigurationException('zlib_decode() is not available');
            }
            $result = zlib_decode((string) $this->cms['content']['encapContentInfo']['eContent']);
            if ($result === false) {
                throw new UnsupportedValueException('zlib_decode() returned false');
            }
            $this->decompressed = $result;
        }
        return $this->decompressed;
    }

    public function toArray(bool $convertPrimitives = false): array
    {
        $this->compile();
        return $this->cms instanceof Constructed ? $this->cms->toArray($convertPrimitives) : $this->cms;
    }
}
