/ src / library / av_processing / HevcDecoder.php
<?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 file turns an HEVC keyframe into a picture. It walks the tree a
 * frame is divided into, decodes each part, and smooths the edges
 * afterwards.
 */
namespace seekquarry\yioop\library\av_processing;
/**
 * HevcContexts where each kind of decision keeps its contexts, and the value
 * each context starts from in a slice coded on its own. A context is a running
 * estimate of how likely one value of a decision is. The starting values are
 * published by the standard; the ones for predicted slices are left out, since
 * only slices coded on their own are decoded here.
 */
final class HevcContexts
{
    /**
     * SAO_MERGE_FLAG is where the probabilities start for whether a block takes
     * its sample offsets from the block beside it.
     * @var mixed
     */
    public const SAO_MERGE_FLAG = 0;
    /**
     * SAO_TYPE_IDX is where the probabilities start for which kind of sample
     * offset a block uses.
     * @var mixed
     */
    public const SAO_TYPE_IDX = 1;
    /**
     * SPLIT_CODING_UNIT_FLAG is where the probabilities start for whether a
     * block is split into four smaller ones.
     * @var mixed
     */
    public const SPLIT_CODING_UNIT_FLAG = 2;
    /**
     * CU_TRANSQUANT_BYPASS_FLAG is where the probabilities start for whether a
     * block stores its samples as they are, with nothing thrown away.
     * @var mixed
     */
    public const CU_TRANSQUANT_BYPASS_FLAG = 5;
    /**
     * SKIP_FLAG is where the probabilities start for whether a block was left
     * out of the stream and is filled from its guess alone.
     * @var mixed
     */
    public const SKIP_FLAG = 6;
    /**
     * CU_QP_DELTA is where the probabilities start for how much a block changes
     * the quantizer in force.
     * @var mixed
     */
    public const CU_QP_DELTA = 9;
    /**
     * PRED_MODE is where the probabilities start for whether a block is guessed
     * from this picture or from another one.
     * @var mixed
     */
    public const PRED_MODE = 12;
    /**
     * PART_MODE is where the probabilities start for how a block is divided
     * into the parts that are guessed separately.
     * @var mixed
     */
    public const PART_MODE = 13;
    /**
     * PREV_INTRA_LUMA_PRED is where the suggested mode flag contexts start.
     */
    public const PREV_INTRA_LUMA_PRED = 17;
    /**
     * INTRA_CHROMA_PRED_MODE is where the probabilities start for how the color
     * planes of a block are guessed.
     * @var mixed
     */
    public const INTRA_CHROMA_PRED_MODE = 18;
    /**
     * MERGE_FLAG is where the probabilities start for whether a block takes its
     * motion from a neighbor rather than carrying its own.
     * @var mixed
     */
    public const MERGE_FLAG = 20;
    /**
     * MERGE_IDX is where the probabilities start for which neighbor it takes
     * that motion from.
     * @var mixed
     */
    public const MERGE_IDX = 21;
    /**
     * INTER_PRED_IDC is where the probabilities start for which other pictures
     * a block leans on.
     * @var mixed
     */
    public const INTER_PRED_IDC = 22;
    /**
     * FIRST_LIST_PICTURE is where the probabilities start for which
     * earlier picture a block leans on, among those listed first.
     */
    public const FIRST_LIST_PICTURE = 27;
    /**
     * SECOND_LIST_PICTURE is where the probabilities start for which
     * picture a block leans on, among the second of the two lists of
     * pictures a frame may lean on.
     */
    public const SECOND_LIST_PICTURE = 29;
    /**
     * MOTION_DIFFERS_FLAG is where the
     * probabilities start for whether a block's motion differs at
     * all from the motion guessed for it.
     */
    public const MOTION_DIFFERS_FLAG = 31;
    /**
     * MOTION_DIFFERS_BY_MORE_FLAG is where they start for
     * whether that difference is larger than one.
     */
    public const MOTION_DIFFERS_BY_MORE_FLAG = 33;
    /**
     * MOTION_GUESS_FLAG is where the probabilities start for which of two
     * motion
     * guesses a block uses.
     * @var mixed
     */
    public const MOTION_GUESS_FLAG = 35;
    /**
     * NO_RESIDUAL_DATA_FLAG is where the probabilities start for whether a
     * block carries any values at all beyond its guess.
     * @var mixed
     */
    public const NO_RESIDUAL_DATA_FLAG = 36;
    /**
     * SPLIT_TRANSFORM_FLAG is where the probabilities start for whether a
     * block's transform is split into four smaller ones.
     * @var mixed
     */
    public const SPLIT_TRANSFORM_FLAG = 37;
    /**
     * CBF_LUMA is where the probabilities start for whether a brightness block
     * carries any values.
     * @var mixed
     */
    public const CBF_LUMA = 40;
    /**
     * CBF_CHROMA is where the chroma coefficient flag contexts start.
     */
    public const CBF_CHROMA = 42;
    /**
     * TRANSFORM_SKIP_FLAG is where the probabilities start for whether a block
     * skips the transform and keeps its values as they are.
     * @var mixed
     */
    public const TRANSFORM_SKIP_FLAG = 47;
    /**
     * STORED_AS_DIFFERENCES_FLAG is where the probabilities start for whether a
     * block's values are stored as differences from the value beside each of
     * them.
     * @var mixed
     */
    public const STORED_AS_DIFFERENCES_FLAG = 49;
    /**
     * DIFFERENCE_DIRECTION_FLAG is where the probabilities start for whether
     * those differences run across the block or down it.
     * @var mixed
     */
    public const DIFFERENCE_DIRECTION_FLAG = 51;
    /**
     * LAST_X_PREFIX is where the last coefficient column contexts start.
     */
    public const LAST_X_PREFIX = 53;
    /**
     * LAST_Y_PREFIX is where the last coefficient row contexts start.
     */
    public const LAST_Y_PREFIX = 71;
    /**
     * SIG_COEFF_GROUP is where the coefficient group flag contexts start.
     */
    public const SIG_COEFF_GROUP = 89;
    /**
     * SIG_COEFF is where the coefficient present flag contexts start.
     */
    public const SIG_COEFF = 93;
    /**
     * COEFF_GREATER1 is where the first coefficient size flag contexts start.
     */
    public const COEFF_GREATER1 = 137;
    /**
     * COEFF_GREATER2 is where the second coefficient size flag contexts start.
     */
    public const COEFF_GREATER2 = 161;
    /**
     * LOG2_RES_SCALE is where the residual scale contexts start.
     */
    public const LOG2_RES_SCALE = 167;
    /**
     * RES_SCALE_SIGN_FLAG is where the probabilities start for the sign of the
     * factor by which one color plane is scaled against the other.
     * @var mixed
     */
    public const RES_SCALE_SIGN_FLAG = 175;
    /**
     * CU_CHROMA_QP_OFFSET_FLAG is where the probabilities start for whether a
     * block changes the color quantizer.
     * @var mixed
     */
    public const CU_CHROMA_QP_OFFSET_FLAG = 177;
    /**
     * CU_CHROMA_QP_OFFSET_IDX is where the probabilities start for which of the
     * offered color quantizer changes it uses.
     * @var mixed
     */
    public const CU_CHROMA_QP_OFFSET_IDX = 178;
    /**
     * TOTAL is how many contexts there are in all.
     */
    public const TOTAL = 179;
    /**
     * INIT is the value each context starts from, before the quantizer is
     * applied.
     */
    public const INIT = [
        153, 200, 139, 141, 157, 154, 154, 154, 154, 154, 154, 154, 154, 184,
        154, 154, 154, 184, 63, 139, 154, 154, 154, 154, 154, 154, 154, 154,
        154, 154, 154, 154, 154, 154, 154, 154, 154, 153, 138, 138, 111, 141,
        94, 138, 182, 154, 154, 139, 139, 139, 139, 139, 139, 110, 110, 124,
        125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79, 108, 123, 63,
        110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111,
        79, 108, 123, 63, 91, 171, 134, 141, 111, 111, 125, 110, 110, 94, 124,
        108, 124, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125,
        107, 125, 141, 179, 153, 125, 140, 139, 182, 182, 152, 136, 152, 136,
        153, 136, 139, 111, 136, 139, 111, 141, 111, 140, 92, 137, 138, 140,
        152, 138, 139, 153, 74, 149, 92, 139, 107, 122, 152, 140, 179, 166, 182,
        140, 227, 122, 197, 138, 153, 136, 167, 152, 152, 154, 154, 154, 154,
        154, 154, 154, 154, 154, 154, 154, 154
    ];
}

/**
 * HevcSliceHeader the header of one slice segment: which picture parameters it
 * uses, what kind of slice it is, its quantizer, and which filters act on it.
 */
final class HevcSliceHeader
{
    /**
     * $first_slice_in_picture stores true when this segment starts the picture.
     * @var bool
     */
    public bool $first_slice_in_picture = true;
    /**
     * $dependent stores true when the segment continues the one before it.
     * @var bool
     */
    public bool $dependent = false;
    /**
     * $picture_settings_id stores which picture parameter set this slice uses.
     * @var int
     */
    public int $picture_settings_id = 0;
    /**
     * $segment_address stores where in the picture this segment starts, in
     * coding tree blocks.
     * @var int
     */
    public int $segment_address = 0;
    /**
     * $slice_type stores 2 for a slice coded on its own, 1 and 0 for the
     * predicted kinds.
     * @var int
     */
    public int $slice_type = 2;
    /**
     * $picture_order_low_bits stores the low bits of the picture order count
     * this
     * slice
     * carries.
     * @var int
     */
    public int $picture_order_low_bits = 0;
    /**
     * $sample_offset_luma stores true when the offset filter acts on the
     * brightness
     * plane.
     * @var bool
     */
    public bool $sample_offset_luma = false;
    /**
     * $sample_offset_chroma stores true when the offset filter acts on the
     * color planes.
     * @var bool
     */
    public bool $sample_offset_chroma = false;
    /**
     * $slice_quantizer stores quantizer index this slice starts at.
     * @var int
     */
    public int $slice_quantizer = 26;
    /**
     * $blue_quantizer_offset stores shift this slice applies to the blue color
     * quantizer.
     * @var int
     */
    public int $blue_quantizer_offset = 0;
    /**
     * $red_quantizer_offset stores shift this slice applies to the red color
     * quantizer.
     * @var int
     */
    public int $red_quantizer_offset = 0;
    /**
     * $deblocking_disabled stores true when smoothing is turned off for this
     * slice.
     * @var bool
     */
    public bool $deblocking_disabled = false;
    /**
     * $beta_offset stores how far the smoothing filter may reach.
     * @var int
     */
    public int $beta_offset = 0;
    /**
     * $move_limit_offset stores how strongly the smoothing filter acts.
     * @var int
     */
    public int $move_limit_offset = 0;
    /**
     * $loop_filter_across_slices stores true when smoothing crosses the edges
     * of this slice.
     * @var bool
     */
    public bool $loop_filter_across_slices = true;
    /**
     * $data_offset stores how many bytes into the unit the coded data starts.
     * @var int
     */
    public int $data_offset = 0;
    /**
     * readSettings reads the header of one slice segment. Only slices coded on
     * their own are read through, since that is all a keyframe holds; anything
     * else is refused rather than guessed at. removed coded against against
     *
     * @param string $unpacked_bytes the unit's payload, with escapes already
     * @param int $stream_unit_type which kind of unit this is
     * @param HevcSps $sequence_settings sequence parameters the picture was
     * @param HevcPps $picture_settings picture parameters this slice was coded
     * @return HevcSliceHeader the parsed header
     */
    public static function readSettings(string $unpacked_bytes,
        int $stream_unit_type,
        HevcSps $sequence_settings,
        HevcPps $picture_settings): self
    {
        $bits = new H264Bits($unpacked_bytes);
        $header = new self();
        $header->first_slice_in_picture = $bits->readBit() === 1;
        if ($stream_unit_type >= 16 && $stream_unit_type <= 23) {
            $bits->readBit();
        }
        $header->picture_settings_id = $bits->readWholeNumber();
        $tree_block_width = intdiv($sequence_settings->frame_width +
            (1 << $sequence_settings->power_of_two_tree_block_size) - 1,
            1 << $sequence_settings->power_of_two_tree_block_size);
        $tree_block_height = intdiv($sequence_settings->frame_height +
            (1 << $sequence_settings->power_of_two_tree_block_size) - 1,
            1 << $sequence_settings->power_of_two_tree_block_size);
        if (!$header->first_slice_in_picture) {
            if ($picture_settings->dependent_slice_segments_enabled) {
                $header->dependent = $bits->readBit() === 1;
            }
            $width = 0;
            while ((1 << $width) < $tree_block_width * $tree_block_height) {
                $width++;
            }
            $header->segment_address = $width > 0 ? $bits->readBits($width) : 0;
        }
        if ($header->dependent) {
            throw new HevcException(
                'dependent slice segments are not supported');
        }
        $bits->skipBits($picture_settings->count_extra_slice_header_bits);
        $header->slice_type = $bits->readWholeNumber();
        if ($header->slice_type !== 2) {
            throw new HevcException(
                'only slices coded on their own are supported');
        }
        if ($picture_settings->output_flag_present) {
            $bits->readBit();
        }
        if ($sequence_settings->separate_color_plane) {
            $bits->readBits(2);
        }
        $is_self_contained = $stream_unit_type === 19 ||
            $stream_unit_type === 20;
        if (!$is_self_contained) {
            /*
                A picture that starts a group without clearing what came
                before still carries the reference lists of the pictures it
                does not use, and they have to be read past exactly.
            */
            $header->picture_order_low_bits =
                $bits->readBits($sequence_settings
                    ->picture_order_low_bit_count);
            if ($bits->readBit() === 0) {
                HevcParamParser::skipReferencePictureSet($bits,
                    $sequence_settings->count_short_term_reference_picture_sets,
                        $sequence_settings->reference_picture_set_counts);
            } elseif ($sequence_settings
                ->count_short_term_reference_picture_sets > 1) {
                $width = 0;
                while ((1 << $width) < $sequence_settings
                    ->count_short_term_reference_picture_sets) {
                    $width++;
                }
                $bits->readBits($width);
            }
            if ($sequence_settings->long_term_reference_pics_present) {
                $from_sequence_settings = 0;
                if ($sequence_settings->count_long_term_reference_pics > 0) {
                    $from_sequence_settings = $bits->readWholeNumber();
                }
                $own = $bits->readWholeNumber();
                for ($index =
                    0; $index < $from_sequence_settings + $own; $index++) {
                    if ($index < $from_sequence_settings) {
                        if ($sequence_settings
                            ->count_long_term_reference_pics > 1) {
                            $width = 0;
                            while ((
                                1 << $width) < $sequence_settings
                                    ->count_long_term_reference_pics) {
                                $width++;
                            }
                            $bits->readBits($width);
                        }
                    } else {
                        $bits->readBits($sequence_settings
                            ->picture_order_low_bit_count);
                        $bits->readBit();
                    }
                    if ($bits->readBit() === 1) {
                        $bits->readWholeNumber();
                    }
                }
            }
            if ($sequence_settings->motion_from_other_pictures_allowed) {
                $bits->readBit();
            }
        }
        if ($sequence_settings->sample_offset_enabled) {
            $header->sample_offset_luma = $bits->readBit() === 1;
            if ($sequence_settings->chroma_format_setting !== 0) {
                $header->sample_offset_chroma = $bits->readBit() === 1;
            }
        }
        $header->slice_quantizer = $picture_settings->starting_quantizer + $bits
            ->readSignedNumber();
        if ($picture_settings->slice_chroma_quantizer_offsets_present) {
            $header->blue_quantizer_offset = $bits->readSignedNumber();
            $header->red_quantizer_offset = $bits->readSignedNumber();
        }
        if ($picture_settings->chroma_quantizer_offset_list_enabled) {
            $bits->readBit();
        }
        $header->deblocking_disabled =
            $picture_settings->deblocking_filter_disabled;
        $header->beta_offset = $picture_settings->beta_offset;
        $header->move_limit_offset = $picture_settings->move_limit_offset;
        if ($picture_settings->deblocking_filter_control_present) {
            $override = false;
            if ($picture_settings->deblocking_filter_override_enabled) {
                $override = $bits->readBit() === 1;
            }
            if ($override) {
                $header->deblocking_disabled = $bits->readBit() === 1;
                if (!$header->deblocking_disabled) {
                    $header->beta_offset = $bits->readSignedNumber() * 2;
                    $header->move_limit_offset = $bits->readSignedNumber() * 2;
                }
            }
        }
        $header->loop_filter_across_slices =
            $picture_settings->loop_filter_across_slices;
        if ($picture_settings->loop_filter_across_slices
            && ($header->sample_offset_luma || $header->sample_offset_chroma
            || !$header->deblocking_disabled)) {
            $header->loop_filter_across_slices = $bits->readBit() === 1;
        }
        if ($picture_settings->tiles_enabled ||
            $picture_settings->entropy_coding_sync_enabled) {
            $points = $bits->readWholeNumber();
            if ($points > 0) {
                $width = $bits->readWholeNumber() + 1;
                for ($point = 0; $point < $points; $point++) {
                    $bits->readBits($width);
                }
            }
        }
        if ($picture_settings->slice_header_extension_present) {
            $length = $bits->readWholeNumber();
            $bits->skipBits($length * 8);
        }
        /* the coded data starts at the next byte boundary */
        $bits->readBit();
        $bits->alignToByte();
        $header->data_offset = $bits->position >> 3;
        return $header;
    }
}

