<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 *
 * This class reads VP9's values. They are written as a range narrowed one
 * bit at a time rather than as whole numbers.
 */
namespace seekquarry\yioop\library\av_processing;

/**
 * Vp9BoolDecoder reads VP9's values, which are written as a range narrowed
 * one bit at a time rather than as whole numbers.
 */
final class Vp9BoolDecoder
{
    /**
     * $bytes stores the part of the frame being read. VP9 splits a frame into
     * parts that can be read side by side, and each gets a reader of its own.
     * @var string
     */
    private string $bytes;
    /**
     * $position stores how far into those bytes the reading has gone. It moves
     * forward only as the range below needs more bits.
     * @var int
     */
    private int $position;
    /**
     * $end stores where this part stops. Reading past it gives zeros and sets
     * the overran flag, which is how a wrong parse shows itself.
     * @var int
     */
    private int $end;
    /**
     * $range stores how wide the range of values still in play is. Each bit
     * read narrows it by how likely that bit was, and it is widened again when
     * it grows too small to split.
     * @var int
     */
    private int $range = 255;
    /**
     * $range_position stores where in that range the number being read sits.
     * Comparing it against the split point is what decides each bit.
     * @var int
     */
    private int $range_position = 0;
    /**
     * $max_bits stores how many bits this part holds altogether. A part ends in
     * zeros, so a parse that has drifted is caught by checking what is left.
     * Read by bitsLeft(), paddingIsZero().
     * @var int
     */
    private int $max_bits = 0;
    /**
     * $held stores the byte being handed out a bit at a time.
     * @var int
     */
    private int $held = 0;
    /**
     * $spare stores bits of that byte still to hand out.
     * @var int
     */
    private int $spare = 0;
    /**
     * __construct sets up a reader over one arithmetic coded part of a frame.
     *
     * @param string $bytes the bytes to read
     * @param int $offset where in the file to start reading
     * @param int $size how many bytes
     */
    public function __construct(string $bytes, int $offset, int $size)
    {
        if ($size < 1 || $offset + $size > strlen($bytes)) {
            throw new VideoException('VP9 partition is truncated');
        }
        $this->bytes = $bytes;
        $this->position = $offset;
        $this->end = $offset + $size;
        $this->range_position = ord($bytes[$offset]);
        $this->position++;
        $this->max_bits = 8 * $size - 8;
        if ($this->readOneBit(128) !== 0) {
            throw new VideoException('VP9 partition marker bit is not zero');
        }
    }
    /**
     * $overran stores set once the decoder has been asked for more bits than
     * the partition holds.
     * @var bool
     */
    public bool $overran = false;
    /**
     * nextBit takes the next bit from the stream, refilling a byte at a time.
     *
     * @return int what was read
     */
    private function nextBit(): int
    {
        if ($this->max_bits > 0) {
            $this->max_bits--;
        } else {
            $this->overran = true;
            return 0;
        }
        if ($this->spare === 0) {
            $this->held = ($this->position < $this->end)
                ? ord($this->bytes[$this->position]) : 0;
            $this->position++;
            $this->spare = 8;
        }
        $taken = ($this->held >> ($this->spare - 1)) & 1;
        $this->spare--;
        return $taken;
    }
    /**
     * readOneBit reads one decision, given how likely a zero is.
     *
     * @param int $prob the probability a value is read with
     * @return int what was read
     */
    public function readOneBit(int $prob): int
    {
        $range = $this->range;
        $value = $this->range_position;
        $split = 1 + ((($range - 1) * $prob) >> 8);
        if ($value < $split) {
            $range = $split;
            $result = 0;
        } else {
            $range -= $split;
            $value -= $split;
            $result = 1;
        }
        /*
            Renormalize by pulling in whole bytes at a time. Reading one
            byte and shifting several bits out of it costs far less than
            fetching from the string once a bit.
        */
        if ($range < 128) {
            $held = $this->held;
            $spare = $this->spare;
            do {
                if ($spare === 0) {
                    if ($this->position < $this->end) {
                        $held = ord($this->bytes[$this->position]);
                    } else {
                        $held = 0;
                    }
                    $this->position++;
                    $spare = 8;
                }
                $range <<= 1;
                $value = ($value << 1) + (($held >> ($spare - 1)) & 1);
                $spare--;
                if ($this->max_bits > 0) {
                    $this->max_bits--;
                } else {
                    $this->overran = true;
                }
            } while ($range < 128);
            $this->held = $held;
            $this->spare = $spare;
        }
        $this->range = $range;
        $this->range_position = $value;
        return $result;
    }
    /**
     * literal reads a plain number of the given width, most significant bit
     * first.
     *
     * @param int $bits the reader the stream's bits are taken from
     * @return int what was read
     */
    public function literal(int $bits): int
    {
        $value = 0;
        for ($i = 0; $i < $bits; $i++) {
            $value = ($value << 1) | $this->readOneBit(128);
        }
        return $value;
    }
    /**
     * bitsLeft bits of the partition still unread
     *
     * @return int what was read
     */
    public function bitsLeft(): int
    {
        return $this->max_bits;
    }
    /**
     * paddingIsZero a partition ends with zero padding, so a parse that has
     * gone wrong almost always shows up here.
     *
     * @return bool what was read
     */
    public function paddingIsZero(): bool
    {
        if ($this->overran) {
            /* the parse ran off the end, so it cannot be right */
            return false;
        }
        while ($this->max_bits > 0) {
            if ($this->nextBit() !== 0) {
                return false;
            }
        }
        return true;
    }
}
