<?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 the values of a VP9 frame. It walks the tree a frame is
 * divided into and gathers what each block carries.
 */
namespace seekquarry\yioop\library\av_processing;

/**
 * Vp9Entropy reads the values of a VP9 frame, walking the tree the frame is
 * divided into.
 */
final class Vp9Entropy
{
    /**
     * BW4 is block sizes, in the specification's order.
     * @var mixed
     */
    private const BW4 = [1, 1, 2, 2, 2, 4, 4, 4, 8, 8, 8, 16, 16];
    /**
     * BH4 is how tall each block size is, counted in the four by four squares
     * the format measures by.
     * @var mixed
     */
    private const BH4 = [1, 2, 1, 2, 4, 2, 4, 8, 4, 8, 16, 8, 16];
    /**
     * B_WIDTH_LOG2 is the b width the power of two the format fixes.
     * @var mixed
     */
    private const B_WIDTH_LOG2 = [0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4];
    /**
     * B_HEIGHT_LOG2 is the b height the power of two the format fixes.
     * @var mixed
     */
    private const B_HEIGHT_LOG2 = [0, 1, 0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4];
    /**
     * MI_WIDTH_LOG2 is the mi width the power of two the format fixes.
     * @var mixed
     */
    private const MI_WIDTH_LOG2 = [0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3];
    /**
     * N8W is how wide each block size is, counted in the eight by eight squares
     * the format measures by.
     * @var mixed
     */
    private const N8W = [1, 1, 1, 1, 1, 2, 2, 2, 4, 4, 4, 8, 8];
    /**
     * MAX_TX_SIZE is the largest transform a block may use, given as a power of
     * two.
     * @var mixed
     */
    private const MAX_TX_SIZE = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3];
    /**
     * TX_MODE_BIGGEST is the setting that says every block uses the largest
     * transform its size allows.
     * @var mixed
     */
    private const TX_MODE_BIGGEST = [0, 1, 2, 3, 3];
    /**
     * SUBSIZE is [partition][square block size] => resulting block size.
     */
    private const SUBSIZE = [
        /* 8x8   -> 8x8, 8x4, 4x8, 4x4 */
        3 => [3, 2, 1, 0],
        /* 16x16 */
        6 => [6, 5, 4, 3],
        /* 32x32 */
        9 => [9, 8, 7, 6],
        /* a sixty-four by sixty-four square */
        12 => [12, 11, 10, 9],
    ];
    /**
     * PARTITION_TREE is the tree of choices that says how a block is split.
     * @var mixed
     */
    private const PARTITION_TREE = [
        [-1, 1], [-2, 2], [-3, -4],
    ];
    /**
     * INTRA_MODE_TREE is the tree of choices that says how a block is guessed.
     * @var mixed
     */
    private const INTRA_MODE_TREE = [
        /* DC */
        [-1, 1],
        /* TM */
        [-10, 2],
        /* V */
        [-2, 3], [4, 6],
        /* H */
        [-3, 5],
        /* D135, D117 */
        [-5, -6],
        /* D45 */
        [-4, 7],
        /* D63 */
        [-9, 8],
        /* D153, D207 */
        [-7, -8],
    ];
    /**
     * SEGMENT_TREE is the tree of choices that says which segment a block
     * belongs to.
     * @var mixed
     */
    private const SEGMENT_TREE = [
        [1, 2], [3, 4], [5, 6],
        [-1, -2], [-3, -4], [-5, -6], [-7, -8],
    ];
    /**
     * MODE_TX_TYPE is transform type chosen by the intra mode: 0 DCT/DCT, 1
     * ADST/DCT, 2 DCT/ADST, 3 ADST/ADST.
     */
    private const MODE_TX_TYPE = [0, 1, 2, 0, 3, 1, 2, 2, 1, 3];
    /**
     * TX_TYPE_SCAN is scan order to use for each transform type.
     */
    private const TX_TYPE_SCAN = [0, 1, 2, 0];
    /**
     * BAND_4X4 is the band four by four the format fixes.
     * @var mixed
     */
    private const BAND_4X4 = [0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5];
    /**
     * BAND_8X8_HEAD is bands for transforms above four by four: band 4 runs to
     * coefficient 20, then band 5.
     */
    private const BAND_8X8_HEAD = [
        0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
    ];
    /**
     * CAT_MIN is the smallest value each large-value group stands for.
     * @var mixed
     */
    private const CAT_MIN = [5, 7, 11, 19, 35, 67];
    /**
     * CAT_PROBS is how likely each bit of those larger values is.
     * @var mixed
     */
    private const CAT_PROBS = [
        [159], [165, 145], [173, 148, 140], [176, 155, 140, 135],
        [180, 157, 141, 134, 130],
        [254, 254, 254, 252, 249, 243, 230, 196, 177, 153, 140, 133, 130, 129],
    ];
    /**
     * ENERGY_CLASS is how much each token contributes to a neighbor's context.
     */
    private const ENERGY_CLASS = [0, 1, 2, 3, 3, 4, 4, 5, 5, 5, 5, 5];
    /**
     * $header stores what the frame's header said about itself, which every
     * choice below is read against.
     * @var Vp9Header
     */
    private Vp9Header $header;
    /**
     * $frame stores the frame's bytes. Each tile is read out of a stretch of
     * them.
     * @var string
     */
    private string $frame;
    /**
     * $block_cols stores how many blocks the frame is across, counting in the
     * eight by eight units VP9 measures by.
     * @var int
     */
    private int $block_cols;
    /**
     * $block_rows stores how many blocks the frame is down, in the same units.
     * @var int
     */
    private int $block_rows;
    /**
     * $superblock_cols stores how many sixty-four by sixty-four squares the
     * frame is
     * across. The frame is walked one square at a time.
     * @var int
     */
    private int $superblock_cols;
    /**
     * $superblock_rows stores how many such squares the frame is down.
     * @var int
     */
    private int $superblock_rows;
    /**
     * $ss_x stores how many pixels across share one color value, taken from the
     * header.
     * @var int
     */
    private int $ss_x;
    /**
     * $ss_y stores how many pixels down share one color value.
     * @var int
     */
    private int $ss_y;
    /**
     * $block_size stores how large each block is, kept for every block of the
     * frame. A neighbor's size is needed while reading the next block.
     * @var array
     */
    private array $block_size = [];
    /**
     * $block_y_mode stores how the brightness of each block was guessed from
     * its
     * neighbors. Kept for the whole frame because a block's guess depends on
     * those around it.
     * @var array
     */
    private array $block_y_mode = [];
    /**
     * $block_sub_modes stores how each of the four smaller squares of a
     * split block was guessed from its neighbors. A block
     * that was split.
     * @var array
     */
    private array $block_sub_modes = [];
    /**
     * $block_skip stores whether each block carried any values at all. A
     * skipped
     * block is filled from its guess alone.
     * @var array
     */
    private array $block_skip = [];
    /**
     * $block_transform_size stores how large a transform each block used.
     * @var array
     */
    private array $block_transform_size = [];
    /**
     * $block_segment stores which segment each block belongs to, where the
     * frame
     * is split into segments with their own quantizers.
     * @var array
     */
    private array $block_segment = [];
    /**
     * $coefficients stores the values read for every block, ready to be turned
     * back into differences from the guess. This is what the picture is built
     * from.
     * @var array
     */
    public array $coefficients = [];
    /**
     * $blocks stores what was read about every block: where it sits, how large
     * it is, and how it was guessed. The picture stage walks this.
     * @var array
     */
    public array $blocks = [];
    /**
     * $above_value_count stores whether the blocks in the row above carried
     * values, one
     * entry per column. How likely the next value is depends on it.
     * @var array
     */
    private array $above_value_count = [];
    /**
     * $left_value_count stores whether the blocks to the left carried values,
     * one entry per row within the tile. How likely the next value
     * is depends on it.
     * @var array
     */
    private array $left_value_count = [];
    /**
     * $above_partition stores how the row above was split, which shifts how
     * likely each split of the next block is.
     * @var array
     */
    private array $above_partition = [];
    /**
     * $left_partition stores how the column to the left was split, one
     * entry per row, which shifts how likely each split of the next
     * block is.
     * @var array
     */
    private array $left_partition = [];
    /**
     * $descale stores the steps each segment's values are scaled by, worked out
     * once from the header rather than for every block.
     * @var array
     */
    private array $descale = [];
    /**
     * $current_segment stores segment of the block being decoded.
     * @var int
     */
    private int $current_segment = 0;
    /**
     * $reader stores the reader for the tile being walked. Each tile has bytes
     * of its own, so the reader is made afresh for each.
     * @var Vp9BoolDecoder
     */
    private Vp9BoolDecoder $reader;
    /**
     * $tile_col_start stores which column the tile being walked begins at. The
     * left-hand neighbors are cleared there, since a tile may not lean on the
     * tile beside it.
     * @var int
     */
    private int $tile_col_start = 0;
    /**
     * $all_tiles_clean stores whether every tile ended where it should. A tile
     * ends in zeros, so a parse that has drifted anywhere shows up here.
     * @var bool
     */
    public bool $all_tiles_clean = true;
    /**
     * $tile_bits_left stores how many bits were left over in each tile, kept so
     * a reader can see which tile went wrong.
     * @var array
     */
    public array $tile_bits_left = [];
    /**
     * $end_of_block_hist stores how often a block has ended at each
     * place so far, which shifts how likely the next block is to end
     * there.
     * @var array
     */
    public array $end_of_block_hist = [];
    /**
     * $guess_log stores a record of how each block was guessed, kept for
     * reading
     * while working on the decoder rather than for the picture.
     * @var array
     */
    public array $guess_log = [];
    /**
     * __construct sets up a decoder for one VP9 keyframe.
     *
     * @param Vp9Header $header the header read off the stream
     * @param string $frame the stored bytes of one frame
     */
    public function __construct(Vp9Header $header, string $frame)
    {
        $this->header = $header;
        $this->frame = $frame;
        $this->block_cols = ($header->frame_width + 7) >> 3;
        $this->block_rows = ($header->frame_height + 7) >> 3;
        $this->superblock_cols = ($this->block_cols + 7) >> 3;
        $this->superblock_rows = ($this->block_rows + 7) >> 3;
        $this->ss_x = $header->subsampling_x;
        $this->ss_y = $header->subsampling_y;
        $this->initDequant();
    }
    /**
     * initDequant works out the numbers each coefficient is multiplied by, for
     * every segment and for luma and chroma separately.
     */
    private function initDequant(): void
    {
        /*
            A picture may be split into segments, each carrying its own
            quantizer index, either replacing the picture's or shifting it.
        */
        $clamp = static function ($value) {
            return max(0, min(255, $value));
        };
        $this->descale = [];
        for ($segment = 0; $segment < 8; $segment++) {
            $quant = $this->header->base_q_position;
            if ($this->header->segmentation_enabled
                && !empty($this->header->segment_feature_enabled[$segment][
                    Vp9Header::SEGMENT_FEATURE_QUANT])) {
                $adjust = $this->header->segment_feature_value[$segment][
                    Vp9Header::SEGMENT_FEATURE_QUANT];
                $quant = $this->header->segment_absolute
                    ? $adjust : $quant + $adjust;
            }
            $quant = $clamp($quant);
            $this->descale[$segment] = [
                [
                    Vp9Tables::DC_QLOOKUP[
                        $clamp($quant + $this->header
                            ->delta_brightness_quantizer_first_value)],
                    Vp9Tables::AC_QLOOKUP[$quant],
                ],
                [
                    Vp9Tables::DC_QLOOKUP[
                        $clamp($quant + $this->header
                            ->delta_color_quantizer_first_value)],
                    Vp9Tables::AC_QLOOKUP[
                        $clamp($quant + $this->header
                            ->delta_color_quantizer_other_values)],
                ],
            ];
        }
    }
    /**
     * decodePicture decodes the whole keyframe: every superblock of every tile,
     * then the filter that smooths the block edges.
     */
    public function decodePicture(): void
    {
        $this->allocatePlanes();
        $count = count($this->header->tiles);
        $tile_cols = 1 << $this->header->tile_cols_power_of_two;
        $tile_rows = 1 << $this->header->tile_rows_power_of_two;
        $size = $this->block_cols;
        $this->above_partition = array_fill(0, $size + 8, 0);
        foreach ([0, 1, 2] as $plane) {
            $weight_four = ($this->block_cols * 2) >> ($plane ? $this
                ->ss_x : 0);
            $this->above_value_count[$plane] = array_fill(0, $weight_four +
                16, 0);
        }
        for ($top_right = 0; $top_right < $tile_rows; $top_right++) {
            for ($move_limit = 0; $move_limit < $tile_cols; $move_limit++) {
                $at = $top_right * $tile_cols + $move_limit;
                [$offset, $length] = $this->header->tiles[$at];
                $this->reader = new Vp9BoolDecoder($this->frame, $offset,
                    $length);
                $col_start = self::tileOffset($move_limit, $this->block_cols,
                    $this->header->tile_cols_power_of_two);
                $col_end = self::tileOffset($move_limit + 1, $this->block_cols,
                    $this->header->tile_cols_power_of_two);
                $row_start = self::tileOffset($top_right, $this->block_rows,
                    $this->header->tile_rows_power_of_two);
                $row_end = self::tileOffset($top_right + 1, $this->block_rows,
                    $this->header->tile_rows_power_of_two);
                $this->tile_col_start = $col_start;
                /* the above contexts restart at every tile column */
                $partition_end = min($col_end, count($this->above_partition));
                for ($i = $col_start; $i < $partition_end; $i++) {
                    $this->above_partition[$i] = 0;
                }
                foreach ([0, 1, 2] as $plane) {
                    $sub = $plane ? $this->ss_x : 0;
                    $from = ($col_start * 2) >> $sub;
                    $to = ($col_end * 2) >> $sub;
                    for ($i
 = $from; $i < min($to, count($this->above_value_count[$plane])); $i++) {
                        $this->above_value_count[$plane][$i] = 0;
                    }
                }
                for ($block_row =
                    $row_start; $block_row < $row_end; $block_row += 8) {
                    $this->left_partition = array_fill(0, 8, 0);
                    foreach ([0, 1, 2] as $plane) {
                        $this->left_value_count[$plane] = array_fill(0, 16, 0);
                    }
                    for ($block_col = $col_start; $block_col < $col_end;
                        $block_col += 8) {
                        $this->decodePartition($block_row, $block_col, 12);
                    }
                }
                $this->tile_bits_left[] = $this->reader->bitsLeft();
                if (!$this->reader->paddingIsZero()) {
                    $this->all_tiles_clean = false;
                }
            }
        }
    }
    /**
     * tileOffset works out where a tile starts, in mode-info units, rounded to
     * superblocks
     *
     * @param int $i which one
     * @param int $block_grid the blocks the frame is divided into
     * @param int $power_of_two the size, given as a power of two
     * @return int what was read
     */
    private static function tileOffset(int $i, int $block_grid,
        int $power_of_two): int
    {
        $superblocks = ($block_grid + 7) >> 3;
        $offset = (($i * $superblocks) >> $power_of_two) << 3;
        return min($offset, $block_grid);
    }
    /**
     * decodePartition decodes one square of the picture, splitting it into
     * smaller squares where the stream says to.
     *
     * @param int $block_row which row of blocks
     * @param int $block_col which column of blocks
     * @param int $b_size how large the block is
     */
    private function decodePartition(int $block_row, int $block_col,
        int $b_size): void
    {
        if ($block_row >= $this->block_rows || $block_col >= $this
            ->block_cols) {
            return;
        }
        $eight_by_eight_count = self::N8W[$b_size];
        $half = $eight_by_eight_count >> 1;
        $has_rows = ($block_row + $half) < $this->block_rows;
        $has_cols = ($block_col + $half) < $this->block_cols;
        $block_slice = self::MI_WIDTH_LOG2[$b_size];
        $above = ($this->above_partition[$block_col] >> $block_slice) & 1;
        $left = ($this->left_partition[$block_row & 7] >> $block_slice) & 1;
        /* the table is stored largest block first, so the level index inverts
          */
        $probs =
            Vp9Tables::KF_PARTITION_PROBS[3 - $block_slice][$left * 2 + $above];
        if ($has_rows && $has_cols) {
            $partition = $this->readTree(self::PARTITION_TREE, $probs);
        } elseif ($has_cols) {
            $partition = $this->reader->readOneBit($probs[1]) ? 3 : 1;
        } elseif ($has_rows) {
            $partition = $this->reader->readOneBit($probs[2]) ? 3 : 2;
        } else {
            $partition = 3;
        }
        $sub_size = self::SUBSIZE[$b_size][$partition];
        if ($sub_size < 3 || $partition === 0) {
            $this->decodeBlock($block_row, $block_col, $sub_size);
        } elseif ($partition === 1) {
            $this->decodeBlock($block_row, $block_col, $sub_size);
            if ($has_rows) {
                $this->decodeBlock($block_row + $half, $block_col, $sub_size);
            }
        } elseif ($partition === 2) {
            $this->decodeBlock($block_row, $block_col, $sub_size);
            if ($has_cols) {
                $this->decodeBlock($block_row, $block_col + $half, $sub_size);
            }
        } else {
            $this->decodePartition($block_row, $block_col, $sub_size);
            $this->decodePartition($block_row, $block_col + $half, $sub_size);
            $this->decodePartition($block_row + $half, $block_col, $sub_size);
            $this->decodePartition($block_row + $half, $block_col + $half,
                $sub_size);
        }
        if ($b_size === 3 || $partition !== 3) {
            /* the context marks how far down the size hierarchy this block */
            /* sits, as a mask read back with the current block's level */
            $a_value = (15 << self::B_WIDTH_LOG2[$sub_size]) & 15;
            $l_value = (15 << self::B_HEIGHT_LOG2[$sub_size]) & 15;
            for ($i = 0; $i < $eight_by_eight_count; $i++) {
                if ($block_col + $i < count($this->above_partition)) {
                    $this->above_partition[$block_col + $i] = $a_value;
                }
                $this->left_partition[($block_row + $i) & 7] = $l_value;
            }
        }
    }
    /**
     * readTree reads one value coded as a walk down a tree of decisions.
     *
     * @param array $tree the tree of choices a value is read from
     * @param array $probs the probabilities values are read with
     * @param int $node where in the tree the reading is
     * @return int what was read
     */
    private function readTree(array $tree, array $probs, int $node = 0): int
    {
        $guard = 0;
        while (true) {
            $next = $tree[$node][$this->reader->readOneBit($probs[$node])];
            if ($next < 0) {
                return -$next - 1;
            }
            $node = $next;
            if (++$guard > 16) {
                throw new VideoException('malformed VP9 tree');
            }
        }
    }
    /**
     * decodeBlock decodes one block: its segment, its prediction modes, its
     * transform size, and its coefficients.
     *
     * @param int $block_row which row of blocks
     * @param int $block_col which column of blocks
     * @param int $b_size how large the block is
     */
    private function decodeBlock(int $block_row, int $block_col,
        int $b_size): void
    {
        $avail_u = $block_row > 0;
        $avail_l = $block_col > $this->tile_col_start;
        $position = $block_row * $this->block_cols + $block_col;
        $segment = 0;
        if ($this->header->segmentation_enabled
            && $this->header->segmentation_update_map) {
            $segment = $this->readTree(self::SEGMENT_TREE,
                $this->header->segment_tree_probs);
        }
        $above_skip = $avail_u ? ($this->block_skip[$position -
            $this->block_cols] ?? 0) : 0;
        $left_skip = $avail_l ? ($this->block_skip[$position - 1] ?? 0) : 0;
        $this->current_segment = $segment;
        $skip = $this->reader->readOneBit($this->header->skip_probs[$above_skip
            + $left_skip]);
        $transform_size = $this->readTxSize($block_row, $block_col, $b_size,
            $avail_u,
            $avail_l,
            $skip);
        $sub_modes = [0, 0, 0, 0];
        if ($b_size >= 3) {
            $mode = $this->readIntraMode($block_row, $block_col, $b_size, 0,
                $avail_u,
                $avail_l, $sub_modes);
            $sub_modes = [$mode, $mode, $mode, $mode];
            $y_mode = $mode;
        } else {
            $y_mode = 0;
            /* 4x4: four sub-modes */
            if ($b_size === 0) {
                for ($i = 0; $i < 4; $i++) {
                    $sub_modes[$i] = $this->readIntraMode($block_row,
                        $block_col,
                        $b_size,
                        $i, $avail_u, $avail_l, $sub_modes);
                }
                $y_mode = $sub_modes[3];
            /* 4x8: two side by side */
            } elseif ($b_size === 1) {
                $middle_zero = $this->readIntraMode($block_row, $block_col,
                    $b_size, 0,
                    $avail_u,
                    $avail_l, $sub_modes);
                $sub_modes[0] = $sub_modes[2] = $middle_zero;
                $middle_one = $this->readIntraMode($block_row, $block_col,
                    $b_size, 1,
                    $avail_u,
                    $avail_l, $sub_modes);
                $sub_modes[1] = $sub_modes[3] = $middle_one;
                $y_mode = $middle_one;
            /* 8x4: two stacked */
            } else {
                $middle_zero = $this->readIntraMode($block_row, $block_col,
                    $b_size, 0,
                    $avail_u,
                    $avail_l, $sub_modes);
                $sub_modes[0] = $sub_modes[1] = $middle_zero;
                $middle_two = $this->readIntraMode($block_row, $block_col,
                    $b_size, 2,
                    $avail_u,
                    $avail_l, $sub_modes);
                $sub_modes[2] = $sub_modes[3] = $middle_two;
                $y_mode = $middle_two;
            }
        }
        $color_mode = $this->readTree(self::INTRA_MODE_TREE,
            Vp9Tables::KF_UVMODE_PROBS[$y_mode]);
        /* record the block over every mode-info unit it covers */
        $weight_eight = self::N8W[$b_size];
        $half_eight = max(1, self::BH4[$b_size] >> 1);
        $weight_eight = max(1, self::BW4[$b_size] >> 1);
        for ($row = 0; $row < $half_eight && $block_row + $row < $this
            ->block_rows; $row++) {
            for ($col = 0; $col < $weight_eight && $block_col
                + $col < $this->block_cols; $col++) {
                $first = ($block_row + $row) * $this
                    ->block_cols + $block_col + $col;
                $this->block_size[$first] = $b_size;
                $this->block_y_mode[$first] = $y_mode;
                $this->block_sub_modes[$first] = $sub_modes;
                $this->block_skip[$first] = $skip;
                $this->block_transform_size[$first] = $transform_size;
                $this->block_segment[$first] = $segment;
            }
        }
        $this->blocks[] = [$block_row, $block_col, $b_size, $y_mode,
            $color_mode,
            $sub_modes,
            $transform_size, $skip];
        $this->residual($block_row, $block_col, $b_size, $transform_size,
            $sub_modes,
            $color_mode,
            $skip);
    }
    /**
     * readTxSize reads how large a transform this block uses.
     *
     * @param int $block_row which row of blocks
     * @param int $block_col which column of blocks
     * @param int $b_size how large the block is
     * @param bool $avail_u whether the block above is there to read
     * @param bool $avail_l whether the block to the left is there to read
     * @param int $skip whether the block was left out of the stream
     * @return int what was read
     */
    private function readTxSize(int $block_row, int $block_col, int $b_size,
        bool $avail_u, bool $avail_l, int $skip): int
    {
        $max_transform = self::MAX_TX_SIZE[$b_size];
        if ($this->header->transform_mode !== 4 || $b_size < 3) {
            return min($max_transform, self::TX_MODE_BIGGEST[$this->header
                ->transform_mode]);
        }
        $position = $block_row * $this->block_cols + $block_col;
        $above_context = ($avail_u && !($this->block_skip[$position -
            $this->block_cols] ?? 0))
            ? ($this->block_transform_size[$position - $this
                ->block_cols] ?? $max_transform) : $max_transform;
        $left_context = ($avail_l && !($this->block_skip[$position - 1] ?? 0))
            ? ($this
                ->block_transform_size[$position - 1] ?? $max_transform)
                    : $max_transform;
        if (!$avail_l) {
            $left_context = $above_context;
        }
        if (!$avail_u) {
            $above_context = $left_context;
        }
        $context = (($above_context + $left_context) > $max_transform) ? 1 : 0;
        if ($max_transform === 1) {
            return $this->reader->readOneBit($this->header
                ->eight_transform_probabilities[$context]);
        }
        if ($max_transform === 2) {
            $prob = $this->header->sixteen_transform_probabilities[$context];
            if ($this->reader->readOneBit($prob[0]) === 0) {
                return 0;
            }
            return $this->reader->readOneBit($prob[1]) === 0 ? 1 : 2;
        }
        $prob = $this->header->thirty_two_transform_probabilities[$context];
        if ($this->reader->readOneBit($prob[0]) === 0) {
            return 0;
        }
        if ($this->reader->readOneBit($prob[1]) === 0) {
            return 1;
        }
        return $this->reader->readOneBit($prob[2]) === 0 ? 2 : 3;
    }
    /**
     * readIntraMode reads a prediction mode, choosing the probabilities from
     * the modes of the blocks above and to the left.
     *
     * @param int $block_row which row of blocks
     * @param int $block_col which column of blocks
     * @param int $b_size how large the block is
     * @param int $block the block's values
     * @param bool $avail_u whether the block above is there to read
     * @param bool $avail_l whether the block to the left is there to read
     * @param array $sub_modes the ways the four smaller blocks are guessed
     * @return int what was read
     */
    private function readIntraMode(
        int $block_row, int $block_col, int $b_size, int $block, bool $avail_u,
        bool $avail_l, array $sub_modes
    ): int {
        $position = $block_row * $this->block_cols + $block_col;
        if ($block === 0 || $block === 1) {
            if (!$avail_u) {
                $above = 0;
            } else {
                $count = $position - $this->block_cols;
                $above = (($this->block_size[$count] ?? 3) < 3)
                    ? ($this->block_sub_modes[$count][$block + 2] ?? 0)
                    : ($this->block_y_mode[$count] ?? 0);
            }
        } else {
            $above = $sub_modes[$block - 2];
        }
        if ($block === 0 || $block === 2) {
            if (!$avail_l) {
                $left = 0;
            } else {
                $count = $position - 1;
                $left = (($this->block_size[$count] ?? 3) < 3)
                    ? ($this->block_sub_modes[$count][$block + 1] ?? 0)
                    : ($this->block_y_mode[$count] ?? 0);
            }
        } else {
            $left = $sub_modes[$block - 1];
        }
        return $this->readTree(self::INTRA_MODE_TREE,
            Vp9Tables::KF_YMODE_PROBS[$above][$left]);
    }
    /**
     * residual reads and reconstructs every transform block of one plane of a
     * block.
     *
     * @param int $block_row which row of blocks
     * @param int $block_col which column of blocks
     * @param int $b_size how large the block is
     * @param int $transform_size how large a transform the block uses
     * @param array $sub_modes the ways the four smaller blocks are guessed
     * @param int $color_mode which way the color blocks are guessed
     * @param int $skip whether the block was left out of the stream
     */
    private function residual(
        int $block_row, int $block_col, int $b_size, int $transform_size,
            array $sub_modes,
        int $color_mode, int $skip
    ): void {
        $plane_size = max($b_size, 3);
        for ($plane = 0; $plane < 3; $plane++) {
            $sub_x = $plane ? $this->ss_x : 0;
            $sub_y = $plane ? $this->ss_y : 0;
            if ($plane === 0) {
                $transform_size = $transform_size;
                $four_blocks_across = self::BW4[$plane_size];
                $four_blocks_down = self::BH4[$plane_size];
            } else {
                $transform_size = min($transform_size,
                    self::MAX_TX_SIZE[
                        $this->uvBlockSize($plane_size, $sub_x, $sub_y)]);
                $four_blocks_across = max(1, self::BW4[$plane_size] >> $sub_x);
                $four_blocks_down = max(1, self::BH4[$plane_size] >> $sub_y);
            }
            $step = 1 << $transform_size;
            $base_x = ($block_col * 8) >> $sub_x;
            $base_y = ($block_row * 8) >> $sub_y;
            $max_x = ($this->block_cols * 8) >> $sub_x;
            $max_y = ($this->block_rows * 8) >> $sub_y;
            $block_position = 0;
            for ($row = 0; $row < $four_blocks_down; $row += $step) {
                for ($col = 0; $col < $four_blocks_across; $col += $step) {
                    $start_x = $base_x + 4 * $col;
                    $start_y = $base_y + 4 * $row;
                    $block_value_four = $start_x >> 2;
                    $block_row_four = $start_y >> 2;
                    if ($start_x < $max_x && $start_y < $max_y) {
                        $mode = ($b_size < 3 && $plane === 0)
                            ? $sub_modes[min(3, $block_position)]
                            : ($plane === 0 ? $sub_modes[0] : $color_mode);
                        $this->predictBlock(
                            $plane, $start_x, $start_y, $transform_size, $mode,
                            $start_x > (($this->tile_col_start * 8) >> $sub_x),
                            $start_y > 0,
                            ($col + $step) < $four_blocks_across
                        );
                        $value = null;
                        if ($skip === 0) {
                            $value = $this->readBlockTokens($plane,
                                $block_value_four,
                                $block_row_four, $transform_size, $mode);
                        } else {
                            $this->setNonzero($plane, $block_value_four,
                                $block_row_four,
                                $transform_size, 0);
                        }
                        if ($value !== null) {
                            $this->addResidual($plane, $start_x, $start_y,
                                $transform_size,
                                $value, $mode);
                        }
                    } else {
                        $this->setNonzero($plane, $block_value_four,
                            $block_row_four,
                            $transform_size, 0);
                    }
                    $block_position += $step * $step;
                }
            }
        }
    }
    /**
     * uvBlockSize the block size the chroma planes use, which is smaller than
     * the luma one when the chroma is subsampled.
     *
     * @param int $b_size how large the block is
     * @param int $sub_x how much the color planes are shrunk across
     * @param int $sub_y how much the color planes are shrunk down
     * @return int what was read
     */
    private function uvBlockSize(int $b_size, int $sub_x, int $sub_y): int
    {
        $wide = max(1, self::BW4[$b_size] >> $sub_x);
        $header = max(1, self::BH4[$b_size] >> $sub_y);
        for ($i = 0; $i < 13; $i++) {
            if (self::BW4[$i] === $wide && self::BH4[$i] === $header) {
                return $i;
            }
        }
        return 3;
    }
    /**
     * setNonzero records which parts of a transform block held coefficients, so
     * the blocks beside it can choose their probabilities.
     *
     * @param int $plane zero for luma, one and two for the chroma planes
     * @param int $block_value_four which four by four block across
     * @param int $block_row_four which four by four block down
     * @param int $transform_size how large a transform the block uses
     * @param int $value the value read
     */
    private function setNonzero(int $plane, int $block_value_four,
        int $block_row_four,
        int $transform_size,
        int $value): void
    {
        $count = 1 << $transform_size;
        for ($i = 0; $i < $count; $i++) {
            if (isset($this
                ->above_value_count[$plane][$block_value_four + $i])) {
                $this->above_value_count[$plane][$block_value_four +
                    $i] = $value;
            }
            $this->left_value_count[$plane][($block_row_four + $i) & 15] =
                $value;
        }
    }
    /**
     * readBlockTokens reads the values of one block, largest first, stopping
     * when the block says it holds no more.
     *
     * @return int the number of coefficients decoded
     * @param int $plane which of the picture's planes, brightness or color
     * @param int $block_value_four which four by four block across
     * @param int $block_row_four which four by four block down
     * @param int $transform_size how large a transform the block uses
     * @param int $mode which way the block is guessed from its neighbors
     */
    private function readBlockTokens(int $plane, int $block_value_four,
        int $block_row_four,
        int $transform_size,
        int $mode): ?array
    {
        $seg_end_of_block = 16 << ($transform_size << 1);
        [$scan, $neighbor] = $this->scanFor($plane, $transform_size, $mode);
        /* the first coefficient's context comes from whether the neighboring
          */
        /* transform blocks had any coefficients at all */
        $count = 1 << $transform_size;
        $above_value_count = 0;
        $left_value_count = 0;
        for ($i = 0; $i < $count; $i++) {
            $above_value_count |= $this
                ->above_value_count[$plane][$block_value_four + $i] ?? 0;
            $left_value_count |= $this
                ->left_value_count[$plane][($block_row_four + $i) & 15];
        }
        $context = ($above_value_count !==
            0 ? 1 : 0) + ($left_value_count !== 0 ? 1 : 0);
        $probs_for_transform = $this->header
            ->value_probs[$transform_size][$plane > 0 ? 1 : 0][0];
        $cache = array_fill(0, $seg_end_of_block, 0);
        $values = array_fill(0, $seg_end_of_block, 0);
        $quantizer_change = $this->descale[$this->current_segment][$plane >
            0 ? 1 : 0];
        $col = 0;
        $check_end_of_block = true;
        while ($col < $seg_end_of_block) {
            $band = ($transform_size === 0) ? self::BAND_4X4[$col]
                : ($col < 21 ? self::BAND_8X8_HEAD[$col] : 5);
            $probs = $probs_for_transform[$band][$context];
            if ($check_end_of_block) {
                if ($this->reader->readOneBit($probs[0]) === 0) {
                    break;
                }
            }
            if ($this->reader->readOneBit($probs[1]) === 0) {
                $cache[$scan[$col]] = 0;
                $col++;
                if ($col >= $seg_end_of_block) {
                    break;
                }
                /* the neighbor table is indexed by the position just decoded
                  */
                $context
                    = (1 + $cache[$neighbor[$col - 1][0]]
                        + $cache[$neighbor[$col - 1][1]]) >> 1;
                $check_end_of_block = false;
                continue;
            }
            $check_end_of_block = true;
            if ($this->reader->readOneBit($probs[2]) === 0) {
                $token = 1;
                $value = 1;
            } else {
                $entry = Vp9Tables::MODEL_PARETO8[$probs[2]];
                if ($this->reader->readOneBit($entry[0]) === 0) {
                    if ($this->reader->readOneBit($entry[1]) === 0) {
                        $token = 2;
                        $value = 2;
                    } else {
                        $token = $this->reader
                            ->readOneBit($entry[2]) === 0 ? 3 : 4;
                        $value = $token;
                    }
                } else {
                    if ($this->reader->readOneBit($entry[3]) === 0) {
                        $block_kind = $this->reader->readOneBit($entry[4]) === 0
                            ? 0 : 1;
                    } elseif ($this->reader->readOneBit($entry[5]) === 0) {
                        $block_kind = $this->reader->readOneBit($entry[6]) === 0
                            ? 2 : 3;
                    } else {
                        $block_kind = $this->reader->readOneBit($entry[7]) === 0
                            ? 4 : 5;
                    }
                    $token = 5 + $block_kind;
                    $extra = 0;
                    foreach (self::CAT_PROBS[$block_kind] as $prob) {
                        $extra = ($extra << 1) | $this->reader
                            ->readOneBit($prob);
                    }
                    $value = self::CAT_MIN[$block_kind] + $extra;
                }
            }
            $position = $scan[$col];
            $quant = ($col === 0) ? $quantizer_change[0] : $quantizer_change[1];
            /*
                Coefficients are held in sixteen bits, so large values wrap.
                The largest transform halves each value, because its two
                passes scale up less than the smaller sizes do. The halving
                is done before the sign is applied, so it rounds toward
                zero rather than downward.
            */
            $scaled = $value * $quant;
            if ($transform_size === 3) {
                $scaled >>= self::TX32_DEQUANT_SHIFT;
            }
            if ($this->reader->readOneBit(128)) {
                $scaled = -$scaled;
            }
            $values[$position] = self::wrapToSixteenBits($scaled);
            $cache[$position] = self::ENERGY_CLASS[$token];
            $col++;
            if ($col >= $seg_end_of_block) {
                break;
            }
            $context = (1 + $cache[$neighbor[$col - 1][0]]
                + $cache[$neighbor[$col - 1][1]]) >> 1;
        }
        $k = $col === 0 ? "0" : ($col === 1 ? "1" : ($col < 5 ? "2-4" : "5+"));
        $this->end_of_block_hist[$k] = ($this->end_of_block_hist[$k] ?? 0) + 1;
        $this->setNonzero($plane, $block_value_four,
            $block_row_four,
            $transform_size,
            $col > 0 ? 1 : 0);
        if ($col > 0) {
            $this
                ->coefficients["$plane:$block_value_four:$block_row_four"] =
                [$transform_size, $values];
            return $values;
        }
        return null;
    }
    /**
     * scanFor gives the order the values of a block are visited in, along with
     * the two neighbors of each place in that order. Which order is used
     * depends on how the block was guessed and how large its transform is, and
     * the neighbors say how likely the next value is.
     *
     * @param int $plane which of the picture's planes, brightness or color
     * @param int $transform_size how large a transform the block uses
     * @param int $mode which way the block is guessed from its neighbors
     * @return array the order, and the two neighbors of each place in it
     */
    private function scanFor(int $plane, int $transform_size,
        int $mode): array
    {
        $use_default = $plane > 0 || $transform_size === 3 || $this->header
            ->lossless;
        $which = $use_default ? 0
            : self::TX_TYPE_SCAN[self::MODE_TX_TYPE[$mode]];
        static $map = [
            0 => [['SCAN_4X4', 'SCAN_4X4_NB'],
                ['ROW_SCAN_4X4', 'ROW_SCAN_4X4_NB'],
                ['COL_SCAN_4X4', 'COL_SCAN_4X4_NB']],
            1 => [['SCAN_8X8', 'SCAN_8X8_NB'],
                ['ROW_SCAN_8X8', 'ROW_SCAN_8X8_NB'],
                ['COL_SCAN_8X8', 'COL_SCAN_8X8_NB']],
            2 => [['SCAN_16X16', 'SCAN_16X16_NB'],
                ['ROW_SCAN_16X16', 'ROW_SCAN_16X16_NB'],
                ['COL_SCAN_16X16', 'COL_SCAN_16X16_NB']],
            3 => [['SCAN_32X32', 'SCAN_32X32_NB'],
                ['SCAN_32X32', 'SCAN_32X32_NB'],
                ['SCAN_32X32', 'SCAN_32X32_NB']],
        ];
        [$sample, $count] = $map[$transform_size][$which];
        /* The class is named in a string here, and a string names
           no namespace, so the whole name is spelled out; without it
           the class was looked for outside this folder. */
        $tables = Vp9Tables::class;
        return [constant("$tables::$sample"),
            constant("$tables::$count")];
    }
    /**
     * $planes stores the brightness and color values of the picture being
     * built.
     * @var array
     */
    private array $planes = [];
    /**
     * $strides stores how many values one row of each plane takes.
     * @var array
     */
    private array $strides = [];
    /**
     * $origin_off stores where the picture proper starts inside each plane,
     * since the decoder works in whole squares and may write past the frame's
     * edge.
     * @var array
     */
    private array $origin_off = [];
    /**
     * $plane_wide stores display width of each plane, which prediction may not
     * read past.
     * @var array
     */
    private array $plane_wide = [];
    /**
     * $plane_tall stores display height of each plane, which prediction may not
     * read past.
     * @var array
     */
    private array $plane_tall = [];
    /**
     * allocatePlanes sets up the sample planes with room for whole superblocks
     * and a border, so a block may reach past the picture without running off
     * the end.
     */
    private function allocatePlanes(): void
    {
        foreach ([0, 1, 2] as $plane_at) {
            /*
                A block may reach past the last mode info unit of the
                picture, by up to a whole superblock, so the planes are
                sized in superblocks rather than in mode info units. The
                samples past the picture are reconstructed like any others
                and neighboring blocks predict from them.
            */
            $wide = ($this->superblock_cols * self::SUPERBLOCK_SAMPLES)
                >> ($plane_at ? $this->ss_x : 0);
            $tall = ($this->superblock_rows * self::SUPERBLOCK_SAMPLES)
                >> ($plane_at ? $this->ss_y : 0);
            /* room for above-right lookups */
            $stride = $wide + 80;
            $this->strides[$plane_at] = $stride;
            $this->origin_off[$plane_at] = $stride + 8;
            $plane = array_fill(0, $stride * ($tall + 1), 129);
            for ($col = 0; $col < $stride; $col++) {
                /* the row above the frame */
                $plane[$col] = 127;
            }
            $this->planes[$plane_at] = $plane;
            /*
                Prediction may not read past the mode info grid, which
                rounds the picture up to whole eight sample units, even
                though the planes hold whole superblocks. Samples past
                that edge repeat the last one inside it.
            */
            $this->plane_wide[$plane_at] = ($this->block_cols * 8)
                >> ($plane_at ? $this->ss_x : 0);
            $this->plane_tall[$plane_at] = ($this->block_rows * 8)
                >> ($plane_at ? $this->ss_y : 0);
        }
    }
    /**
     * edgeSample reads one sample of a plane, repeating the edge sample rather
     * than reading past the picture.
     *
     * @param int $plane_at 0 for luma, 1 and 2 for the chroma planes
     * @param int $col column wanted, which may lie past the picture
     * @param int $line row wanted, which may lie past the picture
     * @return int sample value
     * @param mixed $clip_right how far the picture is cut on the right
     * @param mixed $clip_down how far the picture is cut at the bottom
     */
    private function edgeSample($plane_at, $col, $line, $clip_right,
        $clip_down)
    {
        if ($clip_right) {
            $last = $this->plane_wide[$plane_at] - 1;
            if ($col > $last) {
                $col = $last;
            }
        }
        if ($clip_down) {
            $last = $this->plane_tall[$plane_at] - 1;
            if ($line > $last) {
                $line = $last;
            }
        }
        return $this->planes[$plane_at][$this->sampleAt($plane_at, $col,
            $line)];
    }
    /**
     * overhang says whether a block of the given size reaches past the mode
     * info grid, in which case its prediction repeats the picture's edge
     * samples rather than reading whatever lies beyond.
     *
     * @param int $plane_at 0 for luma, 1 and 2 for the chroma planes
     * @param int $start first column of the block
     * @param int $top first row of the block
     * @param int $smooth_strength samples along one side of the block
     * @return array pair saying whether to clip right and to clip down
     */
    private function overhang($plane_at, $start, $top, $smooth_strength)
    {
        $sub_x = $plane_at ? $this->ss_x : 0;
        $sub_y = $plane_at ? $this->ss_y : 0;
        $grid_wide = ($this->block_cols * 8) >> $sub_x;
        $grid_tall = ($this->block_rows * 8) >> $sub_y;
        return [$start + $smooth_strength > $grid_wide, $top +
            $smooth_strength > $grid_tall];
    }
    /**
     * sampleAt works out where one sample sits in a plane. for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $col which column
     * @param int $row which row
     * @return int what was read
     */
    private function sampleAt(int $plane_at, int $col, int $row): int
    {
        return $this->origin_off[$plane_at] + $row *
            $this->strides[$plane_at] + $col;
    }
    /**
     * holdInsideByte holds a sample inside the range a byte can carry.
     *
     * @param int $value the value read
     * @return int what was read
     */
    private static function holdInsideByte(int $value): int
    {
        return $value < 0 ? 0 : ($value > 255 ? 255 : $value);
    }
    /**
     * wrapToSixteenBits wrap to a signed sixteen-bit value, as the coefficient
     * store does
     *
     * @param int $value the value read
     * @return int what was read
     */
    private static function wrapToSixteenBits(int $value): int
    {
        $value &= 0xFFFF;
        return $value >= 0x8000 ? $value - 0x10000 : $value;
    }
    /**
     * averageOfTwo blends two samples that sit beside one another, rounding up.
     * Guessing a block from its neighbors leans on this, since a guessed sample
     * is usually the average of the ones nearest it.
     *
     * @param int $first the first one
     * @param int $second the other one
     * @return int what was read
     */
    private static function averageOfTwo(int $first, int $second): int
    {
        return ($first + $second + 1) >> 1;
    }
    /**
     * averageOfThree average of three neighboring samples, weighted toward the
     * middle one.
     *
     * @param int $first the first one
     * @param int $second the other one
     * @param int $col which column
     * @return int what was read
     */
    private static function averageOfThree(int $first, int $second,
        int $col): int
    {
        return ($first + 2 * $second + $col + 2) >> 2;
    }
    /**
     * predict4x4 intra prediction for one four by four transform block. Missing
     * edges read 127 above and 129 to the left, which the plane borders provide
     * directly. for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param int $mode which way the block is guessed from its neighbors
     * @param bool $have_left whether the block to the left is there to read
     * @param bool $have_above whether the block above is there to read
     * @param bool $have_right whether the block to the right is there to read
     */
    private function predict4x4(int $plane_at, int $block_x, int $block_y,
        int $mode,
        bool $have_left, bool $have_above, bool $have_right): void
    {
        $stride = $this->strides[$plane_at];
        $base = $this->sampleAt($plane_at, $block_x, $block_y);
        list($clip_right, $clip_down) = $this->overhang($plane_at, $block_x,
            $block_y, 4);
        $above = [];
        if ($have_above) {
            for ($i = 0; $i < 4; $i++) {
                $above[$i] = $this->edgeSample($plane_at, $block_x + $i,
                    $block_y - 1, $clip_right, $clip_down);
            }
            for ($i = 4; $i < 8; $i++) {
                $above[$i] = $have_right
                    ? $this->edgeSample($plane_at, $block_x + $i, $block_y - 1,
                    $clip_right, $clip_down)
                    : $above[3];
            }
            $corner = $have_left ? $this->planes[$plane_at][$base - $stride - 1]
                : 129;
        } else {
            $above = array_fill(0, 8, 127);
            $corner = 127;
        }
        $left = [];
        if ($have_left) {
            for ($i = 0; $i < 4; $i++) {
                $left[$i] = $this->edgeSample($plane_at, $block_x - 1,
                    $block_y + $i, $clip_right, $clip_down);
            }
        } else {
            $left = [129, 129, 129, 129];
        }
        $predicted = array_fill(0, 16, 0);
        switch ($mode) {
            /* DC, with the variant chosen by which edges exist */
            case 0:
                if ($have_above && $have_left) {
                    $first_value
 = (array_sum(array_slice($above, 0, 4)) + array_sum($left) + 4) >> 3;
                } elseif ($have_above) {
                    $first_value = (array_sum(array_slice($above, 0, 4)) +
                        2) >> 2;
                } elseif ($have_left) {
                    $first_value = (array_sum($left) + 2) >> 2;
                } else {
                    $first_value = 128;
                }
                $predicted = array_fill(0, 16, $first_value);
                break;
            /* vertical */
            case 1:
                for ($row = 0; $row < 4; $row++) {
                    for ($col = 0; $col < 4; $col++) {
                        $predicted[$row * 4 + $col] = $above[$col];
                    }
                }
                break;
            /* horizontal */
            case 2:
                for ($row = 0; $row < 4; $row++) {
                    for ($col = 0; $col < 4; $col++) {
                        $predicted[$row * 4 + $col] = $left[$row];
                    }
                }
                break;
            /* down-left */
            case 3:
                $predicted[0 * 4 + 0]
                    = self::averageOfThree($above[0], $above[1], $above[2]);
                $predicted[0 * 4 + 1]
                    = self::averageOfThree($above[1], $above[2], $above[3]);
                $predicted[1 * 4 + 0]
                    = self::averageOfThree($above[1], $above[2], $above[3]);
                $predicted[0 * 4 + 2]
                    = self::averageOfThree($above[2], $above[3], $above[4]);
                $predicted[1 * 4 + 1]
                    = self::averageOfThree($above[2], $above[3], $above[4]);
                    $predicted[2 * 4 + 0]
                        = self::averageOfThree($above[2], $above[3], $above[4]);
                $predicted[0 * 4 + 3]
                    = self::averageOfThree($above[3], $above[4], $above[5]);
                $predicted[1 * 4 + 2]
                    = self::averageOfThree($above[3], $above[4], $above[5]);
                    $predicted[2 * 4 + 1]
                        = self::averageOfThree($above[3], $above[4], $above[5]);
                    $predicted[3 * 4 + 0]
                        = self::averageOfThree($above[3], $above[4], $above[5]);
                $predicted[1 * 4 + 3]
                    = self::averageOfThree($above[4], $above[5], $above[6]);
                $predicted[2 * 4 + 2]
                    = self::averageOfThree($above[4], $above[5], $above[6]);
                    $predicted[3 * 4 + 1]
                        = self::averageOfThree($above[4], $above[5], $above[6]);
                $predicted[2 * 4 + 3]
                    = self::averageOfThree($above[5], $above[6], $above[7]);
                $predicted[3 * 4 + 2]
                    = self::averageOfThree($above[5], $above[6], $above[7]);
                $predicted[3 * 4 + 3] = $above[7];
                break;
            /* down-right */
            case 4:
                $predicted[3 * 4 + 0]
                    = self::averageOfThree($left[1], $left[2], $left[3]);
                $predicted[3 * 4 + 1]
                    = self::averageOfThree($left[0], $left[1], $left[2]);
                $predicted[2 * 4 + 0]
                    = self::averageOfThree($left[0], $left[1], $left[2]);
                $predicted[3 * 4 + 2] = self::averageOfThree($corner,
                    $left[0], $left[1]);
                $predicted[2 * 4 + 1]
                    = self::averageOfThree($corner, $left[0], $left[1]);
                    $predicted[1 * 4 + 0]
                        = self::averageOfThree($corner, $left[0], $left[1]);
                $predicted[3 * 4 + 3]
                    = self::averageOfThree($above[0], $corner, $left[0]);
                    $predicted[2 * 4 + 2]
                        = self::averageOfThree($above[0], $corner, $left[0]);
                    $predicted[1 * 4 + 1]
                        = self::averageOfThree($above[0], $corner, $left[0]);
                    $predicted[0 * 4 + 0]
                        = self::averageOfThree($above[0], $corner, $left[0]);
                $predicted[2 * 4 + 3]
                    = self::averageOfThree($above[1], $above[0], $corner);
                $predicted[1 * 4 + 2]
                    = self::averageOfThree($above[1], $above[0], $corner);
                    $predicted[0 * 4 + 1]
                        = self::averageOfThree($above[1], $above[0], $corner);
                $predicted[1 * 4 + 3]
                    = self::averageOfThree($above[2], $above[1], $above[0]);
                $predicted[0 * 4 + 2]
                    = self::averageOfThree($above[2], $above[1], $above[0]);
                $predicted[0 * 4 + 3]
                    = self::averageOfThree($above[3], $above[2], $above[1]);
                break;
            /* vertical-right */
            case 5:
                $predicted[0 * 4 + 0]
                    = self::averageOfTwo($corner,
                        $above[0]); $predicted[2 * 4 + 1]
                        = self::averageOfTwo($corner, $above[0]);
                $predicted[0 * 4 + 1]
                    = self::averageOfTwo($above[0],
                        $above[1]); $predicted[2 * 4 + 2]
                        = self::averageOfTwo($above[0], $above[1]);
                $predicted[0 * 4 + 2]
                    = self::averageOfTwo($above[1],
                        $above[2]); $predicted[2 * 4 + 3]
                        = self::averageOfTwo($above[1], $above[2]);
                $predicted[0 * 4 + 3] = self::averageOfTwo($above[2],
                    $above[3]);
                $predicted[3 * 4 + 0]
                    = self::averageOfThree($left[2], $left[1], $left[0]);
                $predicted[2 * 4 + 0] = self::averageOfThree($left[1],
                    $left[0], $corner);
                $predicted[1 * 4 + 0]
                    = self::averageOfThree($left[0], $corner, $above[0]);
                    $predicted[3 * 4 + 1]
                        = self::averageOfThree($left[0], $corner, $above[0]);
                $predicted[1 * 4 + 1]
                    = self::averageOfThree($corner, $above[0], $above[1]);
                $predicted[3 * 4 + 2]
                    = self::averageOfThree($corner, $above[0], $above[1]);
                $predicted[1 * 4 + 2]
                    = self::averageOfThree($above[0], $above[1], $above[2]);
                $predicted[3 * 4 + 3]
                    = self::averageOfThree($above[0], $above[1], $above[2]);
                $predicted[1 * 4 + 3]
                    = self::averageOfThree($above[1], $above[2], $above[3]);
                break;
            /* horizontal-down */
            case 6:
                $predicted[0 * 4 + 0]
                    = self::averageOfTwo($left[0],
                        $corner); $predicted[1 * 4 + 2]
                        = self::averageOfTwo($left[0], $corner);
                $predicted[1 * 4 + 0]
                    = self::averageOfTwo($left[1],
                        $left[0]); $predicted[2 * 4 + 2]
                        = self::averageOfTwo($left[1], $left[0]);
                $predicted[2 * 4 + 0]
                    = self::averageOfTwo($left[2],
                        $left[1]); $predicted[3 * 4 + 2]
                        = self::averageOfTwo($left[2], $left[1]);
                $predicted[3 * 4 + 0] = self::averageOfTwo($left[3], $left[2]);
                $predicted[0 * 4 + 3]
                    = self::averageOfThree($above[0], $above[1], $above[2]);
                $predicted[0 * 4 + 2]
                    = self::averageOfThree($corner, $above[0], $above[1]);
                $predicted[0 * 4 + 1]
                    = self::averageOfThree($left[0], $corner, $above[0]);
                    $predicted[1 * 4 + 3]
                        = self::averageOfThree($left[0], $corner, $above[0]);
                $predicted[1 * 4 + 1] = self::averageOfThree($left[1],
                    $left[0], $corner);
                $predicted[2 * 4 + 3] = self::averageOfThree($left[1],
                    $left[0], $corner);
                $predicted[2 * 4 + 1]
                    = self::averageOfThree($left[2], $left[1], $left[0]);
                $predicted[3 * 4 + 3]
                    = self::averageOfThree($left[2], $left[1], $left[0]);
                $predicted[3 * 4 + 1]
                    = self::averageOfThree($left[3], $left[2], $left[1]);
                break;
            /* down-left of the left column */
            case 7:
                $predicted[0 * 4 + 0] = self::averageOfTwo($left[0], $left[1]);
                $predicted[0 * 4 + 2]
                    = self::averageOfTwo($left[1],
                        $left[2]); $predicted[1 * 4 + 0]
                        = self::averageOfTwo($left[1], $left[2]);
                $predicted[1 * 4 + 2]
                    = self::averageOfTwo($left[2],
                        $left[3]); $predicted[2 * 4 + 0]
                        = self::averageOfTwo($left[2], $left[3]);
                $predicted[0 * 4 + 1]
                    = self::averageOfThree($left[0], $left[1], $left[2]);
                $predicted[0 * 4 + 3]
                    = self::averageOfThree($left[1], $left[2], $left[3]);
                $predicted[1 * 4 + 1]
                    = self::averageOfThree($left[1], $left[2], $left[3]);
                $predicted[1 * 4 + 3]
                    = self::averageOfThree($left[2], $left[3], $left[3]);
                $predicted[2 * 4 + 1]
                    = self::averageOfThree($left[2], $left[3], $left[3]);
                $flat_cells = [[3, 2], [2, 2], [0, 3], [1, 3], [2, 3],
                    [3, 3]];
                foreach ($flat_cells as [$col, $row]) {
                    $predicted[$row * 4 + $col] = $left[3];
                }
                break;
            /* vertical-left */
            case 8:
                $predicted[0 * 4 + 0] = self::averageOfTwo($above[0],
                    $above[1]);
                $predicted[0 * 4 + 1]
                    = self::averageOfTwo($above[1],
                        $above[2]); $predicted[2 * 4 + 0]
                        = self::averageOfTwo($above[1], $above[2]);
                $predicted[0 * 4 + 2]
                    = self::averageOfTwo($above[2],
                        $above[3]); $predicted[2 * 4 + 1]
                        = self::averageOfTwo($above[2], $above[3]);
                $predicted[0 * 4 + 3]
                    = self::averageOfTwo($above[3],
                        $above[4]); $predicted[2 * 4 + 2]
                        = self::averageOfTwo($above[3], $above[4]);
                /* this corner uses a two-tap average, unlike the rest of the */
                /* lower triangle */
                $predicted[2 * 4 + 3] = self::averageOfTwo($above[4],
                    $above[5]);
                $predicted[1 * 4 + 0]
                    = self::averageOfThree($above[0], $above[1], $above[2]);
                $predicted[1 * 4 + 1]
                    = self::averageOfThree($above[1], $above[2], $above[3]);
                $predicted[3 * 4 + 0]
                    = self::averageOfThree($above[1], $above[2], $above[3]);
                $predicted[1 * 4 + 2]
                    = self::averageOfThree($above[2], $above[3], $above[4]);
                $predicted[3 * 4 + 1]
                    = self::averageOfThree($above[2], $above[3], $above[4]);
                $predicted[1 * 4 + 3]
                    = self::averageOfThree($above[3], $above[4], $above[5]);
                $predicted[3 * 4 + 2]
                    = self::averageOfThree($above[3], $above[4], $above[5]);
                $predicted[3 * 4 + 3]
                    = self::averageOfThree($above[4], $above[5], $above[6]);
                break;
            /* true motion */
            case 9:
                for ($row = 0; $row < 4; $row++) {
                    for ($col = 0; $col < 4; $col++) {
                        $predicted[$row * 4 + $col]
                            = self::holdInsideByte($left[$row]
                                + $above[$col] - $corner);
                    }
                }
                break;
            default:
                throw new VideoException("bad VP9 intra mode $mode");
        }
        for ($row = 0; $row < 4; $row++) {
            $row_start = $base + $row * $stride;
            for ($col = 0; $col < 4; $col++) {
                $this->planes[$plane_at][$row_start + $col] =
                    $predicted[$row * 4 + $col];
            }
        }
    }
    /**
     * iwht4x4Add the lossless inverse transform, added into the prediction for
     * color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param array $value the values the block was coded as
     */
    private function iwht4x4Add(int $plane_at, int $block_x, int $block_y,
        array $value): void
    {
        $held = array_fill(0, 16, 0);
        for ($i = 0; $i < 4; $i++) {
            $origin = $i * 4;
            $left_one = $value[$origin] >> 2;
            $corner_one = $value[$origin + 1] >> 2;
            $difference_one = $value[$origin + 2] >> 2;
            $bit_one = $value[$origin + 3] >> 2;
            $left_one += $corner_one;
            $difference_one -= $bit_one;
            $edge_one = ($left_one - $difference_one) >> 1;
            $bit_one = $edge_one - $bit_one;
            $corner_one = $edge_one - $corner_one;
            $left_one -= $bit_one;
            $difference_one += $corner_one;
            $held[$origin] =
                self::wrapToSixteenBits($left_one); $held[$origin + 1] =
                self::wrapToSixteenBits($bit_one);
            $held[$origin
                + 2] = self::wrapToSixteenBits($corner_one); $held[$origin + 3]
                = self::wrapToSixteenBits($difference_one);
        }
        $stride = $this->strides[$plane_at];
        $base = $this->sampleAt($plane_at, $block_x, $block_y);
        for ($i = 0; $i < 4; $i++) {
            $left_one = $held[$i];
            $corner_one = $held[4 + $i];
            $difference_one = $held[8 + $i];
            $bit_one = $held[12 + $i];
            $left_one += $corner_one;
            $difference_one -= $bit_one;
            $edge_one = ($left_one - $difference_one) >> 1;
            $bit_one = $edge_one - $bit_one;
            $corner_one = $edge_one - $corner_one;
            $left_one -= $bit_one;
            $difference_one += $corner_one;
            foreach ([$left_one, $bit_one, $corner_one,
                $difference_one] as $k => $value) {
                $at = $base + $k * $stride + $i;
                $this->planes[$plane_at][$at]
                    = self::holdInsideByte($this
                        ->planes[$plane_at][$at] + $value);
            }
        }
    }
    /**
     * toPicture crop to the display size and hand back the planes
     *
     * @return VideoPicture what was read
     */
    public function toPicture(): VideoPicture
    {
        $this->loopFilter();
        $wide = $this->superblock_cols * self::SUPERBLOCK_SAMPLES;
        $tall = $this->superblock_rows * self::SUPERBLOCK_SAMPLES;
        $chroma_wide = $wide >> $this->ss_x;
        $chroma_tall = $tall >> $this->ss_y;
        $luma = array_fill(0, $wide * $tall, 0);
        for ($line = 0; $line < $tall; $line++) {
            $from = $this->sampleAt(0, 0, $line);
            $into = $line * $wide;
            for ($col = 0; $col < $wide; $col++) {
                $luma[$into + $col] = $this->planes[0][$from + $col];
            }
        }
        $blue = array_fill(0, $chroma_wide * $chroma_tall, 0);
        $red = array_fill(0, $chroma_wide * $chroma_tall, 0);
        for ($line = 0; $line < $chroma_tall; $line++) {
            $from_blue = $this->sampleAt(1, 0, $line);
            $from_red = $this->sampleAt(2, 0, $line);
            $into = $line * $chroma_wide;
            for ($col = 0; $col < $chroma_wide; $col++) {
                $blue[$into + $col] = $this->planes[1][$from_blue + $col];
                $red[$into + $col] = $this->planes[2][$from_red + $col];
            }
        }
        return new VideoPicture($luma, $blue, $red, $this->header->frame_width,
            $this->header->frame_height, $wide, $chroma_wide, 0, 0, $this->ss_x,
            $this->ss_y);
    }
    /**
     * predictBlock fills a transform block with a guess made from the samples
     * above it and to its left. for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param int $transform_size how large a transform the block uses
     * @param int $mode which way the block is guessed from its neighbors
     * @param bool $have_left whether the block to the left is there to read
     * @param bool $have_above whether the block above is there to read
     * @param bool $have_right whether the block to the right is there to read
     */
    private function predictBlock(int $plane_at, int $block_x, int $block_y,
        int $transform_size,
        int $mode, bool $have_left, bool $have_above, bool $have_right): void
    {
        if ($transform_size === 0) {
            $this->predict4x4($plane_at, $block_x, $block_y, $mode, $have_left,
                $have_above,
                $have_right);
            return;
        }
        $this->predictN($plane_at, $block_x, $block_y, 4 << $transform_size,
            $mode, $have_left,
            $have_above, $have_right);
    }
    /**
     * addResidual transforms one block of coefficients and adds the result to
     * the prediction already in the plane. for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param int $transform_size how large a transform the block uses
     * @param array $value the values the block was coded as
     * @param int $mode which way the block is guessed from its neighbors
     */
    private function addResidual(int $plane_at, int $block_x, int $block_y,
        int $transform_size,
        array $value, int $mode): void
    {
        if ($this->header->lossless) {
            $this->iwht4x4Add($plane_at, $block_x, $block_y, $value);
            return;
        }
        /* only luma picks a transform type from the prediction mode */
        $transform_type = ($plane_at === 0) ? self::MODE_TX_TYPE[$mode] : 0;
        if ($transform_size === 0) {
            $this->idct4x4Add($plane_at, $block_x, $block_y, $value,
                $transform_type);
            return;
        }
        if ($transform_size === 1) {
            $this->idct8x8Add($plane_at, $block_x, $block_y, $value,
                $transform_type);
            return;
        }
        if ($transform_size === 2) {
            $this->idct16x16Add($plane_at, $block_x, $block_y, $value,
                $transform_type);
            return;
        }
        $this->idct32x32Add($plane_at, $block_x, $block_y, $value);
    }
    /**
     * COSPI_8 is the the cosines the transform multiplies by 8 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_8 = 15137;
    /**
     * COSPI_16 is the the cosines the transform multiplies by 16 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_16 = 11585;
    /**
     * COSPI_24 is the the cosines the transform multiplies by 24 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_24 = 6270;
    /**
     * SINPI_1 is the the sines the transform multiplies by 1 the format fixes.
     * @var mixed
     */
    private const SINPI_1 = 5283;
    /**
     * SINPI_2 is the the sines the transform multiplies by 2 the format fixes.
     * @var mixed
     */
    private const SINPI_2 = 9929;
    /**
     * SINPI_3 is the the sines the transform multiplies by 3 the format fixes.
     * @var mixed
     */
    private const SINPI_3 = 13377;
    /**
     * SINPI_4 is the the sines the transform multiplies by 4 the format fixes.
     * @var mixed
     */
    private const SINPI_4 = 15212;
    /**
     * roundShift the fixed-point rounding every transform stage uses
     *
     * @param int $value the value read
     * @return int what was read
     */
    private static function roundShift(int $value): int
    {
        return ($value + 8192) >> 14;
    }
    /**
     * inverseCosine4 turns four values back into differences from what was
     * guessed, using the transform VP9 uses for most blocks.
     *
     * @param array $values the values to transform
     * @return array what was read
     */
    private static function inverseCosine4(array $values): array
    {
        $sum_zero = self::roundShift(($values[0] + $values[2]) *
            self::COSPI_16);
        $sum_one = self::roundShift(($values[0] - $values[2]) * self::COSPI_16);
        $sum_two = self::roundShift($values[1] * self::COSPI_24
            - $values[3] * self::COSPI_8);
        $sum_three = self::roundShift($values[1] * self::COSPI_8
            + $values[3] * self::COSPI_24);
        return [$sum_zero + $sum_three, $sum_one + $sum_two, $sum_one -
            $sum_two, $sum_zero - $sum_three];
    }
    /**
     * inverseSine4 turns four values back into differences from what was
     * guessed, using the transform that suits a block whose guess
     * runs one way
     * across it, where a different transform suits.
     *
     * @param array $values the values to transform
     * @return array what was read
     */
    private static function inverseSine4(array $values): array
    {
        [$block_x, $value_one, $value_two, $value_three] = $values;
        if (($block_x | $value_one | $value_two | $value_three) === 0) {
            return [0, 0, 0, 0];
        }
        $sum_zero = self::SINPI_1 * $block_x;
        $sum_one = self::SINPI_2 * $block_x;
        $sum_two = self::SINPI_3 * $value_one;
        $sum_three = self::SINPI_4 * $value_two;
        $sum_four = self::SINPI_1 * $value_two;
        $sum_five = self::SINPI_2 * $value_three;
        $sum_six = self::SINPI_4 * $value_three;
        $sum_seven = $block_x - $value_two + $value_three;
        $sum_zero = $sum_zero + $sum_three + $sum_five;
        $sum_one = $sum_one - $sum_four - $sum_six;
        $sum_three = $sum_two;
        $sum_two = self::SINPI_3 * $sum_seven;
        return [
            self::roundShift($sum_zero + $sum_three),
            self::roundShift($sum_one + $sum_three),
            self::roundShift($sum_two),
            self::roundShift($sum_zero + $sum_one - $sum_three),
        ];
    }
    /**
     * idct4x4Add apply the four by four inverse transform pair for a transform
     * type and add the result to the prediction already in the plane. for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param array $value the values the block was coded as
     * @param int $transform_type which transform the block uses
     */
    private function idct4x4Add(int $plane_at, int $block_x, int $block_y,
        array $value,
        int $transform_type): void
    {
        /* rows first, then columns; which one-dimensional transform each pass
          */
        /* uses depends on the transform type */
        $row_sine_transform = ($transform_type === 2 || $transform_type === 3);
        $col_sine_transform = ($transform_type === 1 || $transform_type === 3);
        $held = array_fill(0, 16, 0);
        for ($i = 0; $i < 4; $i++) {
            $offset = $i * 4;
            $row = [$value[$offset], $value[$offset + 1], $value[$offset + 2],
                $value[$offset + 3]];
            $written = $row_sine_transform ? self::inverseSine4($row)
                : self::inverseCosine4($row);
            for ($j = 0; $j < 4; $j++) {
                $held[$i * 4 + $j] = $written[$j];
            }
        }
        $stride = $this->strides[$plane_at];
        $base = $this->sampleAt($plane_at, $block_x, $block_y);
        for ($i = 0; $i < 4; $i++) {
            $col = [$held[$i], $held[4 + $i], $held[8 + $i], $held[12 + $i]];
            $written = $col_sine_transform ? self::inverseSine4($col)
                : self::inverseCosine4($col);
            for ($j = 0; $j < 4; $j++) {
                $at = $base + $j * $stride + $i;
                $this->planes[$plane_at][$at] =
                    self::holdInsideByte($this->planes[$plane_at][$at]
                        + (($written[$j] + 8) >> 4));
            }
        }
    }
    /**
     * COSPI_2 is the the cosines the transform multiplies by 2 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_2 = 16305;
    /**
     * COSPI_4 is the the cosines the transform multiplies by 4 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_4 = 16069;
    /**
     * COSPI_6 is the the cosines the transform multiplies by 6 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_6 = 15679;
    /**
     * COSPI_10 is the the cosines the transform multiplies by 10 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_10 = 14449;
    /**
     * COSPI_12 is the the cosines the transform multiplies by 12 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_12 = 13623;
    /**
     * COSPI_14 is the the cosines the transform multiplies by 14 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_14 = 12665;
    /**
     * COSPI_18 is the the cosines the transform multiplies by 18 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_18 = 10394;
    /**
     * COSPI_20 is the the cosines the transform multiplies by 20 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_20 = 9102;
    /**
     * COSPI_22 is the the cosines the transform multiplies by 22 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_22 = 7723;
    /**
     * COSPI_26 is the the cosines the transform multiplies by 26 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_26 = 4756;
    /**
     * COSPI_28 is the the cosines the transform multiplies by 28 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_28 = 3196;
    /**
     * COSPI_30 is the the cosines the transform multiplies by 30 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_30 = 1606;
    /**
     * inverseCosine8 turns eight values back into differences from what was
     * guessed. A block's rows go through this and then its columns, which is
     * how VP9 undoes the transform its encoder applied.
     *
     * @param array $values The eight values to transform.
     * @return array The eight differences, in picture order.
     */
    private static function inverseCosine8(array $values): array
    {
        $sum_zero = $values[0];
        $sum_one = $values[2];
        $sum_two = $values[4];
        $sum_three = $values[6];
        $sum_four = self::roundShift($values[1] * self::COSPI_28
            - $values[7] * self::COSPI_4);
        $sum_seven = self::roundShift($values[1] * self::COSPI_4
            + $values[7] * self::COSPI_28);
        $sum_five = self::roundShift($values[5] * self::COSPI_12
            - $values[3] * self::COSPI_20);
        $sum_six = self::roundShift($values[5] * self::COSPI_20
            + $values[3] * self::COSPI_12);
        $held_zero = self::roundShift(($sum_zero + $sum_two) * self::COSPI_16);
        $held_one = self::roundShift(($sum_zero - $sum_two) * self::COSPI_16);
        $held_two = self::roundShift($sum_one * self::COSPI_24 -
            $sum_three * self::COSPI_8);
        $held_three = self::roundShift($sum_one * self::COSPI_8 +
            $sum_three * self::COSPI_24);
        $held_four = $sum_four + $sum_five;
        $held_five = $sum_four - $sum_five;
        $held_six = -$sum_six + $sum_seven;
        $held_seven = $sum_six + $sum_seven;
        $upper_zero = $held_zero + $held_three;
        $upper_one = $held_one + $held_two;
        $upper_two = $held_one - $held_two;
        $upper_three = $held_zero - $held_three;
        $upper_four = $held_four;
        $upper_five = self::roundShift(($held_six - $held_five) *
            self::COSPI_16);
        $upper_six = self::roundShift(($held_five + $held_six) *
            self::COSPI_16);
        $upper_seven = $held_seven;
        return [
            $upper_zero + $upper_seven, $upper_one + $upper_six,
                $upper_two + $upper_five, $upper_three + $upper_four,
            $upper_three - $upper_four, $upper_two - $upper_five,
                $upper_one - $upper_six, $upper_zero - $upper_seven,
        ];
    }
    /**
     * inverseSine8 turns eight values back into differences from what
     * was guessed, using the transform that suits a block whose
     * guess runs one way across it.
     *
     * @param array $values the values to transform
     * @return array what was read
     */
    private static function inverseSine8(array $values): array
    {
        $block_x = $values[7]; $value_one = $values[0]; $value_two =
            $values[5]; $value_three =
            $values[2];
        $block_value_four = $values[3]; $value_five =
            $values[4]; $value_six = $values[1]; $value_seven =
            $values[6];
        if (($block_x | $value_one | $value_two | $value_three |
            $block_value_four | $value_five | $value_six |
            $value_seven) === 0) {
            return array_fill(0, 8, 0);
        }
        $sum_zero = self::COSPI_2 * $block_x + self::COSPI_30 * $value_one;
        $sum_one = self::COSPI_30 * $block_x - self::COSPI_2 * $value_one;
        $sum_two = self::COSPI_10 * $value_two + self::COSPI_22 * $value_three;
        $sum_three = self::COSPI_22 * $value_two - self::COSPI_10 *
            $value_three;
        $sum_four = self::COSPI_18 * $block_value_four +
            self::COSPI_14 * $value_five;
        $sum_five = self::COSPI_14 * $block_value_four -
            self::COSPI_18 * $value_five;
        $sum_six = self::COSPI_26 * $value_six + self::COSPI_6 * $value_seven;
        $sum_seven = self::COSPI_6 * $value_six - self::COSPI_26 * $value_seven;
        $block_x = self::roundShift($sum_zero + $sum_four);
        $value_one = self::roundShift($sum_one + $sum_five);
        $value_two = self::roundShift($sum_two + $sum_six);
        $value_three = self::roundShift($sum_three + $sum_seven);
        $block_value_four = self::roundShift($sum_zero - $sum_four);
        $value_five = self::roundShift($sum_one - $sum_five);
        $value_six = self::roundShift($sum_two - $sum_six);
        $value_seven = self::roundShift($sum_three - $sum_seven);
        $sum_zero = $block_x; $sum_one = $value_one; $sum_two =
            $value_two; $sum_three = $value_three;
        $sum_four = self::COSPI_8 * $block_value_four +
            self::COSPI_24 * $value_five;
        $sum_five = self::COSPI_24 * $block_value_four -
            self::COSPI_8 * $value_five;
        $sum_six = -self::COSPI_24 * $value_six + self::COSPI_8 * $value_seven;
        $sum_seven = self::COSPI_8 * $value_six + self::COSPI_24 * $value_seven;
        $block_x = $sum_zero + $sum_two;
        $value_one = $sum_one + $sum_three;
        $value_two = $sum_zero - $sum_two;
        $value_three = $sum_one - $sum_three;
        $block_value_four = self::roundShift($sum_four + $sum_six);
        $value_five = self::roundShift($sum_five + $sum_seven);
        $value_six = self::roundShift($sum_four - $sum_six);
        $value_seven = self::roundShift($sum_five - $sum_seven);
        $held_two = self::roundShift(self::COSPI_16 * ($value_two +
            $value_three));
        $held_three = self::roundShift(self::COSPI_16 * ($value_two -
            $value_three));
        $held_six = self::roundShift(self::COSPI_16 * ($value_six +
            $value_seven));
        $held_seven = self::roundShift(self::COSPI_16 * ($value_six -
            $value_seven));
        return [$block_x, -$block_value_four, $held_six, -$held_two,
            $held_three, -$held_seven, $value_five, -$value_one];
    }
    /**
     * idct8x8Add the eight by eight inverse transform pair, added into the
     * prediction for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param array $value the values the block was coded as
     * @param int $transform_type which transform the block uses
     */
    private function idct8x8Add(int $plane_at, int $block_x, int $block_y,
        array $value,
        int $transform_type): void
    {
        $row_sine_transform = ($transform_type === 2 || $transform_type === 3);
        $col_sine_transform = ($transform_type === 1 || $transform_type === 3);
        $held = array_fill(0, 64, 0);
        for ($i = 0; $i < 8; $i++) {
            $row = array_slice($value, $i * 8, 8);
            $written = $row_sine_transform ? self::inverseSine8($row)
                : self::inverseCosine8($row);
            for ($j = 0; $j < 8; $j++) {
                $held[$i * 8 + $j] = $written[$j];
            }
        }
        $stride = $this->strides[$plane_at];
        $base = $this->sampleAt($plane_at, $block_x, $block_y);
        for ($i = 0; $i < 8; $i++) {
            $col = [];
            for ($j = 0; $j < 8; $j++) {
                $col[$j] = $held[$j * 8 + $i];
            }
            $written = $col_sine_transform ? self::inverseSine8($col)
                : self::inverseCosine8($col);
            for ($j = 0; $j < 8; $j++) {
                $at = $base + $j * $stride + $i;
                $this->planes[$plane_at][$at] =
                    self::holdInsideByte($this->planes[$plane_at][$at]
                        + (($written[$j] + 16) >> 5));
            }
        }
    }
    /**
     * predictN intra prediction for transform blocks of eight samples and
     * above. The directional predictors differ from their four by four
     * counterparts, which is why those are written out separately. for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param int $smooth_strength how strongly the edge is smoothed
     * @param int $mode which way the block is guessed from its neighbors
     * @param bool $have_left whether the block to the left is there to read
     * @param bool $have_above whether the block above is there to read
     * @param bool $have_right whether the block to the right is there to read
     */
    private function predictN(int $plane_at, int $block_x, int $block_y,
        int $smooth_strength, int $mode,
        bool $have_left, bool $have_above, bool $have_right): void
    {
        $stride = $this->strides[$plane_at];
        $base = $this->sampleAt($plane_at, $block_x, $block_y);
        list($clip_right, $clip_down) = $this->overhang($plane_at, $block_x,
            $block_y,
            $smooth_strength);
        $above = [];
        if ($have_above) {
            for ($i = 0; $i < $smooth_strength; $i++) {
                $above[$i] = $this->edgeSample($plane_at, $block_x + $i,
                    $block_y - 1, $clip_right, $clip_down);
            }
            for ($i = $smooth_strength; $i < 2 * $smooth_strength; $i++) {
                $above[$i] = $have_right
                    ? $this->edgeSample($plane_at, $block_x + $i, $block_y - 1,
                    $clip_right, $clip_down)
                        : $above[$smooth_strength - 1];
            }
            $corner = $have_left ? $this->planes[$plane_at][$base - $stride - 1]
                : 129;
        } else {
            $above = array_fill(0, 2 * $smooth_strength, 127);
            $corner = 127;
        }
        $left = [];
        if ($have_left) {
            for ($i = 0; $i < $smooth_strength; $i++) {
                $left[$i] = $this->edgeSample($plane_at, $block_x - 1,
                    $block_y + $i, $clip_right, $clip_down);
            }
        } else {
            $left = array_fill(0, $smooth_strength, 129);
        }
        $predicted = array_fill(0, $smooth_strength * $smooth_strength, 0);
        /* guarded index */
        $at = static fn(int $i): int => $i < 0 ? 0 : $i;
        switch ($mode) {
            case 0:
                if ($have_above && $have_left) {
                    $first_value
 = (array_sum(array_slice($above, 0, $smooth_strength))
                        + array_sum($left) + $smooth_strength)
                        >> (int)(log($smooth_strength, 2) + 1);
                } elseif ($have_above) {
                    $first_value
 = (array_sum(array_slice($above, 0, $smooth_strength)) + $smooth_strength /
     2) >> (int) log($smooth_strength,
                        2);
                } elseif ($have_left) {
                    $first_value = (array_sum($left) + $smooth_strength /
                        2) >> (int) log($smooth_strength, 2);
                } else {
                    $first_value = 128;
                }
                $predicted = array_fill(0, $smooth_strength *
                    $smooth_strength, (int) $first_value);
                break;
            case 1:
                for ($reader = 0; $reader < $smooth_strength; $reader++) {
                    for ($col = 0; $col < $smooth_strength; $col++) {
                        $predicted[$reader * $smooth_strength + $col] =
                            $above[$col];
                    }
                }
                break;
            case 2:
                for ($reader = 0; $reader < $smooth_strength; $reader++) {
                    for ($col = 0; $col < $smooth_strength; $col++) {
                        $predicted[$reader * $smooth_strength + $col] =
                            $left[$reader];
                    }
                }
                break;
            /* down-left */
            case 3:
                for ($reader = 0; $reader < $smooth_strength; $reader++) {
                    for ($col = 0; $col < $smooth_strength; $col++) {
                        $k = $reader + $col;
                        $predicted[$reader * $smooth_strength + $col] =
                            ($k < $smooth_strength - 1)
                            ? self::averageOfThree($above[$k], $above[$k + 1],
                                $above[$k + 2])
                            : $above[$smooth_strength - 1];
                    }
                }
                break;
            /* down-right, built from a border running corner to corner */
            case 4: {
                $border = [];
                for ($i = 0; $i < $smooth_strength - 2; $i++) {
                    $border[$i] =
                        self::averageOfThree($left[$smooth_strength - 3 - $i],
                        $left[$smooth_strength - 2 - $i],
                            $left[$smooth_strength - 1 - $i]);
                }
                $border[$smooth_strength - 2] = self::averageOfThree($corner,
                    $left[0], $left[1]);
                $border[$smooth_strength - 1] = self::averageOfThree($left[0],
                    $corner, $above[0]);
                $border[$smooth_strength] = self::averageOfThree($corner,
                    $above[0],
                    $above[1]);
                for ($i = 0; $i < $smooth_strength - 2; $i++) {
                    $border[$smooth_strength + 1
                        + $i] = self::averageOfThree($above[$i],
                        $above[$i + 1], $above[$i + 2]);
                }
                for ($reader = 0; $reader < $smooth_strength; $reader++) {
                    for ($col = 0; $col < $smooth_strength; $col++) {
                        $predicted[$reader * $smooth_strength + $col]
                            = $border[$smooth_strength - 1 - $reader + $col];
                    }
                }
                break;
            }
            /* vertical-right */
            case 5:
                for ($col = 0; $col < $smooth_strength; $col++) {
                    $predicted[$col]
                        = self::averageOfTwo($col === 0 ? $corner
                            : $above[$col - 1],
                        $above[$col]);
                }
                $predicted[$smooth_strength] =
                    self::averageOfThree($left[0], $corner,
                    $above[0]);
                for ($col = 1; $col < $smooth_strength; $col++) {
                    $predicted[$smooth_strength + $col]
                        = self::averageOfThree($col === 1 ? $corner
                            : $above[$col - 2],
                        $above[$col - 1], $above[$col]);
                }
                $predicted[2 * $smooth_strength] = self::averageOfThree($corner,
                    $left[0], $left[1]);
                for ($reader = 3; $reader < $smooth_strength; $reader++) {
                    $predicted[$reader * $smooth_strength]
                        = self::averageOfThree($left[$reader - 3],
                        $left[$reader - 2], $left[$reader - 1]);
                }
                for ($reader = 2; $reader < $smooth_strength; $reader++) {
                    for ($col = 1; $col < $smooth_strength; $col++) {
                        $predicted[$reader * $smooth_strength + $col]
                            = $predicted[($reader - 2) * $smooth_strength +
                                $col - 1];
                    }
                }
                break;
            /* horizontal-down */
            case 6:
                $predicted[0] = self::averageOfTwo($corner, $left[0]);
                for ($reader = 1; $reader < $smooth_strength; $reader++) {
                    $predicted[$reader * $smooth_strength] =
                        self::averageOfTwo($left[$reader - 1],
                        $left[$reader]);
                }
                $predicted[1] = self::averageOfThree($left[0], $corner,
                    $above[0]);
                $predicted[$smooth_strength + 1] = self::averageOfThree($corner,
                    $left[0], $left[1]);
                for ($reader = 2; $reader < $smooth_strength; $reader++) {
                    $predicted[$reader * $smooth_strength + 1]
                        = self::averageOfThree($left[$reader - 2],
                        $left[$reader - 1], $left[$reader]);
                }
                for ($col = 0; $col < $smooth_strength - 2; $col++) {
                    $predicted[$col + 2]
                        = self::averageOfThree($col === 0 ? $corner
                            : $above[$col - 1],
                        $above[$col], $above[$col + 1]);
                }
                for ($reader = 1; $reader < $smooth_strength; $reader++) {
                    for ($col = 0; $col < $smooth_strength - 2; $col++) {
                        $predicted[$reader * $smooth_strength + $col + 2]
                            = $predicted[($reader - 1) * $smooth_strength +
                                $col];
                    }
                }
                break;
            /* down-left of the left column */
            case 7:
                for ($reader = 0; $reader < $smooth_strength - 1; $reader++) {
                    $predicted[$reader * $smooth_strength] =
                        self::averageOfTwo($left[$reader],
                        $left[$reader + 1]);
                }
                $predicted[($smooth_strength - 1) * $smooth_strength] =
                    $left[$smooth_strength - 1];
                for ($reader = 0; $reader < $smooth_strength - 2; $reader++) {
                    $predicted[$reader * $smooth_strength + 1] =
                        self::averageOfThree($left[$reader],
                        $left[$reader + 1], $left[$reader + 2]);
                }
                $predicted[($smooth_strength - 2) * $smooth_strength + 1] =
                    self::averageOfThree($left[$smooth_strength - 2],
                    $left[$smooth_strength - 1], $left[$smooth_strength - 1]);
                $predicted[($smooth_strength - 1) * $smooth_strength + 1] =
                    $left[$smooth_strength - 1];
                for ($col = 0; $col < $smooth_strength - 2; $col++) {
                    $predicted[($smooth_strength - 1) * $smooth_strength +
                        2 + $col] = $left[$smooth_strength - 1];
                }
                for ($reader = $smooth_strength - 2; $reader >= 0; $reader--) {
                    for ($col = 0; $col < $smooth_strength - 2; $col++) {
                        $predicted[$reader * $smooth_strength + 2 + $col]
                            = $predicted[($reader + 1) * $smooth_strength +
                                $col];
                    }
                }
                break;
            /* vertical-left */
            case 8:
                for ($col = 0; $col < $smooth_strength; $col++) {
                    $predicted[$col] = self::averageOfTwo($above[$col],
                        $above[$col + 1]);
                    $predicted[$smooth_strength + $col] =
                        self::averageOfThree($above[$col],
                        $above[$col + 1], $above[$col + 2]);
                }
                for ($reader = 2, $size = $smooth_strength -
                    2; $reader < $smooth_strength; $reader += 2,
                    $size--) {
                    for ($col = 0; $col < $size; $col++) {
                        $predicted[$reader * $smooth_strength + $col]
                            = $predicted[($reader >> 1) + $col];
                        $predicted[($reader + 1) * $smooth_strength + $col]
                            = $predicted[$smooth_strength + ($reader >> 1) +
                                $col];
                    }
                    for ($col = $size; $col < $smooth_strength; $col++) {
                        $predicted[$reader * $smooth_strength + $col] =
                            $above[$smooth_strength - 1];
                        $predicted[($reader + 1) * $smooth_strength + $col]
                            = $above[$smooth_strength - 1];
                    }
                }
                break;
            case 9:
                for ($reader = 0; $reader < $smooth_strength; $reader++) {
                    for ($col = 0; $col < $smooth_strength; $col++) {
                        $predicted[$reader * $smooth_strength + $col]
 = self::holdInsideByte($left[$reader] + $above[$col] - $corner);
                    }
                }
                break;
            default:
                throw new VideoException("bad VP9 intra mode $mode");
        }
        for ($reader = 0; $reader < $smooth_strength; $reader++) {
            $row = $base + $reader * $stride;
            for ($col = 0; $col < $smooth_strength; $col++) {
                $this->planes[$plane_at][$row + $col]
                    = $predicted[$reader * $smooth_strength + $col];
            }
        }
    }
    /**
     * TX32_DEQUANT_SHIFT is bits a 32 by 32 block's coefficients are scaled
     * down on dequantizing.
     */
    private const TX32_DEQUANT_SHIFT = 1;
    /**
     * IDCT32_ROUND is rounding added before the final shift of a 32 point
     * transform.
     */
    private const IDCT32_ROUND = 32;
    /**
     * IDCT32_SHIFT is bits the two passes of a 32 point transform scale the
     * result up by.
     */
    private const IDCT32_SHIFT = 6;
    /**
     * COSPI_1 is the the cosines the transform multiplies by 1 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_1 = 16364;
    /**
     * COSPI_3 is the the cosines the transform multiplies by 3 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_3 = 16207;
    /**
     * COSPI_5 is the the cosines the transform multiplies by 5 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_5 = 15893;
    /**
     * COSPI_7 is the the cosines the transform multiplies by 7 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_7 = 15426;
    /**
     * COSPI_9 is the the cosines the transform multiplies by 9 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_9 = 14811;
    /**
     * COSPI_11 is the the cosines the transform multiplies by 11 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_11 = 14053;
    /**
     * COSPI_13 is the the cosines the transform multiplies by 13 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_13 = 13160;
    /**
     * COSPI_15 is the the cosines the transform multiplies by 15 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_15 = 12140;
    /**
     * COSPI_17 is the the cosines the transform multiplies by 17 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_17 = 11003;
    /**
     * COSPI_19 is the the cosines the transform multiplies by 19 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_19 = 9760;
    /**
     * COSPI_21 is the the cosines the transform multiplies by 21 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_21 = 8423;
    /**
     * COSPI_23 is the the cosines the transform multiplies by 23 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_23 = 7005;
    /**
     * COSPI_25 is the the cosines the transform multiplies by 25 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_25 = 5520;
    /**
     * COSPI_27 is the the cosines the transform multiplies by 27 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_27 = 3981;
    /**
     * COSPI_29 is the the cosines the transform multiplies by 29 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_29 = 2404;
    /**
     * COSPI_31 is the the cosines the transform multiplies by 31 the format
     * fixes.
     * @var mixed
     */
    private const COSPI_31 = 804;
    /**
     * inverseCosine16 turns sixteen values back into differences from
     * what was guessed, using the transform VP9 uses for most
     * blocks. A larger block
     * uses.
     *
     * @param array $values The sixteen values to transform.
     * @return array The sixteen differences, in picture order.
     */
    private static function inverseCosine16(array $values): array
    {
        $stage = [];
        for ($i = 0; $i < 16; $i++) {
            $stage[$i] = 0;
        }
        /* stage 1: the even half keeps its inputs, the odd half rotates */
        $stage_two = [
            $values[0], $values[8], $values[4], $values[12], $values[2],
                $values[10], $values[6], $values[14],
            $values[1], $values[9], $values[5], $values[13], $values[3],
                $values[11], $values[7], $values[15],
        ];
        $stage_three = $stage_two;
        $stage_three[8]
            = self::roundShift($stage_two[8] * self::COSPI_30
                - $stage_two[15] * self::COSPI_2);
        $stage_three[15]
            = self::roundShift($stage_two[8] * self::COSPI_2
                + $stage_two[15] * self::COSPI_30);
        $stage_three[9]
            = self::roundShift($stage_two[9] * self::COSPI_14
                - $stage_two[14] * self::COSPI_18);
        $stage_three[14]
            = self::roundShift($stage_two[9] * self::COSPI_18
                + $stage_two[14] * self::COSPI_14);
        $stage_three[10]
            = self::roundShift($stage_two[10] * self::COSPI_22
                - $stage_two[13] * self::COSPI_10);
        $stage_three[13]
            = self::roundShift($stage_two[10] * self::COSPI_10
                + $stage_two[13] * self::COSPI_22);
        $stage_three[11]
            = self::roundShift($stage_two[11] * self::COSPI_6
                - $stage_two[12] * self::COSPI_26);
        $stage_three[12]
            = self::roundShift($stage_two[11] * self::COSPI_26
                + $stage_two[12] * self::COSPI_6);
        $stage_four = $stage_three;
        $stage_four[4]
            = self::roundShift($stage_three[4] * self::COSPI_28
                - $stage_three[7] * self::COSPI_4);
        $stage_four[7]
            = self::roundShift($stage_three[4] * self::COSPI_4
                + $stage_three[7] * self::COSPI_28);
        $stage_four[5]
            = self::roundShift($stage_three[5] * self::COSPI_12
                - $stage_three[6] * self::COSPI_20);
        $stage_four[6]
            = self::roundShift($stage_three[5] * self::COSPI_20
                + $stage_three[6] * self::COSPI_12);
        $stage_four[8] = $stage_three[8] + $stage_three[9];
        $stage_four[9] = $stage_three[8] - $stage_three[9];
        $stage_four[10] = -$stage_three[10] + $stage_three[11];
        $stage_four[11] = $stage_three[10] + $stage_three[11];
        $stage_four[12] = $stage_three[12] + $stage_three[13];
        $stage_four[13] = $stage_three[12] - $stage_three[13];
        $stage_four[14] = -$stage_three[14] + $stage_three[15];
        $stage_four[15] = $stage_three[14] + $stage_three[15];
        $stage_five = $stage_four;
        $stage_five[0]
            = self::roundShift(($stage_four[0]
                + $stage_four[1]) * self::COSPI_16);
        $stage_five[1]
            = self::roundShift(($stage_four[0]
                - $stage_four[1]) * self::COSPI_16);
        $stage_five[2]
            = self::roundShift($stage_four[2] * self::COSPI_24
                - $stage_four[3] * self::COSPI_8);
        $stage_five[3]
            = self::roundShift($stage_four[2] * self::COSPI_8
                + $stage_four[3] * self::COSPI_24);
        $stage_five[4] = $stage_four[4] + $stage_four[5];
        $stage_five[5] = $stage_four[4] - $stage_four[5];
        $stage_five[6] = -$stage_four[6] + $stage_four[7];
        $stage_five[7] = $stage_four[6] + $stage_four[7];
        $stage_five[9]
            = self::roundShift(-$stage_four[9] * self::COSPI_8
                + $stage_four[14] * self::COSPI_24);
        $stage_five[14]
            = self::roundShift($stage_four[9] * self::COSPI_24
                + $stage_four[14] * self::COSPI_8);
        $stage_five[10]
            = self::roundShift(-$stage_four[10] * self::COSPI_24
                - $stage_four[13] * self::COSPI_8);
        $stage_five[13]
            = self::roundShift(-$stage_four[10] * self::COSPI_8
                + $stage_four[13] * self::COSPI_24);
        $stage_six = $stage_five;
        $stage_six[0] = $stage_five[0] + $stage_five[3];
        $stage_six[1] = $stage_five[1] + $stage_five[2];
        $stage_six[2] = $stage_five[1] - $stage_five[2];
        $stage_six[3] = $stage_five[0] - $stage_five[3];
        $stage_six[5]
            = self::roundShift(($stage_five[6]
                - $stage_five[5]) * self::COSPI_16);
        $stage_six[6]
            = self::roundShift(($stage_five[5]
                + $stage_five[6]) * self::COSPI_16);
        $stage_six[8] = $stage_five[8] + $stage_five[11];
        $stage_six[9] = $stage_five[9] + $stage_five[10];
        $stage_six[10] = $stage_five[9] - $stage_five[10];
        $stage_six[11] = $stage_five[8] - $stage_five[11];
        $stage_six[12] = -$stage_five[12] + $stage_five[15];
        $stage_six[13] = -$stage_five[13] + $stage_five[14];
        $stage_six[14] = $stage_five[13] + $stage_five[14];
        $stage_six[15] = $stage_five[12] + $stage_five[15];
        $stage_seven = $stage_six;
        $stage_seven[0] = $stage_six[0] + $stage_six[7];
        $stage_seven[1] = $stage_six[1] + $stage_six[6];
        $stage_seven[2] = $stage_six[2] + $stage_six[5];
        $stage_seven[3] = $stage_six[3] + $stage_six[4];
        $stage_seven[4] = $stage_six[3] - $stage_six[4];
        $stage_seven[5] = $stage_six[2] - $stage_six[5];
        $stage_seven[6] = $stage_six[1] - $stage_six[6];
        $stage_seven[7] = $stage_six[0] - $stage_six[7];
        $stage_seven[10]
            = self::roundShift((-$stage_six[10]
                + $stage_six[13]) * self::COSPI_16);
        $stage_seven[13]
            = self::roundShift(($stage_six[10]
                + $stage_six[13]) * self::COSPI_16);
        $stage_seven[11]
            = self::roundShift((-$stage_six[11]
                + $stage_six[12]) * self::COSPI_16);
        $stage_seven[12]
            = self::roundShift(($stage_six[11]
                + $stage_six[12]) * self::COSPI_16);
        $written = [];
        for ($i = 0; $i < 8; $i++) {
            $written[$i] = $stage_seven[$i] + $stage_seven[15 - $i];
            $written[15 - $i] = $stage_seven[$i] - $stage_seven[15 - $i];
        }
        return $written;
    }
    /**
     * iadst16 turns sixteen values back into differences from what was
     * guessed, using the transform that suits a block whose guess
     * runs one way across it.
     *
     * @param array $values the values to transform
     * @return array what was read
     */
    private static function iadst16(array $values): array
    {
        $column = [
            $values[15], $values[0], $values[13], $values[2], $values[11],
                $values[4], $values[9], $values[6],
            $values[7], $values[8], $values[5], $values[10], $values[3],
                $values[12], $values[1], $values[14],
        ];
        $any = false;
        foreach ($column as $stage_four) {
            if ($stage_four !== 0) { $any = true; break; }
        }
        if (!$any) {
            return array_fill(0, 16, 0);
        }
        $pairs = [
            [self::COSPI_1, self::COSPI_31], [self::COSPI_5, self::COSPI_27],
            [self::COSPI_9, self::COSPI_23], [self::COSPI_13, self::COSPI_19],
            [self::COSPI_17, self::COSPI_15], [self::COSPI_21, self::COSPI_11],
            [self::COSPI_25, self::COSPI_7], [self::COSPI_29, self::COSPI_3],
        ];
        $stage = [];
        foreach ($pairs as $k => [$ca, $chroma_blue]) {
            $first = $column[2 * $k];
            $second = $column[2 * $k + 1];
            $stage[2 * $k] = $ca * $first + $chroma_blue * $second;
            $stage[2 * $k + 1] = $chroma_blue * $first - $ca * $second;
        }
        for ($i = 0; $i < 8; $i++) {
            $column[$i] = self::roundShift($stage[$i] + $stage[$i + 8]);
            $column[$i + 8] = self::roundShift($stage[$i] - $stage[$i + 8]);
        }
        for ($i = 0; $i < 8; $i++) {
            $stage[$i] = $column[$i];
        }
        $stage[8] = $column[8] * self::COSPI_4 + $column[9] * self::COSPI_28;
        $stage[9] = $column[8] * self::COSPI_28 - $column[9] * self::COSPI_4;
        $stage[10] = $column[10] * self::COSPI_20
            + $column[11] * self::COSPI_12;
        $stage[11] = $column[10] * self::COSPI_12
            - $column[11] * self::COSPI_20;
        $stage[12] = -$column[12] * self::COSPI_28
            + $column[13] * self::COSPI_4;
        $stage[13] = $column[12] * self::COSPI_4 + $column[13] * self::COSPI_28;
        $stage[14] = -$column[14] * self::COSPI_12
            + $column[15] * self::COSPI_20;
        $stage[15] = $column[14] * self::COSPI_20
            + $column[15] * self::COSPI_12;
        for ($i = 0; $i < 4; $i++) {
            $column[$i] = $stage[$i] + $stage[$i + 4];
            $column[$i + 4] = $stage[$i] - $stage[$i + 4];
            $column[$i + 8]
                = self::roundShift($stage[$i + 8] + $stage[$i + 12]);
            $column[$i + 12]
                = self::roundShift($stage[$i + 8] - $stage[$i + 12]);
        }
        $stage[0] = $column[0]; $stage[1] = $column[1]; $stage[2]
            = $column[2]; $stage[3] = $column[3];
        $stage[4] = $column[4] * self::COSPI_8 + $column[5] * self::COSPI_24;
        $stage[5] = $column[4] * self::COSPI_24 - $column[5] * self::COSPI_8;
        $stage[6] = -$column[6] * self::COSPI_24 + $column[7] * self::COSPI_8;
        $stage[7] = $column[6] * self::COSPI_8 + $column[7] * self::COSPI_24;
        $stage[8] = $column[8]; $stage[9] = $column[9]; $stage[10]
            = $column[10]; $stage[11] = $column[11];
        $stage[12] = $column[12] * self::COSPI_8 + $column[13] * self::COSPI_24;
        $stage[13] = $column[12] * self::COSPI_24 - $column[13] * self::COSPI_8;
        $stage[14] = -$column[14] * self::COSPI_24
            + $column[15] * self::COSPI_8;
        $stage[15] = $column[14] * self::COSPI_8 + $column[15] * self::COSPI_24;
        $column[0] = $stage[0] + $stage[2];
        $column[1] = $stage[1] + $stage[3];
        $column[2] = $stage[0] - $stage[2];
        $column[3] = $stage[1] - $stage[3];
        $column[4] = self::roundShift($stage[4] + $stage[6]);
        $column[5] = self::roundShift($stage[5] + $stage[7]);
        $column[6] = self::roundShift($stage[4] - $stage[6]);
        $column[7] = self::roundShift($stage[5] - $stage[7]);
        $column[8] = $stage[8] + $stage[10];
        $column[9] = $stage[9] + $stage[11];
        $column[10] = $stage[8] - $stage[10];
        $column[11] = $stage[9] - $stage[11];
        $column[12] = self::roundShift($stage[12] + $stage[14]);
        $column[13] = self::roundShift($stage[13] + $stage[15]);
        $column[14] = self::roundShift($stage[12] - $stage[14]);
        $column[15] = self::roundShift($stage[13] - $stage[15]);
        $value_two = self::roundShift(-self::COSPI_16 * ($column[2] +
            $column[3]));
        $value_three = self::roundShift(self::COSPI_16 * ($column[2] -
            $column[3]));
        $value_six = self::roundShift(self::COSPI_16 * ($column[6] +
            $column[7]));
        $value_seven = self::roundShift(self::COSPI_16 * (-$column[6] +
            $column[7]));
        $value_ten = self::roundShift(self::COSPI_16 * ($column[10] +
            $column[11]));
        $value_eleven = self::roundShift(self::COSPI_16 * (-$column[10] +
            $column[11]));
        $value_fourteen = self::roundShift(-self::COSPI_16 * ($column[14] +
            $column[15]));
        $value_fifteen = self::roundShift(self::COSPI_16 * ($column[14] -
            $column[15]));
        return [
            $column[0], -$column[8], $column[12], -$column[4],
            $value_six, $value_fourteen, $value_ten, $value_two,
                $value_three, $value_eleven, $value_fifteen, $value_seven,
            $column[5], -$column[13], $column[9], -$column[1],
        ];
    }
    /**
     * idct16x16Add the sixteen by sixteen inverse transform pair, added into
     * the prediction for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param array $value the values the block was coded as
     * @param int $transform_type which transform the block uses
     */
    private function idct16x16Add(int $plane_at, int $block_x, int $block_y,
        array $value,
        int $transform_type): void
    {
        $row_sine_transform = ($transform_type === 2 || $transform_type === 3);
        $col_sine_transform = ($transform_type === 1 || $transform_type === 3);
        $held = array_fill(0, 256, 0);
        for ($i = 0; $i < 16; $i++) {
            $row = array_slice($value, $i * 16, 16);
            $written = $row_sine_transform ? self::iadst16($row)
                : self::inverseCosine16($row);
            for ($j = 0; $j < 16; $j++) {
                $held[$i * 16 + $j] = $written[$j];
            }
        }
        $stride = $this->strides[$plane_at];
        $base = $this->sampleAt($plane_at, $block_x, $block_y);
        for ($i = 0; $i < 16; $i++) {
            $col = [];
            for ($j = 0; $j < 16; $j++) {
                $col[$j] = $held[$j * 16 + $i];
            }
            $written = $col_sine_transform ? self::iadst16($col)
                : self::inverseCosine16($col);
            for ($j = 0; $j < 16; $j++) {
                $at = $base + $j * $stride + $i;
                $this->planes[$plane_at][$at] =
                    self::holdInsideByte($this->planes[$plane_at][$at]
                        + (($written[$j] + 32) >> 6));
            }
        }
    }
    /**
     * inverseCosine32 runs the 32 point inverse discrete cosine transform on
     * one row or column of coefficients. The transform is factored into seven
     * butterfly stages, each mixing pairs of values and rounding back to the
     * fixed point scale the format uses. Only the discrete cosine variant
     * exists at this size; the alternating sine transform is defined for
     * smaller blocks only.
     *
     * @param array $input 32 coefficients in frequency order
     * @return array 32 values in sample order
     */
    private static function inverseCosine32($input)
    {
        /* stage one reorders the even half and rotates the odd half */
        $first_pass = array_fill(0, 32, 0);
        $even = [0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14, 30];
        for ($i = 0; $i < 16; $i++) {
            $first_pass[$i] = $input[$even[$i]];
        }
        $odd = [
            [1, 31, self::COSPI_31, self::COSPI_1],
            [17, 15, self::COSPI_15, self::COSPI_17],
            [9, 23, self::COSPI_23, self::COSPI_9],
            [25, 7, self::COSPI_7, self::COSPI_25],
            [5, 27, self::COSPI_27, self::COSPI_5],
            [21, 11, self::COSPI_11, self::COSPI_21],
            [13, 19, self::COSPI_19, self::COSPI_13],
            [29, 3, self::COSPI_3, self::COSPI_29],
        ];
        for ($i = 0; $i < 8; $i++) {
            list($low, $high, $first, $second) = $odd[$i];
            $first_pass[16 + $i] = self::roundShift(
                $input[$low] * $first - $input[$high] * $second);
            $first_pass[31 - $i] = self::roundShift(
                $input[$low] * $second + $input[$high] * $first);
        }
        /* stage two rotates the middle eight and pairs the outer sixteen */
        $second_pass = $first_pass;
        $middle = [
            [8, 15, self::COSPI_30, self::COSPI_2],
            [9, 14, self::COSPI_14, self::COSPI_18],
            [10, 13, self::COSPI_22, self::COSPI_10],
            [11, 12, self::COSPI_6, self::COSPI_26],
        ];
        foreach ($middle as $entry) {
            list($low, $high, $first, $second) = $entry;
            $second_pass[$low] = self::roundShift(
                $first_pass[$low] * $first - $first_pass[$high] * $second);
            $second_pass[$high] = self::roundShift(
                $first_pass[$low] * $second + $first_pass[$high] * $first);
        }
        for ($i = 0; $i < 4; $i++) {
            $base = 16 + 4 * $i;
            $second_pass[$base] = $first_pass[$base] + $first_pass[$base + 1];
            $second_pass[$base + 1] = $first_pass[$base] -
                $first_pass[$base + 1];
            $second_pass[$base + 2] = -$first_pass[$base + 2] +
                $first_pass[$base + 3];
            $second_pass[$base + 3] = $first_pass[$base + 2] +
                $first_pass[$base + 3];
        }
        /* stage three */
        $first_pass = $second_pass;
        $first_pass[4] = self::roundShift(
            $second_pass[4] * self::COSPI_28 - $second_pass[7] * self::COSPI_4);
        $first_pass[7] = self::roundShift(
            $second_pass[4] * self::COSPI_4 + $second_pass[7] * self::COSPI_28);
        $first_pass[5] = self::roundShift(
            $second_pass[5] * self::COSPI_12 - $second_pass[6] *
                self::COSPI_20);
        $first_pass[6] = self::roundShift(
            $second_pass[5] * self::COSPI_20 + $second_pass[6] *
                self::COSPI_12);
        $first_pass[8] = $second_pass[8] + $second_pass[9];
        $first_pass[9] = $second_pass[8] - $second_pass[9];
        $first_pass[10] = -$second_pass[10] + $second_pass[11];
        $first_pass[11] = $second_pass[10] + $second_pass[11];
        $first_pass[12] = $second_pass[12] + $second_pass[13];
        $first_pass[13] = $second_pass[12] - $second_pass[13];
        $first_pass[14] = -$second_pass[14] + $second_pass[15];
        $first_pass[15] = $second_pass[14] + $second_pass[15];
        $first_pass[17] = self::roundShift(
            -$second_pass[17] * self::COSPI_4 + $second_pass[30] *
                self::COSPI_28);
        $first_pass[30] = self::roundShift(
            $second_pass[17] * self::COSPI_28 + $second_pass[30] *
                self::COSPI_4);
        $first_pass[18] = self::roundShift(
            -$second_pass[18] * self::COSPI_28 - $second_pass[29] *
                self::COSPI_4);
        $first_pass[29] = self::roundShift(
            -$second_pass[18] * self::COSPI_4 + $second_pass[29] *
                self::COSPI_28);
        $first_pass[21] = self::roundShift(
            -$second_pass[21] * self::COSPI_20 + $second_pass[26] *
                self::COSPI_12);
        $first_pass[26] = self::roundShift(
            $second_pass[21] * self::COSPI_12 + $second_pass[26] *
                self::COSPI_20);
        $first_pass[22] = self::roundShift(
            -$second_pass[22] * self::COSPI_12 - $second_pass[25] *
                self::COSPI_20);
        $first_pass[25] = self::roundShift(
            -$second_pass[22] * self::COSPI_20 + $second_pass[25] *
                self::COSPI_12);
        /* stage four */
        $second_pass = $first_pass;
        $second_pass[0] = self::roundShift(
            ($first_pass[0] + $first_pass[1]) * self::COSPI_16);
        $second_pass[1] = self::roundShift(
            ($first_pass[0] - $first_pass[1]) * self::COSPI_16);
        $second_pass[2] = self::roundShift(
            $first_pass[2] * self::COSPI_24 - $first_pass[3] * self::COSPI_8);
        $second_pass[3] = self::roundShift(
            $first_pass[2] * self::COSPI_8 + $first_pass[3] * self::COSPI_24);
        $second_pass[4] = $first_pass[4] + $first_pass[5];
        $second_pass[5] = $first_pass[4] - $first_pass[5];
        $second_pass[6] = -$first_pass[6] + $first_pass[7];
        $second_pass[7] = $first_pass[6] + $first_pass[7];
        $second_pass[9] = self::roundShift(
            -$first_pass[9] * self::COSPI_8 + $first_pass[14] * self::COSPI_24);
        $second_pass[14] = self::roundShift(
            $first_pass[9] * self::COSPI_24 + $first_pass[14] * self::COSPI_8);
        $second_pass[10] = self::roundShift(
            -$first_pass[10] * self::COSPI_24 - $first_pass[13] *
                self::COSPI_8);
        $second_pass[13] = self::roundShift(
            -$first_pass[10] * self::COSPI_8 + $first_pass[13] *
                self::COSPI_24);
        $second_pass[16] = $first_pass[16] + $first_pass[19];
        $second_pass[17] = $first_pass[17] + $first_pass[18];
        $second_pass[18] = $first_pass[17] - $first_pass[18];
        $second_pass[19] = $first_pass[16] - $first_pass[19];
        $second_pass[20] = -$first_pass[20] + $first_pass[23];
        $second_pass[21] = -$first_pass[21] + $first_pass[22];
        $second_pass[22] = $first_pass[21] + $first_pass[22];
        $second_pass[23] = $first_pass[20] + $first_pass[23];
        $second_pass[24] = $first_pass[24] + $first_pass[27];
        $second_pass[25] = $first_pass[25] + $first_pass[26];
        $second_pass[26] = $first_pass[25] - $first_pass[26];
        $second_pass[27] = $first_pass[24] - $first_pass[27];
        $second_pass[28] = -$first_pass[28] + $first_pass[31];
        $second_pass[29] = -$first_pass[29] + $first_pass[30];
        $second_pass[30] = $first_pass[29] + $first_pass[30];
        $second_pass[31] = $first_pass[28] + $first_pass[31];
        /* stage five */
        $first_pass = $second_pass;
        $first_pass[0] = $second_pass[0] + $second_pass[3];
        $first_pass[1] = $second_pass[1] + $second_pass[2];
        $first_pass[2] = $second_pass[1] - $second_pass[2];
        $first_pass[3] = $second_pass[0] - $second_pass[3];
        $first_pass[5] = self::roundShift(
            ($second_pass[6] - $second_pass[5]) * self::COSPI_16);
        $first_pass[6] = self::roundShift(
            ($second_pass[5] + $second_pass[6]) * self::COSPI_16);
        $first_pass[8] = $second_pass[8] + $second_pass[11];
        $first_pass[9] = $second_pass[9] + $second_pass[10];
        $first_pass[10] = $second_pass[9] - $second_pass[10];
        $first_pass[11] = $second_pass[8] - $second_pass[11];
        $first_pass[12] = -$second_pass[12] + $second_pass[15];
        $first_pass[13] = -$second_pass[13] + $second_pass[14];
        $first_pass[14] = $second_pass[13] + $second_pass[14];
        $first_pass[15] = $second_pass[12] + $second_pass[15];
        $first_pass[18] = self::roundShift(
            -$second_pass[18] * self::COSPI_8 + $second_pass[29] *
                self::COSPI_24);
        $first_pass[29] = self::roundShift(
            $second_pass[18] * self::COSPI_24 + $second_pass[29] *
                self::COSPI_8);
        $first_pass[19] = self::roundShift(
            -$second_pass[19] * self::COSPI_8 + $second_pass[28] *
                self::COSPI_24);
        $first_pass[28] = self::roundShift(
            $second_pass[19] * self::COSPI_24 + $second_pass[28] *
                self::COSPI_8);
        $first_pass[20] = self::roundShift(
            -$second_pass[20] * self::COSPI_24 - $second_pass[27] *
                self::COSPI_8);
        $first_pass[27] = self::roundShift(
            -$second_pass[20] * self::COSPI_8 + $second_pass[27] *
                self::COSPI_24);
        $first_pass[21] = self::roundShift(
            -$second_pass[21] * self::COSPI_24 - $second_pass[26] *
                self::COSPI_8);
        $first_pass[26] = self::roundShift(
            -$second_pass[21] * self::COSPI_8 + $second_pass[26] *
                self::COSPI_24);
        /* stage six */
        $second_pass = $first_pass;
        for ($i = 0; $i < 4; $i++) {
            $second_pass[$i] = $first_pass[$i] + $first_pass[7 - $i];
            $second_pass[7 - $i] = $first_pass[$i] - $first_pass[7 - $i];
        }
        $second_pass[10] = self::roundShift(
            (-$first_pass[10] + $first_pass[13]) * self::COSPI_16);
        $second_pass[13] = self::roundShift(
            ($first_pass[10] + $first_pass[13]) * self::COSPI_16);
        $second_pass[11] = self::roundShift(
            (-$first_pass[11] + $first_pass[12]) * self::COSPI_16);
        $second_pass[12] = self::roundShift(
            ($first_pass[11] + $first_pass[12]) * self::COSPI_16);
        for ($i = 0; $i < 4; $i++) {
            $second_pass[16 + $i] = $first_pass[16 + $i] + $first_pass[23 - $i];
            $second_pass[23 - $i] = $first_pass[16 + $i] - $first_pass[23 - $i];
            $second_pass[24 + $i] = -$first_pass[24 + $i] + $first_pass[31 -
                $i];
            $second_pass[31 - $i] = $first_pass[24 + $i] + $first_pass[31 - $i];
        }
        /* stage seven */
        $first_pass = $second_pass;
        for ($i = 0; $i < 8; $i++) {
            $first_pass[$i] = $second_pass[$i] + $second_pass[15 - $i];
            $first_pass[15 - $i] = $second_pass[$i] - $second_pass[15 - $i];
        }
        for ($i = 0; $i < 4; $i++) {
            $first_pass[20 + $i] = self::roundShift(
                (-$second_pass[20 + $i] + $second_pass[27 - $i]) *
                    self::COSPI_16);
            $first_pass[27 - $i] = self::roundShift(
                ($second_pass[20 + $i] + $second_pass[27 - $i]) *
                    self::COSPI_16);
        }
        $output = array_fill(0, 32, 0);
        for ($i = 0; $i < 16; $i++) {
            $output[$i] = $first_pass[$i] + $first_pass[31 - $i];
            $output[31 - $i] = $first_pass[$i] - $first_pass[31 - $i];
        }
        return $output;
    }
    /**
     * idct32x32Add transforms a 32 by 32 block of coefficients back to sample
     * differences and adds them to the prediction already in the plane. Rows
     * are transformed first, then columns, and the result is scaled back down
     * by the amount the two passes multiplied it up.
     *
     * @param int $plane 0 for luma, 1 and 2 for the chroma planes
     * @param int $left_edge column of the block's first sample
     * @param int $top_edge row of the block's first sample
     * @param array $coefficients 1024 dequantized values in raster order
     */
    private function idct32x32Add($plane, $left_edge, $top_edge,
        $coefficients)
    {
        $intermediate = array_fill(0, 1024, 0);
        for ($i = 0; $i < 32; $i++) {
            $row = array_slice($coefficients, $i * 32, 32);
            $transformed = self::inverseCosine32($row);
            for ($j = 0; $j < 32; $j++) {
                $intermediate[$i * 32 + $j] = $transformed[$j];
            }
        }
        $stride = $this->strides[$plane];
        $base = $this->sampleAt($plane, $left_edge, $top_edge);
        for ($i = 0; $i < 32; $i++) {
            $column = [];
            for ($j = 0; $j < 32; $j++) {
                $column[$j] = $intermediate[$j * 32 + $i];
            }
            $transformed = self::inverseCosine32($column);
            for ($j = 0; $j < 32; $j++) {
                $offset = $base + $j * $stride + $i;
                $this->planes[$plane][$offset] = self::holdInsideByte(
                    $this->planes[$plane][$offset]
                    + (($transformed[$j] + self::IDCT32_ROUND)
                    >> self::IDCT32_SHIFT));
            }
        }
    }
    /**
     * SUPERBLOCK_UNITS is mode info units along one side of a superblock.
     * @var mixed
     */
    private const SUPERBLOCK_UNITS = 8;
    /**
     * SUPERBLOCK_SAMPLES is samples along one side of a superblock.
     * @var mixed
     */
    private const SUPERBLOCK_SAMPLES = 64;
    /**
     * SHARPNESS_STEEP is sharpness above this point costs an extra bit of
     * filter width.
     * @var mixed
     */
    private const SHARPNESS_STEEP = 4;
    /**
     * SHARPNESS_CEILING is highest value the interior limit may take, offset by
     * sharpness.
     * @var mixed
     */
    private const SHARPNESS_CEILING = 9;
    /**
     * HEV_SHIFT is bits of filter level that select the high edge variance
     * threshold.
     * @var mixed
     */
    private const HEV_SHIFT = 4;
    /**
     * DELTA_SCALE_SHIFT is bits of filter level that scale the reference frame
     * adjustment.
     * @var mixed
     */
    private const DELTA_SCALE_SHIFT = 5;
    /**
     * MAX_FILTER_LEVEL is largest filter level the format allows.
     * @var mixed
     */
    private const MAX_FILTER_LEVEL = 63;
    /**
     * loopFilter smooths the sample values either side of every block edge in
     * the frame. Block edges are where quantization error shows as a visible
     * seam, so the format smooths across them by an amount that depends on how
     * strong the frame's filter setting is and on how large the transforms
     * meeting at the edge were. Flat areas get a wide gentle filter, and edges
     * that look like real detail are left alone.
     */
    private function loopFilter()
    {
        if ($this->header->loop_filter_level === 0) {
            return;
        }
        /*
            The passes are interleaved a superblock at a time. A whole plane
            cannot be done in one sweep, because the horizontal pass of one
            superblock must read samples the vertical pass of the next has
            not yet touched.
        */
        for ($top = 0; $top < $this->block_rows; $top +=
            self::SUPERBLOCK_UNITS) {
            for ($start = 0; $start < $this->block_cols;
                    $start += self::SUPERBLOCK_UNITS) {
                for ($plane = 0; $plane < 3; $plane++) {
                    $sub_x = $plane ? $this->ss_x : 0;
                    $sub_y = $plane ? $this->ss_y : 0;
                    $this->filterSuperblockEdges($plane, $sub_x, $sub_y,
                        $top, $start, true);
                    $this->filterSuperblockEdges($plane, $sub_x, $sub_y,
                        $top, $start, false);
                }
            }
        }
    }
    /**
     * filterSuperblockEdges filters the vertical or the horizontal edges inside
     * one superblock of one plane.
     *
     * @param int $plane 0 for luma, 1 and 2 for the chroma planes
     * @param int $sub_x how much the plane is narrowed, as a shift
     * @param int $sub_y how much the plane is shortened, as a shift
     * @param int $top first mode info row of the superblock
     * @param int $start first mode info column of the superblock
     * @param bool $vertical true for edges running down the picture
     */
    private function filterSuperblockEdges($plane, $sub_x, $sub_y, $top,
        $start, $vertical)
    {
        $row_step = 1 << $sub_y;
        $col_step = 1 << $sub_x;
        $stride = $this->strides[$plane];
        $row_end = min($top + self::SUPERBLOCK_UNITS, $this->block_rows);
        $col_end = min($start + self::SUPERBLOCK_UNITS, $this->block_cols);
        for ($row = $top; $row < $row_end; $row += $row_step) {
            for ($col = $start; $col < $col_end; $col += $col_step) {
                $address = $row * $this->block_cols + $col;
                $level = $this->filterLevelFor($address);
                if ($level === 0) {
                    continue;
                }
                $size = $this->filterTransformSize($address, $plane,
                    $sub_x, $sub_y);
                $unit = ($vertical ? ($col >> $sub_x) : ($row >> $sub_y));
                $width = $this->filterWidthAt($size, $unit);
                /*
                    A chroma plane that carries half as many samples has no
                    room for the widest filter against the last row or
                    column of the picture, so it narrows there.
                */
                $at_border = $vertical
                    ? ($sub_x > 0 && $col === $this->block_cols - 1)
                    : ($sub_y > 0 && $row === $this->block_rows - 1);
                if ($at_border && $width === 16) {
                    $width = 8;
                }
                $left = ($col * 8) >> $sub_x;
                $upper = ($row * 8) >> $sub_y;
                $begin = $this->sampleAt($plane, $left, $upper);
                $step = $vertical ? 1 : $stride;
                $along = $vertical ? $stride : 1;
                $on_border = $vertical ? ($col === 0) : ($row === 0);
                if (!$on_border && $width > 0) {
                    $this->filterEdgeLine($plane, $begin, $step, $along,
                        $width, $level);
                }
                /* a block split into 4 by 4 transforms has an inner edge */
                if ($size === 0 && !$at_border) {
                    $this->filterEdgeLine($plane, $begin + 4 * $step, $step,
                        $along, 4, $level);
                }
            }
        }
    }
    /**
     * filterWidthAt works out how wide the filter across one edge should be. A
     * wider filter is used where large transforms meet, and only at positions
     * that are a multiple of that transform's width.
     *
     * @param int $size transform size index, 0 for 4 by 4 up to 3
     * @param int $unit position of the edge in units of four samples
     * @return int number of samples the filter spans, or 0 for no edge
     */
    private function filterWidthAt($size, $unit)
    {
        if ($size === 3) {
            return (($unit & 3) === 0) ? 16 : 0;
        }
        if ($size === 2) {
            return (($unit & 1) === 0) ? 16 : 0;
        }
        if ($size === 1) {
            return 8;
        }
        return (($unit & 3) === 0) ? 8 : 4;
    }
    /**
     * filterTransformSize gives the transform size that decides the filter
     * width at a block. The chroma planes carry smaller transforms than the
     * luma plane when the picture is stored with fewer chroma samples.
     *
     * @param int $address index of the mode info unit
     * @param int $plane 0 for luma, 1 and 2 for the chroma planes
     * @param int $sub_x how much the plane is narrowed, as a shift
     * @param int $sub_y how much the plane is shortened, as a shift
     * @return int transform size index
     */
    private function filterTransformSize($address, $plane, $sub_x, $sub_y)
    {
        $size = $this->block_transform_size[$address] ?? 0;
        if ($plane === 0) {
            return $size;
        }
        $block = max($this->block_size[$address] ?? 3, 3);
        $chroma = $this->uvBlockSize($block, $sub_x, $sub_y);
        return min($size, self::MAX_TX_SIZE[$chroma]);
    }
    /**
     * filterLevelFor gives the filter strength to use at one block. Intra
     * pictures take the frame level plus the adjustment the header carries for
     * blocks predicted from the picture itself.
     *
     * @param int $address index of the mode info unit
     * @return int filter level from 0 to 63
     */
    private function filterLevelFor($address)
    {
        $level = $this->header->loop_filter_level;
        $segment = $this->block_segment[$address] ?? 0;
        if ($this->header->segmentation_enabled
            && !empty($this->header->segment_feature_enabled[$segment][
                Vp9Header::SEGMENT_FEATURE_FILTER])) {
            $adjust = $this->header->segment_feature_value[$segment][
                Vp9Header::SEGMENT_FEATURE_FILTER];
            $level = $this->header->segment_absolute
                ? $adjust : $level + $adjust;
            $level = max(0, min(self::MAX_FILTER_LEVEL, $level));
        }
        if ($this->header->loop_filter_delta_enabled) {
            $scale = 1 << ($level >> self::DELTA_SCALE_SHIFT);
            $level += $this->header->loop_filter_reference_deltas[0] * $scale;
            $level = max(0, min(self::MAX_FILTER_LEVEL, $level));
        }
        return $level;
    }
    /**
     * filterEdgeLine filters eight lines of samples running across one block
     * edge. The work for each line is written out here rather than called,
     * because a picture of any size crosses this point over a million times and
     * the cost of the call would be most of the work.
     *
     * @param int $plane 0 for luma, 1 and 2 for the chroma planes
     * @param int $start offset of the first sample past the edge
     * @param int $step distance between samples across the edge
     * @param int $along distance between the lines being filtered
     * @param int $width number of samples the filter spans
     * @param int $level filter level from 0 to 63
     */
    private function filterEdgeLine($plane, $start, $step, $along, $width,
        $level)
    {
        $sharpness = $this->header->loop_filter_sharpness;
        $inside = $level >> ((int)($sharpness > 0)
            + (int)($sharpness > self::SHARPNESS_STEEP));
        if ($sharpness > 0) {
            $ceiling = self::SHARPNESS_CEILING - $sharpness;
            if ($inside > $ceiling) {
                $inside = $ceiling;
            }
        }
        if ($inside < 1) {
            $inside = 1;
        }
        $limit = 2 * ($level + 2) + $inside;
        $threshold = $level >> self::HEV_SHIFT;
        $samples = &$this->planes[$plane];
        $second_pass = 2 * $step;
        $third_pass = 3 * $step;
        $fourth_pass = 4 * $step;
        for ($line = 0; $line < 8; $line++) {
            $base = $start + $line * $along;
            $before_edge_three = $samples[$base - $fourth_pass];
            $before_edge_two = $samples[$base - $third_pass];
            $before_edge_one = $samples[$base - $second_pass];
            $before_edge = $samples[$base - $step];
            $after_edge = $samples[$base];
            $after_edge_one = $samples[$base + $step];
            $after_edge_two = $samples[$base + $second_pass];
            $after_edge_three = $samples[$base + $third_pass];
            if (abs($before_edge - $after_edge) * 2
                + intdiv(abs($before_edge_one - $after_edge_one), 2) > $limit
                || abs($before_edge_three - $before_edge_two) > $inside
                || abs($before_edge_two - $before_edge_one) > $inside
                || abs($before_edge_one - $before_edge) > $inside
                || abs($after_edge_one - $after_edge) > $inside
                || abs($after_edge_two - $after_edge_one) > $inside
                || abs($after_edge_three - $after_edge_two) > $inside) {
                continue;
            }
            $flat = $width > 4 && abs($before_edge_one - $before_edge) <= 1
                && abs($after_edge_one - $after_edge) <= 1
                && abs($before_edge_two - $before_edge) <= 1
                && abs($after_edge_two - $after_edge) <= 1
                && abs($before_edge_three - $before_edge) <= 1
                && abs($after_edge_three - $after_edge) <= 1;
            if ($flat && $width == 16) {
                /*
                    The widest filter needs eight samples either side, which
                    are only worth fetching once the nearer ones agree.
                */
                $wide = true;
                for ($i = 4; $i < 8; $i++) {
                    if (abs($samples[$base - ($i + 1) * $step] -
                        $before_edge) > 1
                        || abs($samples[$base + $i * $step] - $after_edge) >
                            1) {
                        $wide = false;
                        break;
                    }
                }
                if ($wide) {
                    $this->filterWide($plane, $base, $step);
                    continue;
                }
            }
            if ($flat) {
                $samples[$base - $third_pass] = ($before_edge_three +
                    $before_edge_three + $before_edge_three
                    + 2 * $before_edge_two + $before_edge_one +
                        $before_edge + $after_edge + 4) >> 3;
                $samples[$base - $second_pass] = ($before_edge_three +
                    $before_edge_three + $before_edge_two
                    + 2 * $before_edge_one + $before_edge + $after_edge +
                        $after_edge_one + 4) >> 3;
                $samples[$base - $step] = ($before_edge_three +
                    $before_edge_two + $before_edge_one
                    + 2 * $before_edge + $after_edge + $after_edge_one +
                        $after_edge_two + 4) >> 3;
                $samples[$base] = ($before_edge_two + $before_edge_one +
                    $before_edge
                    + 2 * $after_edge + $after_edge_one + $after_edge_two +
                        $after_edge_three + 4) >> 3;
                $samples[$base + $step] = ($before_edge_one + $before_edge +
                    $after_edge
                    + 2 * $after_edge_one + $after_edge_two +
                        $after_edge_three + $after_edge_three + 4) >> 3;
                $samples[$base + $second_pass] = ($before_edge +
                    $after_edge + $after_edge_one
                    + 2 * $after_edge_two + $after_edge_three +
                        $after_edge_three + $after_edge_three + 4) >> 3;
                continue;
            }
            $steep = abs($before_edge_one - $before_edge) > $threshold
                || abs($after_edge_one - $after_edge) > $threshold;
            $signed_one = $before_edge_one - 128;
            $signed_zero = $before_edge - 128;
            $next_zero = $after_edge - 128;
            $next_one = $after_edge_one - 128;
            $adjust = $steep ? self::clampSigned($signed_one - $next_one) : 0;
            $adjust = self::clampSigned($adjust + 3 * ($next_zero -
                $signed_zero));
            $outer = self::clampSigned($adjust + 4) >> 3;
            $inner = self::clampSigned($adjust + 3) >> 3;
            $samples[$base] = self::clampSigned($next_zero - $outer) + 128;
            $samples[$base - $step] =
                self::clampSigned($signed_zero + $inner) + 128;
            if ($steep) {
                continue;
            }
            $spread = ($outer + 1) >> 1;
            $samples[$base + $step] =
                self::clampSigned($next_one - $spread) + 128;
            $samples[$base - $second_pass] =
                self::clampSigned($signed_one + $spread) + 128;
        }
    }
    /**
     * clampSigned clamps a value to the range a signed byte can hold.
     *
     * @param int $value value to clamp
     * @return int value between -128 and 127
     */
    private static function clampSigned($value)
    {
        if ($value < -128) {
            return -128;
        }
        if ($value > 127) {
            return 127;
        }
        return $value;
    }
    /**
     * filterWide smooths fourteen samples across an edge with a fifteen tap
     * average, used where a large flat area meets a block boundary.
     *
     * @param int $plane 0 for luma, 1 and 2 for the chroma planes
     * @param int $base offset of the first sample past the edge
     * @param int $step distance between samples across the edge
     */
    private function filterWide($plane, $base, $step)
    {
        $samples = &$this->planes[$plane];
        $window = [];
        for ($i = -8; $i < 8; $i++) {
            $window[$i] = $samples[$base + $i * $step];
        }
        for ($i = -7; $i < 7; $i++) {
            $sum = $window[$i];
            /*
                Each output is the average of the fifteen samples centered
                on it, with the far edge sample repeated to fill in past the
                ends of the window.
            */
            for ($offset = -7; $offset <= 7; $offset++) {
                $index = $i + $offset;
                if ($index < -8) {
                    $index = -8;
                }
                if ($index > 7) {
                    $index = 7;
                }
                $sum += $window[$index];
            }
            $result[$i] = ($sum + 8) >> 4;
        }
        foreach ($result as $offset => $value) {
            $samples[$base + $offset * $step] = $value;
        }
    }
}
