<?php
/*
 * This file is part of the PHPASN1 library.
 *
 * Copyright © Friedrich Große <friedrich.grosse@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace iThemesSecurity\Strauss\FG\ASN1;

use iThemesSecurity\Strauss\FG\ASN1\Exception\ParserException;

/**
 * Class ExplicitlyTaggedObject decorate an inner object with an additional tag that gives information about
 * its context specific meaning.
 *
 * Explanation taken from A Layman's Guide to a Subset of ASN.1, BER, and DER:
 * >>> An RSA Laboratories Technical Note
 * >>> Burton S. Kaliski Jr.
 * >>> Revised November 1, 1993
 *
 * [...]
 * Explicitly tagged types are derived from other types by adding an outer tag to the underlying type.
 * In effect, explicitly tagged types are structured types consisting of one component, the underlying type.
 * Explicit tagging is denoted by the ASN.1 keywords [class number] EXPLICIT (see Section 5.2).
 * [...]
 *
 * @see http://luca.ntop.org/Teaching/Appunti/asn1.html
 */
class ExplicitlyTaggedObject extends ASNObject
{
    /** @var \iThemesSecurity\Strauss\FG\ASN1\ASNObject[] */
    private $decoratedObjects;
    private $tag;

    /**
     * @param int $tag
     * @param \iThemesSecurity\Strauss\FG\ASN1\ASNObject $objects,...
     */
    public function __construct($tag, /* HH_FIXME[4858]: variadic + strict */ ...$objects)
    {
        $this->tag = $tag;
        $this->decoratedObjects = $objects;
    }

    protected function calculateContentLength()
    {
        $length = 0;
        foreach ($this->decoratedObjects as $object) {
            $length += $object->getObjectLength();
        }

        return $length;
    }

    protected function getEncodedValue()
    {
        $encoded = '';
        foreach ($this->decoratedObjects as $object) {
            $encoded .= $object->getBinary();
        }

        return $encoded;
    }

    public function getContent()
    {
        return $this->decoratedObjects;
    }

    public function __toString()
    {
        switch ($length = count($this->decoratedObjects)) {
        case 0:
            return "Context specific empty object with tag [{$this->tag}]";
        case 1:
            $decoratedType = Identifier::getShortName($this->decoratedObjects[0]->getType());
            return "Context specific $decoratedType with tag [{$this->tag}]";
        default:
            return "$length context specific objects with tag [{$this->tag}]";
        }
    }

    public function getType()
    {
        return ord($this->getIdentifier());
    }

    public function getIdentifier()
    {
        $identifier = Identifier::create(Identifier::CLASS_CONTEXT_SPECIFIC, true, $this->tag);

        return is_int($identifier) ? chr($identifier) : $identifier;
    }

    public function getTag()
    {
        return $this->tag;
    }

    public static function fromBinary(&$binaryData, &$offsetIndex = 0)
    {
        $identifier = self::parseBinaryIdentifier($binaryData, $offsetIndex);
        $firstIdentifierOctet = ord($identifier);
        assert(Identifier::isContextSpecificClass($firstIdentifierOctet), 'identifier octet should indicate context specific class');
        assert(Identifier::isConstructed($firstIdentifierOctet), 'identifier octet should indicate constructed object');
        $tag = Identifier::getTagNumber($identifier);

        $totalContentLength = self::parseContentLength($binaryData, $offsetIndex);
        $remainingContentLength = $totalContentLength;

        $offsetIndexOfDecoratedObject = $offsetIndex;
        $decoratedObjects = [];

        while ($remainingContentLength > 0) {
            $nextObject = ASNObject::fromBinary($binaryData, $offsetIndex);
            $remainingContentLength -= $nextObject->getObjectLength();
            $decoratedObjects[] = $nextObject;
        }

        if ($remainingContentLength != 0) {
            throw new ParserException("Context-Specific explicitly tagged object [$tag] starting at offset $offsetIndexOfDecoratedObject specifies a length of $totalContentLength octets but $remainingContentLength remain after parsing the content", $offsetIndexOfDecoratedObject);
        }

        $parsedObject = new self($tag, ...$decoratedObjects);
        $parsedObject->setContentLength($totalContentLength);
        return $parsedObject;
    }
}
