<?php

declare(strict_types=1);

namespace PhpWA\Protocol\Binary;

use InvalidArgumentException;

final class BinaryNode
{
    public const CONTENT_NONE = 'none';
    public const CONTENT_BYTES = 'bytes';
    public const CONTENT_TEXT = 'text';
    public const CONTENT_CHILDREN = 'children';

    /** @var string */
    private $tag;
    /** @var array<string,string> */
    private $attributes;
    /** @var string|null */
    private $content;
    /** @var list<self> */
    private $children;
    /** @var string */
    private $contentType;

    /**
     * String content is raw bytes by default. Use BinaryNode::text() when the
     * protocol value must be token/string encoded.
     *
     * @param array<string,string> $attributes
     * @param string|list<self>|null $content
     */
    public function __construct(string $tag, array $attributes = [], $content = null, string $contentType = '')
    {
        if ($tag === '' || strpos($tag, "\0") !== false || !self::validUtf8($tag)) {
            throw new InvalidArgumentException('BinaryNode tag must be non-empty valid UTF-8.');
        }
        foreach ($attributes as $key => $value) {
            if (!is_string($key) || $key === '' || !is_string($value) || !self::validUtf8($key) || !self::validUtf8($value)) {
                throw new InvalidArgumentException('BinaryNode attributes must be valid UTF-8 string pairs.');
            }
        }
        $this->tag = $tag;
        $this->attributes = $attributes;
        $this->content = null;
        $this->children = [];

        if ($content === null) {
            $this->contentType = self::CONTENT_NONE;
        } elseif (is_array($content)) {
            if ($contentType !== '' && $contentType !== self::CONTENT_CHILDREN) {
                throw new InvalidArgumentException('Array node content must use children content type.');
            }
            foreach ($content as $child) {
                if (!$child instanceof self) {
                    throw new InvalidArgumentException('BinaryNode children must be BinaryNode instances.');
                }
            }
            $this->children = array_values($content);
            $this->contentType = self::CONTENT_CHILDREN;
        } elseif (is_string($content)) {
            $type = $contentType === '' ? self::CONTENT_BYTES : $contentType;
            if (!in_array($type, [self::CONTENT_BYTES, self::CONTENT_TEXT], true)) {
                throw new InvalidArgumentException('Invalid BinaryNode string content type.');
            }
            if ($type === self::CONTENT_TEXT && !self::validUtf8($content)) {
                throw new InvalidArgumentException('BinaryNode text content must be valid UTF-8.');
            }
            $this->content = $content;
            $this->contentType = $type;
        } else {
            throw new InvalidArgumentException('Unsupported BinaryNode content.');
        }
    }

    /** @param array<string,string> $attributes */
    public static function bytes(string $tag, array $attributes, string $bytes): self
    {
        return new self($tag, $attributes, $bytes, self::CONTENT_BYTES);
    }

    /** @param array<string,string> $attributes */
    public static function text(string $tag, array $attributes, string $text): self
    {
        return new self($tag, $attributes, $text, self::CONTENT_TEXT);
    }

    /** @param array<string,string> $attributes @param list<self> $children */
    public static function children(string $tag, array $attributes, array $children): self
    {
        return new self($tag, $attributes, $children, self::CONTENT_CHILDREN);
    }

    public function tag(): string { return $this->tag; }
    /** @return array<string,string> */
    public function attributes(): array { return $this->attributes; }
    public function attribute(string $key, ?string $default = null): ?string { return $this->attributes[$key] ?? $default; }
    public function contentType(): string { return $this->contentType; }
    public function bytesContent(): ?string { return $this->contentType === self::CONTENT_BYTES ? $this->content : null; }
    public function textContent(): ?string { return $this->contentType === self::CONTENT_TEXT ? $this->content : null; }
    /** @return list<self> */
    public function childNodes(): array { return $this->contentType === self::CONTENT_CHILDREN ? $this->children : []; }

    public function child(string $tag): ?self
    {
        foreach ($this->childNodes() as $child) {
            if ($child->tag === $tag) {
                return $child;
            }
        }
        return null;
    }

    /** @return list<self> */
    public function childrenNamed(?string $tag = null): array
    {
        if ($tag === null) {
            return $this->childNodes();
        }
        return array_values(array_filter($this->childNodes(), static function (self $child) use ($tag): bool {
            return $child->tag === $tag;
        }));
    }

    /** @return array<string,mixed> */
    public function toDebugArray(): array
    {
        if ($this->contentType === self::CONTENT_CHILDREN) {
            $content = array_map(static function (self $child): array { return $child->toDebugArray(); }, $this->children);
        } elseif ($this->contentType === self::CONTENT_NONE) {
            $content = null;
        } else {
            $content = '<' . strlen((string) $this->content) . ' ' . $this->contentType . ' bytes>';
        }
        return ['tag' => $this->tag, 'attrs' => $this->attributes, 'content' => $content];
    }

    private static function validUtf8(string $value): bool
    {
        return preg_match('//u', $value) === 1;
    }
}
