<?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 FG\ASN1;

use FG\ASN1\Exception\ParserException;
use FG\ASN1\Universal\BitString;
use FG\ASN1\Universal\Boolean;
use FG\ASN1\Universal\Enumerated;
use FG\ASN1\Universal\GeneralizedTime;
use FG\ASN1\Universal\Integer;
use FG\ASN1\Universal\NullObject;
use FG\ASN1\Universal\ObjectIdentifier;
use FG\ASN1\Universal\RelativeObjectIdentifier;
use FG\ASN1\Universal\OctetString;
use FG\ASN1\Universal\Sequence;
use FG\ASN1\Universal\Set;
use FG\ASN1\Universal\UTCTime;
use FG\ASN1\Universal\IA5String;
use FG\ASN1\Universal\PrintableString;
use FG\ASN1\Universal\NumericString;
use FG\ASN1\Universal\UTF8String;
use FG\ASN1\Universal\UniversalString;
use FG\ASN1\Universal\CharacterString;
use FG\ASN1\Universal\GeneralString;
use FG\ASN1\Universal\VisibleString;
use FG\ASN1\Universal\GraphicString;
use FG\ASN1\Universal\BMPString;
use FG\ASN1\Universal\T61String;
use FG\ASN1\Universal\ObjectDescriptor;
use FG\Utility\BigInteger;
use LogicException;

/**
 * Class ASNObject is the base class for all concrete ASN.1 objects.
 */
abstract class ASNObject implements Parsable
{
    private $contentLength;
    private $nrOfLengthOctets;

    /**
     * Must return the number of octets of the content part.
     *
     * @return int
     */
    abstract protected function calculateContentLength();

    /**
     * Encode the object using DER encoding.
     *
     * @see http://en.wikipedia.org/wiki/X.690#DER_encoding
     *
     * @return string the binary representation of an objects value
     */
    abstract protected function getEncodedValue();

    /**
     * Return the content of this object in a non encoded form.
     * This can be used to print the value in human readable form.
     *
     * @return mixed
     */
    abstract public function getContent();

    /**
     * Return the object type octet.
     * This should use the class constants of Identifier.
     *
     * @see Identifier
     *
     * @return int
     */
    abstract public function getType();

    /**
     * Returns all identifier octets. If an inheriting class models a tag with
     * the long form identifier format, it MUST reimplement this method to
     * return all octets of the identifier.
     *
     * @throws LogicException If the identifier format is long form
     *
     * @return string Identifier as a set of octets
     */
    public function getIdentifier()
    {
        $firstOctet = $this->getType();

        if (Identifier::isLongForm($firstOctet)) {
            throw new LogicException(sprintf('Identifier of %s uses the long form and must therefor override "ASNObject::getIdentifier()".', get_class($this)));
        }

        return chr($firstOctet);
    }

    /**
     * Encode this object using DER encoding.
     *
     * @return string the full binary representation of the complete object
     */
    public function getBinary()
    {
        $result  = $this->getIdentifier();
        $result .= $this->createLengthPart();
        $result .= $this->getEncodedValue();

        return $result;
    }

    private function createLengthPart()
    {
        $contentLength = $this->getContentLength();
        $nrOfLengthOctets = $this->getNumberOfLengthOctets($contentLength);

        if ($nrOfLengthOctets == 1) {
            return chr($contentLength);
        } else {
            // the first length octet determines the number subsequent length octets
            $lengthOctets = chr(0x80 | ($nrOfLengthOctets - 1));
            for ($shiftLength = 8 * ($nrOfLengthOctets - 2); $shiftLength >= 0; $shiftLength -= 8) {
                $lengthOctets .= chr($contentLength >> $shiftLength);
            }

            return $lengthOctets;
        }
    }

    protected function getNumberOfLengthOctets($contentLength = null)
    {
        if (!isset($this->nrOfLengthOctets)) {
            if ($contentLength == null) {
                $contentLength = $this->getContentLength();
            }

            $this->nrOfLengthOctets = 1;
            if ($contentLength > 127) {
                do { // long form
                    $this->nrOfLengthOctets++;
                    $contentLength = $contentLength >> 8;
                } while ($contentLength > 0);
            }
        }

        return $this->nrOfLengthOctets;
    }

