<?php

declare(strict_types=1);

namespace PhpWA\Protocol\Binary;

use PhpWA\Exception\ProtocolException;
use PhpWA\Runtime\SafeRuntime;

final class TokenDictionary
{
    /** @var list<string> */
    private $single;
    /** @var list<list<string>> */
    private $double;
    /** @var array<string,array{dict:?int,index:int}> */
    private $map;
    /** @var self|null */
    private static $default;

    /**
     * @param list<string> $single
     * @param list<list<string>> $double
     */
    public function __construct(array $single, array $double)
    {
        if (count($single) !== 236 || count($double) !== 4) {
            throw new ProtocolException('Invalid WABinary token dictionary shape.');
        }
        foreach ($double as $dictionary) {
            if (count($dictionary) !== 256) {
                throw new ProtocolException('Each WABinary double-byte dictionary must contain 256 tokens.');
            }
        }
        $this->single = array_values($single);
        $this->double = array_map('array_values', $double);
        $this->map = [];
        foreach ($this->single as $index => $token) {
            $this->map[$token] = ['dict' => null, 'index' => $index];
        }
        foreach ($this->double as $dictionaryIndex => $dictionary) {
            foreach ($dictionary as $index => $token) {
                // Baileys intentionally lets double-byte entries win collisions.
                $this->map[$token] = ['dict' => $dictionaryIndex, 'index' => $index];
            }
        }
    }

    public static function defaults(): self
    {
        if (self::$default instanceof self) {
            return self::$default;
        }
        $path = dirname(__DIR__, 4) . '/resources/protocol/tokens.json';
        $json = SafeRuntime::call(static function () use ($path) {
            return file_get_contents($path);
        }, false);
        if (!is_string($json)) {
            throw new ProtocolException('WABinary token dictionary is missing.');
        }
        $data = json_decode($json, true);
        if (!is_array($data) || ($data['format'] ?? null) !== 1 || !is_array($data['single'] ?? null) || !is_array($data['double'] ?? null)) {
            throw new ProtocolException('WABinary token dictionary is malformed.');
        }
        $flatDouble = [];
        foreach ($data['double'] as $dictionary) {
            if (!is_array($dictionary)) {
                throw new ProtocolException('WABinary double-byte dictionary is malformed.');
            }
            $flatDouble = array_merge($flatDouble, $dictionary);
        }
        $hash = hash('sha256', implode("\0", array_merge($data['single'], $flatDouble)));
        if (!is_string($data['dictionary_sha256'] ?? null) || !hash_equals($data['dictionary_sha256'], $hash)) {
            throw new ProtocolException('WABinary token dictionary checksum mismatch.');
        }
        self::$default = new self($data['single'], $data['double']);
        return self::$default;
    }

    /** @return array{dict:?int,index:int}|null */
    public function lookup(string $token): ?array
    {
        return $this->map[$token] ?? null;
    }

    public function single(int $index): string
    {
        if ($index < 0 || $index >= count($this->single)) {
            throw new ProtocolException('Invalid WABinary single-byte token index.');
        }
        return $this->single[$index];
    }

    public function double(int $dictionary, int $index): string
    {
        if ($dictionary < 0 || $dictionary >= count($this->double) || $index < 0 || $index >= 256) {
            throw new ProtocolException('Invalid WABinary double-byte token index.');
        }
        return $this->double[$dictionary][$index];
    }

    public function singleCount(): int
    {
        return count($this->single);
    }
}