/**
 * HevcScan holds the orders in which HEVC visits the places of a
 * block: along up-right diagonals, along rows, and down columns.
 * Whichever order a block uses is used both within each group of
 * sixteen values and across the groups of a larger block.
 */
final class HevcScan
{
    /**
     * DIAGONAL is the order that visits a block's places corner to corner,
     * which is the order HEVC uses for most blocks.
     * @var mixed
     */
    public const DIAGONAL = 0;
    /**
     * HORIZONTAL is the order that runs along rows.
     */
    public const HORIZONTAL = 1;
    /**
     * VERTICAL is the order that runs down columns.
     */
    public const VERTICAL = 2;
    /**
     * $cache stores orders worked out already, kept by size and kind.
     * @var array
     */
    private static array $cache = [];
    /**
     * sortByOrder the positions of a square block in one of the three orders.
     *
     * @param int $size how many positions along one side
     * @param int $kind which of the three orders to use
     * @return array each position as its column and row
     */
    public static function sortByOrder(int $size, int $kind): array
    {
        $key = $size . ':' . $kind;
        if (isset(self::$cache[$key])) {
            return self::$cache[$key];
        }
        $written = [];
        if ($kind === self::HORIZONTAL) {
            for ($row = 0; $row < $size; $row++) {
                for ($column = 0; $column < $size; $column++) {
                    $written[] = [$column, $row];
                }
            }
        } elseif ($kind === self::VERTICAL) {
            for ($column = 0; $column < $size; $column++) {
                for ($row = 0; $row < $size; $row++) {
                    $written[] = [$column, $row];
                }
            }
        } else {
            $column = 0;
            $row = 0;
            while (count($written) < $size * $size) {
                while ($row >= 0) {
                    if ($column < $size && $row < $size) {
                        $written[] = [$column, $row];
                    }
                    $row--;
                    $column++;
                }
                $row = $column;
                $column = 0;
            }
        }
        self::$cache[$key] = $written;
        return $written;
    }
}

/**
 * HevcSaoParams the settings of the offset filter for one coding tree block:
 * which of the two kinds of correction it applies to each plane, and by how
 * much.
 */
final class HevcSaoParams
{
    /**
     * $kind stores 0 for none, 1 for a band correction, 2 for an edge
     * correction.
     * @var array
     */
    public array $kind = [0, 0, 0];
    /**
     * $offset stores the four offsets each plane's correction adds.
     * @var array
     */
    public array $offset = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]];
    /**
     * $band_position stores which band a band correction starts at, per plane.
     * @var array
     */
    public array $band_position = [0, 0, 0];
    /**
     * $edge_class stores which direction an edge correction looks along, per
     * plane.
     * @var array
     */
    public array $edge_class = [0, 0, 0];
}

/**
 * HevcTransform turns the coefficients of an H.265 block back into sample
 * differences. The format uses one thirty-two point integer transform; the
 * smaller sizes take every second, fourth or eighth row of it. Blocks of four
 * brightness samples predicted within the picture use a different small
 * transform instead, which suits their flatter statistics.
 */
final class HevcTransform
{
    /**
     * LEVEL_SCALE is how much each quantizer step multiplies a coefficient by.
     */
    private const LEVEL_SCALE = [40, 45, 51, 57, 64, 72];
    /**
     * FLAT_WEIGHT is the weight given to a coefficient when no scaling list is
     * supplied.
     */
    private const FLAT_WEIGHT = 16;
    /**
     * COEFF_MIN is smallest value a coefficient may hold after dequantization.
     */
    private const COEFF_MIN = -32768;
    /**
     * COEFF_MAX is largest value a coefficient may hold after dequantization.
     */
    private const COEFF_MAX = 32767;
    /**
     * FIRST_PASS_SHIFT is bits dropped after the first of the two passes.
     */
    private const FIRST_PASS_SHIFT = 7;
    /**
     * SECOND_PASS_SHIFT is bits dropped after the second pass, at eight bits a
     * sample.
     */
    private const SECOND_PASS_SHIFT = 12;
    /**
     * MATRIX is the thirty-two point transform the standard publishes.
     */
    private const MATRIX = [
        [64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
            64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],
        [90, 90, 88, 85, 82, 78, 73, 67, 61, 54, 46, 38, 31, 22, 13, 4, -4, -13,
            -22, -31, -38, -46, -54, -61, -67, -73, -78, -82, -85, -88, -90,
            -90],
        [90, 87, 80, 70, 57, 43, 25, 9, -9, -25, -43, -57, -70, -80, -87, -90,
            -90, -87, -80, -70, -57, -43, -25, -9, 9, 25, 43, 57, 70, 80, 87,
            90],
        [90, 82, 67, 46, 22, -4, -31, -54, -73, -85, -90, -88, -78, -61, -38,
            -13, 13, 38, 61, 78, 88, 90, 85, 73, 54, 31, 4, -22, -46, -67, -82,
            -90],
        [89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89,
            89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75,
            89],
        [88, 67, 31, -13, -54, -82, -90, -78, -46, -4, 38, 73, 90, 85, 61, 22,
            -22, -61, -85, -90, -73, -38, 4, 46, 78, 90, 82, 54, 13, -31, -67,
            -88],
        [87, 57, 9, -43, -80, -90, -70, -25, 25, 70, 90, 80, 43, -9, -57, -87,
            -87, -57, -9, 43, 80, 90, 70, 25, -25, -70, -90, -80, -43, 9, 57,
            87],
        [85, 46, -13, -67, -90, -73, -22, 38, 82, 88, 54, -4, -61, -90, -78,
            -31, 31, 78, 90, 61, 4, -54, -88, -82, -38, 22, 73, 90, 67, 13, -46,
            -85],
        [83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83,
            83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36,
            83],
        [82, 22, -54, -90, -61, 13, 78, 85, 31, -46, -90, -67, 4, 73, 88, 38,
            -38, -88, -73, -4, 67, 90, 46, -31, -85, -78, -13, 61, 90, 54, -22,
            -82],
        [80, 9, -70, -87, -25, 57, 90, 43, -43, -90, -57, 25, 87, 70, -9, -80,
            -80, -9, 70, 87, 25, -57, -90, -43, 43, 90, 57, -25, -87, -70, 9,
            80],
        [78, -4, -82, -73, 13, 85, 67, -22, -88, -61, 31, 90, 54, -38, -90, -46,
            46, 90, 38, -54, -90, -31, 61, 88, 22, -67, -85, -13, 73, 82, 4,
            -78],
        [75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75,
            75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18,
            75],
        [73, -31, -90, -22, 78, 67, -38, -90, -13, 82, 61, -46, -88, -4, 85, 54,
            -54, -85, 4, 88, 46, -61, -82, 13, 90, 38, -67, -78, 22, 90, 31,
            -73],
        [70, -43, -87, 9, 90, 25, -80, -57, 57, 80, -25, -90, -9, 87, 43, -70,
            -70, 43, 87, -9, -90, -25, 80, 57, -57, -80, 25, 90, 9, -87, -43,
            70],
        [67, -54, -78, 38, 85, -22, -90, 4, 90, 13, -88, -31, 82, 46, -73, -61,
            61, 73, -46, -82, 31, 88, -13, -90, -4, 90, 22, -85, -38, 78, 54,
            -67],
        [64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64,
            64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64,
            64],
        [61, -73, -46, 82, 31, -88, -13, 90, -4, -90, 22, 85, -38, -78, 54, 67,
            -67, -54, 78, 38, -85, -22, 90, 4, -90, 13, 88, -31, -82, 46, 73,
            -61],
        [57, -80, -25, 90, -9, -87, 43, 70, -70, -43, 87, 9, -90, 25, 80, -57,
            -57, 80, 25, -90, 9, 87, -43, -70, 70, 43, -87, -9, 90, -25, -80,
            57],
        [54, -85, -4, 88, -46, -61, 82, 13, -90, 38, 67, -78, -22, 90, -31, -73,
            73, 31, -90, 22, 78, -67, -38, 90, -13, -82, 61, 46, -88, 4, 85,
            -54],
        [50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50,
            50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89,
            50],
        [46, -90, 38, 54, -90, 31, 61, -88, 22, 67, -85, 13, 73, -82, 4, 78,
            -78, -4, 82, -73, -13, 85, -67, -22, 88, -61, -31, 90, -54, -38, 90,
            -46],
        [43, -90, 57, 25, -87, 70, 9, -80, 80, -9, -70, 87, -25, -57, 90, -43,
            -43, 90, -57, -25, 87, -70, -9, 80, -80, 9, 70, -87, 25, 57, -90,
            43],
        [38, -88, 73, -4, -67, 90, -46, -31, 85, -78, 13, 61, -90, 54, 22, -82,
            82, -22, -54, 90, -61, -13, 78, -85, 31, 46, -90, 67, 4, -73, 88,
            -38],
        [36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36,
            36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83,
            36],
        [31, -78, 90, -61, 4, 54, -88, 82, -38, -22, 73, -90, 67, -13, -46, 85,
            -85, 46, 13, -67, 90, -73, 22, 38, -82, 88, -54, -4, 61, -90, 78,
            -31],
        [25, -70, 90, -80, 43, 9, -57, 87, -87, 57, -9, -43, 80, -90, 70, -25,
            -25, 70, -90, 80, -43, -9, 57, -87, 87, -57, 9, 43, -80, 90, -70,
            25],
        [22, -61, 85, -90, 73, -38, -4, 46, -78, 90, -82, 54, -13, -31, 67, -88,
            88, -67, 31, 13, -54, 82, -90, 78, -46, 4, 38, -73, 90, -85, 61,
            -22],
        [18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18,
            18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50,
            18],
        [13, -38, 61, -78, 88, -90, 85, -73, 54, -31, 4, 22, -46, 67, -82, 90,
            -90, 82, -67, 46, -22, -4, 31, -54, 73, -85, 90, -88, 78, -61, 38,
            -13],
        [9, -25, 43, -57, 70, -80, 87, -90, 90, -87, 80, -70, 57, -43, 25, -9,
            -9, 25, -43, 57, -70, 80, -87, 90, -90, 87, -80, 70, -57, 43, -25,
            9],
        [4, -13, 22, -31, 38, -46, 54, -61, 67, -73, 78, -82, 85, -88, 90, -90,
            90, -90, 88, -85, 82, -78, 73, -67, 61, -54, 46, -38, 31, -22, 13,
            -4],
    ];
    /**
     * dequantize scales the coefficients of a block by its quantizer.
     *
     * @param array $values the coefficients as they were written
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param int $quant the quantizer index for this block and plane
     * @return array the scaled coefficients
     */
    public static function dequantize(array $values, int $power_of_two_size,
        int $quant): array
    {
        $scale = self::LEVEL_SCALE[$quant % 6] << intdiv($quant, 6);
        $shift = $power_of_two_size + 3;
        $round = 1 << ($shift - 1);
        $written = [];
        foreach ($values as $spot => $value) {
            if ($value === 0) {
                $written[$spot] = 0;
                continue;
            }
            $scaled = ($value * self::FLAT_WEIGHT * $scale + $round) >> $shift;
            $written[$spot] = max(self::COEFF_MIN, min(self::COEFF_MAX,
                $scaled));
        }
        return $written;
    }
    /**
     * inverse turns a block of scaled coefficients into sample differences.
     * blocks predicted within the picture use
     *
     * @param array $values the scaled coefficients, in raster order
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param bool $small true for the transform the small brightness
     * @return array the sample differences, in raster order
     */
    public static function inverse(array $values, int $power_of_two_size,
        bool $small): array
    {
        $size = 1 << $power_of_two_size;
        $middle = array_fill(0, $size * $size, 0);
        for ($column = 0; $column < $size; $column++) {
            $line = [];
            for ($row = 0; $row < $size; $row++) {
                $line[$row] = $values[$row * $size + $column];
            }
            $line = $small ? self::smallPass($line)
                : self::transformLine($line, $size);
            $round = 1 << (self::FIRST_PASS_SHIFT - 1);
            for ($row = 0; $row < $size; $row++) {
                $middle[$row * $size + $column] = max(self::COEFF_MIN,
                    min(self::COEFF_MAX,
                    ($line[$row] + $round) >> self::FIRST_PASS_SHIFT));
            }
        }
        $written = array_fill(0, $size * $size, 0);
        $round = 1 << (self::SECOND_PASS_SHIFT - 1);
        for ($row = 0; $row < $size; $row++) {
            $line = [];
            for ($column = 0; $column < $size; $column++) {
                $line[$column] = $middle[$row * $size + $column];
            }
            $line = $small ? self::smallPass($line)
                : self::transformLine($line, $size);
            for ($column = 0; $column < $size; $column++) {
                $written[$row * $size + $column]
                    = ($line[$column] + $round) >> self::SECOND_PASS_SHIFT;
            }
        }
        return $written;
    }
    /**
     * transformLine one pass of the main transform over a line of coefficients.
     *
     * @param array $line the coefficients along one row or column
     * @param int $size how many of them there are
     * @return array the transformed line
     */
    private static function transformLine(array $line, int $size): array
    {
        $step = intdiv(32, $size);
        $written = array_fill(0, $size, 0);
        for ($index = 0; $index < $size; $index++) {
            $value = $line[$index];
            if ($value === 0) {
                continue;
            }
            $basis = self::MATRIX[$index * $step];
            for ($spot = 0; $spot < $size; $spot++) {
                $written[$spot] += $basis[$spot] * $value;
            }
        }
        return $written;
    }
    /**
     * smallPass one pass of the small transform used by brightness blocks of
     * four samples predicted within the picture.
     *
     * @param array $line the four coefficients along one row or column
     * @return array the transformed line
     */
    private static function smallPass(array $line): array
    {
        $first = $line[0] + $line[2];
        $second = $line[2] + $line[3];
        $third = $line[0] - $line[3];
        $fourth = 74 * $line[1];
        return [
            29 * $first + 55 * $second + $fourth,
            55 * $third - 29 * $second + $fourth,
            74 * ($line[0] - $line[2] + $line[3]),
            55 * $first + 29 * $third - $fourth,
        ];
    }
}

/**
 * HevcPredict predicts a block of an H.265 picture from the samples above it
 * and to its left, before the coded differences are added back. Thirty-five
 * ways of doing so are defined: a flat average, a gradient, and thirty-three
 * directions. The samples the prediction reads are gathered first, with any
 * that fall outside the decoded part of the picture filled in from their
 * neighbors, and smoothed for the larger blocks.
 */
