<?php

namespace Doom\Picture;

use Illuminate\Contracts\Support\Arrayable;
use InvalidArgumentException;
use function chr, ord, count;

final class Palette implements \ArrayAccess, Arrayable
{
    public const int COLORS = 256;
    public const int BYTES_PER_COLOR = 3;
    public const int PALETTE_BYTES = self::COLORS * self::BYTES_PER_COLOR;

    /** @var array<int, array{0:int,1:int,2:int}> */
    private array $colors;

    /**
     * @param array<int, array{0:int,1:int,2:int}> $colors
     */
    public function __construct(array $colors)
    {
        $this->colors = array_values($colors);
        $this->valid();
    }

    public static function fromBytes(string $bytes): self
    {
        if (\strlen($bytes) !== self::PALETTE_BYTES) {
            throw new InvalidArgumentException('Palette data must be exactly 768 bytes.');
        }

        $colors = [];
        for ($i = 0; $i < self::COLORS; $i++) {
            $base = $i * self::BYTES_PER_COLOR;
            $colors[$i] = [
                ord($bytes[$base]),
                ord($bytes[$base + 1]),
                ord($bytes[$base + 2]),
            ];
        }

        return new self($colors);
    }

    /**
     * @return array{0:int,1:int,2:int}
     */
    public function colorAt(int $index): array
    {
        if (!isset($this->colors[$index])) {
            throw new InvalidArgumentException("Palette color index {$index} is out of bounds.");
        }

        return $this->colors[$index];
    }

    /**
     * @return array<int, array{0:int,1:int,2:int}>
     */
    public function toArray(): array
    {
        return $this->colors;
    }

    public function toBytes(): string
    {
        $bytes = '';
        foreach ($this->colors as [$r, $g, $b]) {
            $bytes .= chr($r) . chr($g) . chr($b);
        }

        return $bytes;
    }

    public function valid(): void
    {
        if (count($this->colors) !== self::COLORS) {
            throw new InvalidArgumentException('Palette must contain exactly 256 colors.');
        }

        foreach ($this->colors as $index => $rgb) {
            if (!\is_array($rgb) || \count($rgb) !== 3) {
                throw new InvalidArgumentException("Palette color at index {$index} must be an RGB triplet.");
            }

            foreach ($rgb as $channel) {
                if (!\is_int($channel) || $channel < 0 || $channel > 255) {
                    throw new InvalidArgumentException("Palette channel at index {$index} must be an int in range 0..255.");
                }
            }
        }
    }

    public function offsetExists($offset)
    {
        return $offset >= 0 && $offset < self::COLORS;
    }

    public function offsetGet($offset)
    {
        return $this->colors[$offset];
    }

    public function offsetSet($offset, $value)
    {
        $this->colors[$offset] = $value;
    }

    public function offsetUnset($offset)
    {
        $this->colors[$offset] = [0, 0, 0];
    }
}