    protected function getContentLength()
    {
        if (!isset($this->contentLength)) {
            $this->contentLength = $this->calculateContentLength();
        }

        return $this->contentLength;
    }

    protected function setContentLength($newContentLength)
    {
        $this->contentLength = $newContentLength;
        $this->getNumberOfLengthOctets($newContentLength);
    }

    /**
     * Returns the length of the whole object (including the identifier and length octets).
     */
    public function getObjectLength()
    {
        $nrOfIdentifierOctets = strlen($this->getIdentifier());
        $contentLength = $this->getContentLength();
        $nrOfLengthOctets = $this->getNumberOfLengthOctets($contentLength);

        return $nrOfIdentifierOctets + $nrOfLengthOctets + $contentLength;
    }

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

    /**
     * Returns the name of the ASN.1 Type of this object.
     *
     * @see Identifier::getName()
     */
    public function getTypeName()
    {
        return Identifier::getName($this->getType());
    }

    /**
     * @param string $binaryData
     * @param int $offsetIndex
     *
     * @throws ParserException
     *
     * @return \FG\ASN1\ASNObject
     */
    public static function fromBinary(&$binaryData, &$offsetIndex = 0)
    {
        if (strlen($binaryData) <= $offsetIndex) {
            throw new ParserException('Can not parse binary from data: Offset index larger than input size', $offsetIndex);
        }

        $identifierOctet = ord($binaryData[$offsetIndex]);
        if (Identifier::isContextSpecificClass($identifierOctet) && Identifier::isConstructed($identifierOctet)) {
            return ExplicitlyTaggedObject::fromBinary($binaryData, $offsetIndex);
        }

        switch ($identifierOctet) {
            case Identifier::BITSTRING:
                return BitString::fromBinary($binaryData, $offsetIndex);
            case Identifier::BOOLEAN:
                return Boolean::fromBinary($binaryData, $offsetIndex);
            case Identifier::ENUMERATED:
                return Enumerated::fromBinary($binaryData, $offsetIndex);
            case Identifier::INTEGER:
                return Integer::fromBinary($binaryData, $offsetIndex);
            case Identifier::NULL:
                return NullObject::fromBinary($binaryData, $offsetIndex);
            case Identifier::OBJECT_IDENTIFIER:
                return ObjectIdentifier::fromBinary($binaryData, $offsetIndex);
            case Identifier::RELATIVE_OID:
                return RelativeObjectIdentifier::fromBinary($binaryData, $offsetIndex);
            case Identifier::OCTETSTRING:
                return OctetString::fromBinary($binaryData, $offsetIndex);
            case Identifier::SEQUENCE:
                return Sequence::fromBinary($binaryData, $offsetIndex);
            case Identifier::SET:
                return Set::fromBinary($binaryData, $offsetIndex);
            case Identifier::UTC_TIME:
                return UTCTime::fromBinary($binaryData, $offsetIndex);
            case Identifier::GENERALIZED_TIME:
                return GeneralizedTime::fromBinary($binaryData, $offsetIndex);
            case Identifier::IA5_STRING:
                return IA5String::fromBinary($binaryData, $offsetIndex);
            case Identifier::PRINTABLE_STRING:
                return PrintableString::fromBinary($binaryData, $offsetIndex);
            case Identifier::NUMERIC_STRING:
                return NumericString::fromBinary($binaryData, $offsetIndex);
            case Identifier::UTF8_STRING:
                return UTF8String::fromBinary($binaryData, $offsetIndex);
            case Identifier::UNIVERSAL_STRING:
                return UniversalString::fromBinary($binaryData, $offsetIndex);
            case Identifier::CHARACTER_STRING:
                return CharacterString::fromBinary($binaryData, $offsetIndex);
            case Identifier::GENERAL_STRING:
                return GeneralString::fromBinary($binaryData, $offsetIndex);
            case Identifier::VISIBLE_STRING:
                return VisibleString::fromBinary($binaryData, $offsetIndex);
            case Identifier::GRAPHIC_STRING:
                return GraphicString::fromBinary($binaryData, $offsetIndex);
            case Identifier::BMP_STRING:
                return BMPString::fromBinary($binaryData, $offsetIndex);
            case Identifier::T61_STRING:
                return T61String::fromBinary($binaryData, $offsetIndex);
            case Identifier::OBJECT_DESCRIPTOR:
                return ObjectDescriptor::fromBinary($binaryData, $offsetIndex);
            default:
                // At this point the identifier may be >1 byte.
                if (Identifier::isConstructed($identifierOctet)) {
                    return new UnknownConstructedObject($binaryData, $offsetIndex);
                } else {
                    $identifier = self::parseBinaryIdentifier($binaryData, $offsetIndex);
                    $lengthOfUnknownObject = self::parseContentLength($binaryData, $offsetIndex);
                    $offsetIndex += $lengthOfUnknownObject;

                    return new UnknownObject($identifier, $lengthOfUnknownObject);
                }
        }
    }