final class HevcPredict
{
    /**
     * PLANAR is the mode that predicts a gradient across the block.
     */
    public const PLANAR = 0;
    /**
     * DC is the mode that predicts one average value.
     */
    public const DC = 1;
    /**
     * HORIZONTAL is the mode that predicts straight across from the left.
     */
    public const HORIZONTAL = 10;
    /**
     * VERTICAL is the mode that predicts straight down from above.
     */
    public const VERTICAL = 26;
    /**
     * ANGLE is how far each direction steps, for modes two to thirty-four.
     */
    private const ANGLE = [
        32, 26, 21, 17, 13, 9, 5, 2, 0, -2, -5, -9, -13, -17, -21, -26, -32,
        -26, -21, -17, -13, -9, -5, -2, 0, 2, 5, 9, 13, 17, 21, 26, 32,
    ];
    /**
     * INVERSE_ANGLE is the step back along the other edge, for the modes that
     * need it.
     */
    private const INVERSE_ANGLE = [
        -4096, -1638, -910, -630, -482, -390, -315, -256, -315, -390, -482,
        -630, -910, -1638, -4096,
    ];
    /**
     * SMOOTH_THRESHOLD is how far from straight a direction must be before
     * smoothing, by size.
     */
    private const SMOOTH_THRESHOLD = [3 => 7, 4 => 1, 5 => 0];
    /**
     * MIDDLE_SAMPLE is the value used when nothing around the block has been
     * decoded.
     */
    private const MIDDLE_SAMPLE = 128;
    /**
     * STEP_PARTS is how many parts a step is divided into.
     */
    private const STEP_PARTS = 32;
    /**
     * fillReferences fills in the samples the prediction will read. Positions
     * outside the picture, or in parts of it not yet decoded, take the value of
     * the nearest position that is available, working from the bottom of the
     * left edge around to the end of the top edge. positions that were not
     * available left out
     *
     * @param array $gathered the values found, keyed by position, with
     * @param int $size how many samples the block spans
     * @return array the full set of references, keyed from the corner
     */
    public static function fillReferences(array $gathered, int $size): array
    {
        $count = 4 * $size + 1;
        $refs = array_fill(0, $count, self::MIDDLE_SAMPLE);
        $first = null;
        for ($spot = 0; $spot < $count; $spot++) {
            if (isset($gathered[$spot])) {
                $first = $spot;
                break;
            }
        }
        if ($first === null) {
            return $refs;
        }
        for ($spot = 0; $spot < $first; $spot++) {
            $refs[$spot] = $gathered[$first];
        }
        $last = $gathered[$first];
        for ($spot = $first; $spot < $count; $spot++) {
            if (isset($gathered[$spot])) {
                $last = $gathered[$spot];
            }
            $refs[$spot] = $last;
        }
        return $refs;
    }
    /**
     * smoothSamples smooths the reference samples, which the larger blocks do
     * when the direction is far enough from straight across or straight down.
     *
     * @param array $refs the references, keyed from the corner
     * @param int $size how many samples the block spans
     * @param int $power_of_two_size the power of two of that
     * @param int $mode which of the thirty-five ways the block is predicted
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param bool $strong true when the stream allows the wider smoothing
     * @return array the references, smoothed or as they were
     */
    public static function smoothSamples(array $refs, int $size,
        int $power_of_two_size,
        int $mode, int $plane, bool $strong): array
    {
        if ($plane !== 0 || $mode === self::DC || $size === 4) {
            return $refs;
        }
        $distance = min(abs($mode - self::VERTICAL),
            abs($mode - self::HORIZONTAL));
        if ($mode !== self::PLANAR
            && $distance <= (self::SMOOTH_THRESHOLD[$power_of_two_size] ?? 7)) {
            return $refs;
        }
        $corner = 2 * $size;
        if ($strong && $size === 32
            && abs($refs[$corner] + $refs[$corner + 2 * $size]
            - 2 * $refs[$corner + $size]) < 8
            && abs($refs[$corner] + $refs[$corner - 2 * $size]
            - 2 * $refs[$corner - $size]) < 8) {
            return self::smoothStrongly($refs, $size, $corner);
        }
        $written = $refs;
        $count = 4 * $size + 1;
        for ($spot = 1; $spot < $count - 1; $spot++) {
            $written[$spot] = ($refs[$spot - 1] + 2 * $refs[$spot]
                + $refs[$spot + 1] + 2) >> 2;
        }
        return $written;
    }
    /**
     * smoothStrongly replaces the references of a large flat block with a
     * straight line between its ends, which suits a smooth gradient better than
     * a three sample average.
     *
     * @param array $refs the references, keyed from the corner
     * @param int $size how many samples the block spans
     * @param int $corner where the corner sample sits in the references
     * @return array the references, drawn as two straight lines
     */
    private static function smoothStrongly(array $refs, int $size,
        int $corner): array
    {
        $written = $refs;
        $middle = $refs[$corner];
        $bottom = $refs[$corner - 2 * $size];
        $right = $refs[$corner + 2 * $size];
        for ($step = 1; $step < 2 * $size; $step++) {
            $written[$corner - $step] = ((2 * $size - $step) * $middle
                + $step * $bottom + $size) >> ($size === 32 ? 6 : 5);
            $written[$corner + $step] = ((2 * $size - $step) * $middle
                + $step * $right + $size) >> ($size === 32 ? 6 : 5);
        }
        return $written;
    }
    /**
     * predict guesses one block's samples from the row above it and the column
     * to its left, in whichever of the thirty-five ways the stream said. The
     * coded values are added to this guess afterwards, so a block is only ever
     * the difference from what could be worked out from its neighbors.
     *
     * @param array $refs the references, keyed from the corner
     * @param int $size how many samples the block spans
     * @param int $power_of_two_size the power of two of that
     * @param int $mode which of the thirty-five ways to predict
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @return array the predicted samples, in raster order
     */
    public static function predict(array $refs, int $size,
        int $power_of_two_size,
        int $mode, int $plane): array
    {
        if ($mode === self::PLANAR) {
            return self::predictGradient($refs, $size, $power_of_two_size);
        }
        if ($mode === self::DC) {
            return self::predictFlat($refs, $size, $power_of_two_size, $plane);
        }
        return self::predictAlongAngle($refs, $size, $mode, $plane);
    }
    /**
     * predictGradient predicts a gradient running between the four edges.
     *
     * @param array $refs the references, keyed from the corner
     * @param int $size how many samples the block spans
     * @param int $power_of_two_size the power of two of that
     * @return array the predicted samples, in raster order
     */
    private static function predictGradient(array $refs, int $size,
        int $power_of_two_size): array
    {
        $corner = 2 * $size;
        $written = [];
        $below_left = $refs[$corner - $size - 1];
        $above_right = $refs[$corner + $size + 1];
        for ($row = 0; $row < $size; $row++) {
            $left = $refs[$corner - $row - 1];
            for ($column = 0; $column < $size; $column++) {
                $above = $refs[$corner + $column + 1];
                $written[$row * $size + $column] = (
                    ($size - 1 - $column) * $left
                    + ($column + 1) * $above_right
                    + ($size - 1 - $row) * $above
                    + ($row + 1) * $below_left + $size) >>
                        ($power_of_two_size + 1);
            }
        }
        return $written;
    }
    /**
     * predictFlat predicts one value across the whole block, the average of the
     * samples along its two decoded edges.
     *
     * @param array $refs the references, keyed from the corner
     * @param int $size how many samples the block spans
     * @param int $power_of_two_size the power of two of that
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @return array the predicted samples, in raster order
     */
    private static function predictFlat(array $refs, int $size,
        int $power_of_two_size,
        int $plane): array
    {
        $corner = 2 * $size;
        $sum = $size;
        for ($step = 1; $step <= $size; $step++) {
            $sum += $refs[$corner + $step] + $refs[$corner - $step];
        }
        $value = $sum >> ($power_of_two_size + 1);
        $written = array_fill(0, $size * $size, $value);
        if ($plane !== 0 || $size >= 32) {
            return $written;
        }
        /* the edges next to the decoded samples are eased toward them */
        $written[0] = ($refs[$corner - 1] + 2 * $value + $refs[$corner + 1] + 2)
            >> 2;
        for ($column = 1; $column < $size; $column++) {
            $written[$column] = ($refs[$corner + $column + 1] + 3 * $value + 2)
                >> 2;
        }
        for ($row = 1; $row < $size; $row++) {
            $written[$row * $size] = ($refs[$corner - $row - 1] + 3 *
                $value + 2)
                >> 2;
        }
        return $written;
    }
    /**
     * predictAlongAngle predicts along one of the thirty-three directions.
     *
     * @param array $refs the references, keyed from the corner
     * @param int $size how many samples the block spans
     * @param int $mode which direction to predict along
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @return array the predicted samples, in raster order
     */
    private static function predictAlongAngle(array $refs, int $size, int $mode,
        int $plane): array
    {
        $corner = 2 * $size;
        $angle = self::ANGLE[$mode - 2];
        $downward = $mode >= 18;
        /*
            A direction pointing down reads along the top edge and a
            direction pointing across reads along the left edge. The two
            cases are the same once the edges are swapped, so one array is
            built running away from the corner.
        */
        $line = [];
        for ($step = 0; $step <= $size; $step++) {
            $line[$step] = $downward
                ? $refs[$corner + $step] : $refs[$corner - $step];
        }
        if ($angle < 0) {
            $reach = ($size * $angle) >> 5;
            if ($reach < -1) {
                $inverse = self::INVERSE_ANGLE[$mode - 11];
                for ($step = -1; $step >= $reach; $step--) {
                    $back = ($step * $inverse + 128) >> 8;
                    $line[$step] = $downward
                        ? $refs[$corner - $back] : $refs[$corner + $back];
                }
            }
        } else {
            for ($step = $size + 1; $step <= 2 * $size; $step++) {
                $line[$step] = $downward
                    ? $refs[$corner + $step] : $refs[$corner - $step];
            }
        }
        $written = [];
        for ($row = 0; $row < $size; $row++) {
            for ($column = 0; $column < $size; $column++) {
                $along = $downward ? $row : $column;
                $across = $downward ? $column : $row;
                $step = (($along + 1) * $angle) >> 5;
                $part = (($along + 1) * $angle) & (self::STEP_PARTS - 1);
                $spot = $across + $step + 1;
                $value = $part === 0 ? $line[$spot]
                    : ((self::STEP_PARTS - $part) * $line[$spot]
                    + $part * $line[$spot + 1] + 16) >> 5;
                $written[$row * $size + $column] = $value;
            }
        }
        if ($plane === 0 && $size < 32
            && ($mode === self::VERTICAL || $mode === self::HORIZONTAL)) {
            $written = self::easeEdge($written, $refs, $size, $corner,
                $mode === self::VERTICAL);
        }
        return $written;
    }
    /**
     * easeEdge eases the edge of a block predicted straight down or straight
     * across, which otherwise leaves a step against its neighbor.
     *
     * @param array $written the predicted samples
     * @param array $refs the references, keyed from the corner
     * @param int $size how many samples the block spans
     * @param int $corner where the corner sample sits in the references
     * @param bool $downward true when the prediction ran downward
     * @return array the predicted samples with one edge eased
     */
    private static function easeEdge(array $written, array $refs, int $size,
        int $corner, bool $downward): array
    {
        for ($step = 0; $step < $size; $step++) {
            $edge = $downward ? $refs[$corner + 1] : $refs[$corner - 1];
            $other = $downward
                ? $refs[$corner - $step - 1] : $refs[$corner + $step + 1];
            $value = $edge + (($other - $refs[$corner]) >> 1);
            $spot = $downward ? $step * $size : $step;
            $written[$spot] = max(0, min(255, $value));
        }
        return $written;
    }
}

/**
 * HevcDeblock smooths the sample values either side of a block edge, so that
 * coding the blocks separately does not leave a visible seam between them.
 * Edges fall on a grid of eight samples. How strongly each is smoothed depends
 * on the quantizer of the blocks it separates and on how flat the samples
 * already are: a soft gradient is smoothed, an edge that belongs to the picture
 * is left alone.
 */
final class HevcDeblock
{
    /**
     * EDGE_GRID is how far apart the edges that may be smoothed sit.
     */
    private const EDGE_GRID = 8;
    /**
     * SEGMENT is how many lines of samples one decision covers.
     */
    private const SEGMENT = 4;
    /**
     * BETA is the flatness limit, by quantizer index.
     */
    private const BETA = [
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 11,
        12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38,
        40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64,
    ];
    /**
     * CLAMP is how far a sample may be moved, by quantizer index.
     */
    private const CLAMP = [
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 8, 9,
        10, 11, 13, 14, 16, 18, 20, 22, 24,
    ];
    /**
     * applyOffsets smooths every edge of one plane. row, its strength, its
     * quantizer and how many lines it covers limit limit
     *
     * @param array $plane the samples, changed in place
     * @param int $wide how many samples across the plane holds
     * @param int $tall how many samples down it holds
     * @param array $edges one entry per edge, each holding its column, its
     * @param bool $vertical true for the edges running down the picture
     * @param int $plane_index 0 for brightness, 1 and 2 for the color planes
     * @param int $beta_offset the shift the slice applies to the flatness
     * @param int $clamp_offset the shift the slice applies to the movement
     */
    public static function applyOffsets(array &$plane, int $wide, int $tall,
        array $edges, bool $vertical, int $plane_index, int $beta_offset,
        int $clamp_offset): void
    {
        $step = $vertical ? 1 : $wide;
        $along = $vertical ? $wide : 1;
        $limit = $vertical ? $wide : $tall;
        foreach ($edges as $edge) {
            list($column, $row, $strength, $quant, $lines) = $edge;
            if ($strength === 0) {
                continue;
            }
            $at = $vertical ? $column : $row;
            if ($at === 0 || $at >= $limit) {
                continue;
            }
            $beta = self::BETA[max(0, min(51, $quant + $beta_offset))];
            $clamp = self::CLAMP[max(0, min(53, $quant
                + 2 * ($strength - 1) + $clamp_offset))];
            /*
                A plane whose size is not a whole number of squares has a
                shorter run of lines at its far edge.
            */
            $lines = min($lines,
                $vertical ? $tall - $row : $wide - $column);
            if ($lines < 1) {
                continue;
            }
            if ($plane_index !== 0) {
                if ($strength < 2 || $clamp === 0) {
                    continue;
                }
                self::chromaEdge($plane, $row * $wide + $column, $step,
                    $along, $clamp, $lines);
                continue;
            }
            self::lumaEdge($plane, $row * $wide + $column, $step, $along,
                $beta, $clamp, $lines);
        }
    }
    /**
     * lumaEdge smooths one brightness edge, deciding for each group of four
     * lines whether to move three samples either side or only one.
     *
     * @param array $plane the samples, changed in place
     * @param int $start where the first line crosses the edge
     * @param int $step distance between samples across the edge
     * @param int $along distance between the lines
     * @param int $beta the flatness limit
     * @param int $clamp how far a sample may be moved
     * @param int $lines how many lines of the plane remain here
     */
    private static function lumaEdge(array &$plane, int $start, int $step,
        int $along, int $beta, int $clamp, int $lines): void
    {
        for ($half = 0; $half * self::SEGMENT < $lines; $half++) {
            $base = $start + $half * self::SEGMENT * $along;
            $near = self::easeEdge($plane, $base, $step);
            $far = self::easeEdge($plane, $base + 3 * $along, $step);
            if ($near[0] + $near[1] + $far[0] + $far[1] >= $beta) {
                continue;
            }
            $strong = self::wantsStrong($plane, $base, $step, $along, $beta,
                $clamp, $near, $far);
            $spread = ($beta + ($beta >> 1)) >> 3;
            $move_prior = $near[0] + $far[0] < $spread;
            $move_after = $near[1] + $far[1] < $spread;
            for ($line = 0; $line < self::SEGMENT; $line++) {
                $spot = $base + $line * $along;
                if ($strong) {
                    self::strongLine($plane, $spot, $step, $clamp << 1);
                } else {
                    self::weakLine($plane, $spot, $step, $clamp, $move_prior,
                        $move_after);
                }
            }
        }
    }
    /**
     * easeEdge works out how far the samples bend either side of the edge on
     * one line, which is what says whether the picture is flat there.
     *
     * @param array $plane the samples
     * @param int $spot where the line crosses the edge
     * @param int $step distance between samples across the edge
     * @return array the bend before the edge and the bend after it
     */
    private static function easeEdge(array $plane, int $spot, int $step): array
    {
        return [
            abs($plane[$spot - 3 * $step] - 2 * $plane[$spot - 2 * $step]
                + $plane[$spot - $step]),
            abs($plane[$spot + 2 * $step] - 2 * $plane[$spot + $step]
                + $plane[$spot]),
        ];
    }
    /**
     * wantsStrong says whether an edge is flat enough on both sides for the
     * wider smoothing that moves three samples either way.
     *
     * @param array $plane the samples
     * @param int $base where the first line crosses the edge
     * @param int $step distance between samples across the edge
     * @param int $along distance between the lines
     * @param int $beta the flatness limit
     * @param int $clamp how far a sample may be moved
     * @param array $near the bends on the first line
     * @param array $far the bends on the fourth line
     * @return bool true when the wider smoothing should be used
     */
    private static function wantsStrong(array $plane, int $base, int $step,
        int $along, int $beta, int $clamp, array $near, array $far): bool
    {
        $last = $base + 3 * $along;
        $reach = ($clamp * 5 + 1) >> 1;
        foreach ([$base, $last] as $spot) {
            $sides = abs($plane[$spot - 4 * $step] - $plane[$spot - $step])
                + abs($plane[$spot + 3 * $step] - $plane[$spot]);
            if ($sides >= ($beta >> 3)
                || abs($plane[$spot - $step] - $plane[$spot]) >= $reach) {
                return false;
            }
        }
        return (($near[0] + $near[1]) << 1) < ($beta >> 2)
            && (($far[0] + $far[1]) << 1) < ($beta >> 2);
    }
    /**
     * strongLine moves three samples either side of the edge toward a straight
     * line between the ones further out.
     *
     * @param array $plane the samples, changed in place
     * @param int $spot where this line crosses the edge
     * @param int $step distance between samples across the edge
     * @param int $clamp how far a sample may be moved
     */
    private static function strongLine(array &$plane, int $spot, int $step,
        int $clamp): void
    {
        $before = [];
        $after = [];
        for ($index = 0; $index < 4; $index++) {
            $before[$index] = $plane[$spot - ($index + 1) * $step];
            $after[$index] = $plane[$spot + $index * $step];
        }
        $plane[$spot - $step] = $before[0] + self::holdWithinLimit(
            (($before[2] + 2 * $before[1] + 2 * $before[0] + 2 * $after[0]
            + $after[1] + 4) >> 3) - $before[0], $clamp);
        $plane[$spot - 2 * $step] = $before[1] + self::holdWithinLimit(
            (($before[2] + $before[1] + $before[0] + $after[0] + 2) >> 2)
            - $before[1], $clamp);
        $plane[$spot - 3 * $step] = $before[2] + self::holdWithinLimit(
            ((2 * $before[3] + 3 * $before[2] + $before[1] + $before[0]
            + $after[0] + 4) >> 3) - $before[2], $clamp);
        $plane[$spot] = $after[0] + self::holdWithinLimit(
            (($before[1] + 2 * $before[0] + 2 * $after[0] + 2 * $after[1]
            + $after[2] + 4) >> 3) - $after[0], $clamp);
        $plane[$spot + $step] = $after[1] + self::holdWithinLimit(
            (($before[0] + $after[0] + $after[1] + $after[2] + 2) >> 2)
            - $after[1], $clamp);
        $plane[$spot + 2 * $step] = $after[2] + self::holdWithinLimit(
            ((2 * $after[3] + 3 * $after[2] + $after[1] + $after[0]
            + $before[0] + 4) >> 3) - $after[2], $clamp);
    }
    /**
     * weakLine moves one sample either side of the edge, and the next one out
     * when that side is flat enough.
     *
     * @param array $plane the samples, changed in place
     * @param int $spot where this line crosses the edge
     * @param int $step distance between samples across the edge
     * @param int $clamp how far a sample may be moved
     * @param bool $move_prior true when the second sample before may move
     * @param bool $move_after true when the second sample after may move
     */
    private static function weakLine(array &$plane, int $spot, int $step,
        int $clamp, bool $move_prior, bool $move_after): void
    {
        $second = $plane[$spot - 3 * $step];
        $first = $plane[$spot - 2 * $step];
        $prior = $plane[$spot - $step];
        $next = $plane[$spot];
        $following = $plane[$spot + $step];
        $third = $plane[$spot + 2 * $step];
        $move = (9 * ($next - $prior) - 3 * ($following - $first) + 8) >> 4;
        if (abs($move) >= 10 * $clamp) {
            return;
        }
        $move = self::holdWithinLimit($move, $clamp);
        $plane[$spot - $step] = self::holdInsideByte($prior + $move);
        $plane[$spot] = self::holdInsideByte($next - $move);
        $half = $clamp >> 1;
        if ($move_prior) {
            $shift = self::holdWithinLimit(((($second + $prior + 1) >> 1)
                - $first
                + $move) >> 1, $half);
            $plane[$spot - 2 * $step] = self::holdInsideByte($first + $shift);
        }
        if ($move_after) {
            $shift = self::holdWithinLimit(((($third + $next + 1) >> 1)
                - $following
                - $move) >> 1, $half);
            $plane[$spot + $step] = self::holdInsideByte($following + $shift);
        }
    }
    /**
     * chromaEdge smooths one color edge, which moves only the sample either
     * side.
     *
     * @param array $plane the samples, changed in place
     * @param int $start where the first line crosses the edge
     * @param int $step distance between samples across the edge
     * @param int $along distance between the lines
     * @param int $clamp how far a sample may be moved
     * @param int $lines how many lines of the plane remain here
     */
    private static function chromaEdge(array &$plane, int $start, int $step,
        int $along, int $clamp, int $lines): void
    {
        for ($line = 0; $line < $lines; $line++) {
            $spot = $start + $line * $along;
            $first = $plane[$spot - 2 * $step];
            $prior = $plane[$spot - $step];
            $next = $plane[$spot];
            $following = $plane[$spot + $step];
            $move = self::holdWithinLimit(((($next - $prior) * 4) + $first
                - $following
                + 4) >> 3, $clamp);
            $plane[$spot - $step] = self::holdInsideByte($prior + $move);
            $plane[$spot] = self::holdInsideByte($next - $move);
        }
    }
    /**
     * holdWithinLimit holds a movement within the limit either way.
     *
     * @param int $value how far a sample would move
     * @param int $limit the largest it may move
     * @return int the movement, held within the limit
     */
    private static function holdWithinLimit(int $value, int $limit): int
    {
        return max(-$limit, min($limit, $value));
    }
    /**
     * holdInsideByte holds a sample within the range a byte can carry.
     *
     * @param int $value the sample
     * @return int the sample, held in range
     */
    private static function holdInsideByte(int $value): int
    {
        return max(0, min(255, $value));
    }
}

