<?php

declare(strict_types=1);

namespace Logliy\SpomkyLabs\Pki\X501\ASN1;

use ArrayIterator;
use Countable;
use IteratorAggregate;
use RangeException;
use Logliy\SpomkyLabs\Pki\ASN1\Element;
use Logliy\SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use Logliy\SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use Logliy\SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
use Logliy\SpomkyLabs\Pki\X501\DN\DNParser;
use Stringable;
use function count;

/**
 * Implements *Name* ASN.1 type.
 *
 * Since *Name* is a CHOICE only supporting *RDNSequence* type, this class implements *RDNSequence* semantics as well.
 *
 * @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x501/2012/InformationFramework.html#InformationFramework.Name
 */
final class Name implements Countable, IteratorAggregate, Stringable
{
    /**
     * Relative distinguished name components.
     *
     * @var RDN[]
     */
    private readonly array $rdns;

    /**
     * @param RDN ...$rdns RDN components
     */
    private function __construct(RDN ...$rdns)
    {
        $this->rdns = $rdns;
    }

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

    public static function create(RDN ...$rdns): self
    {
        return new self(...$rdns);
    }

    /**
     * Initialize from ASN.1.
     */
    public static function fromASN1(Sequence $seq): self
    {
        $rdns = array_map(static fn (UnspecifiedType $el) => RDN::fromASN1($el->asSet()), $seq->elements());
        return self::create(...$rdns);
    }

    /**
     * Initialize from distinguished name string.
     *
     * @see https://tools.ietf.org/html/rfc1779
     */
    public static function fromString(string $str): self
    {
        $rdns = [];
        foreach (DNParser::parseString($str) as $nameComponent) {
            $attribs = [];
            foreach ($nameComponent as [$name, $val]) {
                $type = AttributeType::fromName($name);
                // hexstrings are parsed to ASN.1 elements
                if ($val instanceof Element) {
                    $el = $val;
                } else {
                    $el = AttributeType::asn1StringForType($type->oid(), $val);
                }
                $value = AttributeValue::fromASN1ByOID($type->oid(), $el->asUnspecified());
                $attribs[] = AttributeTypeAndValue::create($type, $value);
            }
            $rdns[] = RDN::create(...$attribs);
        }
        return self::create(...$rdns);
    }

    /**
     * Generate ASN.1 structure.
     */
    public function toASN1(): Sequence
    {
        $elements = array_map(static fn (RDN $rdn) => $rdn->toASN1(), $this->rdns);
        return Sequence::create(...$elements);
    }

    /**
     * Get distinguised name string conforming to RFC 2253.
     *
     * @see https://tools.ietf.org/html/rfc2253#section-2.1
     */
    public function toString(): string
    {
        $parts = array_map(static fn (RDN $rdn) => $rdn->toString(), array_reverse($this->rdns));
        return implode(',', $parts);
    }

    /**
     * Whether name is semantically equal to other.
     *
     * Comparison conforms to RFC 4518 string preparation algorithm.
     *
     * @see https://tools.ietf.org/html/rfc4518
     *
     * @param Name $other Object to compare to
     */
    public function equals(self $other): bool
    {
        // if RDN count doesn't match
        if (count($this) !== count($other)) {
            return false;
        }
        for ($i = count($this) - 1; $i >= 0; --$i) {
            $rdn1 = $this->rdns[$i];
            $rdn2 = $other->rdns[$i];
            if (! $rdn1->equals($rdn2)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Get all RDN objects.
     *
     * @return RDN[]
     */
    public function all(): array
    {
        return $this->rdns;
    }

    /**
     * Get the first AttributeValue of given type.
     *
     * Relative name components shall be traversed in encoding order, which is reversed in regards to the string
     * representation. Multi-valued RDN with multiple attributes of the requested type is ambiguous and shall throw an
     * exception.
     *
     * @param string $name Attribute OID or name
     */
    public function firstValueOf(string $name): AttributeValue
    {
        $oid = AttributeType::attrNameToOID($name);
        foreach ($this->rdns as $rdn) {
            $tvs = $rdn->allOf($oid);
            if (count($tvs) > 1) {
                throw new RangeException("RDN with multiple {$name} attributes.");
            }
            if (count($tvs) === 1) {
                return $tvs[0]->value();
            }
        }
        throw new RangeException("Attribute {$name} not found.");
    }

    /**
     * @see \Countable::count()
     */
    public function count(): int
    {
        return count($this->rdns);
    }

    /**
     * Get the number of attributes of given type.
     *
     * @param string $name Attribute OID or name
     */
    public function countOfType(string $name): int
    {
        $oid = AttributeType::attrNameToOID($name);
        return array_sum(array_map(static fn (RDN $rdn): int => count($rdn->allOf($oid)), $this->rdns));
    }

    /**
     * @see \IteratorAggregate::getIterator()
     */
    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator($this->rdns);
    }
}