    protected static function parseIdentifier($identifierOctet, $expectedIdentifier, $offsetForExceptionHandling)
    {
        if (is_string($identifierOctet) || is_numeric($identifierOctet) == false) {
            $identifierOctet = ord($identifierOctet);
        }

        if ($identifierOctet != $expectedIdentifier) {
            $message = 'Can not create an '.Identifier::getName($expectedIdentifier).' from an '.Identifier::getName($identifierOctet);
            throw new ParserException($message, $offsetForExceptionHandling);
        }
    }

    protected static function parseBinaryIdentifier($binaryData, &$offsetIndex)
    {
        if (strlen($binaryData) <= $offsetIndex) {
            throw new ParserException('Can not parse identifier from data: Offset index larger than input size', $offsetIndex);
        }

        $identifier = $binaryData[$offsetIndex++];

        if (Identifier::isLongForm(ord($identifier)) == false) {
            return $identifier;
        }

        while (true) {
            if (strlen($binaryData) <= $offsetIndex) {
                throw new ParserException('Can not parse identifier (long form) from data: Offset index larger than input size', $offsetIndex);
            }
            $nextOctet = $binaryData[$offsetIndex++];
            $identifier .= $nextOctet;

            if ((ord($nextOctet) & 0x80) === 0) {
                // the most significant bit is 0 to we have reached the end of the identifier
                break;
            }
        }

        return $identifier;
    }

    protected static function parseContentLength(&$binaryData, &$offsetIndex, $minimumLength = 0)
    {
        if (strlen($binaryData) <= $offsetIndex) {
            throw new ParserException('Can not parse content length from data: Offset index larger than input size', $offsetIndex);
        }

        $contentLength = ord($binaryData[$offsetIndex++]);
        if (($contentLength & 0x80) != 0) {
            // bit 8 is set -> this is the long form
            $nrOfLengthOctets = $contentLength & 0x7F;
            $contentLength = BigInteger::create(0x00);
            for ($i = 0; $i < $nrOfLengthOctets; $i++) {
                if (strlen($binaryData) <= $offsetIndex) {
                    throw new ParserException('Can not parse content length (long form) from data: Offset index larger than input size', $offsetIndex);
                }
                $contentLength = $contentLength->shiftLeft(8)->add(ord($binaryData[$offsetIndex++]));
            }

            if ($contentLength->compare(PHP_INT_MAX) > 0) {
                throw new ParserException("Can not parse content length from data: length > maximum integer", $offsetIndex);
            }

            $contentLength = $contentLength->toInteger();
        }

        if ($contentLength < $minimumLength) {
            throw new ParserException('A '.get_called_class()." should have a content length of at least {$minimumLength}. Extracted length was {$contentLength}", $offsetIndex);
        }

        $lenDataRemaining = strlen($binaryData) - $offsetIndex;

        if ($lenDataRemaining < $contentLength) {
            throw new ParserException("Content length {$contentLength} exceeds remaining data length {$lenDataRemaining}", $offsetIndex);
        }

        return $contentLength;
    }
}