/**
 * HevcSao corrects the samples of a block after the edges have been smoothed,
 * by adding a small offset chosen from how each sample sits against its
 * neighbors or from how bright it is. Two kinds of correction are defined. A
 * band correction shifts every sample whose brightness falls in one of four
 * neighboring bands. An edge correction compares each sample with the two on
 * either side along one of four directions, and shifts it by an amount chosen
 * from whether it is a peak, a valley, or on a slope.
 */
final class HevcSao
{
    /**
     * NONE is no correction is applied to this block.
     */
    public const NONE = 0;
    /**
     * BAND is the correction chosen by how bright a sample is.
     */
    public const BAND = 1;
    /**
     * EDGE is the correction chosen by how a sample sits against its neighbors.
     */
    public const EDGE = 2;
    /**
     * BANDS is how many bands the range of brightness is divided into.
     */
    private const BANDS = 32;
    /**
     * BAND_SPAN is how many bands one correction covers.
     */
    private const BAND_SPAN = 4;
    /**
     * BAND_SHIFT is how far to shift a sample to find its band, at eight bits.
     */
    private const BAND_SHIFT = 3;
    /**
     * DIRECTIONS is the two neighbors each direction compares against.
     */
    private const DIRECTIONS = [
        [[-1, 0], [1, 0]],
        [[0, -1], [0, 1]],
        [[-1, -1], [1, 1]],
        [[1, -1], [-1, 1]],
    ];
    /**
     * applyOffsets corrects one block of one plane. The values are read from
     * the picture as it stood before this pass and written to a second copy,
     * because a sample and its neighbor are each corrected from what the other
     * was, not from what it becomes.
     *
     * @param array $source the samples as the smoothing left them
     * @param array $target the samples being written, changed in place
     * @param int $wide how many samples across the plane holds
     * @param int $tall how many samples down it holds
     * @param int $left first column of the block
     * @param int $top first row of the block
     * @param int $across how many columns of the block to correct
     * @param int $down how many rows of it to correct
     * @param int $kind which of the two corrections to apply
     * @param array $offsets the four amounts the correction may add
     * @param int $band which band a band correction starts at
     * @param int $direction which way an edge correction looks
     */
    public static function applyOffsets(array $source, array &$target,
        int $wide,
        int $tall, int $left, int $top, int $across, int $down, int $kind,
        array $offsets, int $band, int $direction): void
    {
        if ($kind === self::NONE) {
            return;
        }
        if ($kind === self::BAND) {
            self::shiftByBrightness($source, $target, $wide, $left, $top,
                $across, $down,
                $offsets, $band);
            return;
        }
        self::shiftByNeighbors($source, $target, $wide, $tall, $left, $top,
            $across,
            $down, $offsets, $direction);
    }
    /**
     * shiftByBrightness shifts every sample whose brightness falls in one of
     * four neighboring bands.
     *
     * @param array $source the samples as the smoothing left them
     * @param array $target the samples being written, changed in place
     * @param int $wide how many samples across the plane holds
     * @param int $left first column of the block
     * @param int $top first row of the block
     * @param int $across how many columns of the block to correct
     * @param int $down how many rows of it to correct
     * @param array $offsets the four amounts the correction may add
     * @param int $band which band the correction starts at
     */
    private static function shiftByBrightness(array $source, array &$target,
        int $wide,
        int $left, int $top, int $across, int $down, array $offsets,
        int $band): void
    {
        $table = array_fill(0, self::BANDS, 0);
        for ($step = 0; $step < self::BAND_SPAN; $step++) {
            $table[($band + $step) & (self::BANDS - 1)] = $step + 1;
        }
        for ($row = 0; $row < $down; $row++) {
            $base = ($top + $row) * $wide + $left;
            for ($column = 0; $column < $across; $column++) {
                $spot = $base + $column;
                $which = $table[$source[$spot] >> self::BAND_SHIFT];
                if ($which === 0) {
                    continue;
                }
                $target[$spot] = max(0, min(255,
                    $source[$spot] + $offsets[$which - 1]));
            }
        }
    }
    /**
     * shiftByNeighbors shifts each sample by an amount chosen from how it
     * compares with the two samples on either side of it along one direction.
     *
     * @param array $source the samples as the smoothing left them
     * @param array $target the samples being written, changed in place
     * @param int $wide how many samples across the plane holds
     * @param int $tall how many samples down it holds
     * @param int $left first column of the block
     * @param int $top first row of the block
     * @param int $across how many columns of the block to correct
     * @param int $down how many rows of it to correct
     * @param array $offsets the four amounts the correction may add
     * @param int $direction which way to look
     */
    private static function shiftByNeighbors(array $source, array &$target,
        int $wide,
        int $tall, int $left, int $top, int $across, int $down,
        array $offsets, int $direction): void
    {
        list($first, $second) = self::DIRECTIONS[$direction];
        /* a sample whose neighbors fall outside the picture is left alone */
        $table = [1, 2, 0, 3, 4];
        for ($row = 0; $row < $down; $row++) {
            $line = $top + $row;
            for ($column = 0; $column < $across; $column++) {
                $spot = $left + $column;
                $one_column = $spot + $first[0];
                $one_row = $line + $first[1];
                $two_column = $spot + $second[0];
                $two_row = $line + $second[1];
                if ($one_column < 0 || $one_column >= $wide || $one_row < 0
                    || $one_row >= $tall || $two_column < 0
                    || $two_column >= $wide || $two_row < 0
                    || $two_row >= $tall) {
                    continue;
                }
                $value = $source[$line * $wide + $spot];
                $sum = 2 + self::readSign($value
                    - $source[$one_row * $wide + $one_column])
                    + self::readSign($value
                    - $source[$two_row * $wide + $two_column]);
                $which = $table[$sum];
                if ($which === 0) {
                    continue;
                }
                $target[$line * $wide + $spot] = max(0, min(255,
                    $value + $offsets[$which - 1]));
            }
        }
    }
    /**
     * readSign works out which way one value sits against another.
     *
     * @param int $value the difference between two samples
     * @return int one, zero or minus one
     */
    private static function readSign(int $value): int
    {
        if ($value > 0) {
            return 1;
        }
        return $value < 0 ? -1 : 0;
    }
}

/**
 * HevcCabac the arithmetic decoder H.265 uses. The engine is the same one H.264
 * uses, so its range and transition tables are shared; what differs is how each
 * context's starting state is worked out from the slice quantizer, and how many
 * contexts there are.
 */
final class HevcCabac
{
    /**
     * START_RANGE is range the engine starts each slice with.
     */
    private const START_RANGE = 510;
    /**
     * WINDOW_BITS is bits of the stream loaded into the window when a slice
     * starts.
     */
    private const WINDOW_BITS = 9;
    /**
     * MIN_START_STATE is lowest state a context may start in.
     */
    private const MIN_START_STATE = 1;
    /**
     * MAX_START_STATE is highest state a context may start in.
     */
    private const MAX_START_STATE = 126;
    /**
     * $bits stores the reader this arithmetic coding takes its bits
     * from. HEVC
     * writes its numbers the same way H.264 does, so the reader written for
     * that format is used here too.
     * @var H264Bits
     */
    private H264Bits $bits;
    /**
     * $state stores how likely the more probable value is, one entry per
     * context.
     * @var array
     */
    private array $state = [];
    /**
     * $value_likelier stores which value is the more probable one, one entry
     * per
     * context.
     * @var array
     */
    private array $value_likelier = [];
    /**
     * $range stores width of the range the arithmetic decoder is working in.
     * @var int
     */
    private int $range = 0;
    /**
     * $offset stores where in that range the coded value sits.
     * @var int
     */
    private int $offset = 0;
    /**
     * __construct sets up the decoder for one slice, giving every context the
     * starting state its table entry and the slice quantizer call for.
     *
     * @param H264Bits $bits reader positioned at the slice data
     * @param array $starting_values one starting value per context
     * @param int $slice_quantizer quantizer index the slice starts at
     */
    public function __construct(H264Bits $bits, array $starting_values,
        int $slice_quantizer)
    {
        $this->bits = $bits;
        $quant = max(0, min(51, $slice_quantizer));
        foreach ($starting_values as $index => $value) {
            $slope = (($value >> 4) * 5) - 45;
            $shift = (($value & 15) << 3) - 16;
            $start = (($slope * $quant) >> 4) + $shift;
            $start = max(self::MIN_START_STATE,
                min(self::MAX_START_STATE, $start));
            if ($start <= 63) {
                $this->state[$index] = 63 - $start;
                $this->value_likelier[$index] = 0;
            } else {
                $this->state[$index] = $start - 64;
                $this->value_likelier[$index] = 1;
            }
        }
        $this->range = self::START_RANGE;
        $this->offset = 0;
        for ($step = 0; $step < self::WINDOW_BITS; $step++) {
            $this->offset = ($this->offset << 1) | $this->nextBit();
        }
    }
    /**
     * decision reads one decision using the given context, and moves that
     * context toward whichever value it just saw.
     *
     * @param int $index which context to read with
     * @return int the decision, zero or one
     */
    public function decision(int $index): int
    {
        $state = $this->state[$index];
        $likelier = $this->value_likelier[$index];
        $quant = ($this->range >> 6) & 3;
        $less_likely = H264Tables::RANGE_TAB_LPS[$state][$quant];
        $this->range -= $less_likely;
        if ($this->offset >= $this->range) {
            $result = 1 - $likelier;
            $this->offset -= $this->range;
            $this->range = $less_likely;
            if ($state === 0) {
                $this->value_likelier[$index] = 1 - $likelier;
            }
            $this->state[$index] = H264Tables::TRANS_IDX_LPS[$state];
        } else {
            $result = $likelier;
            $this->state[$index] = H264Tables::TRANS_IDX_MPS[$state];
        }
        while ($this->range < 256) {
            $this->range <<= 1;
            $this->offset = ($this->offset << 1) | $this->nextBit();
        }
        return $result;
    }
    /**
     * readBypassBit reads one bit for which either value is equally likely, so
     * no context is kept for it.
     *
     * @return int the bit, zero or one
     */
    public function readBypassBit(): int
    {
        $this->offset = ($this->offset << 1) | $this->nextBit();
        if ($this->offset >= $this->range) {
            $this->offset -= $this->range;
            return 1;
        }
        return 0;
    }
    /**
     * bypassBits reads a number of bits for which either value is equally
     * likely.
     *
     * @param int $count how many bits to read
     * @return int the value, most significant bit first
     */
    public function bypassBits(int $count): int
    {
        $value = 0;
        for ($step = 0; $step < $count; $step++) {
            $value = ($value << 1) | $this->readBypassBit();
        }
        return $value;
    }
    /**
     * saveContexts a copy of every context's state, for a later row to start
     * from. When the rows of a picture are coded so they can be decoded in
     * parallel, each row begins from the state the row above had reached after
     * its second block rather than from the table.
     *
     * @return array the state and the more probable value of each context
     */
    public function saveContexts(): array
    {
        return [$this->state, $this->value_likelier];
    }
    /**
     * restoreContexts puts back a copy of the contexts taken earlier.
     *
     * @param array $saved what saveContexts gave back
     */
    public function restoreContexts(array $saved): void
    {
        list($this->state, $this->value_likelier) = $saved;
    }
    /**
     * restart starts the engine again at the reader's current position, which a
     * new row of blocks begins from.
     */
    public function restart(): void
    {
        $this->range = self::START_RANGE;
        $this->offset = 0;
        for ($step = 0; $step < self::WINDOW_BITS; $step++) {
            $this->offset = ($this->offset << 1) | $this->nextBit();
        }
    }
    /**
     * terminate reads the decision that says whether the slice has ended.
     *
     * @return int one when no more blocks follow
     */
    public function terminate(): int
    {
        $this->range -= 2;
        if ($this->offset >= $this->range) {
            return 1;
        }
        while ($this->range < 256) {
            $this->range <<= 1;
            $this->offset = ($this->offset << 1) | $this->nextBit();
        }
        return 0;
    }
    /**
     * nextBit takes the next bit of the stream, or a zero once it is spent. The
     * engine keeps a window of bits ahead of the value it is decoding, so at
     * the end of a slice it asks for bits that were never written.
     *
     * @return int the bit, or zero past the end
     */
    private function nextBit(): int
    {
        if ($this->bits->bitsLeft() < 1) {
            return 0;
        }
        return $this->bits->readBit();
    }
}

/**
 * HevcSyntax reads the block level syntax of an H.265 slice coded on its own.
 * This walks the picture a coding tree block at a time, reading the filter
 * settings that go with each and how far the block splits.
 */
final class HevcSyntax
{
    /**
     * SAO_OFFSETS is how many offsets a correction carries.
     */
    private const SAO_OFFSETS = 4;
    /**
     * SAO_MAX_OFFSET is the largest an offset may be at eight bits a sample.
     */
    private const SAO_MAX_OFFSET = 7;
    /**
     * SAO_BAND_BITS is how many bands the sample range is divided into.
     */
    private const SAO_BAND_BITS = 5;
    /**
     * SAO_CLASS_BITS is how many directions an edge correction may look along.
     */
    private const SAO_CLASS_BITS = 2;
    /**
     * $arithmetic_reader stores the arithmetic decoder the decisions are read
     * from.
     * @var HevcCabac
     */
    private HevcCabac $arithmetic_reader;
    /**
     * $sequence_settings stores sequence parameters the picture was coded
     * against.
     * @var HevcSps
     */
    private HevcSps $sequence_settings;
    /**
     * $header stores what the slice being decoded says about itself: which
     * settings it uses, where it starts, and how strongly its edges are
     * smoothed. Read by deblock(), deblockPlane(), decodeSlice().
     * @var HevcSliceHeader
     */
    private HevcSliceHeader $header;
    /**
     * $sample_offset stores the filter settings of each coding tree block read
     * so far.
     * @var array
     */
    public array $sample_offset = [];
    /**
     * __construct sets up the reader for one slice. coded against
     *
     * @param HevcCabac $arithmetic_reader decoder positioned at the slice data
     * @param HevcSps $sequence_settings sequence parameters the picture was
     * @param HevcSliceHeader $header this slice's own header
     */
    public function __construct(HevcCabac $arithmetic_reader,
        HevcSps $sequence_settings,
        HevcSliceHeader $header)
    {
        $this->arithmetic_reader = $arithmetic_reader;
        $this->sequence_settings = $sequence_settings;
        $this->header = $header;
    }
    /**
     * readSao reads the offset filter settings of one coding tree block. A
     * block may take the settings of the block to its left or above it rather
     * than carrying its own, which is what the two merge decisions at the start
     * say.
     *
     * @param int $column which coding tree block across the picture
     * @param int $row which coding tree block down the picture
     * @param int $columns how many coding tree blocks fit across
     * @return HevcSaoParams the settings, whether read or inherited
     */
    public function readSao(int $column, int $row, int $columns):
        HevcSaoParams
    {
        $params = new HevcSaoParams();
        if (!$this->header->sample_offset_luma && !$this->header
            ->sample_offset_chroma) {
            return $params;
        }
        if ($column > 0
            && $this->arithmetic_reader
                ->decision(HevcContexts::SAO_MERGE_FLAG) === 1) {
            return $this->sample_offset[$row * $columns + $column - 1];
        }
        if ($row > 0
            && $this->arithmetic_reader
                ->decision(HevcContexts::SAO_MERGE_FLAG) === 1) {
            return $this->sample_offset[($row - 1) * $columns + $column];
        }
        for ($plane = 0; $plane < 3; $plane++) {
            $wanted = $plane === 0
                ? $this->header->sample_offset_luma : $this->header
                    ->sample_offset_chroma;
            if (!$wanted) {
                continue;
            }
            if ($plane === 2) {
                $params->kind[2] = $params->kind[1];
            } else {
                $params->kind[$plane] = $this->readSaoKind();
            }
            if ($params->kind[$plane] === 0) {
                continue;
            }
            $offsets = [];
            for ($entry = 0; $entry < self::SAO_OFFSETS; $entry++) {
                $offsets[$entry] = $this->readSaoOffsetSize();
            }
            if ($params->kind[$plane] === 1) {
                for ($entry = 0; $entry < self::SAO_OFFSETS; $entry++) {
                    if ($offsets[$entry] !== 0
                        && $this->arithmetic_reader->readBypassBit() === 1) {
                        $offsets[$entry] = -$offsets[$entry];
                    }
                }
                $params->band_position[$plane]
                    = $this->arithmetic_reader->bypassBits(self::SAO_BAND_BITS);
            } else {
                if ($plane !== 2) {
                    $params->edge_class[$plane]
                        =
                            $this->arithmetic_reader
                                ->bypassBits(self::SAO_CLASS_BITS);
                } else {
                    $params->edge_class[2] = $params->edge_class[1];
                }
                /* an edge correction always raises the first two and
                   lowers the last two, so no signs are written */
                $offsets[2] = -$offsets[2];
                $offsets[3] = -$offsets[3];
            }
            $params->offset[$plane] = $offsets;
        }
        return $params;
    }
    /**
     * readSaoKind reads which kind of correction a plane carries.
     *
     * @return int 0 for none, 1 for a band correction, 2 for an edge one
     */
    private function readSaoKind(): int
    {
        if ($this->arithmetic_reader
            ->decision(HevcContexts::SAO_TYPE_IDX) === 0) {
            return 0;
        }
        return $this->arithmetic_reader->readBypassBit() === 0 ? 1 : 2;
    }
    /**
     * readSaoOffsetSize reads how large one offset is, written as a run of
     * ones.
     *
     * @return int the size, from zero up to the limit the depth allows
     */
    private function readSaoOffsetSize(): int
    {
        $size = 0;
        while ($size < self::SAO_MAX_OFFSET
            && $this->arithmetic_reader->readBypassBit() === 1) {
            $size++;
        }
        return $size;
    }
}

/**
 * HevcResidual reads the coefficients of one transform block of an H.265 slice.
 * The coefficients are written from the last one back toward the first, in
 * groups of sixteen, with a flag saying which groups hold anything at all. This
 * is the largest single piece of the block syntax.
 */
final class HevcResidual
{
    /**
     * SIG_MAP is which context each position within a group of sixteen
     * coefficients uses, by scan order. The first row is for the smallest
     * blocks; the next three are chosen by whether the groups to the right and
     * below hold anything; the last is used when a block skipped its transform.
     */
    private const SIG_MAP = [
        [
            [0, 2, 1, 6, 3, 4, 7, 6, 4, 5, 7, 8, 5, 8, 8, 8],
            [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [2, 1, 2, 0, 1, 2, 0, 0, 1, 2, 0, 0, 1, 0, 0, 0],
            [2, 2, 1, 2, 1, 0, 2, 1, 0, 0, 1, 0, 0, 0, 0, 0],
            [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
        ],
        [
            [0, 1, 4, 5, 2, 3, 4, 5, 6, 6, 8, 8, 7, 7, 8, 8],
            [1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
            [2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
            [2, 1, 0, 0, 2, 1, 0, 0, 2, 1, 0, 0, 2, 1, 0, 0],
            [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
        ],
        [
            [0, 2, 6, 7, 1, 3, 6, 7, 4, 4, 8, 8, 5, 5, 8, 8],
            [1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
            [2, 1, 0, 0, 2, 1, 0, 0, 2, 1, 0, 0, 2, 1, 0, 0],
            [2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
            [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
        ],
    ];
    /**
     * GROUP_SIZE is how many positions a group of coefficients holds.
     */
    private const GROUP_SIZE = 16;
    /**
     * GREATER1_LIMIT is how many size flags are read before the rest is written
     * out in full.
     */
    private const GREATER1_LIMIT = 8;
    /**
     * SIGN_HIDING_SPAN is how far apart two coefficients must be for a sign to
     * be inferred.
     */
    private const SIGN_HIDING_SPAN = 4;
    /**
     * MAX_RICE is the largest the length parameter of a plain level may grow
     * to.
     */
    private const MAX_RICE = 4;
    /**
     * CHROMA_SIG_OFFSET is where the color contexts start within the
     * coefficient flags.
     */
    private const CHROMA_SIG_OFFSET = 27;
    /**
     * CHROMA_GREATER1_OFFSET is where the color contexts start within the first
     * size flags.
     */
    private const CHROMA_GREATER1_OFFSET = 16;
    /**
     * CHROMA_GREATER2_OFFSET is where the color contexts start within the
     * second size flags.
     */
    private const CHROMA_GREATER2_OFFSET = 4;
    /**
     * $arithmetic_reader stores the arithmetic decoder the decisions are read
     * from.
     * @var HevcCabac
     */
    private HevcCabac $arithmetic_reader;
    /**
     * $sign_hiding stores true when a coefficient sign may be inferred from the
     * levels.
     * @var bool
     */
    private bool $sign_hiding;
    /**
     * __construct sets up the reader.
     *
     * @param HevcCabac $arithmetic_reader decoder positioned in the slice data
     * @param bool $sign_hiding true when the picture hides one sign per group
     */
    public function __construct(HevcCabac $arithmetic_reader, bool $sign_hiding)
    {
        $this->arithmetic_reader = $arithmetic_reader;
        $this->sign_hiding = $sign_hiding;
    }
    /**
     * readBlockValues reads one transform block.
     *
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param int $scan which of the three orders the block is written in
     * @return array the coefficients, in raster order within the block
     */
    public function readBlockValues(int $power_of_two_size, int $plane,
        int $scan): array
    {
        $size = 1 << $power_of_two_size;
        $values = array_fill(0, $size * $size, 0);
        list($last_x, $last_y) = $this->readLastPosition($power_of_two_size,
            $plane,
            $scan);
        if ($last_x >= $size || $last_y >= $size) {
            throw new HevcException('H.265 coefficient position is outside'
                . ' its block, so the stream is damaged');
        }
        $in_group = HevcScan::sortByOrder(4, $scan);
        $groups_across = max(1, $size >> 2);
        $groups = HevcScan::sortByOrder($groups_across, $scan);
        $last_group = 0;
        $last_in_group = 0;
        foreach ($groups as $index => $spot) {
            if ($spot[0] === ($last_x >> 2) && $spot[1] === ($last_y >> 2)) {
                $last_group = $index;
            }
        }
        foreach ($in_group as $index => $spot) {
            if ($spot[0] === ($last_x & 3) && $spot[1] === ($last_y & 3)) {
                $last_in_group = $index;
            }
        }
        $filled = [];
        $over_one_context = 1;
        for ($group = $last_group; $group >= 0; $group--) {
            $group_x = $groups[$group][0];
            $group_y = $groups[$group][1];
            $implicit = false;
            if ($group < $last_group && $group > 0) {
                $near = 0;
                if ($group_x < $groups_across - 1) {
                    $near += $filled[($group_y << 3) + $group_x + 1] ?? 0;
                }
                if ($group_y < $groups_across - 1) {
                    $near += $filled[(($group_y + 1) << 3) + $group_x] ?? 0;
                }
                $context = min(1, $near) + ($plane ? 2 : 0);
                $filled[($group_y << 3) + $group_x] =
                    $this->arithmetic_reader->decision(
                    HevcContexts::SIG_COEFF_GROUP + $context);
                $implicit = true;
            } else {
                $filled[($group_y << 3) + $group_x]
                    = ($group === $last_group || $group === 0) ? 1 : 0;
            }
            if ($filled[($group_y << 3) + $group_x] === 0) {
                continue;
            }
            $near = 0;
            if ($group_x < $groups_across - 1) {
                $near = $filled[($group_y << 3) + $group_x + 1] ?? 0;
            }
            if ($group_y < $groups_across - 1) {
                $near += ($filled[(($group_y + 1) << 3) + $group_x] ?? 0) << 1;
            }
            $found = [];
            $offset = $this->sigOffset($power_of_two_size, $plane, $group_x,
                $group_y,
                $scan);
            $row = $power_of_two_size === 2 ? 0 : $near + 1;
            $first = $group === $last_group
                ? $last_in_group - 1 : self::GROUP_SIZE - 1;
            if ($group === $last_group) {
                $found[] = $last_in_group;
            }
            $before = count($found);
            for ($entry = $first; $entry > 0; $entry--) {
                $context = self::SIG_MAP[$scan][$row][$entry] + $offset;
                if ($this->arithmetic_reader->decision(
                    HevcContexts::SIG_COEFF + $context) === 1) {
                    $found[] = $entry;
                }
            }
            if ($first >= 0) {
                if ($implicit && count($found) === $before) {
                    $found[] = 0;
                } else {
                    $context = $group === 0
                        ? ($plane ? self::CHROMA_SIG_OFFSET : 0)
                        : 2 + $offset;
                    if ($this->arithmetic_reader->decision(
                        HevcContexts::SIG_COEFF + $context) === 1) {
                        $found[] = 0;
                    }
                }
            }
            if ($found === []) {
                continue;
            }
            $over_one_context = $this->readLevels($values, $found, $in_group,
                $group_x, $group_y, $size, $plane, $group, $last_group,
                $over_one_context);
        }
        return $values;
    }
    /**
     * readLevels reads the sizes and signs of the coefficients found in one
     * group and writes them into the block.
     *
     * @param array $values block being filled in, changed in place
     * @param array $found which positions of the group hold a coefficient
     * @param array $in_group the order positions within a group are visited
     * @param int $group_x which group across the block
     * @param int $group_y which group down the block
     * @param int $size how many samples the block spans
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param int $group which group this is in scan order
     * @param int $last_group the group holding the final coefficient
     * @param int $over_one_context carried over from the group before this one
     * @return int the context to carry into the next group
     */
    private function readLevels(array &$values, array $found, array $in_group,
        int $group_x, int $group_y, int $size, int $plane, int $group,
        int $last_group, int $over_one_context): int
    {
        $count = count($found);
        $set = ($group > 0 && $plane === 0) ? 2 : 0;
        if ($group !== $last_group && $over_one_context === 0) {
            $set++;
        }
        $over_one_context = 1;
        $checked = min($count, self::GREATER1_LIMIT);
        $levels = array_fill(0, $count, 1);
        $first_over_one = -1;
        for ($entry = 0; $entry < $checked; $entry++) {
            $step = ($set << 2) + $over_one_context
                + ($plane ? self::CHROMA_GREATER1_OFFSET : 0);
            if ($this->arithmetic_reader->decision(
                HevcContexts::COEFF_GREATER1 + $step) === 1) {
                $levels[$entry] = 2;
                if ($first_over_one < 0) {
                    $first_over_one = $entry;
                }
                $over_one_context = 0;
            } elseif ($over_one_context > 0 && $over_one_context < 3) {
                $over_one_context++;
            }
        }
        $carry = $over_one_context;
        if ($first_over_one >= 0) {
            $step = $set + ($plane ? self::CHROMA_GREATER2_OFFSET : 0);
            $levels[$first_over_one] += $this->arithmetic_reader->decision(
                HevcContexts::COEFF_GREATER2 + $step);
        }
        $hidden = $this->sign_hiding
            && ($found[0] - $found[$count - 1]) >= self::SIGN_HIDING_SPAN;
        $signs = [];
        $to_read = $hidden ? $count - 1 : $count;
        for ($entry = 0; $entry < $to_read; $entry++) {
            $signs[$entry] = $this->arithmetic_reader->readBypassBit();
        }
        if ($hidden) {
            $signs[$count - 1] = 0;
        }
        $rice = 0;
        $sum = 0;
        for ($entry = 0; $entry < $count; $entry++) {
            if ($entry < $checked) {
                $wanted = $entry === $first_over_one ? 3 : 2;
                if ($levels[$entry] === $wanted) {
                    $levels[$entry] += $this->readRemaining($rice);
                    if ($levels[$entry] > (3 << $rice)) {
                        $rice = min(self::MAX_RICE, $rice + 1);
                    }
                }
            } else {
                $levels[$entry] = 1 + $this->readRemaining($rice);
                if ($levels[$entry] > (3 << $rice)) {
                    $rice = min(self::MAX_RICE, $rice + 1);
                }
            }
            $sum += $levels[$entry];
        }
        if ($hidden && ($sum & 1) === 1) {
            $signs[$count - 1] = 1;
        }
        for ($entry = 0; $entry < $count; $entry++) {
            $spot = $in_group[$found[$entry]];
            $column = ($group_x << 2) + $spot[0];
            $line = ($group_y << 2) + $spot[1];
            $values[$line * $size + $column] = $signs[$entry] === 1
                ? -$levels[$entry] : $levels[$entry];
        }
        return $carry;
    }
    /**
     * sigOffset works out where in the coefficient flag contexts this group
     * starts, which depends on the block size, the plane and where the group
     * sits.
     *
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param int $group_x which group across the block
     * @param int $group_y which group down the block
     * @param int $scan which of the three orders the block is written in
     * @return int the offset to add to the position's own context
     */
    private function sigOffset(int $power_of_two_size, int $plane, int $group_x,
        int $group_y, int $scan): int
    {
        $offset = $plane ? self::CHROMA_SIG_OFFSET : 0;
        if ($power_of_two_size === 2) {
            return $offset;
        }
        if ($plane === 0) {
            if ($group_x > 0 || $group_y > 0) {
                $offset += 3;
            }
            if ($power_of_two_size === 3) {
                $offset += $scan === HevcScan::DIAGONAL ? 9 : 15;
            } else {
                $offset += 21;
            }
        } else {
            $offset += $power_of_two_size === 3 ? 9 : 12;
        }
        return $offset;
    }
    /**
     * readLastPosition reads where the last coefficient of the block sits.
     *
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param int $scan which of the three orders the block is written in
     * @return array the column and row of the last coefficient
     */
    private function readLastPosition(int $power_of_two_size, int $plane,
        int $scan): array
    {
        $across = $this->readLastPrefix($power_of_two_size, $plane,
            HevcContexts::LAST_X_PREFIX);
        $down = $this->readLastPrefix($power_of_two_size, $plane,
            HevcContexts::LAST_Y_PREFIX);
        if ($across > 3) {
            $width = ($across >> 1) - 1;
            $across = (1 << $width) * (2 + ($across & 1))
                + $this->arithmetic_reader->bypassBits($width);
        }
        if ($down > 3) {
            $width = ($down >> 1) - 1;
            $down = (1 << $width) * (2 + ($down & 1))
                + $this->arithmetic_reader->bypassBits($width);
        }
        if ($scan === HevcScan::VERTICAL) {
            return [$down, $across];
        }
        return [$across, $down];
    }
    /**
     * readLastPrefix reads the leading part of the last coefficient's column or
     * row.
     *
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param int $base where these contexts start
     * @return int the leading part, which a suffix may extend
     */
    private function readLastPrefix(int $power_of_two_size, int $plane,
        int $base): int
    {
        $offset = $plane === 0
            ? 3 * ($power_of_two_size - 2) + (($power_of_two_size - 1) >>
                2) : 15;
        $shift = $plane ===
            0 ? ($power_of_two_size + 1) >> 2 : $power_of_two_size - 2;
        $limit = ($power_of_two_size << 1) - 1;
        $value = 0;
        while ($value < $limit && $this->arithmetic_reader->decision(
            $base + $offset + ($value >> $shift)) === 1) {
            $value++;
        }
        return $value;
    }
    /**
     * readRemaining reads how much larger a coefficient is than the flags
     * already said. The value is written as a run of ones, then a fixed number
     * of plain bits, with longer values switching to a wider form.
     *
     * @param int $rice how many plain bits the short form carries
     * @return int the extra size
     */
    private function readRemaining(int $rice): int
    {
        $prefix = 0;
        while ($prefix < 32 && $this->arithmetic_reader
            ->readBypassBit() === 1) {
            $prefix++;
        }
        if ($prefix < 3) {
            return ($prefix << $rice) +
                $this->arithmetic_reader->bypassBits($rice);
        }
        $width = $prefix - 3 + $rice;
        return (((1 << ($prefix - 3)) + 3 - 1) << $rice)
            + $this->arithmetic_reader->bypassBits($width);
    }
}

/**
 * HevcWalk walks the blocks of an H.265 slice coded on its own. The picture is
 * covered by coding tree blocks; each splits into a tree of square coding
 * blocks, each of those carries a prediction mode, and below that a second tree
 * of transform blocks holds the coefficients.
 */
final class HevcWalk
{
    /**
     * MODE_PLANAR is the mode that predicts a flat gradient.
     */
    private const MODE_PLANAR = 0;
    /**
     * MODE_DC is the mode that predicts one average value.
     */
    private const MODE_DC = 1;
    /**
     * MODE_VERTICAL is the mode that predicts straight down.
     */
    private const MODE_VERTICAL = 26;
    /**
     * MODE_HORIZONTAL is the mode that predicts straight across.
     */
    private const MODE_HORIZONTAL = 10;
    /**
     * MODE_LAST_ANGLE is the last of the angled modes.
     */
    private const MODE_LAST_ANGLE = 34;
    /**
     * ANGLE_COUNT is how many angled modes there are to step through.
     */
    private const ANGLE_COUNT = 32;
    /**
     * REMAINING_MODE_BITS is bits in a mode written out rather than taken from
     * the neighbors.
     */
    private const REMAINING_MODE_BITS = 5;
    /**
     * MODE_GRID is how many samples along one side of a prediction mode grid
     * square.
     */
    private const MODE_GRID = 4;
    /**
     * MODE_GRID_SHIFT is the power of two of that, for turning a position into
     * a grid
     * square.
     */
    private const MODE_GRID_SHIFT = 2;
    /**
     * QP_DELTA_PREFIX_MAX is how large a quantizer change may be before it is
     * written out in full.
     */
    private const QP_DELTA_PREFIX_MAX = 5;
    /**
     * QP_RANGE is how many quantizer indexes there are to wrap around.
     */
    private const QP_RANGE = 52;
    /**
     * EDGE_GRID is how far apart the edges that may be smoothed sit.
     */
    private const EDGE_GRID = 8;
    /**
     * FULL_STRENGTH is the strength every edge carries in a picture coded on
     * its own.
     */
    private const FULL_STRENGTH = 2;
    /**
     * CHROMA_QP is the color quantizers for the indexes where the two part
     * company.
     */
    private const CHROMA_QP = [29, 30, 31, 32, 33, 33, 34, 34, 35, 35, 36,
        36, 37, 37];
    /**
     * $arithmetic_reader stores the arithmetic decoder the decisions are read
     * from.
     * @var HevcCabac
     */
    private HevcCabac $arithmetic_reader;
    /**
     * $sequence_settings stores sequence parameters the picture was coded
     * against.
     * @var HevcSps
     */
    private HevcSps $sequence_settings;
    /**
     * $picture_settings stores picture parameters this slice was coded against.
     * @var HevcPps
     */
    private HevcPps $picture_settings;
    /**
     * $header stores what the slice being decoded says about itself: which
     * settings it uses, where it starts, and how strongly its edges are
     * smoothed.
     * @var HevcSliceHeader
     */
    private HevcSliceHeader $header;
    /**
     * $residual stores the reader that turns a block's coded values back into
     * differences from what was guessed. transformUnit() hands it each block.
     * @var HevcResidual
     */
    private HevcResidual $residual;
    /**
     * $depth_at stores how deep the tree went at each smallest block, for the
     * split context.
     * @var array
     */
    private array $depth_at = [];
    /**
     * $mode_at stores the prediction mode of each smallest block, for the mode
     * guesses.
     * @var array
     */
    private array $mode_at = [];
    /**
     * $min_across stores how many smallest blocks fit across the picture.
     * @var int
     */
    private int $min_across = 0;
    /**
     * $mode_across stores how many four sample squares fit across the picture.
     * @var int
     */
    private int $mode_across = 0;
    /**
     * $blocks stores the coefficients of every transform block read so far.
     * @var array
     */
    public array $blocks = [];
    /**
     * $units stores the coding blocks read so far, for checking the tree covers
     * the area.
     * @var array
     */
    public array $units = [];
    /**
     * $predictions stores what each block was predicted from, for checking
     * against a decoder.
     * @var array
     */
    public array $predictions = [];
    /**
     * $pending stores blocks read but not yet rebuilt, waiting for the
     * quantizer.
     * @var array
     */
    private array $pending = [];
    /**
     * $down_edges stores which eight sample squares start a block, running down
     * the picture.
     * @var array
     */
    private array $down_edges = [];
    /**
     * $across_edges stores which eight sample squares start a block, running
     * across it.
     * @var array
     */
    private array $across_edges = [];
    /**
     * $quantizer_unit stores the quantizer covering each eight sample square.
     * @var array
     */
    private array $quantizer_unit = [];
    /**
     * $quantizer_delta_read stores true once a quantizer change has been read
     * for the
     * current block.
     * @var bool
     */
    private bool $quantizer_delta_read = false;
    /**
     * $quantizer_delta stores how far the current block's quantizer sits from
     * the
     * predicted one.
     * @var int
     */
    private int $quantizer_delta = 0;
    /**
     * $quantizer_at stores the quantizer of each smallest coding block, for its
     * neighbors.
     * @var array
     */
    private array $quantizer_at = [];
    /**
     * $quantizer_previous stores the quantizer the last block of the previous
     * group
     * settled on.
     * @var int
     */
    private int $quantizer_previous = 0;
    /**
     * $group_left stores first column of the group of blocks sharing one
     * quantizer.
     * @var int
     */
    private int $group_left = 0;
    /**
     * $group_top stores first row of that group.
     * @var int
     */
    private int $group_top = 0;
    /**
     * $quantizer_trace stores what went into the last quantizer worked out, for
     * checking.
     * @var array
     */
    public array $quantizer_trace = [];
    /**
     * $quantizer stores the quantizer of the block being reconstructed.
     * @var int
     */
    private int $quantizer = 0;
    /**
     * $quantizer_known stores true once the current block's quantizer has been
     * worked
     * out.
     * @var bool
     */
    private bool $quantizer_known = false;
    /**
     * $planes stores the samples of each plane.
     * @var array
     */
    public array $planes = [];
    /**
     * $plane_wide stores how many samples across each plane holds.
     * @var array
     */
    public array $plane_wide = [];
    /**
     * $plane_tall stores how many samples down each plane holds.
     * @var array
     */
    public array $plane_tall = [];
    /**
     * $written stores which four sample squares of each plane have been
     * written.
     * @var array
     */
    private array $written = [];
    /**
     * $chroma_has_values stores whether the color planes of the current block
     * hold
     * coefficients.
     * @var array
     */
    private array $chroma_has_values = [];
    /**
     * $self_guessed_split stores true when the current coding block splits its
     * transform four ways.
     * @var bool
     */
    private bool $self_guessed_split = false;
    /**
     * $unit_left stores first column of the coding block being read.
     * @var int
     */
    private int $unit_left = 0;
    /**
     * $unit_top stores first row of the coding block being read.
     * @var int
     */
    private int $unit_top = 0;
    /**
     * $part_span stores how many samples each predicted part of it spans.
     * @var int
     */
    private int $part_span = 0;
    /**
     * $bits stores the reader the coded bits come from.
     * @var H264Bits
     */
    private H264Bits $bits;
    /**
     * $syntax stores the offset filter settings of every block.
     * @var HevcSyntax
     */
    private HevcSyntax $syntax;
    /**
     * $columns stores how many coding tree blocks fit across the picture.
     * @var int
     */
    public int $columns = 0;
    /**
     * $rows stores how many coding tree blocks fit down the picture.
     * @var int
     */
    public int $rows = 0;
    /**
     * __construct sets up the walk for one slice. coded against against
     *
     * @param H264Bits $bits reader positioned at the slice data
     * @param HevcCabac $arithmetic_reader decoder reading from that same reader
     * @param HevcSps $sequence_settings sequence parameters the picture was
     * @param HevcPps $picture_settings picture parameters this slice was coded
     * @param HevcSliceHeader $header this slice's own header
     */
    public function __construct(H264Bits $bits, HevcCabac $arithmetic_reader,
        HevcSps $sequence_settings, HevcPps $picture_settings,
            HevcSliceHeader $header)
    {
        $this->bits = $bits;
        $this->arithmetic_reader = $arithmetic_reader;
        $this->sequence_settings = $sequence_settings;
        $this->picture_settings = $picture_settings;
        $this->header = $header;
        $this->residual = new HevcResidual($arithmetic_reader,
            $picture_settings->sign_data_hiding);
        $this->syntax = new HevcSyntax($arithmetic_reader,
            $sequence_settings, $header);
        $tree_block_size = 1 << $sequence_settings
            ->power_of_two_tree_block_size;
        $this->columns = intdiv($sequence_settings
            ->frame_width + $tree_block_size - 1,
            $tree_block_size);
        $this->rows = intdiv($sequence_settings
            ->frame_height + $tree_block_size - 1,
            $tree_block_size);
        $this->quantizer_previous = $header->slice_quantizer;
        $this->quantizer = $header->slice_quantizer;
        foreach ([0, 1, 2] as $plane) {
            $shift = $plane === 0 ? 0 : 1;
            $this->plane_wide[$plane] = $sequence_settings
                ->frame_width >> $shift;
            $this->plane_tall[$plane] = $sequence_settings
                ->frame_height >> $shift;
            $this->planes[$plane] = array_fill(0,
                $this->plane_wide[$plane] * $this->plane_tall[$plane], 0);
            $this->written[$plane] = [];
        }
        $this->min_across = ($sequence_settings->frame_width +
            (1 << $sequence_settings->power_of_two_min_blue_size) - 1)
            >> $sequence_settings->power_of_two_min_blue_size;
        /*
            Prediction modes are kept for every four sample square, which is
            the smallest a predicted part can be. A coarser grid would let
            the four parts of a small block share one entry.
        */
        $this->mode_across = ($sequence_settings
            ->frame_width + self::MODE_GRID - 1)
            >> self::MODE_GRID_SHIFT;
    }
    /**
     * decodeSlice walks the whole slice, block by block. When the picture is
     * coded so its rows can be decoded in parallel, each row starts the engine
     * again from the state the row above had reached after its second block.
     *
     * @return bool true when the slice ended where it should
     */
    public function decodeSlice(): bool
    {
        $tree_block_size = 1 << $this->sequence_settings
            ->power_of_two_tree_block_size;
        $parallel = $this->picture_settings->entropy_coding_sync_enabled;
        $saved = null;
        $total = $this->columns * $this->rows;
        for ($index = 0; $index < $total; $index++) {
            $column = $index % $this->columns;
            $row = intdiv($index, $this->columns);
            if ($parallel && $column === 0 && $row > 0) {
                $this->quantizer_previous = $this->header->slice_quantizer;
                $this->quantizer = $this->header->slice_quantizer;
                $this->bits->alignToByte();
                $this->arithmetic_reader->restart();
                if ($saved !== null) {
                    $this->arithmetic_reader->restoreContexts($saved);
                }
            }
            $this->syntax->sample_offset[$index] = $this->syntax
                ->readSao($column,
                $row, $this->columns);
            $this->codingTreeBlock($column * $tree_block_size,
                $row * $tree_block_size);
            if ($parallel && $column === 1) {
                $saved = $this->arithmetic_reader->saveContexts();
            }
            if ($this->arithmetic_reader->terminate() === 1) {
                return $index === $total - 1;
            }
            if ($parallel && $column === $this->columns - 1) {
                $this->arithmetic_reader->terminate();
            }
        }
        return false;
    }
    /**
     * codingTreeBlock walks one coding tree block and everything inside it.
     *
     * @param int $left first column of the block in samples
     * @param int $top first row of the block in samples
     */
    public function codingTreeBlock(int $left, int $top): void
    {
        $this->quadtree($left, $top,
            $this->sequence_settings->power_of_two_tree_block_size, 0);
    }
    /**
     * quadtree walks one square of the coding tree, splitting it where the
     * stream says to.
     *
     * @param int $left first column of the square in samples
     * @param int $top first row of the square in samples
     * @param int $power_of_two_size the power of two of how many samples the
     *     square spans
     * @param int $depth how far down the tree this square sits
     */
    private function quadtree(int $left, int $top, int $power_of_two_size,
        int $depth): void
    {
        $size = 1 << $power_of_two_size;
        $fits = $left + $size <= $this->sequence_settings->frame_width
            && $top + $size <= $this->sequence_settings->frame_height;
        $split = $power_of_two_size > $this->sequence_settings
            ->power_of_two_min_blue_size;
        if ($fits && $split) {
            $split = $this->arithmetic_reader->decision(
                HevcContexts::SPLIT_CODING_UNIT_FLAG
                + $this->splitContext($left, $top, $depth)) === 1;
        }
        if ($this->picture_settings->block_quantizer_delta_enabled
            && $power_of_two_size >= $this->sequence_settings
                ->power_of_two_tree_block_size
            - $this->picture_settings->difference_block_quantizer_delta_depth) {
            /*
                A new group of blocks shares one quantizer, predicted from
                what its neighbors settled on and from the group before it.
            */
            $this->quantizer_previous = $this->quantizer;
            $this->quantizer_delta_read = false;
            $this->quantizer_delta = 0;
            $this->group_left = $left;
            $this->group_top = $top;
        }
        if (!$split) {
            $this->codingUnit($left, $top, $power_of_two_size, $depth);
            return;
        }
        $half = $size >> 1;
        $this->quadtree($left, $top, $power_of_two_size - 1, $depth + 1);
        if ($left + $half < $this->sequence_settings->frame_width) {
            $this->quadtree($left + $half, $top, $power_of_two_size - 1,
                $depth + 1);
        }
        if ($top + $half < $this->sequence_settings->frame_height) {
            $this->quadtree($left, $top + $half, $power_of_two_size - 1,
                $depth + 1);
        }
        if ($left + $half < $this->sequence_settings->frame_width
            && $top + $half < $this->sequence_settings->frame_height) {
            $this->quadtree($left + $half, $top + $half, $power_of_two_size - 1,
                $depth + 1);
        }
    }
    /**
     * splitContext works out which context the split decision uses, given how
     * deep the squares to the left and above went.
     *
     * @param int $left first column of the square in samples
     * @param int $top first row of the square in samples
     * @param int $depth how far down the tree this square sits
     * @return int the context to read the decision with
     */
    private function splitContext(int $left, int $top, int $depth): int
    {
        $shift = $this->sequence_settings->power_of_two_min_blue_size;
        $context = 0;
        if ($left > 0) {
            $spot = (($top >> $shift) * $this->min_across)
                + (($left - 1) >> $shift);
            if (($this->depth_at[$spot] ?? 0) > $depth) {
                $context++;
            }
        }
        if ($top > 0) {
            $spot = ((($top - 1) >> $shift) * $this->min_across)
                + ($left >> $shift);
            if (($this->depth_at[$spot] ?? 0) > $depth) {
                $context++;
            }
        }
        return $context;
    }
    /**
     * codingUnit reads one coding block: how it is predicted, and the transform
     * tree that holds its coefficients.
     *
     * @param int $left first column of the block in samples
     * @param int $top first row of the block in samples
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param int $depth how far down the tree this block sits
     */
    private function codingUnit(int $left, int $top, int $power_of_two_size,
        int $depth): void
    {
        $size = 1 << $power_of_two_size;
        $shift = $this->sequence_settings->power_of_two_min_blue_size;
        for ($row = 0; $row < ($size >> $shift); $row++) {
            for ($column = 0; $column < ($size >> $shift); $column++) {
                $spot = ((($top >> $shift) + $row) * $this->min_across)
                    + ($left >> $shift) + $column;
                $this->depth_at[$spot] = $depth;
            }
        }
        $this->units[] = [$left, $top, $size];
        $this->quantizer_known = false;
        if ($this->picture_settings->plain_samples_allowed) {
            $this->arithmetic_reader
                ->decision(HevcContexts::CU_TRANSQUANT_BYPASS_FLAG);
        }
        $quarters = false;
        if ($power_of_two_size === $this->sequence_settings
            ->power_of_two_min_blue_size) {
            $quarters =
                $this->arithmetic_reader
                    ->decision(HevcContexts::PART_MODE) === 0;
        }
        $this->self_guessed_split = $quarters;
        if (!$quarters && $this->sequence_settings->plain_samples_enabled
            && $power_of_two_size >= $this->sequence_settings
                ->power_of_two_min_plain_samples_blue_size
            && $power_of_two_size <= $this->sequence_settings
                ->power_of_two_max_plain_samples_blue_size) {
            if ($this->arithmetic_reader->terminate() === 1) {
                throw new HevcException(
                    'blocks storing their samples are not supported');
            }
        }
        $parts = $quarters ? 2 : 1;
        $step = intdiv($size, $parts);
        $this->unit_left = $left;
        $this->unit_top = $top;
        $this->part_span = $step;
        $suggested = [];
        for ($part = 0; $part < $parts * $parts; $part++) {
            $suggested[$part] = $this->arithmetic_reader->decision(
                HevcContexts::PREV_INTRA_LUMA_PRED);
        }
        $modes = [];
        for ($part = 0; $part < $parts * $parts; $part++) {
            $column = $left + ($part % $parts) * $step;
            $row = $top + intdiv($part, $parts) * $step;
            $guesses = $this->modeGuesses($column, $row);
            if ($suggested[$part] === 1) {
                $which = 0;
                if ($this->arithmetic_reader->readBypassBit() === 1) {
                    $which = $this->arithmetic_reader
                        ->readBypassBit() === 1 ? 2 : 1;
                }
                $modes[$part] = $guesses[$which];
            } else {
                $value =
                    $this->arithmetic_reader
                        ->bypassBits(self::REMAINING_MODE_BITS);
                sort($guesses);
                foreach ($guesses as $guess) {
                    if ($value >= $guess) {
                        $value++;
                    }
                }
                $modes[$part] = $value;
            }
            $this->recordMode($column, $row, $step, $modes[$part]);
        }
        $chroma_mode = $this->readChromaMode($modes[0]);
        $this->chroma_has_values = [1 => [], 2 => []];
        $this->transformTree($left, $top, $left, $top, $power_of_two_size, 0, 0,
            $modes, $chroma_mode, $parts);
        /*
            The blocks are rebuilt only once the whole coding unit has been
            read, because the change to the quantizer is written partway
            through and every block of the unit is scaled by the result.
        */
        $this->settleQuantizer();
        foreach ($this->pending as $entry) {
            $this->reconstruct($entry[0], $entry[1], $entry[2], $entry[3],
                $entry[4], $entry[5]);
        }
        $this->pending = [];
        for ($row = 0; $row < ($size >> $shift); $row++) {
            for ($column = 0; $column < ($size >> $shift); $column++) {
                $spot = ((($top >> $shift) + $row) * $this->min_across)
                    + ($left >> $shift) + $column;
                $this->quantizer_at[$spot] = $this->quantizer;
            }
        }
    }
    /**
     * modeGuesses the three modes a block may take from its neighbors rather
     * than writing out.
     *
     * @param int $left first column of the block in samples
     * @param int $top first row of the block in samples
     * @return array the three modes, in the order the stream indexes them
     */
    private function modeGuesses(int $left, int $top): array
    {
        $shift = self::MODE_GRID_SHIFT;
        $tree_block_mask = (1 << $this->sequence_settings
            ->power_of_two_tree_block_size) - 1;
        $from_left = self::MODE_DC;
        if ($left > 0) {
            $spot = (($top >> $shift) * $this->mode_across)
                + (($left - 1) >> $shift);
            $from_left = $this->mode_at[$spot] ?? self::MODE_DC;
        }
        $from_above = self::MODE_DC;
        if ($top > 0 && ($top & $tree_block_mask) !== 0) {
            $spot = ((($top - 1) >> $shift) * $this->mode_across)
                + ($left >> $shift);
            $from_above = $this->mode_at[$spot] ?? self::MODE_DC;
        }
        if ($from_left === $from_above) {
            if ($from_left < 2) {
                return [self::MODE_PLANAR, self::MODE_DC,
                    self::MODE_VERTICAL];
            }
            return [
                $from_left,
                2 + (($from_left + self::ANGLE_COUNT - 3) % self::ANGLE_COUNT),
                2 + (($from_left - 1) % self::ANGLE_COUNT),
            ];
        }
        $third = self::MODE_VERTICAL;
        if ($from_left !== self::MODE_PLANAR
            && $from_above !== self::MODE_PLANAR) {
            $third = self::MODE_PLANAR;
        } elseif ($from_left !== self::MODE_DC &&
            $from_above !== self::MODE_DC) {
            $third = self::MODE_DC;
        }
        return [$from_left, $from_above, $third];
    }
    /**
     * recordMode remembers the prediction mode of a part so its neighbors can
     * guess their own from it.
     *
     * @param int $left first column of the part in samples
     * @param int $top first row of the part in samples
     * @param int $span how many samples the part covers
     * @param int $mode the mode it was given
     */
    private function recordMode(int $left, int $top, int $span,
        int $mode): void
    {
        $shift = self::MODE_GRID_SHIFT;
        $steps = max(1, $span >> $shift);
        for ($row = 0; $row < $steps; $row++) {
            for ($column = 0; $column < $steps; $column++) {
                $spot = ((($top >> $shift) + $row) * $this->mode_across)
                    + ($left >> $shift) + $column;
                $this->mode_at[$spot] = $mode;
            }
        }
    }
    /**
     * readChromaMode reads which mode the color planes are predicted with.
     *
     * @param int $luma_mode the mode the brightness plane uses
     * @return int the mode the color planes use
     */
    private function readChromaMode(int $luma_mode): int
    {
        if ($this->arithmetic_reader->decision(
            HevcContexts::INTRA_CHROMA_PRED_MODE) === 0) {
            return $luma_mode;
        }
        $choice = $this->arithmetic_reader->bypassBits(2);
        $table = [self::MODE_PLANAR, self::MODE_VERTICAL,
            self::MODE_HORIZONTAL, self::MODE_DC];
        $mode = $table[$choice];
        return $mode === $luma_mode ? self::MODE_LAST_ANGLE : $mode;
    }
    /**
     * transformTree walks the tree of transform blocks below one coding block.
     *
     * @param int $left first column of this square in samples
     * @param int $top first row of this square in samples
     * @param int $base_left first column of the parent square
     * @param int $base_top first row of the parent square
     * @param int $power_of_two_size the power of two of how many samples this
     *     square spans
     * @param int $depth how far down the transform tree this square sits
     * @param int $index which quarter of its parent this square is
     * @param array $modes the brightness modes of the coding block's parts
     * @param int $chroma_mode the mode the color planes use
     * @param int $parts how many parts the coding block was split into
     */
    private function transformTree(int $left, int $top, int $base_left,
        int $base_top, int $power_of_two_size, int $depth, int $index,
            array $modes,
        int $chroma_mode, int $parts): void
    {
        $max_depth = $this->sequence_settings->max_transform_depth_self_guessed
            + ($this->self_guessed_split ? 1 : 0);
        $split = $power_of_two_size > $this->sequence_settings
            ->largest_transform_power;
        if ($power_of_two_size <= $this->sequence_settings
            ->largest_transform_power
            && $power_of_two_size > $this->sequence_settings
                ->smallest_transform_power
            && $depth < $max_depth && !($this->self_guessed_split &&
                $depth === 0)) {
            $split = $this->arithmetic_reader->decision(
                HevcContexts::SPLIT_TRANSFORM_FLAG + 5 -
                    $power_of_two_size) === 1;
        } elseif ($this->self_guessed_split && $depth === 0) {
            $split = true;
        }
        if ($power_of_two_size > 2) {
            if ($depth === 0 || ($this
                ->chroma_has_values[1][$depth - 1] ?? 0) === 1) {
                $this->chroma_has_values[1][$depth] =
                    $this->arithmetic_reader->decision(
                    HevcContexts::CBF_CHROMA + $depth);
            } else {
                $this->chroma_has_values[1][$depth] = 0;
            }
            if ($depth === 0 || ($this
                ->chroma_has_values[2][$depth - 1] ?? 0) === 1) {
                $this->chroma_has_values[2][$depth] =
                    $this->arithmetic_reader->decision(
                    HevcContexts::CBF_CHROMA + $depth);
            } else {
                $this->chroma_has_values[2][$depth] = 0;
            }
        }
        if ($split) {
            $half = 1 << ($power_of_two_size - 1);
            for ($quarter = 0; $quarter < 4; $quarter++) {
                $this->transformTree($left + ($quarter & 1) * $half,
                    $top + (($quarter >> 1) & 1) * $half, $left, $top,
                    $power_of_two_size - 1, $depth + 1, $quarter, $modes,
                    $chroma_mode, $parts);
            }
            return;
        }
        $luma_has_values = $this->arithmetic_reader->decision(
            HevcContexts::CBF_LUMA + ($depth === 0 ? 1 : 0));
        $this->transformUnit($left, $top, $base_left, $base_top,
            $power_of_two_size,
            $depth, $index, $luma_has_values, $modes, $chroma_mode, $parts);
    }
    /**
     * transformUnit reads the coefficients of one transform block and of the
     * color blocks that go with it.
     *
     * @param int $left first column of the block in samples
     * @param int $top first row of the block in samples
     * @param int $base_left first column of the parent square
     * @param int $base_top first row of the parent square
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param int $depth how far down the transform tree this block sits
     * @param int $index which quarter of its parent this block is
     * @param int $luma_has_values whether the brightness block holds
     *     coefficients
     * @param array $modes the brightness modes of the coding block's parts
     * @param int $chroma_mode the mode the color planes use
     * @param int $parts how many parts the coding block was split into
     */
    private function transformUnit(int $left, int $top, int $base_left,
        int $base_top, int $power_of_two_size, int $depth, int $index,
            int $luma_has_values,
        array $modes, int $chroma_mode, int $parts): void
    {
        $chroma_here = $power_of_two_size > 2 || $index === 3;
        $blue_has_values = 0;
        $red_has_values = 0;
        if ($power_of_two_size > 2) {
            $blue_has_values = $this->chroma_has_values[1][$depth] ?? 0;
            $red_has_values = $this->chroma_has_values[2][$depth] ?? 0;
        } else {
            /*
                The color planes of four small blocks are carried together
                on the last of them, but every one of the four counts the
                parent's flags when deciding whether it holds anything.
            */
            $blue_has_values = $this->chroma_has_values[1][$depth - 1] ?? 0;
            $red_has_values = $this->chroma_has_values[2][$depth - 1] ?? 0;
        }
        if ($luma_has_values === 0 && $blue_has_values ===
            0 && $red_has_values === 0) {
            $mode = $modes[$parts > 1
                ? $this->partShape($left, $top, $parts) : 0];
            $this->pending[] = [0, $left, $top, $power_of_two_size, $mode,
                null];
            if ($chroma_here) {
                $chroma_power_of_two =
                    $power_of_two_size > 2 ? $power_of_two_size - 1 : 2;
                $chroma_left = $power_of_two_size > 2 ? $left : $base_left;
                $chroma_top = $power_of_two_size > 2 ? $top : $base_top;
                foreach ([1, 2] as $plane) {
                    $this->pending[] = [$plane, $chroma_left >> 1,
                        $chroma_top >> 1, $chroma_power_of_two,
                            $chroma_mode, null];
                }
            }
            return;
        }
        if ($this->picture_settings->block_quantizer_delta_enabled &&
            !$this->quantizer_delta_read) {
            $this->readQuantizerChange();
            $this->quantizer_delta_read = true;
        }
        $mode = $modes[$parts > 1 ? $this->partShape($left, $top, $parts) : 0];
        $values = null;
        if ($luma_has_values === 1) {
            $values = $this->residual->readBlockValues($power_of_two_size, 0,
                $this->scanFor($power_of_two_size, 0, $mode));
            $this->blocks[] = [0, $left, $top, $power_of_two_size, $values];
        }
        $this->pending[] = [0, $left, $top, $power_of_two_size, $mode, $values];
        if (!$chroma_here) {
            return;
        }
        $chroma_power_of_two =
            $power_of_two_size > 2 ? $power_of_two_size - 1 : 2;
        $chroma_left = $power_of_two_size > 2 ? $left : $base_left;
        $chroma_top = $power_of_two_size > 2 ? $top : $base_top;
        foreach ([1 => $blue_has_values,
            2 => $red_has_values] as $plane => $flag) {
            $values = null;
            if ($flag === 1) {
                $values = $this->residual
                    ->readBlockValues($chroma_power_of_two, $plane,
                    $this->scanFor($chroma_power_of_two, $plane, $chroma_mode));
                $this->blocks[] = [$plane, $chroma_left, $chroma_top,
                    $chroma_power_of_two, $values];
            }
            $this->pending[] = [$plane, $chroma_left >> 1, $chroma_top >> 1,
                $chroma_power_of_two, $chroma_mode, $values];
        }
    }
    /**
     * partShape works out which part of a coding block split into four a
     * position falls in.
     *
     * @param int $left column of the position in samples
     * @param int $top row of the position in samples
     * @param int $parts how many parts the block was split into per side
     * @return int the index of the part
     */
    private function partShape(int $left, int $top, int $parts): int
    {
        $across = intdiv($left - $this->unit_left, $this->part_span);
        $down = intdiv($top - $this->unit_top, $this->part_span);
        $across = max(0, min($parts - 1, $across));
        $down = max(0, min($parts - 1, $down));
        return $down * $parts + $across;
    }
    /**
     * scanFor works out which order a transform block is written in, which for
     * the smaller intra blocks follows the direction they are predicted from.
     *
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param int $mode the mode the block is predicted with
     * @return int the scan order to use
     */
    private function scanFor(int $power_of_two_size, int $plane, int $mode): int
    {
        if ($power_of_two_size > 3 || ($power_of_two_size ===
            3 && $plane !== 0)) {
            return HevcScan::DIAGONAL;
        }
        if ($mode >= 6 && $mode <= 14) {
            return HevcScan::VERTICAL;
        }
        if ($mode >= 22 && $mode <= 30) {
            return HevcScan::HORIZONTAL;
        }
        return HevcScan::DIAGONAL;
    }
    /**
     * readQuantizerChange reads how far this block's quantizer sits from the
     * slice's.
     */
    private function readQuantizerChange(): void
    {
        $prefix = 0;
        while ($prefix < self::QP_DELTA_PREFIX_MAX
            && $this->arithmetic_reader->decision(HevcContexts::CU_QP_DELTA
            + ($prefix === 0 ? 0 : 1)) === 1) {
            $prefix++;
        }
        $value = $prefix;
        if ($prefix === self::QP_DELTA_PREFIX_MAX) {
            $width = 0;
            while ($this->arithmetic_reader->readBypassBit() === 1) {
                $width++;
            }
            $value += (1 << $width) - 1 +
                $this->arithmetic_reader->bypassBits($width);
        }
        if ($value > 0 && $this->arithmetic_reader->readBypassBit() === 1) {
            $value = -$value;
        }
        $this->quantizer_delta = $value;
    }
    /**
     * settleQuantizer works out the quantizer of the current coding block,
     * once, after any change it carries has been read.
     */
    private function settleQuantizer(): void
    {
        if ($this->quantizer_known) {
            return;
        }
        $this->quantizer = $this->quantizerFor($this->group_left,
            $this->group_top);
        $this->quantizer_known = true;
    }
    /**
     * quantizerFor works out the quantizer of a coding block, which is written
     * as a change from what its decoded neighbors suggest.
     *
     * @param int $left first column of the block in samples
     * @param int $top first row of the block in samples
     * @return int the quantizer index for its brightness plane
     */
    private function quantizerFor(int $left, int $top): int
    {
        $shift = $this->sequence_settings->power_of_two_min_blue_size;
        $block_shift = $this->sequence_settings->power_of_two_tree_block_size;
        $from_left = $this->quantizer_previous;
        if ($left > 0
            && (($left - 1) >> $block_shift) === ($left >> $block_shift)
            && ($top >> $block_shift) === ($top >> $block_shift)) {
            $spot = (($top >> $shift) * $this->min_across)
                + (($left - 1) >> $shift);
            $from_left = $this->quantizer_at[$spot] ?? $this
                ->quantizer_previous;
        }
        $from_above = $this->quantizer_previous;
        if ($top > 0
            && (($top - 1) >> $block_shift) === ($top >> $block_shift)) {
            $spot = ((($top - 1) >> $shift) * $this->min_across)
                + ($left >> $shift);
            $from_above = $this->quantizer_at[$spot] ?? $this
                ->quantizer_previous;
        }
        $predicted = ($from_left + $from_above + 1) >> 1;
        $this->quantizer_trace = [$left, $top, $from_left, $from_above,
            $this->quantizer_previous, $this->quantizer_delta];
        return ($predicted + $this->quantizer_delta + self::QP_RANGE)
            % self::QP_RANGE;
    }
    /**
     * chromaQuantizer the quantizer a color plane uses, which follows the
     * brightness one through a table that eases off at the higher indexes.
     *
     * @param int $luma the quantizer index of the brightness plane
     * @param int $offset the shift this plane and slice apply
     * @return int the quantizer index for the color plane
     */
    private function chromaQuantizer(int $luma, int $offset): int
    {
        $index = max(0, min(57, $luma + $offset));
        if ($index < 30) {
            return $index;
        }
        if ($index > 43) {
            return $index - 6;
        }
        return self::CHROMA_QP[$index - 30];
    }
    /**
     * gatherReferences gathers the samples a block predicts from, leaving out
     * any that fall outside the picture or in a part of it not yet
     * reconstructed.
     *
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param int $left first column of the block in samples
     * @param int $top first row of the block in samples
     * @param int $size how many samples the block spans
     * @return array the values found, keyed by reference position
     */
    private function gatherReferences(int $plane, int $left, int $top,
        int $size): array
    {
        $corner = 2 * $size;
        $wide = $this->plane_wide[$plane];
        $tall = $this->plane_tall[$plane];
        $found = [];
        if ($left > 0 && $top > 0
            && $this->isWritten($plane, $left - 1, $top - 1)) {
            $found[$corner] = $this->planes[$plane][($top - 1) * $wide
                + $left - 1];
        }
        for ($step = 1; $step <= $corner; $step++) {
            $row = $top + $step - 1;
            if ($left > 0 && $row < $tall
                && $this->isWritten($plane, $left - 1, $row)) {
                $found[$corner - $step] = $this->planes[$plane][$row * $wide
                    + $left - 1];
            }
            $column = $left + $step - 1;
            if ($top > 0 && $column < $wide
                && $this->isWritten($plane, $column, $top - 1)) {
                $found[$corner + $step] = $this->planes[$plane][($top - 1) *
                    $wide + $column];
            }
        }
        return $found;
    }
    /**
     * isWritten says whether a sample has been reconstructed yet.
     *
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param int $column which column of the plane
     * @param int $row which row of the plane
     * @return bool true when the sample holds a decoded value
     */
    private function isWritten(int $plane, int $column, int $row): bool
    {
        $across = ($this->plane_wide[$plane] + 3) >> 2;
        return isset($this->written[$plane][(($row >> 2) * $across)
            + ($column >> 2)]);
    }
    /**
     * reconstruct predicts one transform block, adds its coded differences
     * back, and writes the result into its plane.
     *
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param int $left first column of the block in samples
     * @param int $top first row of the block in samples
     * @param int $power_of_two_size the power of two of how many samples the
     *     block spans
     * @param int $mode which of the thirty-five ways it is predicted
     * @param array $values its coefficients, or null when it has none
     */
    private function reconstruct(int $plane, int $left, int $top,
        int $power_of_two_size, int $mode, ?array $values): void
    {
        $size = 1 << $power_of_two_size;
        $refs = HevcPredict::fillReferences(
            $this->gatherReferences($plane, $left, $top, $size), $size);
        $refs = HevcPredict::smoothSamples($refs, $size, $power_of_two_size,
            $mode,
            $plane,
            $this->sequence_settings->strong_self_guessed_smoothing);
        $predicted = HevcPredict::predict($refs, $size, $power_of_two_size,
            $mode,
            $plane);
        $this->predictions[] = [$plane, $left, $top, $power_of_two_size, $mode,
            $refs, $predicted, $this->quantizer, $this->quantizer_trace];
        $residual = null;
        if ($values !== null) {
            $quant = $plane === 0 ? $this->quantizer
                : $this->chromaQuantizer($this->quantizer,
                $plane === 1
                ? $this->picture_settings->blue_quantizer_offset +
                    $this->header->blue_quantizer_offset
                : $this->picture_settings->red_quantizer_offset +
                    $this->header->red_quantizer_offset);
            $scaled = HevcTransform::dequantize($values, $power_of_two_size,
                $quant);
            $small = $plane === 0 && $power_of_two_size === 2;
            $residual = HevcTransform::inverse($scaled, $power_of_two_size,
                $small);
        }
        $wide = $this->plane_wide[$plane];
        $tall = $this->plane_tall[$plane];
        $across = ($wide + 3) >> 2;
        for ($row = 0; $row < $size; $row++) {
            $line = $top + $row;
            if ($line >= $tall) {
                break;
            }
            for ($column = 0; $column < $size; $column++) {
                $spot = $left + $column;
                if ($spot >= $wide) {
                    break;
                }
                $value = $predicted[$row * $size + $column];
                if ($residual !== null) {
                    $value += $residual[$row * $size + $column];
                }
                $this->planes[$plane][$line * $wide + $spot]
                    = max(0, min(255, $value));
            }
        }
        for ($row = 0; $row < $size; $row += 4) {
            for ($column = 0; $column < $size; $column += 4) {
                $this->written[$plane][((($top + $row) >> 2) * $across)
                    + (($left + $column) >> 2)] = true;
            }
        }
        if ($plane === 0) {
            $this->markEdges($left, $top, $size);
        }
    }
    /**
     * markEdges records where a block starts, so its edges can be smoothed
     * later, and which quantizer covers the squares it fills. Only the edges
     * that fall on the grid of eight samples are smoothed, and in a picture
     * coded on its own every such edge is smoothed at full strength.
     *
     * @param int $left first column of the block in samples
     * @param int $top first row of the block in samples
     * @param int $size how many samples the block spans
     */
    private function markEdges(int $left, int $top, int $size): void
    {
        $across = intdiv($this->plane_wide[0] + self::EDGE_GRID - 1,
            self::EDGE_GRID);
        for ($step = 0; $step < $size; $step += self::EDGE_GRID) {
            $key = ((($top + $step) >> 3) * $across) + ($left >> 3);
            if (($left % self::EDGE_GRID) === 0) {
                $this->down_edges[$key] = self::FULL_STRENGTH;
            }
            $key = (($top >> 3) * $across) + (($left + $step) >> 3);
            if (($top % self::EDGE_GRID) === 0) {
                $this->across_edges[$key] = self::FULL_STRENGTH;
            }
        }
        for ($row = 0; $row < $size; $row += self::EDGE_GRID) {
            for ($column = 0; $column < $size; $column += self::EDGE_GRID) {
                $key = ((($top + $row) >> 3) * $across)
                    + (($left + $column) >> 3);
                $this->quantizer_unit[$key] = $this->quantizer;
            }
        }
    }
    /**
     * deblock smooths the block edges of the whole picture, running down the
     * picture first and then across it.
     */
    public function deblock(): void
    {
        if ($this->header->deblocking_disabled) {
            return;
        }
        foreach ([true, false] as $vertical) {
            $edges = $vertical ? $this->down_edges : $this->across_edges;
            $this->deblockPlane(0, $edges, $vertical);
            foreach ([1, 2] as $plane) {
                $this->deblockPlane($plane, $edges, $vertical);
            }
        }
    }
    /**
     * offsetFilter corrects the samples of the whole picture after the edges
     * have been smoothed, block by block, using the settings each block
     * carries.
     */
    public function offsetFilter(): void
    {
        if (!$this->header->sample_offset_luma && !$this->header
            ->sample_offset_chroma) {
            return;
        }
        $tree_block_size = 1 << $this->sequence_settings
            ->power_of_two_tree_block_size;
        $source = $this->planes;
        for ($index = 0; $index < $this->columns * $this->rows; $index++) {
            $params = $this->syntax->sample_offset[$index] ?? null;
            if ($params === null) {
                continue;
            }
            $column = ($index % $this->columns) * $tree_block_size;
            $row = intdiv($index, $this->columns) * $tree_block_size;
            foreach ([0, 1, 2] as $plane) {
                $shift = $plane === 0 ? 0 : 1;
                $wide = $this->plane_wide[$plane];
                $tall = $this->plane_tall[$plane];
                $left = $column >> $shift;
                $top = $row >> $shift;
                HevcSao::applyOffsets($source[$plane], $this->planes[$plane],
                    $wide, $tall, $left, $top,
                    min($tree_block_size >> $shift, $wide - $left),
                    min($tree_block_size >> $shift, $tall - $top),
                    $params->kind[$plane], $params->offset[$plane],
                    $params->band_position[$plane],
                    $params->edge_class[$plane]);
            }
        }
    }
    /**
     * deblockPlane smooths the edges of one plane in one direction.
     *
     * @param int $plane 0 for brightness, 1 and 2 for the color planes
     * @param array $edges which squares start a block, and how strongly
     * @param bool $vertical true for the edges running down the picture
     */
    private function deblockPlane(int $plane, array $edges,
        bool $vertical): void
    {
        $across = intdiv($this->plane_wide[0] + self::EDGE_GRID - 1,
            self::EDGE_GRID);
        $wide = $this->plane_wide[$plane];
        $tall = $this->plane_tall[$plane];
        $list = [];
        foreach ($edges as $key => $strength) {
            $column = ($key % $across) * self::EDGE_GRID;
            $row = intdiv($key, $across) * self::EDGE_GRID;
            $lines = self::EDGE_GRID;
            if ($plane !== 0) {
                $span = self::EDGE_GRID * 2;
                if ($vertical && ($column % $span) !== 0) {
                    continue;
                }
                if (!$vertical && ($row % $span) !== 0) {
                    continue;
                }
                /*
                    A color plane holds half as many samples, so one run of
                    eight brightness lines covers four of its own.
                */
                $column >>= 1;
                $row >>= 1;
                $lines = self::EDGE_GRID >> 1;
            }
            $prior_key = $vertical ? $key - 1 : $key - $across;
            $mine = $this->quantizer_unit[$key] ?? $this->header
                ->slice_quantizer;
            $other = $this->quantizer_unit[$prior_key] ?? $mine;
            $quant = ($mine + $other + 1) >> 1;
            if ($plane !== 0) {
                $offset = $plane === 1
                    ? $this->picture_settings->blue_quantizer_offset +
                        $this->header->blue_quantizer_offset
                    : $this->picture_settings->red_quantizer_offset +
                        $this->header->red_quantizer_offset;
                $quant = $this->chromaQuantizer($quant, $offset);
            }
            $list[] = [$column, $row, $strength, $quant, $lines];
        }
        HevcDeblock::applyOffsets($this->planes[$plane], $wide, $tall, $list,
            $vertical, $plane, $this->header->beta_offset,
            $this->header->move_limit_offset);
    }
}

/**
 * HevcDecoder decodes one H.265 keyframe into a picture. The parameter sets a
 * container carries are read once; a frame is then split into its coded units,
 * its slice walked, and the two filters run over the result before the part
 * that is shown is handed back.
 */
final class HevcDecoder
{
    /**
     * SPS_UNIT is the unit type that carries a sequence parameter set.
     */
    private const SPS_UNIT = 33;
    /**
     * PPS_UNIT is the unit type that carries a picture parameter set.
     */
    private const PPS_UNIT = 34;
    /**
     * LAST_SLICE_UNIT is the last unit type that carries a slice of a picture.
     */
    private const LAST_SLICE_UNIT = 21;
    /**
     * $sequences stores the sequence parameter sets seen so far, by their
     * identifiers.
     * @var array
     */
    private array $sequences = [];
    /**
     * $pictures stores the picture parameter sets seen so far, by their
     * identifiers.
     * @var array
     */
    private array $pictures = [];
    /**
     * consume reads any parameter sets a run of units holds.
     *
     * @param array $units units as the splitter returned them
     */
    public function consume(array $units): void
    {
        foreach ($units as $unit) {
            if ($unit['type'] === self::SPS_UNIT) {
                list($id,
                    $set) =
                        HevcParamParser::readSequenceSettings($unit['rbsp']);
                $this->sequences[$id] = $set;
            }
            if ($unit['type'] === self::PPS_UNIT) {
                list($id,
                    $set) = HevcParamParser::readPictureSettings($unit['rbsp']);
                $this->pictures[$id] = $set;
            }
        }
    }
    /**
     * consumeStored reads parameter sets a container stores outside the frames.
     * unit header still in front
     *
     * @param array $sets each set as its stored bytes, with the two byte
     */
    public function consumeStored(array $sets): void
    {
        $units = [];
        foreach ($sets as $set) {
            if (strlen($set) < 3) {
                continue;
            }
            $units[] = [
                'type' => (ord($set[0]) >> 1) & 0x3F,
                'rbsp' => H264Bits::unescape(substr($set, 2)),
            ];
        }
        $this->consume($units);
    }
    /**
     * decodeKeyframe decodes one keyframe.
     *
     * @param string $frame the frame as start coded units
     * @return VideoPicture the part of the picture that is shown
     */
    public function decodeKeyframe(string $frame): VideoPicture
    {
        $units = HevcNal::unitsFromStream($frame);
        $this->consume($units);
        foreach ($units as $unit) {
            if ($unit['type'] > self::LAST_SLICE_UNIT) {
                continue;
            }
            return $this->decodeSliceUnit($unit);
        }
        throw new HevcException('no H.265 picture found in this frame');
    }
    /**
     * decodeSliceUnit decodes one slice that covers a whole picture.
     *
     * @param array $unit the slice unit as the splitter returned it
     * @return VideoPicture the part of the picture that is shown
     */
    private function decodeSliceUnit(array $unit): VideoPicture
    {
        if ($this->pictures === [] || $this->sequences === []) {
            throw new HevcException('H.265 parameter sets are missing');
        }
        $bits = new H264Bits($unit['rbsp']);
        $probe = new H264Bits($unit['rbsp']);
        $probe->readBit();
        if ($unit['type'] >= 16 && $unit['type'] <= 23) {
            $probe->readBit();
        }
        $picture_settings_id = $probe->readWholeNumber();
        if (!isset($this->pictures[$picture_settings_id])) {
            throw new HevcException("the stream has not carried "
                . "picture settings $picture_settings_id");
        }
        $picture_settings = $this->pictures[$picture_settings_id];
        if (!isset($this->sequences[$picture_settings->sequence_settings_id])) {
            throw new HevcException('H.265 sequence parameters are missing');
        }
        $sequence_settings = $this->sequences[$picture_settings
            ->sequence_settings_id];
        if ($sequence_settings->chroma_format_setting !== 1) {
            throw new HevcException('only 4:2:0 chroma is supported');
        }
        if ($sequence_settings->bit_depth_luma !== 8 ||
            $sequence_settings->bit_depth_chroma !== 8) {
            throw new HevcException('only 8-bit video is supported');
        }
        $header = HevcSliceHeader::readSettings($unit['rbsp'], $unit['type'],
            $sequence_settings,
            $picture_settings);
        $data = new H264Bits(substr($unit['rbsp'], $header->data_offset));
        $arithmetic_reader = new HevcCabac($data, HevcContexts::INIT,
            $header->slice_quantizer);
        $walk = new HevcWalk($data, $arithmetic_reader, $sequence_settings,
            $picture_settings, $header);
        $walk->decodeSlice();
        $walk->deblock();
        $walk->offsetFilter();
        return new VideoPicture($walk->planes[0], $walk->planes[1],
            $walk->planes[2], $sequence_settings->croppedWidth(),
                $sequence_settings->croppedHeight(),
            $walk->plane_wide[0], $walk->plane_wide[1], 2 *
                $sequence_settings->crop_left,
            2 * $sequence_settings->crop_top, 1, 1);
    }
}
X