<?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 H.264 keyframe into a picture. It reads the settings
 * the stream carries, guesses each block from its neighbors, adds the
 * coded differences, and smooths the block edges at the end.
 */
namespace seekquarry\yioop\library\av_processing;
/**
 * H264Nal splits Annex-B, the form an H.264 stream takes on its own (start-code
 * delimited) or length-prefixed byte streams into NAL (a unit an H.264 or HEVC
 * stream is cut into), a unit an H.264 or HEVC stream is cut into units.
 */
final class H264Nal
{
    /**
     * unitsFromStream stream was cut into
     *
     * @return array the units the
     * @param string $source the file or bytes being read
     */
    public static function unitsFromStream(string $source): array
    {
        $written = [];
        $count = strlen($source);
        /* offset of the start-code prefix */
        $code_at = [];
        $payload_at = [];
        for ($i = 0; $i + 3 <= $count; $i++) {
            if ($source[$i] === "\x00" && $source[$i + 1] === "\x00") {
                if ($source[$i + 2] === "\x01") {
                    $code_at[] = $i;
                    $payload_at[] = $i + 3;
                    $i += 2;
                } elseif ($i + 4 <= $count && $source[$i + 2] === "\x00"
                    && $source[$i + 3] === "\x01") {
                    $code_at[] = $i;
                    $payload_at[] = $i + 4;
                    $i += 3;
                }
            }
        }
        $letter = count($code_at);
        for ($k = 0; $k < $letter; $k++) {
            $from = $payload_at[$k];
            $to = ($k + 1 < $letter) ? $code_at[$k + 1] : $count;
            /* trailing_zero_8bits may pad the end of a unit */
            while ($to > $from && $source[$to - 1] === "\x00") {
                $to--;
            }
            $unit = substr($source, $from, $to - $from);
            if ($unit !== '') {
                $written[] = self::parseUnit($unit);
            }
        }
        return $written;
    }
    /**
     * parseUnit reads one unit of the stream and says which kind it is and what
     * it holds. A container hands units over one at a time.
     *
     * @return array what the unit says
     * @param string $unit the unit read out of the stream
     */
    public static function parseUnit(string $unit): array
    {
        $high = ord($unit[0]);
        return [
            'type'   => $high & 0x1F,
            'refIdc' => ($high >> 5) & 3,
            'rbsp'   => H264Bits::unescape(substr($unit, 1)),
        ];
    }
}

/**
 * H264Exception raised when an H.264 stream cannot be read, either because it
 * is damaged or because it uses a feature this decoder does not carry.
 */
class H264Exception extends VideoException {}

/**
 * H264Sps a sequence parameter set: the picture size, sample depth, cropping
 * and scaling values that hold for a run of pictures.
 */
final class H264Sps
{
    /**
     * $id stores which numbered set of settings this is. A stream may carry
     * several and each slice names the one it uses.
     * @var int
     */
    public int $id = 0;
    /**
     * $profile_setting stores which of H.264's profiles the stream uses, which
     * says
     * what a decoder must be able to do.
     * @var int
     */
    public int $profile_setting = 0;
    /**
     * $constraint_flags stores further limits the stream promises to keep
     * within, read alongside the profile.
     * @var int
     */
    public int $constraint_flags = 0;
    /**
     * $level_setting stores the level the stream claims, which bounds its
     * picture
     * size and rate.
     * @var int
     */
    public int $level_setting = 0;
    /**
     * $chroma_format_setting stores how the color planes are shrunk against the
     * brightness: one means half in each direction, which is what most video
     * uses.
     * @var int
     */
    public int $chroma_format_setting = 1;
    /**
     * $separate_color_plane stores whether the three planes are coded as
     * separate pictures rather than together.
     * @var bool
     */
    public bool $separate_color_plane = false;
    /**
     * $bit_depth_luma stores how many bits one brightness sample takes.
     * @var int
     */
    public int $bit_depth_luma = 8;
    /**
     * $bit_depth_chroma stores how many bits one color sample takes.
     * @var int
     */
    public int $bit_depth_chroma = 8;
    /**
     * $lossless_blocks_allowed stores whether a block coded at the lowest
     * quantizer skips the transform and stores its samples as they are. A
     * stream that throws nothing away uses this; most do not.
     * @var bool
     */
    public bool $lossless_blocks_allowed = false;
    /**
     * $frame_number_bits stores how many bits the frame counter takes, so a
     * decoder knows when the count wraps back to zero. The stream writes the
     * number of bits rather than the largest count.
     * @var int
     */
    public int $frame_number_bits = 4;
    /**
     * $picture_order_count_type stores which of three ways the stream
     * uses to say what order its pictures are shown in.
     * @var int
     */
    public int $picture_order_count_type = 0;
    /**
     * $picture_order_low_bit_count stores how many bits the showing-order
     * counter takes, so a decoder knows when the count wraps back to
     * zero.
     * @var int
     */
    public int $picture_order_low_bit_count = 4;
    /**
     * $delta_picture_order_always_zero stores whether the stream leaves
     * out the fields that nudge a picture's place in showing order.
     * Where it does, a picture is shown in the order it was decoded.
     * @var bool
     */
    public bool $delta_picture_order_always_zero = false;
    /**
     * $count_reference_frames_in_picture_order_cycle stores the number of
     * reference frames in
     * picture order cycle.
     * @var int
     */
    public int $count_reference_frames_in_picture_order_cycle = 0;
    /**
     * $max_count_reference_frames stores the largest number of reference
     * frames.
     * @var int
     */
    public int $max_count_reference_frames = 0;
    /**
     * $picture_width_in_macroblocks stores the picture width in mbs.
     * @var int
     */
    public int $picture_width_in_macroblocks = 0;
    /**
     * $picture_height_in_map_units stores how tall a picture is, counted
     * in rows of macroblocks. A decoder works out the height in
     * pixels from this and the size of a macroblock.
     * @var int
     */
    public int $picture_height_in_map_units = 0;
    /**
     * $frame_macroblocks_only stores whether every picture is a whole frame. A
     * stream
     * that codes half-pictures is refused, since a thumbnail from one would be
     * half a picture.
     * @var bool
     */
    public bool $frame_macroblocks_only = true;
    /**
     * $macroblock_adaptive_frame_field stores whether each macroblock may
     * choose to hold a whole picture or half of one. This decoder
     * reads whole pictures only.
     * @var bool
     */
    public bool $macroblock_adaptive_frame_field = false;
    /**
     * $frame_cropping stores whether the shown picture is smaller than the
     * coded one, which happens when the size is not a whole number of
     * macroblocks.
     * @var bool
     */
    public bool $frame_cropping = false;
    /**
     * $crop_left stores how much is cut from the left when it is.
     * @var int
     */
    public int $crop_left = 0;
    /**
     * $crop_right stores how much is cut from the right.
     * @var int
     */
    public int $crop_right = 0;
    /**
     * $crop_top stores how much is cut from the top.
     * @var int
     */
    public int $crop_top = 0;
    /**
     * $crop_bottom stores how much is cut from the bottom.
     * @var int
     */
    public int $crop_bottom = 0;
    /**
     * $scaling_matrix_present stores whether the stream carries its own tables
     * for scaling values rather than using the fixed ones.
     * @var bool
     */
    public bool $scaling_matrix_present = false;
    /**
     * $scaling_values stores those tables, six for the four by four blocks and
     * six for the eight by eight ones.
     * @var array
     */
    public array $scaling_values = [];
    /**
     * croppedWidth cropped output size
     *
     * @return int what was read
     */
    public function croppedWidth(): int
    {
        $sub_w = ($this->chroma_format_setting === 3 ||
            $this->chroma_format_setting === 0)
            ? 1 : 2;
        return $this->picture_width_in_macroblocks * 16
            - $sub_w * ($this->crop_left + $this->crop_right);
    }
    /**
     * croppedHeight height of the picture after the cropping the header asks
     * for, which is what a player shows.
     *
     * @return int what was read
     */
    public function croppedHeight(): int
    {
        $sub_h = ($this->chroma_format_setting === 1) ? 2 : 1;
        $mult = $this->frame_macroblocks_only ? 1 : 2;
        return $this->picture_height_in_map_units * 16 *
            ($this->frame_macroblocks_only ? 1 : 2)
            - $sub_h * $mult * ($this->crop_top + $this->crop_bottom);
    }
}

/**
 * H264Pps a picture parameter set: the quantizer offsets, the choice of entropy
 * coding, and any scaling values that hold for one picture.
 */
final class H264Pps
{
    /**
     * $id stores which numbered set of picture settings this is. A slice names
     * the one it uses.
     * @var int
     */
    public int $id = 0;
    /**
     * $sequence_settings_id stores which sequence settings these picture
     * settings belong to.
     * @var int
     */
    public int $sequence_settings_id = 0;
    /**
     * $entropy_coding_mode stores whether the stream uses the arithmetic coding
     * or the simpler one. The two are read by different classes.
     * @var bool
     */
    public bool $entropy_coding_mode = false;
    /**
     * $bottom_field_picture_order_present stores whether a slice
     * carries a second showing-order field for the lower half of an
     * interlaced picture.
     * present.
     * @var bool
     */
    public bool $bottom_field_picture_order_present = false;
    /**
     * $count_slice_groups stores the number of slice groups.
     * @var int
     */
    public int $count_slice_groups = 1;
    /**
     * $weighted_guess stores whether frames that lean on one other frame weight
     * it, which a keyframe never does.
     * @var bool
     */
    public bool $weighted_guess = false;
    /**
     * $two_picture_weighting_setting stores how a frame leaning on two other
     * pictures weighs them against each other. A frame that stands
     * on its own never leans on any, so a decoder reading only
     * keyframes passes over it.
     * @var int
     */
    public int $two_picture_weighting_setting = 0;
    /**
     * $picture_starting_quantizer stores the quantizer a slice starts from
     * before
     * its own
     * change is added.
     * @var int
     */
    public int $picture_starting_quantizer = 26;
    /**
     * $chroma_quantizer_index_offset stores the color quantizer index offset.
     * @var int
     */
    public int $chroma_quantizer_index_offset = 0;
    /**
     * $second_chroma_quantizer_index_offset stores the second color quantizer
     * index
     * offset.
     * @var int
     */
    public int $second_chroma_quantizer_index_offset = 0;
    /**
     * $deblocking_filter_control_present stores whether each slice says for
     * itself how strongly its block edges are smoothed. Where a stream leaves
     * this off, every slice is smoothed the same way.
     * @var bool
     */
    public bool $deblocking_filter_control_present = false;
    /**
     * $constrained_self_guessed_guess stores whether a block may be guessed
     * from
     * neighbors that leaned on another frame.
     * @var bool
     */
    public bool $constrained_self_guessed_guess = false;
    /**
     * $redundant_picture_count_present stores whether a slice
     * carries a number saying it repeats an earlier picture. A
     * stream may send a picture twice so a viewer that lost the
     * first copy can still show it.
     * @var bool
     */
    public bool $redundant_picture_count_present = false;
    /**
     * $larger_transform_allowed stores whether eight by eight transforms are
     * allowed
     * as well as four by four.
     * @var bool
     */
    public bool $larger_transform_allowed = false;
    /**
     * $scaling_matrix_present stores whether these picture settings carry their
     * own tables for scaling values rather than leaning on the sequence ones.
     * @var bool
     */
    public bool $scaling_matrix_present = false;
    /**
     * $scaling_values stores those tables, six for four by four blocks and six
     * for eight by eight ones.
     * @var array
     */
    public array $scaling_values = [];
}

/**
 * H264ParamParser reads sequence and picture parameter sets, and slice headers,
 * out of a stream of coded units.
 */
final class H264ParamParser
{
    /**
     * scalingList reads one table of weights that a stream carries for
     * scaling the values of a block. The weights arrive in the order
     * the format visits a block's places, which runs corner to
     * corner rather than row by row, and they are handed back in
     * that same order
     *
     * @param H264Bits $bits the reader the stream's bits are taken from
     * @param int $size how many bytes
     * @param bool $use_default whether the format's own table is used
     * @return array what was read
     */
    private static function scalingList(H264Bits $bits, int $size, bool
        &$use_default): array
    {
        $list = array_fill(0, $size, 8);
        $last_scale = 8;
        $next_scale = 8;
        $use_default = false;
        for ($j = 0; $j < $size; $j++) {
            if ($next_scale !== 0) {
                $delta = $bits->readSignedNumber();
                $next_scale = ($last_scale + $delta + 256) % 256;
                if ($j === 0 && $next_scale === 0) {
                    $use_default = true;
                }
            }
            $list[$j] = ($next_scale === 0) ? $last_scale : $next_scale;
            $last_scale = $list[$j];
        }
        return $list;
    }
    /**
     * scalingMatrix reads every table of scaling weights a stream
     * carries, and fills in the fall-
     * back rules of 7.4.2.1.1. (eight by eight) lists), null for SPS
     *
     * @param array $written filled with lists 0..5 (four by four) and 6..11
     * @param array $fallback_b PPS fall-back source (SPS
     * @param H264Bits $bits the reader the stream's bits are taken from
     * @param int $count how many
     */
    private static function scalingMatrix(H264Bits $bits, int $count, array
        &$written,
            ?array $fallback_b): void
    {
        for ($i = 0; $i < $count; $i++) {
            $is_eight_by_eight = $i >= 6;
            $size = $is_eight_by_eight ? 64 : 16;
            $starting_weight_sent = $bits->readBit() === 1;
            if ($starting_weight_sent) {
                $use_default = false;
                $list = self::scalingList($bits, $size, $use_default);
                if ($use_default) {
                    $list = self::defaultList($i);
                }
                $written[$i] = $list;
                continue;
            }
            /* not present: fall-back rule A (SPS) or B (PPS) */
            if ($fallback_b !== null) {
                if ($i === 0 || $i === 3 || $i === 6 || $i === 7) {
                    $written[$i] = $fallback_b[$i] ?? self::defaultList($i);
                } else {
                    $written[$i] = $written[$i - 1];
                }
            } else {
                if ($i === 0 || $i === 3 || $i === 6 || $i === 7) {
                    $written[$i] = self::defaultList($i);
                } else {
                    $written[$i] = $written[$i - 1];
                }
            }
        }
    }
    /**
     * defaultList the scaling values used when the stream supplies none.
     *
     * @param int $i which one
     * @return array what was read
     */
    private static function defaultList(int $i): array
    {
        if ($i < 3) {
            return H264Scan::DEFAULT_4X4_INTRA;
        }
        if ($i < 6) {
            return H264Scan::DEFAULT_4X4_INTER;
        }
        return ($i % 2 === 0) ? H264Scan::DEFAULT_8X8_INTRA
            : H264Scan::DEFAULT_8X8_INTER;
    }
    /**
     * ppsExtensionAllowed works out whether the picture settings
     * carry their trailing fields, which say whether eight by eight
     * transforms are allowed and may hold tables of scaling weights.
     * Asking only whether bits are left over is not enough, since the
     * padding at the end of a unit reads as a plausible field. The
     * profile the stream claims settles it: profiles that cannot use
     * eight by eight transforms never carry those fields.
     *
     * @param H264Sps $sequence_settings the sequence settings the stream
     * @return bool what was read
     */
    private static function ppsExtensionAllowed(
        H264Sps $sequence_settings): bool
    {
        $position = $sequence_settings->profile_setting;
        if (($position === 66 || $position === 77 || $position === 88)
            && ($sequence_settings->constraint_flags & 0xE0) !== 0) {
            return false;
        }
        return true;
    }
    /**
     * readSequenceSettings reads a sequence parameter set: the picture size,
     * the sample depth, the cropping and the scaling values. taken out
     *
     * @param string $unpacked the unit's bytes with the packing markers
     * @return H264Sps what was read
     */
    public static function readSequenceSettings(string $unpacked): H264Sps
    {
        $bits = new H264Bits($unpacked);
        $source = new H264Sps();
        $source->profile_setting = $bits->readBits(8);
        $source->constraint_flags = $bits->readBits(8);
        $source->level_setting = $bits->readBits(8);
        $source->id = $bits->readWholeNumber();
        $high_profiles
            = [100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135];
        if (in_array($source->profile_setting, $high_profiles, true)) {
            $source->chroma_format_setting = $bits->readWholeNumber();
            if ($source->chroma_format_setting === 3) {
                $source->separate_color_plane = $bits->readBit() === 1;
            }
            $source->bit_depth_luma = $bits->readWholeNumber() + 8;
            $source->bit_depth_chroma = $bits->readWholeNumber() + 8;
            $source->lossless_blocks_allowed = $bits->readBit() === 1;
            $source->scaling_matrix_present = $bits->readBit() === 1;
            if ($source->scaling_matrix_present) {
                $count = ($source->chroma_format_setting !== 3) ? 8 : 12;
                self::scalingMatrix($bits, $count,
                    $source->scaling_values, null);
            }
        }
        /* lists not signaled at all default to flat 16 */
        for ($i = 0; $i < 12; $i++) {
            if (!isset($source->scaling_values[$i])) {
                $source->scaling_values[$i] = array_fill(0, $i >= 6 ? 64 : 16,
                    16);
            }
        }
        $source->frame_number_bits = $bits->readWholeNumber() + 4;
        $source->picture_order_count_type = $bits->readWholeNumber();
        if ($source->picture_order_count_type === 0) {
            $source->picture_order_low_bit_count = $bits
                ->readWholeNumber() + 4;
        } elseif ($source->picture_order_count_type === 1) {
            $source->delta_picture_order_always_zero = $bits->readBit() === 1;
            /* offset_for_non_ref_pic */
            $bits->readSignedNumber();
            /* offset_for_top_to_bottom_field */
            $bits->readSignedNumber();
            $source->count_reference_frames_in_picture_order_cycle = $bits
                ->readWholeNumber();
            for ($i = 0; $i < $source
                ->count_reference_frames_in_picture_order_cycle; $i++) {
                $bits->readSignedNumber();
            }
        }
        $source->max_count_reference_frames = $bits->readWholeNumber();
        /* gaps_in_frame_num_value_allowed_flag */
        $bits->readBit();
        $source->picture_width_in_macroblocks = $bits->readWholeNumber() + 1;
        $source->picture_height_in_map_units = $bits->readWholeNumber() + 1;
        /* sanity bound so a corrupt sequence header cannot ask for a picture */
        /* larger than any defined level (MaxFS for level 6.2 is 139264 MBs) */
        if ($source->picture_width_in_macroblocks > 1055 ||
            $source->picture_height_in_map_units > 1055
            || $source->picture_width_in_macroblocks *
                $source->picture_height_in_map_units > 139264) {
            throw new H264Exception('picture size out of range: '
                . $source->picture_width_in_macroblocks . 'x' .
                    $source->picture_height_in_map_units
                    . ' macroblocks');
        }
        $source->frame_macroblocks_only = $bits->readBit() === 1;
        if (!$source->frame_macroblocks_only) {
            $source->macroblock_adaptive_frame_field = $bits->readBit() === 1;
        }
        /* direct_8x8_inference_flag */
        $bits->readBit();
        $source->frame_cropping = $bits->readBit() === 1;
        if ($source->frame_cropping) {
            $source->crop_left = $bits->readWholeNumber();
            $source->crop_right = $bits->readWholeNumber();
            $source->crop_top = $bits->readWholeNumber();
            $source->crop_bottom = $bits->readWholeNumber();
        }
        /* VUI is not needed for pixel reconstruction */
        return $source;
    }
    /**
     * readPictureSettings reads a picture parameter set: the quantizer offsets,
     * which entropy coding is used, and any scaling values of its own. taken
     * out carries
     *
     * @param string $unpacked the unit's bytes with the packing markers
     * @param H264Sps $sequence_settings the sequence settings the stream
     * @return H264Pps what was read
     */
    public static function readPictureSettings(string $unpacked,
        H264Sps $sequence_settings): H264Pps
    {
        $bits = new H264Bits($unpacked);
        $position = new H264Pps();
        $position->id = $bits->readWholeNumber();
        $position->sequence_settings_id = $bits->readWholeNumber();
        $position->entropy_coding_mode = $bits->readBit() === 1;
        $position->bottom_field_picture_order_present = $bits->readBit() === 1;
        $position->count_slice_groups = $bits->readWholeNumber() + 1;
        if ($position->count_slice_groups > 1) {
            throw new H264Exception(
                'slices written out of order or in groups are not '
                . 'read');
        }
        /* num_ref_idx_l0_default_active_minus1 */
        $bits->readWholeNumber();
        /* num_ref_idx_l1_default_active_minus1 */
        $bits->readWholeNumber();
        $position->weighted_guess = $bits->readBit() === 1;
        $position->two_picture_weighting_setting = $bits->readBits(2);
        $position->picture_starting_quantizer = $bits->readSignedNumber() + 26;
        /* pic_init_qs_minus26 */
        $bits->readSignedNumber();
        $position->chroma_quantizer_index_offset = $bits->readSignedNumber();
        $position->second_chroma_quantizer_index_offset
            = $position->chroma_quantizer_index_offset;
        $position->deblocking_filter_control_present = $bits->readBit() === 1;
        $position->constrained_self_guessed_guess = $bits->readBit() === 1;
        $position->redundant_picture_count_present = $bits->readBit() === 1;
        $position->scaling_values = $sequence_settings->scaling_values;
        if ($bits->hasMoreToRead() &&
            self::ppsExtensionAllowed($sequence_settings)) {
            $position->larger_transform_allowed = $bits->readBit() === 1;
            $position->scaling_matrix_present = $bits->readBit() === 1;
            if ($position->scaling_matrix_present) {
                $count = 6
                    + (($sequence_settings->chroma_format_setting !== 3) ? 2
                        : 6) * ($position->larger_transform_allowed ? 1 : 0);
                $lists = [];
                /* fall-back rule set B falls back to the SPS lists only when
                  the */
                /* SPS actually carried a scaling matrix; otherwise to the
                  defaults */
                self::scalingMatrix(
                    $bits, $count, $lists,
                    $sequence_settings->scaling_matrix_present
                        ? $sequence_settings->scaling_values : null);
                foreach ($lists as $i => $level) {
                    $position->scaling_values[$i] = $level;
                }
            }
            $position->second_chroma_quantizer_index_offset = $bits
                ->readSignedNumber();
        }
        return $position;
    }
}

/**
 * H264SliceHeader slice header fields the reconstruction path needs.
 */
final class H264SliceHeader
{
    /**
     * $first_macroblock_in_slice stores which macroblock this slice
     * starts at, counting across the picture from its top left.
     * @var int
     */
    public int $first_macroblock_in_slice = 0;
    /**
     * $slice_type stores what kind of slice this is. Two and seven
     * both mean a slice that stands on its own, which is the only
     * kind this decoder reads.
     * @var int
     */
    public int $slice_type = 0;
    /**
     * $picture_settings_id stores which picture settings this slice uses.
     * @var int
     */
    public int $picture_settings_id = 0;
    /**
     * $frame_count stores which frame of the sequence this is, as the stream
     * counts them.
     * @var int
     */
    public int $frame_count = 0;
    /**
     * $field_picture stores whether this slice codes half a picture. A stream
     * that
     * does is refused.
     * @var bool
     */
    public bool $field_picture = false;
    /**
     * $slice_quantizer stores the quantizer this slice starts at.
     * @var int
     */
    public int $slice_quantizer = 26;
    /**
     * $disable_deblocking_setting stores whether the edges of this slice's
     * blocks
     * are smoothed, and whether smoothing crosses into the next slice.
     * @var int
     */
    public int $disable_deblocking_setting = 0;
    /**
     * $slice_edge_threshold_step stores how far this slice moves the
     * threshold an edge must be sharper than before it is smoothed.
     * The stream writes half the step, so the value read is doubled.
     * @var int
     */
    public int $slice_edge_threshold_step = 0;
    /**
     * $slice_neighbor_threshold_step stores how far this slice moves
     * the threshold a neighboring sample must be within before it is
     * moved. The stream writes half the step, so it is doubled.
     * @var int
     */
    public int $slice_neighbor_threshold_step = 0;
    /**
     * $arithmetic_starting_setting stores which set of starting probabilities
     * the
     * arithmetic
     * coding begins from.
     * @var int
     */
    public int $arithmetic_starting_setting = 0;
    /**
     * readSettings reads a slice header, which says which picture parameters
     * the slice uses and where in the picture it starts. carries
     *
     * @param H264Bits $bits the reader the stream's bits are taken from
     * @param int $stream_unit_type which kind of unit it is
     * @param int $stream_unit_reference_setting how much later frames lean on
     *     this one
     * @param H264Sps $sequence_settings the sequence settings the stream
     * @param H264Pps $picture_settings the picture settings the stream carries
     * @return self what was read
     */
    public static function readSettings(H264Bits $bits, int $stream_unit_type,
        int $stream_unit_reference_setting,
        H264Sps $sequence_settings, H264Pps $picture_settings): self
    {
        $header = new self();
        $header->first_macroblock_in_slice = $bits->readWholeNumber();
        $header->slice_type = $bits->readWholeNumber();
        $header->picture_settings_id = $bits->readWholeNumber();
        $st = $header->slice_type % 5;
        if ($st !== 2 && $st !== 4) {
            $kind = $header->slice_type;
            throw new H264Exception(
                "only I and SI slices are supported (slice_type=$kind)");
        }
        if ($sequence_settings->separate_color_plane) {
            $bits->readBits(2);
        }
        $header->frame_count =
            $bits->readBits($sequence_settings->frame_number_bits);
        if (!$sequence_settings->frame_macroblocks_only) {
            $header->field_picture = $bits->readBit() === 1;
            if ($header->field_picture) {
                throw new H264Exception(
                    'field/interlaced coding is not supported');
            }
        }
        $is_self_contained = ($stream_unit_type === 5);
        if ($is_self_contained) {
            /* idr_pic_id */
            $bits->readWholeNumber();
        }
        if ($sequence_settings->picture_order_count_type === 0) {
            $bits->readBits($sequence_settings
                ->picture_order_low_bit_count);
            if ($picture_settings->bottom_field_picture_order_present &&
                !$header->field_picture) {
                $bits->readSignedNumber();
            }
        } elseif ($sequence_settings->picture_order_count_type === 1
            && !$sequence_settings->delta_picture_order_always_zero) {
            $bits->readSignedNumber();
            if ($picture_settings->bottom_field_picture_order_present &&
                !$header->field_picture) {
                $bits->readSignedNumber();
            }
        }
        if ($picture_settings->redundant_picture_count_present) {
            $bits->readWholeNumber();
        }
        if ($stream_unit_reference_setting !== 0) {
            if ($is_self_contained) {
                /* no_output_of_prior_pics_flag */
                $bits->readBit();
                /* long_term_reference_flag */
                $bits->readBit();
            } else {
                /* adaptive_ref_pic_marking_mode_flag */
                if ($bits->readBit() === 1) {
                    while (true) {
                        $value = $bits->readWholeNumber();
                        if ($value === 0) {
                            break;
                        }
                        if ($value === 1 || $value === 3) {
                            $bits->readWholeNumber();
                        }
                        if ($value === 2) {
                            $bits->readWholeNumber();
                        }
                        if ($value === 3 || $value === 6) {
                            $bits->readWholeNumber();
                        }
                        if ($value === 4) {
                            $bits->readWholeNumber();
                        }
                        if ($value === 5) {
                            break;
                        }
                    }
                }
            }
        }
        /* cabac_init_idc is absent for I slices */
        $header->slice_quantizer = $picture_settings
            ->picture_starting_quantizer + $bits
            ->readSignedNumber();
        if ($picture_settings->deblocking_filter_control_present) {
            $header->disable_deblocking_setting = $bits->readWholeNumber();
            if ($header->disable_deblocking_setting !== 1) {
                $header->slice_edge_threshold_step = $bits->readSignedNumber();
                $header->slice_neighbor_threshold_step = $bits
                    ->readSignedNumber();
            }
        }
        return $header;
    }
}

/**
 * H264Transform inverse scaling and transforms, clause 8.5 of ITU-T H.264. All
 * arrays are flat, row-major.
 */
final class H264Transform
{
    /**
     * levelScale4x4 the table four by four values are scaled by, worked out
     * once for each of the six quantizer remainders from the stream's own
     * weights and the fixed adjustments
     *
     * @param array $weight_zigzag the order the values are written in
     * @return array what was read
     */
    public static function levelScale4x4(array $weight_zigzag): array
    {
        /* weight list arrives in zig-zag order; convert to raster */
        $wide = array_fill(0, 16, 16);
        foreach (H264Scan::ZZ4 as $block_order => $raster) {
            $wide[$raster] = $weight_zigzag[$block_order];
        }
        $level_scale = [];
        for ($matches = 0; $matches < 6; $matches++) {
            $row = [];
            for ($at = 0; $at < 16; $at++) {
                $across = $at & 3;
                $down = $at >> 2;
                $column = (($across & 1) === ($down & 1)) ?
                    ($across & 1) : 2;
                $row[$at]
                    = $wide[$at] * H264Scan::V4[$matches][$column];
            }
            $level_scale[$matches] = $row;
        }
        return $level_scale;
    }
    /**
     * levelScale8x8 levelScale8x8[m][64]
     *
     * @param array $weight_zigzag the order the values are written in
     * @return array what was read
     */
    public static function levelScale8x8(array $weight_zigzag): array
    {
        $wide = array_fill(0, 64, 16);
        foreach (H264Scan::ZZ8 as $block_order => $raster) {
            $wide[$raster] = $weight_zigzag[$block_order];
        }
        $level_scale = [];
        for ($matches = 0; $matches < 6; $matches++) {
            $row = [];
            for ($at = 0; $at < 64; $at++) {
                $across = $at & 7;
                $down = $at >> 3;
                $column = H264Scan::eightColumnTransform($across, $down);
                $row[$at]
                    = $wide[$at] * H264Scan::V8[$matches][$column];
            }
            $level_scale[$matches] = $row;
        }
        return $level_scale;
    }
    /**
     * dequant4x4 scales the coded values of a four by four block back to
     * their own size, using the quantizer in force and the weights
     * the stream carries. The standard calls
     * this clause 8.5.12.1. $skip_first_value leaves index 0 untouched
     * (I_16x16 luma and
     * chroma, whose DC arrives already scaled from the DC transform).
     *
     * @param array $chunk the reader this part of the frame is taken from
     * @param array $level_scale the table the values are scaled by
     * @param int $q_p the quantizer the block was coded at
     * @param bool $skip_first_value whether the first value is left out
     * @return array what was read
     */
    public static function dequant4x4(array $chunk, array $level_scale,
        int $q_p,
        bool $skip_first_value): array
    {
        $matches = $q_p % 6;
        $source = intdiv($q_p, 6);
        $level_scale = $level_scale[$matches];
        $payload = array_fill(0, 16, 0);
        $start = $skip_first_value ? 1 : 0;
        if ($skip_first_value) {
            $payload[0] = $chunk[0];
        }
        if ($q_p >= 24) {
            $sh = $source - 4;
            for ($i = $start; $i < 16; $i++) {
                if ($chunk[$i] !== 0) {
                    $payload[$i] = ($chunk[$i] * $level_scale[$i]) << $sh;
                }
            }
        } else {
            $sh = 4 - $source;
            $rounding = 1 << (3 - $source);
            for ($i = $start; $i < 16; $i++) {
                if ($chunk[$i] !== 0) {
                    $payload[$i] = ($chunk[$i] * $level_scale[$i] +
                        $rounding) >> $sh;
                }
            }
        }
        return $payload;
    }
    /**
     * dequant8x8 scales the coded values of an eight by eight block back
     * to their own size the same arithmetic uses for the smaller
     * blocks. The standard calls
     * this clause 8.5.13.1.
     *
     * @param array $chunk the reader this part of the frame is taken from
     * @param array $level_scale the table the values are scaled by
     * @param int $q_p the quantizer the block was coded at
     * @return array what was read
     */
    public static function dequant8x8(
        array $chunk, array $level_scale, int $q_p): array
    {
        $matches = $q_p % 6;
        $source = intdiv($q_p, 6);
        $level_scale = $level_scale[$matches];
        $payload = array_fill(0, 64, 0);
        if ($q_p >= 36) {
            $sh = $source - 6;
            for ($i = 0; $i < 64; $i++) {
                if ($chunk[$i] !== 0) {
                    $payload[$i] = ($chunk[$i] * $level_scale[$i]) << $sh;
                }
            }
        } else {
            $sh = 6 - $source;
            $rounding = 1 << (5 - $source);
            for ($i = 0; $i < 64; $i++) {
                if ($chunk[$i] !== 0) {
                    $payload[$i] = ($chunk[$i] * $level_scale[$i] +
                        $rounding) >> $sh;
                }
            }
        }
        return $payload;
    }
    /**
     * inverse4x4 turns the sixteen values of a four by four block back into
     * differences from what was guessed, rounded as the standard asks. It calls
     * this clause 8.5.12.2.
     *
     * @param array $payload the bytes the element carries
     * @return array what was read
     */
    public static function inverse4x4(array $payload): array
    {
        $entry = [];
        for ($i = 0; $i < 4; $i++) {
            $offset = $i * 4;
            $difference_zero = $payload[$offset];
            $difference_one = $payload[$offset + 1];
            $difference_two = $payload[$offset + 2];
            $difference_three = $payload[$offset + 3];
            $edge_zero = $difference_zero + $difference_two;
            $edge_one = $difference_zero - $difference_two;
            $edge_two = ($difference_one >> 1) - $difference_three;
            $edge_three = $difference_one + ($difference_three >> 1);
            $entry[$offset]     = $edge_zero + $edge_three;
            $entry[$offset + 1] = $edge_one + $edge_two;
            $entry[$offset + 2] = $edge_one - $edge_two;
            $entry[$offset + 3] = $edge_zero - $edge_three;
        }
        $run = [];
        for ($j = 0; $j < 4; $j++) {
            $group_zero = $entry[$j];
            $group_one = $entry[4 + $j];
            $group_two = $entry[8 + $j];
            $group_three = $entry[12 + $j];
            $half_zero = $group_zero + $group_two;
            $half_one = $group_zero - $group_two;
            $half_two = ($group_one >> 1) - $group_three;
            $half_three = $group_one + ($group_three >> 1);
            $run[$j]      = ($half_zero + $half_three + 32) >> 6;
            $run[4 + $j]  = ($half_one + $half_two + 32) >> 6;
            $run[8 + $j]  = ($half_one - $half_two + 32) >> 6;
            $run[12 + $j] = ($half_zero - $half_three + 32) >> 6;
        }
        ksort($run);
        return $run;
    }
    /**
     * inverse8x8 turns the sixty-four values of an eight by eight block
     * back into differences from what was guessed. The standard calls
     * this clause 8.5.13.2.
     *
     * @param array $payload the bytes the element carries
     * @return array what was read
     */
    public static function inverse8x8(array $payload): array
    {
        $group = [];
        for ($i = 0; $i < 8; $i++) {
            $offset = $i * 8;
            $difference_zero = $payload[$offset];     $difference_one =
                $payload[$offset + 1]; $difference_two
                = $payload[$offset + 2]; $difference_three
                = $payload[$offset + 3];
            $difference_four = $payload[$offset + 4]; $difference_five =
                $payload[$offset + 5]; $difference_six
                = $payload[$offset + 6]; $difference_seven
                = $payload[$offset + 7];
            $edge_zero = $difference_zero + $difference_four;
            $edge_one = -$difference_three + $difference_five -
                $difference_seven - ($difference_seven >> 1);
            $edge_two = $difference_zero - $difference_four;
            $edge_three = $difference_one + $difference_seven -
                $difference_three - ($difference_three >> 1);
            $edge_four = ($difference_two >> 1) - $difference_six;
            $edge_five = -$difference_one + $difference_seven +
                $difference_five + ($difference_five >> 1);
            $edge_six = $difference_two + ($difference_six >> 1);
            $edge_seven = $difference_three + $difference_five +
                $difference_one + ($difference_one >> 1);
            $filtered_zero = $edge_zero + $edge_six;
            $filtered_one = $edge_one + ($edge_seven >> 2);
            $filtered_two = $edge_two + $edge_four;
            $filtered_three = $edge_three + ($edge_five >> 2);
            $filtered_four = $edge_two - $edge_four;
            $filtered_five = ($edge_three >> 2) - $edge_five;
            $filtered_six = $edge_zero - $edge_six;
            $filtered_seven = $edge_seven - ($edge_one >> 2);
            $group[$offset]     = $filtered_zero + $filtered_seven;
            $group[$offset + 1] = $filtered_two + $filtered_five;
            $group[$offset + 2] = $filtered_four + $filtered_three;
            $group[$offset + 3] = $filtered_six + $filtered_one;
            $group[$offset + 4] = $filtered_six - $filtered_one;
            $group[$offset + 5] = $filtered_four - $filtered_three;
            $group[$offset + 6] = $filtered_two - $filtered_five;
            $group[$offset + 7] = $filtered_zero - $filtered_seven;
        }
        $run = array_fill(0, 64, 0);
        for ($j = 0; $j < 8; $j++) {
            $difference_zero = $group[$j];      $difference_one = $group[8 +
                $j];  $difference_two
                = $group[16 + $j]; $difference_three
                = $group[24 + $j];
            $difference_four = $group[32 + $j]; $difference_five =
                $group[40 + $j]; $difference_six
                = $group[48 + $j]; $difference_seven
                = $group[56 + $j];
            $edge_zero = $difference_zero + $difference_four;
            $edge_one = -$difference_three + $difference_five -
                $difference_seven - ($difference_seven >> 1);
            $edge_two = $difference_zero - $difference_four;
            $edge_three = $difference_one + $difference_seven -
                $difference_three - ($difference_three >> 1);
            $edge_four = ($difference_two >> 1) - $difference_six;
            $edge_five = -$difference_one + $difference_seven +
                $difference_five + ($difference_five >> 1);
            $edge_six = $difference_two + ($difference_six >> 1);
            $edge_seven = $difference_three + $difference_five +
                $difference_one + ($difference_one >> 1);
            $filtered_zero = $edge_zero + $edge_six;
            $filtered_one = $edge_one + ($edge_seven >> 2);
            $filtered_two = $edge_two + $edge_four;
            $filtered_three = $edge_three + ($edge_five >> 2);
            $filtered_four = $edge_two - $edge_four;
            $filtered_five = ($edge_three >> 2) - $edge_five;
            $filtered_six = $edge_zero - $edge_six;
            $filtered_seven = $edge_seven - ($edge_one >> 2);
            $run[$j]      = ($filtered_zero + $filtered_seven + 32) >> 6;
            $run[8 + $j]  = ($filtered_two + $filtered_five + 32) >> 6;
            $run[16 + $j] = ($filtered_four + $filtered_three + 32) >> 6;
            $run[24 + $j] = ($filtered_six + $filtered_one + 32) >> 6;
            $run[32 + $j] = ($filtered_six - $filtered_one + 32) >> 6;
            $run[40 + $j] = ($filtered_four - $filtered_three + 32) >> 6;
            $run[48 + $j] = ($filtered_two - $filtered_five + 32) >> 6;
            $run[56 + $j] = ($filtered_zero - $filtered_seven + 32) >> 6;
        }
        return $run;
    }
    /**
     * brightnessFirstValues turns the first value of each of a
     * macroblock's sixteen brightness blocks back into a difference
     * from what was guessed. A macroblock guessed as one whole square
     * codes those first values together, so they are read and scaled
     * together. The standard calls this clause 8.5.10.
     *
     * @param array $chunk the reader this part of the frame is taken from
     * @param array $level_scale_four_by_four the table four by four values are
     *     scaled by
     * @param int $q_p the quantizer the block was coded at
     * @return array what was read
     */
    public static function brightnessFirstValues(
        array $chunk, array $level_scale_four_by_four, int $q_p): array
    {
        $frame = [];
        for ($i = 0; $i < 4; $i++) {
            $offset = $i * 4;
            $left_zero = $chunk[$offset] + $chunk[$offset + 1] +
                $chunk[$offset + 2]
                + $chunk[$offset + 3];
            $left_one = $chunk[$offset] + $chunk[$offset + 1] -
                $chunk[$offset + 2]
                - $chunk[$offset + 3];
            $left_two = $chunk[$offset] - $chunk[$offset + 1] -
                $chunk[$offset + 2]
                + $chunk[$offset + 3];
            $left_three = $chunk[$offset] - $chunk[$offset + 1] +
                $chunk[$offset + 2]
                - $chunk[$offset + 3];
            $frame[$offset] = $left_zero; $frame[$offset + 1]
                = $left_one; $frame[$offset + 2] =
                    $left_two; $frame[$offset + 3] = $left_three;
        }
        $group = array_fill(0, 16, 0);
        for ($j = 0; $j < 4; $j++) {
            $right_zero = $frame[$j] + $frame[4 + $j] + $frame[8 + $j]
                + $frame[12 + $j];
            $bit_one = $frame[$j] + $frame[4 + $j] - $frame[8 + $j]
                - $frame[12 + $j];
            $right_two = $frame[$j] - $frame[4 + $j] - $frame[8 + $j]
                + $frame[12 + $j];
            $right_three = $frame[$j] - $frame[4 + $j] + $frame[8 + $j]
                - $frame[12 + $j];
            $group[$j] = $right_zero; $group[4 + $j] = $bit_one; $group[8 + $j]
                = $right_two; $group[12 + $j] = $right_three;
        }
        $scale = $level_scale_four_by_four[$q_p % 6][0];
        $source = intdiv($q_p, 6);
        $written = [];
        if ($q_p >= 36) {
            $sh = $source - 6;
            for ($i = 0; $i < 16; $i++) {
                $written[$i] = ($group[$i] * $scale) << $sh;
            }
        } else {
            $sh = 6 - $source;
            $rounding = 1 << (5 - $source);
            for ($i = 0; $i < 16; $i++) {
                $written[$i] = ($group[$i] * $scale + $rounding) >> $sh;
            }
        }
        return $written;
    }
    /**
     * chromaDc turns the four first values of a macroblock's color
     * blocks back into differences from what was guessed, using a
     * transform of sums and differences alone, and scales them by the
     * quantizer in force. The standard calls this clause 8.5.11.
     *
     * @param array $chunk the reader this part of the frame is taken from
     * @param array $level_scale_four_by_four the table four by four values are
     *     scaled by
     * @param int $q_p the quantizer the block was coded at
     * @return array what was read
     */
    public static function chromaDc(
        array $chunk, array $level_scale_four_by_four, int $q_p): array
    {
        $filtered_zero = array_sum(array_slice($chunk, 0, 4));
        $filtered_one = $chunk[0] - $chunk[1] + $chunk[2] - $chunk[3];
        $filtered_two = $chunk[0] + $chunk[1] - $chunk[2] - $chunk[3];
        $filtered_three = $chunk[0] - $chunk[1] - $chunk[2] + $chunk[3];
        $scale = $level_scale_four_by_four[$q_p % 6][0];
        $source = intdiv($q_p, 6);
        $written = [];
        foreach ([$filtered_zero, $filtered_one, $filtered_two,
            $filtered_three] as $i => $value) {
            $written[$i] = (($value * $scale) << $source) >> 5;
        }
        return $written;
    }
}

/**
 * H264Intra intra sample prediction, clause 8.3 of ITU-T H.264. Every method
 * reads its neighboring samples straight out of the plane being reconstructed
 * and returns the predicted block as a flat row-major array. Unavailable
 * neighbors are filled with 1 << (how many bits a sample takes - 1) so that a
 * corrupt stream degrades instead of crashing; conforming streams never select
 * a mode whose neighbors are missing.
 */
final class H264Intra
{
    /**
     * smooth3 average of three neighboring samples, weighted toward the middle
     * one. This is how the format smooths an edge it predicts from.
     *
     * @param int $first sample before the middle one
     * @param int $middle sample the result sits on
     * @param int $last sample after the middle one
     * @return int the smoothed value
     */
    private static function smooth3($first, $middle, $last)
    {
        return ($first + 2 * $middle + $last + 2) >> 2;
    }
    /**
     * smooth2 average of two neighboring samples, rounded upward.
     *
     * @param int $first one sample
     * @param int $second the sample beside it
     * @return int the average
     */
    private static function smooth2($first, $second)
    {
        return ($first + $second + 1) >> 1;
    }
    /**
     * MID is the middle value a sample can take, which is what a block is
     * filled with where it has no neighbors to be guessed from.
     * @var mixed
     */
    private const MID = 128;
    /**
     * gatherReferences collects the samples above and to the left of a block,
     * marking which of them exist. to read to read
     *
     * @param array $plane zero for luma, one and two for the chroma planes
     * @param int $stride how many values one row of the picture takes
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param int $top_count how many values the blocks above carried
     * @param int $left_count how many values the blocks to the left carried
     * @param bool $left_there whether the block to the left is there to read
     * @param bool $above_there whether the block above is there to read
     * @param bool $above_left_there whether the block above and left is there
     * @param bool $above_right_there whether the block above and right is there
     * @param int $top_right_start where the transform starts
     * @return array what was read
     */
    private static function gatherReferences(
        array $plane, int $stride, int $block_x, int $block_y,
        int $top_count, int $left_count,
        bool $left_there, bool $above_there, bool $above_left_there,
            bool $above_right_there, int $top_right_start
    ): array {
        $above = [];
        $left = [];
        if ($above_there) {
            $base = ($block_y - 1) * $stride + $block_x;
            for ($across = 0; $across < $top_right_start; $across++) {
                $above[$across] = $plane[$base + $across];
            }
            if ($above_right_there) {
                for ($across = $top_right_start; $across <
                    $top_count; $across++) {
                    $above[$across] = $plane[$base + $across];
                }
            } else {
                /* 8.3.1.2: substitute with the last available top sample */
                $rep = $above[$top_right_start - 1];
                for ($across = $top_right_start; $across <
                    $top_count; $across++) {
                    $above[$across] = $rep;
                }
            }
        } else {
            for ($across = 0; $across < $top_count; $across++) {
                $above[$across] = self::MID;
            }
        }
        if ($left_there) {
            for ($down = 0; $down < $left_count; $down++) {
                $left[$down] = $plane[($block_y + $down) * $stride +
                    $block_x - 1];
            }
        } else {
            for ($down = 0; $down < $left_count; $down++) {
                $left[$down] = self::MID;
            }
        }
        $top_left = $above_left_there ? $plane[($block_y - 1) * $stride +
            $block_x - 1] : self::MID;
        $above[-1] = $top_left;
        $left[-1] = $top_left;
        return [$above, $left, $top_left];
    }
    /**
     * holdInsideByte holds a sample inside the range a byte can carry.
     *
     * @param int $value the value read
     * @return int what was read
     */
    private static function holdInsideByte(int $value): int
    {
        return $value < 0 ? 0 : ($value > 255 ? 255 : $value);
    }
    /**
     * pred4x4 guesses one four by four block of brightness from the
     * samples above and to the left of it, and hands back the sixteen
     * samples it guessed, row by row. The standard calls this clause
     * 8.3.1.2.
     *
     * @param int $mode which way the block is guessed from its neighbors
     * @param array $plane which of the picture's planes, brightness or color
     * @param int $stride how many values one row of the picture takes
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param bool $left_there whether the block to the left is there to read
     * @param bool $above_there whether the block above is there to read
     * @param bool $above_left_there whether the block above and left is there
     * @param bool $above_right_there whether the block above and right is there
     * @return array what was read
     */
    public static function pred4x4(
        int $mode, array $plane, int $stride, int $block_x, int $block_y,
        bool $left_there, bool $above_there, bool $above_left_there,
            bool $above_right_there
    ): array {
        [$above, $left, $top_left] = self::gatherReferences(
            $plane, $stride, $block_x, $block_y, 8, 4, $left_there,
                $above_there, $above_left_there, $above_right_there, 4);
        $position = array_fill(0, 16, 0);
        switch ($mode) {
            /* vertical */
            case 0:
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        $position[$down * 4 + $across] = $above[$across];
                    }
                }
                break;
            /* horizontal */
            case 1:
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        $position[$down * 4 + $across] = $left[$down];
                    }
                }
                break;
            /* DC */
            case 2:
                if ($above_there && $left_there) {
                    $first_value = (array_sum(array_slice($above, 0, 4))
                        + array_sum(array_slice($left, 0, 4)) + 4) >> 3;
                } elseif ($left_there) {
                    $first_value = (array_sum(array_slice($left, 0, 4)) +
                        2) >> 2;
                } elseif ($above_there) {
                    $first_value = (array_sum(array_slice($above, 0, 4)) +
                        2) >> 2;
                } else {
                    $first_value = self::MID;
                }
                $position = array_fill(0, 16, $first_value);
                break;
            /* diagonal down left */
            case 3:
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        $position[$down * 4 + $across]
                            = ($across === 3 && $down === 3)
                            ? ($above[6] + 3 * $above[7] + 2) >> 2
                            : self::smooth3(
                                $above[$across + $down],
                                $above[$across + $down + 1],
                                $above[$across + $down + 2]);
                    }
                }
                break;
            /* diagonal down right */
            case 4:
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        if ($across > $down) {
                            $value
                                = self::smooth3(
                                    $above[$across - $down - 2],
                                    $above[$across - $down - 1],
                                    $above[$across - $down]);
                        } elseif ($across < $down) {
                            $value
                                = self::smooth3(
                                    $left[$down - $across - 2],
                                    $left[$down - $across - 1],
                                    $left[$down - $across]);
                        } else {
                            $value
                                = ($above[0] + 2 * $top_left + $left[0] +
                                    2) >> 2;
                        }
                        $position[$down * 4 + $across] = $value;
                    }
                }
                break;
            /* vertical right */
            case 5:
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        $last = 2 * $across - $down;
                        $slant = $across - ($down >> 1);
                        if ($last >= 0 && ($last & 1) === 0) {
                            $value
                                = self::smooth2(
                                    $above[$slant - 1], $above[$slant]);
                        } elseif ($last > 0) {
                            $value
                                = self::smooth3(
                                    $above[$slant - 2], $above[$slant - 1],
                                    $above[$slant]);
                        } elseif ($last === -1) {
                            $value
                                = ($left[0] + 2 * $top_left + $above[0] +
                                    2) >> 2;
                        } else {
                            $value
                                = self::smooth3(
                                    $left[$down - 1], $left[$down - 2],
                                    $left[$down - 3]);
                        }
                        $position[$down * 4 + $across] = $value;
                    }
                }
                break;
            /* horizontal down */
            case 6:
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        $last = 2 * $down - $across;
                        $slant = $down - ($across >> 1);
                        if ($last >= 0 && ($last & 1) === 0) {
                            $value
                                = self::smooth2(
                                    $left[$slant - 1], $left[$slant]);
                        } elseif ($last > 0) {
                            $value
                                = self::smooth3(
                                    $left[$slant - 2], $left[$slant - 1],
                                    $left[$slant]);
                        } elseif ($last === -1) {
                            $value
                                = ($left[0] + 2 * $top_left + $above[0] +
                                    2) >> 2;
                        } else {
                            $value
                                = self::smooth3(
                                    $above[$across - 1], $above[$across - 2],
                                    $above[$across - 3]);
                        }
                        $position[$down * 4 + $across] = $value;
                    }
                }
                break;
            /* vertical left */
            case 7:
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        $header = $down >> 1;
                        $position[$down * 4 + $across] = (($down & 1) === 0)
                            ? self::smooth2(
                                $above[$across + $header],
                                $above[$across + $header + 1])
                            : self::smooth3(
                                $above[$across + $header],
                                $above[$across + $header + 1],
                                $above[$across + $header + 2]);
                    }
                }
                break;
            /* horizontal up */
            case 8:
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        $last = $across + 2 * $down;
                        $header = $across >> 1;
                        if ($last < 5 && ($last & 1) === 0) {
                            $value
                                = self::smooth2(
                                    $left[$down + $header],
                                    $left[$down + $header + 1]);
                        } elseif ($last < 5) {
                            $value
                                = self::smooth3(
                                    $left[$down + $header],
                                    $left[$down + $header + 1],
                                    $left[$down + $header + 2]);
                        } elseif ($last === 5) {
                            $value = ($left[2] + 3 * $left[3] + 2) >> 2;
                        } else {
                            $value = $left[3];
                        }
                        $position[$down * 4 + $across] = $value;
                    }
                }
                break;
            default:
                throw new H264Exception("bad Intra_4x4 mode $mode");
        }
        return $position;
    }
    /**
     * filter8x8 smooths the samples an eight by eight block is guessed from,
     * which the format asks for before the guess is made. The standard calls
     * this clause 8.3.2.2.1. to read
     *
     * @param array $above what the row above holds
     * @param array $left what the column to the left holds
     * @param int $top_left the tl
     * @param bool $left_there whether the block to the left is there to read
     * @param bool $above_there whether the block above is there to read
     * @param bool $above_left_there whether the block above and left is there
     * @return array what was read
     */
    private static function filter8x8(array $above, array $left, int $top_left,
        bool $left_there, bool $above_there, bool $above_left_there): array
    {
        $transform = [];
        $smoothing = [];
        if ($above_left_there) {
            if ($above_there && $left_there) {
                $top_left_filtered = ($left[0] + 2 * $top_left + $above[0] +
                    2) >> 2;
            } elseif ($above_there) {
                $top_left_filtered = (3 * $top_left + $above[0] + 2) >> 2;
            } elseif ($left_there) {
                $top_left_filtered = (3 * $top_left + $left[0] + 2) >> 2;
            } else {
                $top_left_filtered = $top_left;
            }
        } else {
            $top_left_filtered = $top_left;
        }
        if ($above_there) {
            $transform[0] = $above_left_there
                ? ($top_left + 2 * $above[0] + $above[1] + 2) >> 2
                : (3 * $above[0] + $above[1] + 2) >> 2;
            for ($across = 1; $across < 15; $across++) {
                $transform[$across]
                    = self::smooth3(
                        $above[$across - 1], $above[$across],
                        $above[$across + 1]);
            }
            $transform[15] = ($above[14] + 3 * $above[15] + 2) >> 2;
        } else {
            for ($across = 0; $across < 16; $across++) {
                $transform[$across] = $above[$across];
            }
        }
        if ($left_there) {
            $smoothing[0] = $above_left_there
                ? ($top_left + 2 * $left[0] + $left[1] + 2) >> 2
                : (3 * $left[0] + $left[1] + 2) >> 2;
            for ($down = 1; $down < 7; $down++) {
                $smoothing[$down]
                    = self::smooth3(
                        $left[$down - 1], $left[$down], $left[$down + 1]);
            }
            $smoothing[7] = ($left[6] + 3 * $left[7] + 2) >> 2;
        } else {
            for ($down = 0; $down < 8; $down++) {
                $smoothing[$down] = $left[$down];
            }
        }
        $transform[-1] = $top_left_filtered;
        $smoothing[-1] = $top_left_filtered;
        return [$transform, $smoothing, $top_left_filtered];
    }
    /**
     * pred8x8 guesses one eight by eight block of brightness from the
     * samples above and to the left of it, and hands back its
     * sixty-four samples row by row. The standard calls this clause
     * 8.3.2.2.
     *
     * @param int $mode which way the block is guessed from its neighbors
     * @param array $plane which of the picture's planes, brightness or color
     * @param int $stride how many values one row of the picture takes
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param bool $left_there whether the block to the left is there to read
     * @param bool $above_there whether the block above is there to read
     * @param bool $above_left_there whether the block above and left is there
     * @param bool $above_right_there whether the block above and right is there
     * @return array what was read
     */
    public static function pred8x8(
        int $mode, array $plane, int $stride, int $block_x, int $block_y,
        bool $left_there, bool $above_there, bool $above_left_there,
            bool $above_right_there
    ): array {
        [$held_zero, $first_pictures, $top_left_zero] = self::gatherReferences(
            $plane, $stride, $block_x, $block_y, 16, 8, $left_there,
                $above_there, $above_left_there, $above_right_there, 8);
        [$above, $left, $top_left]
            = self::filter8x8($held_zero, $first_pictures, $top_left_zero,
                $left_there, $above_there,
                $above_left_there);
        $position = array_fill(0, 64, 0);
        switch ($mode) {
            case 0:
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        $position[$down * 8 + $across] = $above[$across];
                    }
                }
                break;
            case 1:
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        $position[$down * 8 + $across] = $left[$down];
                    }
                }
                break;
            case 2:
                if ($above_there && $left_there) {
                    $source = 0;
                    for ($i = 0; $i < 8; $i++) {
                        $source += $above[$i] + $left[$i];
                    }
                    $first_value = ($source + 8) >> 4;
                } elseif ($above_there) {
                    $source = 0;
                    for ($i = 0; $i < 8; $i++) {
                        $source += $above[$i];
                    }
                    $first_value = ($source + 4) >> 3;
                } elseif ($left_there) {
                    $source = 0;
                    for ($i = 0; $i < 8; $i++) {
                        $source += $left[$i];
                    }
                    $first_value = ($source + 4) >> 3;
                } else {
                    $first_value = self::MID;
                }
                $position = array_fill(0, 64, $first_value);
                break;
            case 3:
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        $position[$down * 8 + $across]
                            = ($across === 7 && $down === 7)
                            ? ($above[14] + 3 * $above[15] + 2) >> 2
                            : self::smooth3(
                                $above[$across + $down],
                                $above[$across + $down + 1],
                                $above[$across + $down + 2]);
                    }
                }
                break;
            case 4:
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        if ($across > $down) {
                            $value
                                = self::smooth3(
                                    $above[$across - $down - 2],
                                    $above[$across - $down - 1],
                                    $above[$across - $down]);
                        } elseif ($across < $down) {
                            $value
                                = self::smooth3(
                                    $left[$down - $across - 2],
                                    $left[$down - $across - 1],
                                    $left[$down - $across]);
                        } else {
                            $value
                                = ($above[0] + 2 * $top_left + $left[0] +
                                    2) >> 2;
                        }
                        $position[$down * 8 + $across] = $value;
                    }
                }
                break;
            case 5:
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        $last = 2 * $across - $down;
                        $slant = $across - ($down >> 1);
                        if ($last >= 0 && ($last & 1) === 0) {
                            $value
                                = self::smooth2(
                                    $above[$slant - 1], $above[$slant]);
                        } elseif ($last > 0) {
                            $value
                                = self::smooth3(
                                    $above[$slant - 2], $above[$slant - 1],
                                    $above[$slant]);
                        } elseif ($last === -1) {
                            $value
                                = ($left[0] + 2 * $top_left + $above[0] +
                                    2) >> 2;
                        } else {
                            $k = $down - 2 * $across;
                            $value
                                = self::smooth3($left[$k - 1],  $left[$k - 2],
                                    $left[$k - 3]);
                        }
                        $position[$down * 8 + $across] = $value;
                    }
                }
                break;
            case 6:
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        $last = 2 * $down - $across;
                        $slant = $down - ($across >> 1);
                        if ($last >= 0 && ($last & 1) === 0) {
                            $value
                                = self::smooth2(
                                    $left[$slant - 1], $left[$slant]);
                        } elseif ($last > 0) {
                            $value
                                = self::smooth3(
                                    $left[$slant - 2], $left[$slant - 1],
                                    $left[$slant]);
                        } elseif ($last === -1) {
                            $value
                                = ($left[0] + 2 * $top_left + $above[0] +
                                    2) >> 2;
                        } else {
                            $k = $across - 2 * $down;
                            $value
                                = self::smooth3(
                                    $above[$k - 1], $above[$k - 2],
                                    $above[$k - 3]);
                        }
                        $position[$down * 8 + $across] = $value;
                    }
                }
                break;
            case 7:
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        $header = $down >> 1;
                        $position[$down * 8 + $across] = (($down & 1) === 0)
                            ? self::smooth2(
                                $above[$across + $header],
                                $above[$across + $header + 1])
                            : self::smooth3(
                                $above[$across + $header],
                                $above[$across + $header + 1],
                                $above[$across + $header + 2]);
                    }
                }
                break;
            case 8:
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        $last = $across + 2 * $down;
                        $header = $across >> 1;
                        if ($last < 13 && ($last & 1) === 0) {
                            $value
                                = self::smooth2(
                                    $left[$down + $header],
                                    $left[$down + $header + 1]);
                        } elseif ($last < 13) {
                            $value
                                = self::smooth3(
                                    $left[$down + $header],
                                    $left[$down + $header + 1],
                                    $left[$down + $header + 2]);
                        } elseif ($last === 13) {
                            $value = ($left[6] + 3 * $left[7] + 2) >> 2;
                        } else {
                            $value = $left[7];
                        }
                        $position[$down * 8 + $across] = $value;
                    }
                }
                break;
            default:
                throw new H264Exception("bad Intra_8x8 mode $mode");
        }
        return $position;
    }
    /**
     * pred16x16 guesses a whole sixteen by sixteen macroblock of brightness at
     * once, and hands back its samples row by row. The standard calls this
     * clause 8.3.3. to read
     *
     * @param int $mode which way the block is guessed from its neighbors
     * @param array $plane which of the picture's planes, brightness or color
     * @param int $stride how many values one row of the picture takes
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param bool $left_there whether the block to the left is there to read
     * @param bool $above_there whether the block above is there to read
     * @param bool $above_left_there whether the block above and left is there
     * @return array what was read
     */
    public static function pred16x16(
        int $mode, array $plane, int $stride, int $block_x, int $block_y,
        bool $left_there, bool $above_there, bool $above_left_there
    ): array {
        $above = array_fill(0, 16, self::MID);
        $left = array_fill(0, 16, self::MID);
        if ($above_there) {
            $base = ($block_y - 1) * $stride + $block_x;
            for ($across = 0; $across < 16; $across++) {
                $above[$across] = $plane[$base + $across];
            }
        }
        if ($left_there) {
            for ($down = 0; $down < 16; $down++) {
                $left[$down] = $plane[($block_y + $down) * $stride +
                    $block_x - 1];
            }
        }
        $position = array_fill(0, 256, 0);
        switch ($mode) {
            /* vertical */
            case 0:
                for ($down = 0; $down < 16; $down++) {
                    for ($across = 0; $across < 16; $across++) {
                        $position[$down * 16 + $across]
                            = $above[$across] ?? self::MID;
                    }
                }
                break;
            /* horizontal */
            case 1:
                for ($down = 0; $down < 16; $down++) {
                    $value = $left[$down] ?? self::MID;
                    for ($across = 0; $across < 16; $across++) {
                        $position[$down * 16 + $across] = $value;
                    }
                }
                break;
            /* DC */
            case 2:
                if ($above_there && $left_there) {
                    $first_value = (array_sum($above) + array_sum($left) +
                        16) >> 5;
                } elseif ($above_there) {
                    $first_value = (array_sum($above) + 8) >> 4;
                } elseif ($left_there) {
                    $first_value = (array_sum($left) + 8) >> 4;
                } else {
                    $first_value = self::MID;
                }
                $position = array_fill(0, 256, $first_value);
                break;
            /* plane */
            case 3:
                $top_left = $above_left_there ? $plane[($block_y - 1) *
                    $stride + $block_x - 1] : self::MID;
                $above[-1] = $top_left;
                $left[-1] = $top_left;
                $tall = 0;
                $red_part = 0;
                for ($i = 0; $i < 8; $i++) {
                    $tall += ($i + 1) * ($above[8 + $i] - $above[6 - $i]);
                    $red_part += ($i + 1) * ($left[8 + $i]
                        - $left[6 - $i]);
                }
                $amount = 16 * ($left[15] + $above[15]);
                $bits = (5 * $tall + 32) >> 6;
                $chunk = (5 * $red_part + 32) >> 6;
                for ($down = 0; $down < 16; $down++) {
                    for ($across = 0; $across < 16; $across++) {
                        $slope = $bits * ($across - 7)
                            + $chunk * ($down - 7);
                        $position[$down * 16 + $across]
                            = self::holdInsideByte(($amount + $slope
                                + 16) >> 5);
                    }
                }
                break;
            default:
                throw new H264Exception("bad Intra_16x16 mode $mode");
        }
        return $position;
    }
    /**
     * predChroma guesses both color planes of a macroblock from the
     * samples above and to the left of it. The ways of guessing are
     * numbered differently from brightness: zero fills the block with
     * one value, one guesses across, two guesses down, and three
     * follows the slope between the two edges. The standard calls this
     * clause 8.3.4.
     *
     * @param int $mode which way the block is guessed from its neighbors
     * @param array $plane which of the picture's planes, brightness or color
     * @param int $stride how many values one row of the picture takes
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param bool $left_there whether the block to the left is there to read
     * @param bool $above_there whether the block above is there to read
     * @param bool $above_left_there whether the block above and left is there
     * @return array what was read
     */
    public static function predChroma(
        int $mode, array $plane, int $stride, int $block_x, int $block_y,
        bool $left_there, bool $above_there, bool $above_left_there
    ): array {
        $above = array_fill(0, 8, self::MID);
        $left = array_fill(0, 8, self::MID);
        if ($above_there) {
            $base = ($block_y - 1) * $stride + $block_x;
            for ($across = 0; $across < 8; $across++) {
                $above[$across] = $plane[$base + $across];
            }
        }
        if ($left_there) {
            for ($down = 0; $down < 8; $down++) {
                $left[$down] = $plane[($block_y + $down) * $stride +
                    $block_x - 1];
            }
        }
        $position = array_fill(0, 64, 0);
        switch ($mode) {
            /* DC, computed per 4x4 sub-block */
            case 0:
                for ($block_y = 0; $block_y < 2; $block_y++) {
                    for ($block_x = 0; $block_x < 2; $block_x++) {
                        $x_o = $block_x * 4;
                        $y_o = $block_y * 4;
                        $sum_t = 0;
                        $sum_l = 0;
                        if ($above_there) {
                            for ($i = 0; $i < 4; $i++) {
                                $sum_t += $above[$x_o + $i];
                            }
                        }
                        if ($left_there) {
                            for ($i = 0; $i < 4; $i++) {
                                $sum_l += $left[$y_o + $i];
                            }
                        }
                        $corner = ($x_o === 0 && $y_o === 0)
                            || ($x_o > 0 && $y_o > 0);
                        if ($corner) {
                            if ($above_there && $left_there) {
                                $first_value = ($sum_t + $sum_l + 4) >> 3;
                            } elseif ($above_there) {
                                $first_value = ($sum_t + 2) >> 2;
                            } elseif ($left_there) {
                                $first_value = ($sum_l + 2) >> 2;
                            } else {
                                $first_value = self::MID;
                            }
                        /* top-right sub-block prefers the top row */
                        } elseif ($x_o > 0) {
                            if ($above_there) {
                                $first_value = ($sum_t + 2) >> 2;
                            } elseif ($left_there) {
                                $first_value = ($sum_l + 2) >> 2;
                            } else {
                                $first_value = self::MID;
                            }
                        /* bottom-left sub-block prefers the left column */
                        } else {
                            if ($left_there) {
                                $first_value = ($sum_l + 2) >> 2;
                            } elseif ($above_there) {
                                $first_value = ($sum_t + 2) >> 2;
                            } else {
                                $first_value = self::MID;
                            }
                        }
                        for ($down = 0; $down < 4; $down++) {
                            for ($across = 0; $across < 4; $across++) {
                                $position[($y_o + $down) * 8 + $x_o + $across]
                                    = $first_value;
                            }
                        }
                    }
                }
                break;
            /* horizontal */
            case 1:
                for ($down = 0; $down < 8; $down++) {
                    $value = $left[$down] ?? self::MID;
                    for ($across = 0; $across < 8; $across++) {
                        $position[$down * 8 + $across] = $value;
                    }
                }
                break;
            /* vertical */
            case 2:
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        $position[$down * 8 + $across]
                            = $above[$across] ?? self::MID;
                    }
                }
                break;
            /* plane */
            case 3:
                $top_left = $above_left_there ? $plane[($block_y - 1) *
                    $stride + $block_x - 1] : self::MID;
                $above[-1] = $top_left;
                $left[-1] = $top_left;
                $tall = 0;
                $red_part = 0;
                for ($i = 0; $i < 4; $i++) {
                    $tall += ($i + 1) * ($above[4 + $i] - $above[2 - $i]);
                    $red_part += ($i + 1) * ($left[4 + $i]
                        - $left[2 - $i]);
                }
                $amount = 16 * ($left[7] + $above[7]);
                $bits = (34 * $tall + 32) >> 6;
                $chunk = (34 * $red_part + 32) >> 6;
                for ($down = 0; $down < 8; $down++) {
                    for ($across = 0; $across < 8; $across++) {
                        $slope = $bits * ($across - 3)
                            + $chunk * ($down - 3);
                        $position[$down * 8 + $across]
                            = self::holdInsideByte(($amount + $slope
                                + 16) >> 5);
                    }
                }
                break;
            default:
                throw new H264Exception("bad chroma prediction mode $mode");
        }
        return $position;
    }
}

/**
 * The H264Cavlc class reads a block's values with the simpler of
 * the two codings H.264 offers, which the standard sets out in its
 * clause 9.2.
 * The tables that turn a run of bits back into a value are built
 * once, the first time a block is read, and looked up by how many
 * bits were read and what they held.
 */
final class H264Cavlc
{
    /**
     * $value_token stores the tables that turn the bits of a block's first
     * field
     * back into how many values it holds. Built once and shared.
     * @var array
     */
    private static array $value_token = [];
    /**
     * $total_zeros stores the tables for how many zeros sit before the last
     * value.
     * @var array
     */
    private static array $total_zeros = [];
    /**
     * $chroma_first_value_total_zeros stores the color first value total zeros.
     * @var array
     */
    private static array $chroma_first_value_total_zeros = [];
    /**
     * $run_before stores the tables for how many zeros sit between values.
     * @var array
     */
    private static array $run_before = [];
    /**
     * $built stores whether those tables have been built yet, since they are
     * made once for the life of the process.
     * @var bool
     */
    private static bool $built = false;
    /**
     * buildPicture builds the code tables the coefficients are read with, once
     * for the whole run of the program.
     */
    private static function buildPicture(): void
    {
        if (self::$built) {
            return;
        }
        /* coeff_token: tables 0..3 keyed by nC range, table 4 is chroma DC (nC
          == -1) */
        for ($target = 0; $target < 4; $target++) {
            $length = H264Tables::COEFF_TOKEN_LEN[$target];
            $bits = H264Tables::COEFF_TOKEN_BITS[$target];
            for ($i = 0; $i < 68; $i++) {
                if ($length[$i] === 0) {
                    continue;
                }
                self::$value_token[$target][$length[$i]][$bits[$i]]
                    = [intdiv($i, 4), $i % 4];
            }
        }
        $length = H264Tables::CHROMA_DC_TOKEN_LEN;
        $bits = H264Tables::CHROMA_DC_TOKEN_BITS;
        for ($i = 0; $i < 20; $i++) {
            if ($length[$i] === 0) {
                continue;
            }
            self::$value_token[4][$length[$i]][$bits[$i]] = [intdiv($i, 4),
                $i % 4];
        }
        foreach (H264Tables::TOTAL_ZEROS_LEN as $row => $lens) {
            foreach ($lens as $value => $level) {
                if ($level === 0 && $value !== 0) {
                    continue;
                }
                $width = H264Tables::TOTAL_ZEROS_BITS[$row][$value];
                self::$total_zeros[$row][$level][$width] = $value;
            }
        }
        foreach (H264Tables::COLOR_FIRST_ZERO_COUNT_LENGTHS as $row => $lens) {
            foreach ($lens as $value => $level) {
                if ($level === 0) {
                    continue;
                }
                $width = H264Tables::COLOR_FIRST_ZERO_COUNT_BITS[$row][$value];
                self::$chroma_first_value_total_zeros[$row][$level]
                    [$width] = $value;
            }
        }
        foreach (H264Tables::RUN_LEN as $row => $lens) {
            foreach ($lens as $value => $level) {
                if ($level === 0) {
                    continue;
                }
                self::$run_before[$row][$level]
                    [H264Tables::RUN_BITS[$row][$value]] = $value;
            }
        }
        self::$built = true;
    }
    /**
     * readFromCodeTable reads a value written with a code table, where a
     * shorter code stands for a commoner value. The table says which value each
     * run of bits means.
     *
     * @param array $map [len][bits] => value
     * @param H264Bits $bits the reader the stream's bits are taken from
     * @param int $max_length the most bytes that may be read
     * @param string $what which kind
     * @return mixed the value the map holds for the bits read
     */
    private static function readFromCodeTable(
        H264Bits $bits, array $map, int $max_length, string $what)
    {
        for ($level = 1; $level <= $max_length; $level++) {
            if (!isset($map[$level])) {
                continue;
            }
            $value = $bits->lookAtBits($level);
            if (isset($map[$level][$value])) {
                $bits->skipBits($level);
                return $map[$level][$value];
            }
        }
        throw new H264Exception("invalid $what code");
    }
    /**
     * residual the readBlockValues method reads the coded values of one block
     * with the simpler of the two codings H.264 offers. How many values a block
     * holds is written first, then their sizes, then the runs of zeros between
     * them, each read from a table chosen by how many values the neighboring
     * blocks held. The standard calls this clause 9.2. held, which chooses the
     * table; zero less than one asks for the table used for the first values of
     * the color planes. AC-only, 4 for chroma DC) how many values the block
     * holds]
     *
     * @param int $n_c How many values the blocks above and to the left
     * @param int $max_value number of coefficients in the block (16, 15 for
     * @param int $start_position first scan position written (1 for AC-only
     *     blocks)
     * @return array [levels indexed by scan position,
     * @param H264Bits $bits the reader the stream's bits are taken from
     */
    public static function residual(H264Bits $bits, int $n_c,
        int $max_value,
        int $start_position): array
    {
        self::buildPicture();
        if ($n_c === -1) {
            $table = 4;
        } elseif ($n_c < 2) {
            $table = 0;
        } elseif ($n_c < 4) {
            $table = 1;
        } elseif ($n_c < 8) {
            $table = 2;
        } else {
            $table = 3;
        }
        $written = array_fill(0, $start_position + $max_value, 0);
        if ($table === 3) {
            /* Where the neighbors held eight values or more, the
               count is written as a fixed six bit code rather than
               through a table: four times one less than how many
               values the block holds, plus how many ones sit at its
               end, with 3
              meaning zero coefficients */
            $value = $bits->readBits(6);
            if ($value === 3) {
                $total_value = 0;
                $trailing_ones = 0;
            } else {
                $total_value = intdiv($value, 4) + 1;
                $trailing_ones = $value % 4;
            }
        } else {
            [$total_value, $trailing_ones]
                = self::readFromCodeTable(
                    $bits, self::$value_token[$table], 16, 'coeff_token');
        }
        if ($total_value === 0) {
            return [$written, 0];
        }
        if ($total_value > $max_value || $trailing_ones > 3) {
            throw new H264Exception(
                "coeff_token out of range ($total_value/$trailing_ones)");
        }
        $levels = [];
        $suffix_length = ($total_value > 10 && $trailing_ones < 3) ? 1 : 0;
        for ($i = 0; $i < $total_value; $i++) {
            if ($i < $trailing_ones) {
                $levels[$i] = $bits->readBit() === 1 ? -1 : 1;
                continue;
            }
            $starting_weight_scale = 0;
            while ($bits->readBit() === 0) {
                $starting_weight_scale++;
                if ($starting_weight_scale > 32) {
                    throw new H264Exception('invalid level_prefix');
                }
            }
            $suffix_size = $suffix_length;
            if ($starting_weight_scale === 14 && $suffix_length === 0) {
                $suffix_size = 4;
            } elseif ($starting_weight_scale >= 15) {
                $suffix_size = $starting_weight_scale - 3;
            }
            $level_code = min(15, $starting_weight_scale) << $suffix_length;
            if ($suffix_size > 0) {
                $level_code += $bits->readBits($suffix_size);
            }
            if ($starting_weight_scale >= 15 && $suffix_length === 0) {
                $level_code += 15;
            }
            if ($starting_weight_scale >= 16) {
                $level_code += (1 << ($starting_weight_scale - 3)) - 4096;
            }
            if ($i === $trailing_ones && $trailing_ones < 3) {
                $level_code += 2;
            }
            $levels[$i] = ($level_code % 2 === 0)
                ? ($level_code + 2) >> 1
                : (-$level_code - 1) >> 1;
            if ($suffix_length === 0) {
                $suffix_length = 1;
            }
            if (abs($levels[$i]) > (3 << ($suffix_length - 1))
                && $suffix_length < 6) {
                $suffix_length++;
            }
        }
        $zeros_left = 0;
        if ($total_value < $max_value) {
            if ($n_c === -1) {
                $zeros_left
                    = self::readFromCodeTable($bits,
                        self::$chroma_first_value_total_zeros[$total_value
                        - 1], 8, 'chroma total_zeros');
            } else {
                $zeros_left
                    = self::readFromCodeTable($bits,
                        self::$total_zeros[$total_value
                        - 1], 16, 'total_zeros');
            }
        }
        $runs = array_fill(0, $total_value, 0);
        for ($i = 0; $i < $total_value - 1; $i++) {
            if ($zeros_left <= 0) {
                break;
            }
            $row = min($zeros_left, 7) - 1;
            $run = self::readFromCodeTable(
                $bits, self::$run_before[$row], 11, 'run_before');
            $runs[$i] = $run;
            $zeros_left -= $run;
        }
        $runs[$total_value - 1] = $zeros_left;
        $value_count = -1;
        for ($i = $total_value - 1; $i >= 0; $i--) {
            $value_count += $runs[$i] + 1;
            $position = $start_position + $value_count;
            if ($position >= count($written)) {
                throw new H264Exception('coefficient position out of range');
            }
            $written[$position] = $levels[$i];
        }
        return [$written, $total_value];
    }
}

/**
 * The H264Cabac class reads a block's values with the arithmetic
 * coding
 *     H.264 and HEVC may use decoding, clause 9.3 of
 *     ITU-T H.264.
 *
 * Reads a slice written with the arithmetic coding: the engine itself,
 * and the working out of which probability to use for every
 * syntax element that can appear in an I slice.
 */
final class H264Cabac
{
    /**
     * $bits stores the reader the arithmetic coding takes its bits from.
     * @var H264Bits
     */
    public H264Bits $bits;
    /**
     * $range stores how wide the range of values still in play is. Each value
     * read narrows it by how likely that value was.
     * @var int
     */
    private int $range = 510;
    /**
     * $offset stores where in that range the number being read sits.
     * @var int
     */
    private int $offset = 0;
    /**
     * $weight_state stores how likely the more probable value is, one entry for
     * each setting the coding keeps.
     * @var array
     */
    private array $weight_state = [];
    /**
     * $likelier_value stores which value is the more probable one, one entry
     * per setting.
     * @var array
     */
    private array $likelier_value = [];
    /**
     * $slice_quantizer stores the quantizer the slice started at, which
     * decides the
     * probabilities the coding begins from.
     * @var int
     */
    private int $slice_quantizer = 26;
    /**
     * $previous_changed_quantizer stores whether the macroblock read before
     * this one changed the quantizer. The coding weighs the next such change by
     * whether the last one happened, so this is kept from one macroblock to the
     * next.
     * @var bool
     */
    public bool $previous_changed_quantizer = false;
    /**
     * CTX_MB_TYPE_I is where each kind of decision's probabilities start in the
     * one long list the coding keeps. The standard fixes these in its tables
     * 9-11 and 9-34.
     * @var mixed
     */
    private const CTX_MB_TYPE_I        = 3;
    /**
     * CTX_CHROMA_PRED is where the probabilities for chroma pred start in the
     * one long list the coding keeps.
     * @var mixed
     */
    private const CTX_CHROMA_PRED      = 64;
    /**
     * CTX_PREV_INTRA_FLAG is where the probabilities for prev intra flag start
     * in the one long list the coding keeps.
     * @var mixed
     */
    private const CTX_PREV_INTRA_FLAG  = 68;
    /**
     * CTX_REM_INTRA is where the probabilities for rem intra start in the one
     * long list the coding keeps.
     * @var mixed
     */
    private const CTX_REM_INTRA        = 69;
    /**
     * CTX_MB_QP_DELTA is where the probabilities for mb qp delta start in the
     * one long list the coding keeps.
     * @var mixed
     */
    private const CTX_MB_QP_DELTA      = 60;
    /**
     * CTX_CBP_LUMA is where the probabilities for which blocks carry values
     * luma start in the one
     * long list the coding keeps.
     * @var mixed
     */
    private const CTX_CBP_LUMA         = 73;
    /**
     * CTX_CBP_CHROMA is where the probabilities for which blocks carry values
     * chroma start in the one
     * long list the coding keeps.
     * @var mixed
     */
    private const CTX_CBP_CHROMA       = 77;
    /**
     * CTX_CBF is where the probabilities for cbf start in the one long list the
     * coding keeps.
     * @var mixed
     */
    private const CTX_CBF              = 85;
    /**
     * CTX_SIG is where the probabilities for sig start in the one long list the
     * coding keeps.
     * @var mixed
     */
    private const CTX_SIG              = 105;
    /**
     * CTX_LAST is where the probabilities for last start in the one long list
     * the coding keeps.
     * @var mixed
     */
    private const CTX_LAST             = 166;
    /**
     * CTX_ABS is where the probabilities for abs start in the one long list the
     * coding keeps.
     * @var mixed
     */
    private const CTX_ABS              = 227;
    /**
     * CTX_SIG_8X8 is where the probabilities for sig eight by eight start in
     * the one long list the coding keeps.
     * @var mixed
     */
    private const CTX_SIG_8X8          = 402;
    /**
     * CTX_LAST_8X8 is where the probabilities for last eight by eight start in
     * the one long list the coding keeps.
     * @var mixed
     */
    private const CTX_LAST_8X8         = 417;
    /**
     * CTX_ABS_8X8 is where the probabilities for abs eight by eight start in
     * the one long list the coding keeps.
     * @var mixed
     */
    private const CTX_ABS_8X8          = 426;
    /**
     * CTX_TRANSFORM_8X8 is where the probabilities for transform eight by eight
     * start in the one long list the coding keeps.
     * @var mixed
     */
    private const CTX_TRANSFORM_8X8    = 399;
    /**
     * CTX_TERMINATE is where the probabilities for terminate start in the one
     * long list the coding keeps.
     * @var mixed
     */
    private const CTX_TERMINATE        = 276;
    /**
     * CBF_CAT_OFFSET is where the probabilities for whether a block carries any
     * values start, for each kind of block.
     * @var mixed
     */
    private const CBF_CAT_OFFSET  = [0, 4, 8, 12, 16];
    /**
     * SIG_CAT_OFFSET is where the probabilities for which places in a block
     * hold a value start, for each kind of block.
     * @var mixed
     */
    private const SIG_CAT_OFFSET  = [0, 15, 29, 44, 47];
    /**
     * ABS_CAT_OFFSET is where the probabilities for how large a value is start,
     * for each kind of block.
     * @var mixed
     */
    private const ABS_CAT_OFFSET  = [0, 10, 20, 30, 39];
    /**
     * __construct sets up the arithmetic decoder for one slice.
     *
     * @param H264Bits $bits the reader the stream's bits are taken from
     */
    public function __construct(H264Bits $bits)
    {
        $this->bits = $bits;
    }
    /**
     * startReading sets up the arithmetic coding at the beginning of a slice.
     * The coding weighs every decision it reads by how likely each answer is,
     * and it keeps one such weight for each kind of decision it can make. Those
     * weights have to start somewhere: the standard fixes a pair of numbers for
     * each kind, and this works the starting weight out from that pair and from
     * the quantizer the slice was coded at, since a coarsely coded slice has
     * different odds from a finely coded one. It then reads the first nine bits
     * of the slice, which set the range the reading works within.
     *
     * @param int $slice_quantizer The quantizer the slice was coded at.
     */
    public function startReading(int $slice_quantizer): void
    {
        $this->slice_quantizer = $slice_quantizer;
        $quantizer =
            $slice_quantizer < 0 ? 0 : ($slice_quantizer > 51 ? 51
                : $slice_quantizer);
        foreach (H264Tables::CTX_INIT_I as $i => [$matches, $number]) {
            $starting_weight = ((($matches * $quantizer) >> 4) + $number);
            if ($starting_weight < 1) {
                $starting_weight = 1;
            } elseif ($starting_weight > 126) {
                $starting_weight = 126;
            }
            if ($starting_weight <= 63) {
                $this->weight_state[$i] = 63 - $starting_weight;
                $this->likelier_value[$i] = 0;
            } else {
                $this->weight_state[$i] = $starting_weight - 64;
                $this->likelier_value[$i] = 1;
            }
        }
        $this->loadFirstBits();
    }
    /**
     * loadFirstBits reads the nine bits that set where the reading begins
     * within its range. Every value read afterwards narrows that range, so it
     * must be filled before anything else.
     */
    private function loadFirstBits(): void
    {
        $this->range = 510;
        $this->offset = $this->bits->readBits(9);
    }
    /**
     * startAfresh sets the arithmetic reading up again after a macroblock whose
     * samples were stored as they are, with nothing coded. Such a macroblock is
     * written on byte boundaries rather than through the coding, so the range
     * and the place within it have to be filled afresh from the bits that
     * follow it.
     */
    public function startAfresh(): void
    {
        $this->loadFirstBits();
    }
    /**
     * decodeDecision reads one value with the arithmetic coding. It narrows the
     * range by how likely that value was, takes in more bits as the range
     * shrinks, and moves the weight for that kind of decision toward whichever
     * answer it read. The standard calls this clause 9.3.3.2.1.
     *
     * @param int $context_at which probability to read with
     * @return int the value read, zero or one
     */
    public function decodeDecision(int $context_at): int
    {
        $state = $this->weight_state[$context_at];
        $likelier = $this->likelier_value[$context_at];
        $quant = ($this->range >> 6) & 3;
        $less_likely_range = H264Tables::RANGE_TAB_LPS[$state][$quant];
        $this->range -= $less_likely_range;
        if ($this->offset >= $this->range) {
            $bin = 1 - $likelier;
            $this->offset -= $this->range;
            $this->range = $less_likely_range;
            if ($state === 0) {
                $this->likelier_value[$context_at] = 1 - $likelier;
            }
            $this->weight_state[$context_at] =
                H264Tables::TRANS_IDX_LPS[$state];
        } else {
            $bin = $likelier;
            $this->weight_state[$context_at] =
                H264Tables::TRANS_IDX_MPS[$state];
        }
        while ($this->range < 256) {
            $this->range <<= 1;
            $this->offset = ($this->offset << 1) | $this->bits->readBit();
        }
        return $bin;
    }
    /**
     * decodeBypass reads one value that the coding treats as equally likely
     * either way, which costs a single bit and leaves the probabilities alone.
     * The standard calls this clause 9.3.3.2.3.
     *
     * @return int the value read, zero or one
     */
    public function decodeBypass(): int
    {
        $this->offset = ($this->offset << 1) | $this->bits->readBit();
        if ($this->offset >= $this->range) {
            $this->offset -= $this->range;
            return 1;
        }
        return 0;
    }
    /**
     * decodeTerminate reads the value that says a slice has ended, which the
     * coding writes with a fixed probability. The standard calls this clause
     * 9.3.3.2.4.
     *
     * @return int what was read
     */
    public function decodeTerminate(): int
    {
        $this->range -= 2;
        if ($this->offset >= $this->range) {
            return 1;
        }
        while ($this->range < 256) {
            $this->range <<= 1;
            $this->offset = ($this->offset << 1) | $this->bits->readBit();
        }
        return 0;
    }
    /**
     * pcmResyncBitPos works out where in the stream a macroblock's plain
     * samples begin, for a macroblock whose samples were stored as they are
     * rather than coded. After a terminating bin the engine still holds nine
     * look-ahead bits that were never consumed, so the reader is wound back
     * before byte alignment.
     *
     * @return int what was read
     */
    public function pcmResyncBitPos(): int
    {
        return $this->bits->position - 7;
    }
    /**
     * neighbors says which macroblocks sit above and to the left of the one
     * being decoded, and whether they may be read. A block is guessed from
     * those two, and a macroblock in another slice may not be used, so a block
     * at a slice edge is guessed from fewer neighbors. number of the one above,
     * each zero less than one where there is none to read.
     *
     * @param H264SliceDecoder $slice The slice being decoded.
     * @return array The number of the macroblock to the left and the
     */
    private function neighbors(H264SliceDecoder $slice): array
    {
        $frame = $slice->frameAt();
        $across = $slice->macroblockAcross();
        $down = $slice->macroblockDown();
        $amount = $slice->mbAvail($across - 1, $down)
            ? ($down * $frame->macroblock_across + $across - 1) : -1;
        $bits = $slice->mbAvail($across, $down - 1)
            ? (($down - 1) * $frame->macroblock_across + $across) : -1;
        return [$amount, $bits];
    }
    /**
     * readMacroblockKind mb_type for I slices, Tables 9-36 and 9-39
     *
     * @param H264SliceDecoder $slice the slice being decoded
     * @return int what was read
     */
    public function readMacroblockKind(H264SliceDecoder $slice): int
    {
        $frame = $slice->frameAt();
        [$amount, $bits] = $this->neighbors($slice);
        $step = 0;
        if ($amount >= 0 && $frame->macroblock_kind[$amount] !== 0) {
            $step++;
        }
        if ($bits >= 0 && $frame->macroblock_kind[$bits] !== 0) {
            $step++;
        }
        if ($this->decodeDecision(self::CTX_MB_TYPE_I + $step) === 0) {
            /* I_NxN */
            return 0;
        }
        if ($this->decodeTerminate() === 1) {
            /* macroblocks whose samples are stored as they are */
            return 25;
        }
        $base = self::CTX_MB_TYPE_I + 2;
        $macroblock_kind = 1;
        $macroblock_kind += 12 * $this->decodeDecision($base + 1);
        if ($this->decodeDecision($base + 2) === 1) {
            $macroblock_kind += 4 + 4 * $this->decodeDecision($base + 3);
        }
        $macroblock_kind += 2 * $this->decodeDecision($base + 4);
        $macroblock_kind += $this->decodeDecision($base + 5);
        return $macroblock_kind;
    }
    /**
     * decodeTransform8x8 reads whether a macroblock uses the larger transform.
     *
     * @param H264SliceDecoder $slice the slice being decoded
     * @return int what was read
     */
    public function decodeTransform8x8(H264SliceDecoder $slice): int
    {
        $frame = $slice->frameAt();
        [$amount, $bits] = $this->neighbors($slice);
        $step = 0;
        if ($amount >= 0 && $frame->macroblock_larger_transform[$amount] ===
            1) {
            $step++;
        }
        if ($bits >= 0 && $frame->macroblock_larger_transform[$bits] === 1) {
            $step++;
        }
        return $this->decodeDecision(self::CTX_TRANSFORM_8X8 + $step);
    }
    /**
     * decodePrevIntraPredModeFlag reads whether a block takes the prediction
     * mode its neighbors suggest.
     *
     * @return int what was read
     */
    public function decodePrevIntraPredModeFlag(): int
    {
        return $this->decodeDecision(self::CTX_PREV_INTRA_FLAG);
    }
    /**
     * decodeRemIntraPredMode reads which way a block is guessed where
     * the expected way was refused. Three bits are read with the same
     * probability, lowest bit first.
     *
     * @return int The way of guessing the stream named.
     */
    public function decodeRemIntraPredMode(): int
    {
        $variant = $this->decodeDecision(self::CTX_REM_INTRA);
        $variant |= $this->decodeDecision(self::CTX_REM_INTRA) << 1;
        $variant |= $this->decodeDecision(self::CTX_REM_INTRA) << 2;
        return $variant;
    }
    /**
     * decodeChromaPredMode reads the prediction mode of the chroma planes.
     *
     * @param H264SliceDecoder $slice the slice being decoded
     * @return int what was read
     */
    public function decodeChromaPredMode(H264SliceDecoder $slice): int
    {
        $frame = $slice->frameAt();
        [$amount, $bits] = $this->neighbors($slice);
        $step = 0;
        if ($amount >= 0 && $frame->macroblock_kind[$amount] !== 25
            && $frame->macroblock_chroma_mode[$amount] !== 0) {
            $step++;
        }
        if ($bits >= 0 && $frame->macroblock_kind[$bits] !== 25
            && $frame->macroblock_chroma_mode[$bits] !== 0) {
            $step++;
        }
        if ($this->decodeDecision(self::CTX_CHROMA_PRED + $step) === 0) {
            return 0;
        }
        if ($this->decodeDecision(self::CTX_CHROMA_PRED + 3) === 0) {
            return 1;
        }
        return $this->decodeDecision(self::CTX_CHROMA_PRED + 3) === 0 ? 2 : 3;
    }
    /**
     * readBlocksWithValues reads which parts of a macroblock carry values at
     * all. Parts that carry none are filled from the guess alone. carry values]
     *
     * @return array [which brightness blocks carry values, which color blocks
     * @param H264SliceDecoder $slice the slice being decoded
     */
    public function readBlocksWithValues(H264SliceDecoder $slice): array
    {
        $frame = $slice->frameAt();
        [$amount, $bits] = $this->neighbors($slice);
        $coded_blocks_a = $amount >= 0 ? $frame
            ->macroblock_coded_blocks_luma[$amount] : -1;
        $coded_blocks_b = $bits >= 0 ? $frame
            ->macroblock_coded_blocks_luma[$bits] : -1;
        $plain_samples_a = $amount >= 0 && $frame
            ->macroblock_kind[$amount] === 25;
        $plain_samples_b = $bits >= 0 && $frame->macroblock_kind[$bits] === 25;
        /* 9.3.3.1.1.4: condTermFlagN is 0 when the neighboring 8x8 block has
          */
        /* residual data (or the neighbor is unavailable or macroblocks whose
           samples are stored as they are) */
        $bit_of
            = function (int $position, int $coded_block_pattern,
                bool $plain_samples,
                int $block_at) use ($frame): int {
            if ($position < 0 || $plain_samples) {
                return 0;
            }
            return (($coded_block_pattern >> $block_at) & 1) !== 0 ? 0 : 1;
        };
        $coded_block_pattern = 0;
        for ($i = 0; $i < 4; $i++) {
            /* left neighbor of 8x8 block i */
            if (($i & 1) === 1) {
                $cond_a = ((($coded_block_pattern >> ($i - 1)) & 1) !== 0)
                    ? 0 : 1;
            } else {
                $cond_a = $bit_of($amount, $coded_blocks_a,
                    $plain_samples_a, $i + 1);
            }
            /* top neighbor of 8x8 block i */
            if (($i & 2) === 2) {
                $cond_b = ((($coded_block_pattern >> ($i - 2)) & 1) !== 0)
                    ? 0 : 1;
            } else {
                $cond_b = $bit_of($bits, $coded_blocks_b, $plain_samples_b,
                    $i + 2);
            }
            $coded_block_pattern |= $this
                ->decodeDecision(self::CTX_CBP_LUMA + $cond_a
                + 2 * $cond_b) << $i;
        }
        $channel_a = $amount >= 0 ? $frame
            ->macroblock_coded_blocks_chroma[$amount] : 0;
        $channel_b = $bits >= 0 ? $frame
            ->macroblock_coded_blocks_chroma[$bits] : 0;
        $corner_zero_a = ($amount >= 0 && ($plain_samples_a || $channel_a !==
            0)) ? 1 : 0;
        $corner_zero_b = ($bits >= 0 && ($plain_samples_b || $channel_b !==
            0)) ? 1 : 0;
        $coded_blocks_chroma = 0;
        if ($this->decodeDecision(self::CTX_CBP_CHROMA + $corner_zero_a
            + 2 * $corner_zero_b) === 1) {
            $corner_one_a = ($amount >= 0 && ($plain_samples_a || $channel_a ===
                2)) ? 1 : 0;
            $corner_one_b = ($bits >= 0 && ($plain_samples_b || $channel_b ===
                2)) ? 1 : 0;
            $coded_blocks_chroma = 1
                + $this->decodeDecision(self::CTX_CBP_CHROMA + 4 + $corner_one_a
                    + 2 * $corner_one_b);
        }
        return [$coded_block_pattern, $coded_blocks_chroma];
    }
    /**
     * readQuantizerChange reads how far this macroblock's quantizer sits from
     * the slice's.
     *
     * @return int what was read
     */
    public function readQuantizerChange(): int
    {
        $step = $this->previous_changed_quantizer ? 1 : 0;
        if ($this->decodeDecision(self::CTX_MB_QP_DELTA + $step) === 0) {
            return 0;
        }
        $k = 1;
        if ($this->decodeDecision(self::CTX_MB_QP_DELTA + 2) === 1) {
            $k = 2;
            while ($this->decodeDecision(self::CTX_MB_QP_DELTA + 3) === 1) {
                $k++;
                if ($k > 128) {
                    throw new H264Exception('mb_qp_delta out of range');
                }
            }
        }
        $mag = ($k + 1) >> 1;
        return ($k & 1) ? $mag : -$mag;
    }
    /**
     * residual reads a block's values with the arithmetic
     * coding method reads the coded values of one block with the arithmetic
     * coding. It reads first whether the block holds anything at all, then
     * which places in it hold a value, then how large each of those is, with
     * every decision weighed by probabilities the coding keeps and updates. The
     * standard calls this clauses 9.3.2.3 and 9.3.3.1.3. which probabilities to
     * read it with. index, or chroma 4x4 index
     *
     * @param int $block_kind Which kind of block this is, which says
     * @param int $max_value number of coefficients in the list
     * @param int $start_position scan position of list entry 0
     * @param int $block_at luma four by four z-index, luma eight by eight
     * @param int $plane    0 = Cb, 1 = Cr (chroma categories only)
     * @return array the values the block was coded as
     * @param H264SliceDecoder $slice the slice being decoded
     */
    public function residual(
        H264SliceDecoder $slice, int $block_kind, int $max_value,
            int $start_position,
            int $block_at,
        int $plane
    ): array {
        $written = array_fill(0, $start_position + $max_value, 0);
        if ($block_kind !== 5) {
            $step = $this->valuePresenceContext($slice, $block_kind,
                $block_at, $plane);
            $context =
                self::CTX_CBF + self::CBF_CAT_OFFSET[$block_kind] + $step;
            if ($this->decodeDecision($context) === 0) {
                return [$written, 0];
            }
        }
        if ($block_kind === 5) {
            $present_base = self::CTX_SIG_8X8;
            $last_base = self::CTX_LAST_8X8;
            $size_base = self::CTX_ABS_8X8;
        } else {
            $present_base = self::CTX_SIG + self::SIG_CAT_OFFSET[$block_kind];
            $last_base = self::CTX_LAST + self::SIG_CAT_OFFSET[$block_kind];
            $size_base = self::CTX_ABS + self::ABS_CAT_OFFSET[$block_kind];
        }
        $present = array_fill(0, $max_value, 0);
        $count_value = $max_value;
        for ($i = 0; $i < $count_value - 1; $i++) {
            if ($block_kind === 5) {
                $present_step = H264Tables::SIG_COEFF_8X8[$i];
                $last_step = H264Tables::LAST_COEFF_8X8[$i];
            } elseif ($block_kind === 3) {
                $present_step = min($i, 2);
                $last_step = $present_step;
            } else {
                $present_step = $i;
                $last_step = $i;
            }
            if ($this->decodeDecision($present_base + $present_step) === 1) {
                $present[$i] = 1;
                if ($this->decodeDecision($last_base + $last_step) === 1) {
                    $count_value = $i + 1;
                    break;
                }
            }
        }
        $present[$count_value - 1] = 1;
        $count_equal_to_one = 0;
        $count_over_one = 0;
        $total = 0;
        for ($i = $count_value - 1; $i >= 0; $i--) {
            if ($present[$i] === 0) {
                continue;
            }
            $step_zero = ($count_over_one !== 0) ? 0 : min(4, 1 +
                $count_equal_to_one);
            $level = 1;
            if ($this->decodeDecision($size_base + $step_zero) === 1) {
                $step_n = 5 + min(4 - ($block_kind === 3 ? 1 : 0),
                    $count_over_one);
                $k = 1;
                while ($k < 14
                    && $this->decodeDecision($size_base + $step_n) === 1) {
                    $k++;
                }
                if ($k === 14) {
                    $k += $this->readLargeValueTail();
                }
                $level = $k + 1;
                $count_over_one++;
            } else {
                $count_equal_to_one++;
            }
            if ($this->decodeBypass() === 1) {
                $level = -$level;
            }
            $written[$start_position + $i] = $level;
            $total++;
        }
        return [$written, $total];
    }
    /**
     * readLargeValueTail reads the part of a value that is larger than the
     * coding writes through its probabilities. Those bits are read without
     * weighing either answer, since a value that large is rare enough that
     * weighing would not pay.
     *
     * @return int The number those bits spell out.
     */
    private function readLargeValueTail(): int
    {
        $k = 0;
        $value = 0;
        while ($this->decodeBypass() === 1) {
            $value += 1 << $k;
            $k++;
            if ($k > 30) {
                throw new H264Exception('coefficient magnitude out of range');
            }
        }
        while ($k > 0) {
            $k--;
            $value += $this->decodeBypass() << $k;
        }
        return $value;
    }
    /**
     * valuePresenceContext works out which probability to read the
     * whether-this-block-carries-values decision with. The answer
     * depends on whether the blocks above and to the left carried any,
     * so a block at the edge of a slice is read with a different
     * probability. The standard calls this clause 9.3.3.1.1.9.
     *
     * @param H264SliceDecoder $slice the slice being decoded
     * @param int $block_kind which kind of block the values belong to
     * @param int $block_at which block within the macroblock
     * @param int $plane which of the picture's planes, brightness or color
     * @return int what was read
     */
    private function valuePresenceContext(H264SliceDecoder $slice,
        int $block_kind,
        int $block_at,
        int $plane): int
    {
        $frame = $slice->frameAt();
        [$amount, $bits] = $this->neighbors($slice);
        switch ($block_kind) {
            /* Intra16x16 luma DC */
            case 0:
                $cond_a
                    = $this->cbfCond($amount < 0,
                         $amount >= 0 && $frame
                             ->macroblock_kind[$amount] === 25,
                    $amount >= 0 && $frame->macroblock_kind[$amount] >= 1
                        && $frame->macroblock_kind[$amount] <= 24,
                    $amount >= 0 ? $frame
                        ->has_values_first_value_y[$amount] : 0);
                $cond_b
                    = $this->cbfCond($bits < 0,
                         $bits >= 0 && $frame->macroblock_kind[$bits] === 25, 
                    $bits >= 0 && $frame->macroblock_kind[$bits] >= 1
                        && $frame->macroblock_kind[$bits] <= 24,
                    $bits >= 0 ? $frame->has_values_first_value_y[$bits] : 0);
                return $cond_a + 2 * $cond_b;
            case 1:
            case 2: {
                [$block_value_four, $block_row_four] =
                    self::xyFromZ($block_at);
                $pixel_x = $slice
                    ->macroblockAcross() * 16 + $block_value_four * 4;
                $pixel_y = $slice->macroblockDown() * 16 +
                    $block_row_four * 4;
                $left_values = $slice->nnzLumaBlk($pixel_x - 1, $pixel_y,
                    $block_at);
                $above_values = $slice->nnzLumaBlk($pixel_x, $pixel_y - 1,
                    $block_at);
                $cond_a = $this->cbfNeighborLuma($slice, $pixel_x - 1, $pixel_y,
                    $left_values);
                $cond_b = $this->cbfNeighborLuma($slice, $pixel_x, $pixel_y - 1,
                    $above_values);
                return $cond_a + 2 * $cond_b;
            }
            /* chroma DC */
            case 3: {
                $first_value_a = $plane === 0
                    ? ($amount >= 0 ? $frame
                        ->has_values_first_value_blue[$amount] : 0)
                    : ($amount >= 0 ? $frame
                        ->has_values_first_value_red[$amount] : 0);
                $first_value_b = $plane === 0 ? ($bits >= 0 ?
                    $frame->has_values_first_value_blue[$bits] : 0)
                    : ($bits >= 0 ? $frame
                        ->has_values_first_value_red[$bits] : 0);
                $cond_a
                    = $this->cbfCond($amount < 0,
                         $amount >= 0 && $frame
                             ->macroblock_kind[$amount] === 25,
                    $amount >= 0 && $frame
                        ->macroblock_coded_blocks_chroma[$amount] !== 0,
                        $first_value_a);
                $cond_b
                    = $this->cbfCond($bits < 0,
                         $bits >= 0 && $frame->macroblock_kind[$bits] === 25, 
                    $bits >= 0 && $frame
                        ->macroblock_coded_blocks_chroma[$bits] !== 0,
                            $first_value_b);
                return $cond_a + 2 * $cond_b;
            }
            /* chroma AC */
            case 4: {
                $pixel_x = $slice->macroblockAcross() * 8 + ($block_at & 1) * 4;
                $pixel_y = $slice->macroblockDown() * 8 + ($block_at >> 1) * 4;
                $cond_a = $this->cbfNeighborChroma($slice, $plane, $pixel_x - 1,
                    $pixel_y);
                $cond_b = $this->cbfNeighborChroma($slice, $plane, $pixel_x,
                    $pixel_y - 1);
                return $cond_a + 2 * $cond_b;
            }
        }
        throw new H264Exception("bad ctxBlockCat $block_kind");
    }
    /**
     * cbfCond works out which set of probabilities to read a coefficient flag
     * with, given what the neighboring blocks held. read
     *
     * @param bool $macroblock_unavailable whether the macroblock is there to
     *     read
     * @param bool $is_plain_samples whether the block's values are stored as
     *     they are
     * @param bool $block_available whether the neighboring block is there to
     * @param int $has_values whether the block carries any values at all
     * @return int what was read
     */
    private function cbfCond(bool $macroblock_unavailable,
        bool $is_plain_samples,
        bool $block_available, int $has_values): int
    {
        if ($macroblock_unavailable) {
            /* current macroblock is always intra in an I slice */
            return 1;
        }
        if ($is_plain_samples) {
            return 1;
        }
        if (!$block_available) {
            return 0;
        }
        return $has_values !== 0 ? 1 : 0;
    }
    /**
     * cbfNeighborLuma works out whether the luma block beside a given one held
     * any coefficients.
     *
     * @param H264SliceDecoder $slice the slice being decoded
     * @param int $pixel_x how far across the frame the pixel is
     * @param int $pixel_y how far down the frame the pixel is
     * @param int $value_count how many values the block carried
     * @return int what was read
     */
    private function cbfNeighborLuma(H264SliceDecoder $slice, int $pixel_x,
        int $pixel_y,
        int $value_count): int
    {
        $frame = $slice->frameAt();
        if ($pixel_x < 0 || $pixel_y < 0 || $pixel_x >= $frame->coded_width
            || $pixel_y >= $frame->coded_height) {
            return 1;
        }
        $macroblock_x = $pixel_x >> 4;
        $macroblock_y = $pixel_y >> 4;
        $position = $macroblock_y * $frame->macroblock_across + $macroblock_x;
        $is_current = ($macroblock_x === $slice
            ->macroblockAcross() && $macroblock_y === $slice
            ->macroblockDown());
        if (!$is_current && !$slice->mbAvail($macroblock_x, $macroblock_y)) {
            return 1;
        }
        if (!$is_current && $frame->macroblock_kind[$position] === 25) {
            return 1;
        }
        return ($value_count > 0) ? 1 : 0;
    }
    /**
     * cbfNeighborChroma works out whether the chroma block beside a given one
     * held any coefficients.
     *
     * @param H264SliceDecoder $slice the slice being decoded
     * @param int $plane zero for luma, one and two for the chroma planes
     * @param int $pixel_x how far across the frame the pixel is
     * @param int $pixel_y how far down the frame the pixel is
     * @return int what was read
     */
    private function cbfNeighborChroma(H264SliceDecoder $slice, int $plane,
        int $pixel_x, int $pixel_y): int
    {
        $frame = $slice->frameAt();
        if ($pixel_x < 0 || $pixel_y < 0 || $pixel_x >= $frame->color_width
            || $pixel_y >= $frame->color_height) {
            return 1;
        }
        $macroblock_x = $pixel_x >> 3;
        $macroblock_y = $pixel_y >> 3;
        $position = $macroblock_y * $frame->macroblock_across + $macroblock_x;
        $is_current = ($macroblock_x === $slice
            ->macroblockAcross() && $macroblock_y === $slice
            ->macroblockDown());
        if (!$is_current && !$slice->mbAvail($macroblock_x, $macroblock_y)) {
            return 1;
        }
        if (!$is_current && $frame->macroblock_kind[$position] === 25) {
            return 1;
        }
        $number = $slice->nnzChromaBlk($plane, $pixel_x, $pixel_y);
        return ($number > 0) ? 1 : 0;
    }
    /**
     * xyFromZ the column and row of a block from its position in the order the
     * format visits them.
     *
     * @param int $last the last one
     * @return array what was read
     */
    private static function xyFromZ(int $last): array
    {
        return [((($last >> 2) & 1) << 1)
            + ($last & 1), ((($last >> 3) & 1) << 1)
            + (($last >> 1) & 1)];
    }
}

/**
 * H264Frame reconstructed picture plus the per-macroblock state neighbors need.
 */
final class H264Frame
{
    /**
     * $macroblock_across stores how many macroblocks the frame is across.
     * @var int
     */
    public int $macroblock_across;
    /**
     * $macroblock_down stores how many macroblocks the frame is down.
     * @var int
     */
    public int $macroblock_down;
    /**
     * $coded_width stores how wide the coded frame is, in samples.
     * @var int
     */
    public int $coded_width;
    /**
     * $coded_height stores how tall the coded frame is, in samples.
     * @var int
     */
    public int $coded_height;
    /**
     * $color_width stores how wide each color plane is, which is half the
     * brightness plane for the usual streams.
     * @var int
     */
    public int $color_width;
    /**
     * $color_height stores how tall each color plane is.
     * @var int
     */
    public int $color_height;
    /**
     * $luma stores the brightness of every sample in the frame being built.
     * @var array
     */
    public array $luma;
    /**
     * $blue stores how blue every sample is, away from gray.
     * @var array
     */
    public array $blue;
    /**
     * $red stores how red every sample is, away from gray.
     * @var array
     */
    public array $red;
    /**
     * $macroblock_kind stores what kind each macroblock is, kept for the whole
     * frame
     * because the smoothing afterwards asks about neighbors.
     * @var array
     */
    public array $macroblock_kind;
    /**
     * $macroblock_quantizer_y stores the quantizer each macroblock used, which
     * the smoothing
     * reads to decide how far a value may move.
     * @var array
     */
    public array $macroblock_quantizer_y;
    /**
     * $macroblock_quantizer_blue stores the quantizer each macroblock's
     * blue plane was coded at, kept per macroblock so the smoothing
     * that follows knows how coarse each one is.
     * @var array
     */
    public array $macroblock_quantizer_blue;
    /**
     * $macroblock_quantizer_red stores the same for each macroblock's
     * red plane.
     * @var array
     */
    public array $macroblock_quantizer_red;
    /**
     * $macroblock_larger_transform stores whether each macroblock used the
     * larger
     * transform.
     * @var array
     */
    public array $macroblock_larger_transform;
    /**
     * $macroblock_slice stores slice id, -1 when not yet decoded
     * @var array
     */
    public array $macroblock_slice;
    /**
     * $macroblock_coded_blocks_luma stores the macroblock coded block
     * brightness.
     * @var array
     */
    public array $macroblock_coded_blocks_luma;
    /**
     * $macroblock_coded_blocks_chroma stores the macroblock coded block color.
     * @var array
     */
    public array $macroblock_coded_blocks_chroma;
    /**
     * $macroblock_chroma_mode stores how each macroblock's color was guessed.
     * @var array
     */
    public array $macroblock_chroma_mode;
    /**
     * $macroblock_smoothing_off stores whether each macroblock's edges are
     * smoothed,
     * which a slice may turn off.
     * @var array
     */
    public array $macroblock_smoothing_off;
    /**
     * $macroblock_alpha_off stores how far each macroblock's smoothing
     * threshold is
     * nudged.
     * @var array
     */
    public array $macroblock_alpha_off;
    /**
     * $macroblock_beta_off stores how far its neighbor threshold is nudged.
     * @var array
     */
    public array $macroblock_beta_off;
    /**
     * $brightness_value_count stores how many values each four by four block
     * of brightness
     * carried. The smoothing looks at it, and so does the reading of the next
     * block along.
     * @var array
     */
    public array $brightness_value_count;
    /**
     * $four_by_four_guess stores how each four by four block of brightness was
     * guessed,
     * kept because a block's guess depends on the blocks beside it.
     * @var array
     */
    public array $four_by_four_guess;
    /**
     * $blue_value_count stores the value count blue color.
     * @var array
     */
    public array $blue_value_count;
    /**
     * $red_value_count stores the value count red color.
     * @var array
     */
    public array $red_value_count;
    /**
     * $has_values_first_value_y stores may use) coded_block_flag of the DC
     * blocks. The cbf
     * first value y.
     * @var array
     */
    public array $has_values_first_value_y;
    /**
     * $has_values_first_value_blue stores the cbf first value blue color.
     * @var array
     */
    public array $has_values_first_value_blue;
    /**
     * $has_values_first_value_red stores the cbf first value red color.
     * @var array
     */
    public array $has_values_first_value_red;
    /**
     * __construct sets up an empty frame of the given size in macroblocks.
     *
     * @param int $macroblock_across width of the frame in macroblocks
     * @param int $macroblock_down height of the frame in macroblocks
     */
    public function __construct(int $macroblock_across, int $macroblock_down)
    {
        $this->macroblock_across = $macroblock_across;
        $this->macroblock_down = $macroblock_down;
        $this->coded_width = $macroblock_across * 16;
        $this->coded_height = $macroblock_down * 16;
        $this->color_width = $macroblock_across * 8;
        $this->color_height = $macroblock_down * 8;
        $this->luma = array_fill(0, $this->coded_width * $this
            ->coded_height, 0);
        $this->blue = array_fill(0, $this->color_width * $this
            ->color_height, 0);
        $this->red = array_fill(0, $this->color_width * $this->color_height, 0);
        $number = $macroblock_across * $macroblock_down;
        $this->macroblock_kind = array_fill(0, $number, -1);
        $this->macroblock_quantizer_y = array_fill(0, $number, 0);
        $this->macroblock_quantizer_blue = array_fill(0, $number, 0);
        $this->macroblock_quantizer_red = array_fill(0, $number, 0);
        $this->macroblock_larger_transform = array_fill(0, $number, 0);
        $this->macroblock_slice = array_fill(0, $number, -1);
        $this->macroblock_coded_blocks_luma = array_fill(0, $number, 0);
        $this->macroblock_coded_blocks_chroma = array_fill(0, $number, 0);
        $this->macroblock_chroma_mode = array_fill(0, $number, 0);
        $this->macroblock_smoothing_off = array_fill(0, $number, 0);
        $this->macroblock_alpha_off = array_fill(0, $number, 0);
        $this->macroblock_beta_off = array_fill(0, $number, 0);
        $this->has_values_first_value_y = array_fill(0, $number, 0);
        $this->has_values_first_value_blue = array_fill(0, $number, 0);
        $this->has_values_first_value_red = array_fill(0, $number, 0);
        $this->brightness_value_count = array_fill(0,
            (4 * $macroblock_across) * (4 * $macroblock_down), 0);
        $this->four_by_four_guess = array_fill(0,
            (4 * $macroblock_across) * (4 * $macroblock_down), 2);
        $this->blue_value_count = array_fill(0,
            (2 * $macroblock_across) * (2 * $macroblock_down), 0);
        $this->red_value_count = array_fill(0,
            (2 * $macroblock_across) * (2 * $macroblock_down), 0);
    }
}

/**
 * H264SliceDecoder decodes one I slice: macroblock syntax, intra prediction and
 * residual reconstruction (clauses 7.3.5, 8.3, 8.5).
 */
final class H264SliceDecoder
{
    /**
     * $frame stores the frame being built, which each decoded block is written
     * into.
     * @var H264Frame
     */
    public H264Frame $frame;
    /**
     * $bits stores the reader the slice takes its bits from.
     * @var H264Bits
     */
    public H264Bits $bits;
    /**
     * Z-scan index of a four by four luma block from its position in the
     * macroblock.
     */
    /**
     * Z_FROM_XY is computed inline.
     * @var mixed
     */
    public const Z_FROM_XY = null;
    /**
     * $sequence_settings stores the settings that cover the whole sequence.
     * @var H264Sps
     */
    private H264Sps $sequence_settings;
    /**
     * $picture_settings stores the settings that cover this picture.
     * @var H264Pps
     */
    private H264Pps $picture_settings;
    /**
     * $header stores what this slice said about itself.
     * @var H264SliceHeader
     */
    private H264SliceHeader $header;
    /**
     * $arithmetic_reader stores the arithmetic reader, or nothing where there
     * is none.
     * @var H264Cabac
     */
    private ?H264Cabac $arithmetic_reader = null;
    /**
     * $slice_id stores which slice of the picture this is, counted from zero.
     * Smoothing may not cross from one slice into another.
     * @var int
     */
    private int $slice_id;
    /**
     * $macroblock_x stores which macroblock across is being decoded.
     * @var int
     */
    private int $macroblock_x = 0;
    /**
     * $macroblock_y stores which macroblock down is being decoded.
     * @var int
     */
    private int $macroblock_y = 0;
    /**
     * $macroblock_position stores that macroblock's number, counting across
     * then down.
     * @var int
     */
    private int $macroblock_position = 0;
    /**
     * $quantizer_y stores the quantizer in force for brightness, which each
     * macroblock
     * may change.
     * @var int
     */
    private int $quantizer_y;
    /**
     * $quantizer_blue stores the quantizer for the blue color plane, worked
     * out from the
     * brightness one.
     * @var int
     */
    private int $quantizer_blue = 0;
    /**
     * $quantizer_red stores the quantizer for the red color plane.
     * @var int
     */
    private int $quantizer_red = 0;
    /**
     * $macroblock_kind stores what kind the macroblock being decoded is, which
     * says how
     * it was guessed and what it carries. Read by curMbType(), predIntraMode(),
     * storeIntraModes().
     * @var int
     */
    private int $macroblock_kind = 0;
    /**
     * $is_sixteen_by_sixteen stores whether this macroblock was guessed as one
     * sixteen by
     * sixteen square rather than in smaller pieces.
     * @var bool
     */
    private bool $is_sixteen_by_sixteen = false;
    /**
     * $is_plain_samples stores whether this macroblock's samples were stored
     * as they are,
     * with nothing coded.
     * @var bool
     */
    private bool $is_plain_samples = false;
    /**
     * $whole_macroblock_guess stores how the whole macroblock was guessed,
     * where it was
     * guessed as one square.
     * @var int
     */
    private int $whole_macroblock_guess = 0;
    /**
     * $chroma_mode stores how this macroblock's color was guessed.
     * @var int
     */
    private int $chroma_mode = 0;
    /**
     * $coded_blocks_luma stores which four by four groups of the brightness
     * carry
     * values.
     * @var int
     */
    private int $coded_blocks_luma = 0;
    /**
     * $coded_blocks_chroma stores whether the color planes carry values, and
     * of what
     * kind.
     * @var int
     */
    private int $coded_blocks_chroma = 0;
    /**
     * $eight_by_eight_transform stores whether this macroblock used the larger
     * transform.
     * @var bool
     */
    private bool $eight_by_eight_transform = false;
    /**
     * $bypass stores whether the coding reads this macroblock's values without
     * weighing probabilities.
     * @var bool
     */
    private bool $bypass = false;
    /**
     * $luma_value stores the values read for the brightness blocks of this
     * macroblock: sixteen blocks of sixteen values each, or four blocks of
     * sixty-four where the larger transform was used.
     * @var array
     */
    private array $luma_value = [];
    /**
     * $luma_first_value stores the first value of each brightness block, which
     * is coded
     * apart from the rest when the macroblock is one square.
     * @var array
     */
    private array $luma_first_value = [];
    /**
     * $chroma_first_value stores the first value of each color block of this
     * macroblock. Those first values are coded together, apart from
     * the rest, so they are read and scaled together. The
     * same way.
     * @var array
     */
    private array $chroma_first_value = [[], []];
    /**
     * $chroma_value stores the color values of the macroblock apart
     * from the first of each block, which are read together and kept
     * in $chroma_first_value.
     * @var array
     */
    private array $chroma_value = [[], []];
    /**
     * $ls_block_row_four stores the table brightness values are scaled by, for
     * four
     * by four
     * blocks. Worked out once for the slice.
     * @var array
     */
    private array $ls_block_row_four;
    /**
     * $level_scale_eight_by_eight stores the table an eight by eight
     * brightness block's
     * values are scaled by, worked out once for the slice from the
     * stream's own weights and the quantizer in force.
     * @var array
     */
    private array $level_scale_eight_by_eight;
    /**
     * $ls_blue stores the table the blue color values are scaled by.
     * @var array
     */
    private array $ls_blue;
    /**
     * $ls_red stores the table the red color values are scaled by.
     * @var array
     */
    private array $ls_red;
    /**
     * __construct sets up a decoder for one slice of a picture. coded against
     * against
     *
     * @param H264Frame $frame frame the samples are written into
     * @param H264Sps $sequence_settings sequence parameters the picture was
     * @param H264Pps $picture_settings picture parameters this slice was coded
     * @param H264SliceHeader $header this slice's own header
     * @param H264Bits $bits reader positioned at the slice data
     * @param int $slice_id number of this slice within the picture
     */
    public function __construct(
        H264Frame $frame, H264Sps $sequence_settings,
            H264Pps $picture_settings, H264SliceHeader $header,
        H264Bits $bits, int $slice_id
    ) {
        $this->frame = $frame;
        $this->sequence_settings = $sequence_settings;
        $this->picture_settings = $picture_settings;
        $this->header = $header;
        $this->bits = $bits;
        $this->slice_id = $slice_id;
        $this->quantizer_y = $header->slice_quantizer;
        $this->ls_block_row_four =
            H264Transform::levelScale4x4($picture_settings->scaling_values[0]);
        $this->ls_blue =
            H264Transform::levelScale4x4($picture_settings->scaling_values[1]);
        $this->ls_red =
            H264Transform::levelScale4x4($picture_settings->scaling_values[2]);
        $this->level_scale_eight_by_eight
            = H264Transform::levelScale8x8($picture_settings->scaling_values[6]
                ?? array_fill(0, 64, 16));
    }
    /**
     * xyFromZ the column and row of a block from its position in that order.
     *
     * @param int $last the last one
     * @return array what was read
     */
    private static function xyFromZ(int $last): array
    {
        return [((($last >> 2) & 1) << 1)
            + ($last & 1), ((($last >> 3) & 1) << 1)
            + (($last >> 1) & 1)];
    }
    /**
     * mbAvailable says whether a neighboring macroblock can be used for
     * prediction, which needs it to exist and to belong to the same slice.
     *
     * @param int $macroblock_x which macroblock across
     * @param int $macroblock_y which macroblock down
     * @return bool what was read
     */
    private function mbAvailable(int $macroblock_x, int $macroblock_y): bool
    {
        if ($macroblock_x < 0 || $macroblock_y < 0 || $macroblock_x >= $this
            ->frame->macroblock_across
            || $macroblock_y >= $this->frame->macroblock_down) {
            return false;
        }
        return $this->frame->macroblock_slice[$macroblock_y * $this->frame
            ->macroblock_across
            + $macroblock_x] === $this->slice_id;
    }
    /** is the luma sample at (px,py) already reconstructed and usable for
      prediction? */
    /**
     * availLuma says whether the luma block beside a given one can be used.
     *
     * @param int $pixel_x how far across the frame the pixel is
     * @param int $pixel_y how far down the frame the pixel is
     * @param int $order_position where in that order the reading is
     * @return bool what was read
     */
    private function availLuma(int $pixel_x, int $pixel_y,
        int $order_position): bool
    {
        if ($pixel_x < 0 || $pixel_y < 0 || $pixel_x >= $this->frame
            ->coded_width
            || $pixel_y >= $this->frame->coded_height) {
            return false;
        }
        $macroblock_x = $pixel_x >> 4;
        $macroblock_y = $pixel_y >> 4;
        if ($macroblock_x !== $this->macroblock_x || $macroblock_y !== $this
            ->macroblock_y) {
            return $this->mbAvailable($macroblock_x, $macroblock_y);
        }
        $block_value_four = ($pixel_x & 15) >> 2;
        $block_row_four = ($pixel_y & 15) >> 2;
        $last = (($block_row_four >> 1) << 3) +
            (($block_value_four >> 1) << 2) +
            (($block_row_four & 1) << 1) + ($block_value_four & 1);
        return $last < $order_position;
    }
    /**
     * availChroma says whether the chroma block beside a given one can be used.
     *
     * @param int $pixel_x how far across the frame the pixel is
     * @param int $pixel_y how far down the frame the pixel is
     * @return bool what was read
     */
    private function availChroma(int $pixel_x, int $pixel_y): bool
    {
        if ($pixel_x < 0 || $pixel_y < 0 || $pixel_x >= $this->frame
            ->color_width
            || $pixel_y >= $this->frame->color_height) {
            return false;
        }
        $macroblock_x = $pixel_x >> 3;
        $macroblock_y = $pixel_y >> 3;
        if ($macroblock_x === $this->macroblock_x && $macroblock_y === $this
            ->macroblock_y) {
            /* blocks inside the current macroblock are parsed in order */
            return true;
        }
        return $this->mbAvailable($macroblock_x, $macroblock_y);
    }
    /**
     * useCabac says whether this slice uses arithmetic coding rather than the
     * variable length codes.
     *
     * @return bool what was read
     */
    private function useCabac(): bool
    {
        return $this->arithmetic_reader !== null;
    }
    /**
     * Number of non-zero coefficients in the neighboring luma four by four
     * block, or -1 if unavailable.
     */
    /**
     * nnzLumaAt works out how many coefficients the neighboring luma block
     * held, which sets the code lengths for the current one.
     *
     * @param int $pixel_x how far across the frame the pixel is
     * @param int $pixel_y how far down the frame the pixel is
     * @param int $order_position where in that order the reading is
     * @return int what was read
     */
    private function nnzLumaAt(int $pixel_x, int $pixel_y,
        int $order_position): int
    {
        if (!$this->availLuma($pixel_x, $pixel_y, $order_position)) {
            return -1;
        }
        $block_x = $pixel_x >> 2;
        $block_y = $pixel_y >> 2;
        return $this->frame->brightness_value_count[$block_y * (4 * $this->frame
            ->macroblock_across) + $block_x];
    }
    /**
     * nnzChromaAt works out how many coefficients the neighboring chroma block
     * held.
     *
     * @param int $plane zero for luma, one and two for the chroma planes
     * @param int $pixel_x how far across the frame the pixel is
     * @param int $pixel_y how far down the frame the pixel is
     * @return int what was read
     */
    private function nnzChromaAt(int $plane, int $pixel_x, int $pixel_y): int
    {
        if (!$this->availChroma($pixel_x, $pixel_y)) {
            return -1;
        }
        $block_x = $pixel_x >> 2;
        $block_y = $pixel_y >> 2;
        $at = $block_y * (2 * $this->frame->macroblock_across) + $block_x;
        return $plane === 0 ? $this->frame->blue_value_count[$at] : $this->frame
            ->red_value_count[$at];
    }
    /**
     * combineNc combines the counts from the block above and the block to the
     * left into the single number the code tables are chosen by.
     *
     * @param int $left_values what the block to the left carried
     * @param int $above_values what the block above carried
     * @return int what was read
     */
    private static function combineNc(int $left_values, int $above_values): int
    {
        if ($left_values >= 0 && $above_values >= 0) {
            return ($left_values + $above_values + 1) >> 1;
        }
        if ($left_values >= 0) {
            return $left_values;
        }
        if ($above_values >= 0) {
            return $above_values;
        }
        return 0;
    }
    /**
     * decodePicture decodes every macroblock of the slice into the frame.
     */
    public function decodePicture(): void
    {
        $macroblock_across = $this->frame->macroblock_across;
        $total = $macroblock_across * $this->frame->macroblock_down;
        $this->macroblock_position = $this->header->first_macroblock_in_slice;
        if ($this->picture_settings->entropy_coding_mode) {
            $this->bits->alignToByte();
            $this->arithmetic_reader = new H264Cabac($this->bits);
            $this->arithmetic_reader->startReading($this->header
                ->slice_quantizer);
        }
        while (true) {
            if ($this->macroblock_position >= $total) {
                break;
            }
            $this->macroblock_x = $this
                ->macroblock_position % $macroblock_across;
            $this->macroblock_y = intdiv($this->macroblock_position,
                $macroblock_across);
            $this->decodeMacroblock();
            $this->frame->macroblock_slice[$this->macroblock_position] = $this
                ->slice_id;
            $this->frame->macroblock_smoothing_off[$this->macroblock_position]
                = $this->header->disable_deblocking_setting;
            $this->frame->macroblock_alpha_off[$this->macroblock_position]
                = $this->header->slice_edge_threshold_step * 2;
            $this->frame->macroblock_beta_off[$this->macroblock_position]
                = $this->header->slice_neighbor_threshold_step * 2;
            $this->macroblock_position++;
            if ($this->useCabac()) {
                if ($this->arithmetic_reader->decodeTerminate() === 1) {
                    break;
                }
            } else {
                if (!$this->bits->hasMoreToRead()) {
                    break;
                }
            }
        }
    }
    /**
     * decodeMacroblock decodes one macroblock: its type, its prediction modes,
     * its coefficients, and the samples that come out of them.
     */
    private function decodeMacroblock(): void
    {
        $this->luma_value = [];
        $this->luma_first_value = array_fill(0, 16, 0);
        $this->chroma_first_value = [array_fill(0, 4, 0), array_fill(0, 4, 0)];
        $this->chroma_value = [[], []];
        $this->eight_by_eight_transform = false;
        $this->is_plain_samples = false;
        $this->coded_blocks_luma = 0;
        $this->coded_blocks_chroma = 0;
        $macroblock_kind = $this->useCabac() ? $this->arithmetic_reader
            ->readMacroblockKind($this)
            : $this->bits->readWholeNumber();
        $this->macroblock_kind = $macroblock_kind;
        if ($macroblock_kind === 25) {
            $this->is_plain_samples = true;
            $this->decodePcm();
            return;
        }
        $four_by_four_guesses = array_fill(0, 16, 2);
        if ($macroblock_kind === 0) {
            $this->is_sixteen_by_sixteen = false;
            if ($this->picture_settings->larger_transform_allowed) {
                $this->eight_by_eight_transform = $this->useCabac()
                    ? $this->arithmetic_reader->decodeTransform8x8($this) === 1
                    : $this->bits->readBit() === 1;
            }
            $count = $this->eight_by_eight_transform ? 4 : 16;
            for ($i = 0; $i < $count; $i++) {
                $block_in_order = $this->eight_by_eight_transform ? $i * 4 : $i;
                $predicted = $this->predIntraMode($block_in_order);
                if ($this->useCabac()) {
                    $previous_flag =
                        $this->arithmetic_reader->decodePrevIntraPredModeFlag();
                    $mode = $previous_flag === 1 ? $predicted
                        : $this->arithmetic_reader->decodeRemIntraPredMode();
                    if ($previous_flag !== 1) {
                        $mode = $mode + ($mode >= $predicted ? 1 : 0);
                    }
                } else {
                    if ($this->bits->readBit() === 1) {
                        $mode = $predicted;
                    } else {
                        $remainder = $this->bits->readBits(3);
                        $mode = $remainder + ($remainder >= $predicted ? 1 : 0);
                    }
                }
                if ($this->eight_by_eight_transform) {
                    for ($k = 0; $k < 4; $k++) {
                        $four_by_four_guesses[$i * 4 + $k] = $mode;
                    }
                } else {
                    $four_by_four_guesses[$i] = $mode;
                }
                /* record immediately: later blocks in this macroblock predict
                  from it */
                $this->storeIntraModes($four_by_four_guesses);
            }
        } else {
            $this->is_sixteen_by_sixteen = true;
            $target = $macroblock_kind - 1;
            $this->whole_macroblock_guess = $target & 3;
            $this->coded_blocks_chroma = intdiv($target, 4) % 3;
            $this->coded_blocks_luma = ($target >= 12) ? 15 : 0;
        }
        $this->storeIntraModes($four_by_four_guesses);
        /* intra_chroma_pred_mode */
        $this->chroma_mode = $this->useCabac()
            ? $this->arithmetic_reader->decodeChromaPredMode($this)
            : $this->bits->readWholeNumber();
        if ($this->chroma_mode > 3) {
            throw new H264Exception('bad intra_chroma_pred_mode');
        }
        $this->frame->macroblock_chroma_mode[$this->macroblock_position] = $this
            ->chroma_mode;
        if (!$this->is_sixteen_by_sixteen) {
            if ($this->useCabac()) {
                [$this->coded_blocks_luma, $this->coded_blocks_chroma]
                    = $this->arithmetic_reader->readBlocksWithValues($this);
            } else {
                $code = $this->bits->readWholeNumber();
                if ($code > 47) {
                    throw new H264Exception('bad coded_block_pattern');
                }
                $coded_block_pattern =
                    H264Tables::CODE_TO_BLOCKS_WITH_VALUES[$code];
                $this->coded_blocks_luma = $coded_block_pattern & 15;
                $this->coded_blocks_chroma = $coded_block_pattern >> 4;
            }
            if ($this->coded_blocks_luma > 0 &&
                $this->picture_settings->larger_transform_allowed
                && !$this->eight_by_eight_transform) {
                /* transform_size_8x8_flag can also follow the CBP for Intra_NxN
                  */
                /* when it was not present earlier; that only happens for inter
                  */
                /* macroblocks, so nothing to do here. */
            }
        }
        $this->frame->macroblock_coded_blocks_luma[$this->macroblock_position] =
            $this->coded_blocks_luma;
        $this->frame->macroblock_coded_blocks_chroma[$this
            ->macroblock_position] = $this->coded_blocks_chroma;
        $this->frame->macroblock_larger_transform[$this->macroblock_position] =
            $this->eight_by_eight_transform ? 1 : 0;
        $this->frame->macroblock_kind[$this->macroblock_position] = $this
            ->macroblock_kind;
        if ($this->coded_blocks_luma > 0 || $this
            ->coded_blocks_chroma > 0 || $this->is_sixteen_by_sixteen) {
            $delta = $this->useCabac() ? $this->arithmetic_reader
                ->readQuantizerChange()
                : $this->bits->readSignedNumber();
            if ($this->useCabac()) {
                $this->arithmetic_reader->previous_changed_quantizer =
                    ($delta !== 0);
            }
            if ($delta !== 0) {
                $this->quantizer_y = (($this
                    ->quantizer_y + $delta + 52 + 52) % 52);
            }
            $this->parseResidual();
        } else {
            if ($this->useCabac()) {
                $this->arithmetic_reader->previous_changed_quantizer = false;
            }
            $this->clearNnz();
        }
        $this->quantizer_blue = H264Scan::CHROMA_QP[$this
            ->holdQuantizerInRange($this
            ->quantizer_y
            + $this->picture_settings->chroma_quantizer_index_offset)];
        $this->quantizer_red = H264Scan::CHROMA_QP[$this
            ->holdQuantizerInRange($this
            ->quantizer_y
            + $this->picture_settings->second_chroma_quantizer_index_offset)];
        $this->bypass =
            $this->sequence_settings->lossless_blocks_allowed
            && $this->quantizer_y === 0;
        $this->frame->macroblock_quantizer_y[$this->macroblock_position] = $this
            ->quantizer_y;
        $this->frame->macroblock_quantizer_blue[$this->macroblock_position] =
            $this->quantizer_blue;
        $this->frame->macroblock_quantizer_red[$this->macroblock_position] =
            $this->quantizer_red;
        $this->reconstruct();
    }
    /**
     * holdQuantizerInRange holds a quantizer index inside the range the format
     * allows.
     *
     * @param int $value the value read
     * @return int what was read
     */
    private function holdQuantizerInRange(int $value): int
    {
        return $value < 0 ? 0 : ($value > 51 ? 51 : $value);
    }
    /**
     * storeIntraModes records the prediction modes of a macroblock so its
     * neighbors can predict their own from them.
     *
     * @param array $modes the ways the blocks are guessed
     */
    private function storeIntraModes(array $modes): void
    {
        $stride = 4 * $this->frame->macroblock_across;
        $block_start_x = $this->macroblock_x * 4;
        $block_start_y = $this->macroblock_y * 4;
        for ($last = 0; $last < 16; $last++) {
            [$block_value_four, $block_row_four] =
                self::xyFromZ($last);
            $this->frame
                ->four_by_four_guess[($block_start_y +
                    $block_row_four) * $stride + $block_start_x +
                    $block_value_four] =
                ($this->macroblock_kind === 0) ? $modes[$last] : 2;
        }
    }
    /**
     * predIntraMode works out which way a block is expected to be guessed, from
     * the ways its neighbors were guessed. A stream writes only the difference
     * from this expectation, so it must be worked out the same way on both
     * sides. The standard calls this clause 8.3.1.1. them
     *
     * @param int $block_in_order which block, in the order the format visits
     * @return int what was read
     */
    private function predIntraMode(int $block_in_order): int
    {
        [$block_value_four, $block_row_four] =
            self::xyFromZ($block_in_order);
        $pixel_x = $this->macroblock_x * 16 + $block_value_four * 4;
        $pixel_y = $this->macroblock_y * 16 + $block_row_four * 4;
        $stride = 4 * $this->frame->macroblock_across;
        $mode_a = 2;
        $mode_b = 2;
        $ok_a = $this->availLuma($pixel_x - 1, $pixel_y, $block_in_order);
        $ok_b = $this->availLuma($pixel_x, $pixel_y - 1, $block_in_order);
        if (!$ok_a || !$ok_b) {
            return 2;
        }
        $a_macroblock = (($pixel_y) >> 4) * $this->frame
            ->macroblock_across + (($pixel_x - 1) >> 4);
        $b_macroblock = ((($pixel_y - 1) >> 4) * $this->frame
            ->macroblock_across) + ($pixel_x >> 4);
        $a_is_wide_n_n = ($this->frame->macroblock_kind[$a_macroblock] === 0)
            || ($a_macroblock === $this->macroblock_position && $this
                ->macroblock_kind === 0);
        $b_is_wide_n_n = ($this->frame->macroblock_kind[$b_macroblock] === 0)
            || ($b_macroblock === $this->macroblock_position && $this
                ->macroblock_kind === 0);
        if ($a_is_wide_n_n) {
            $mode_a = $this->frame->four_by_four_guess[($pixel_y >> 2) *
                $stride +
                (($pixel_x - 1) >> 2)];
        }
        if ($b_is_wide_n_n) {
            $mode_b = $this->frame->four_by_four_guess[(($pixel_y - 1) >> 2) *
                $stride +
                ($pixel_x >> 2)];
        }
        return min($mode_a, $mode_b);
    }
    /**
     * clearNnz forgets the coefficient counts of a macroblock, used when it
     * carries none.
     */
    private function clearNnz(): void
    {
        $sum_four = 4 * $this->frame->macroblock_across;
        $sum_two = 2 * $this->frame->macroblock_across;
        for ($down = 0; $down < 4; $down++) {
            for ($across = 0; $across < 4; $across++) {
                $this->frame->brightness_value_count[($this->macroblock_y * 4 +
                    $down) * $sum_four +
                    $this->macroblock_x * 4
                    + $across] = 0;
            }
        }
        for ($down = 0; $down < 2; $down++) {
            for ($across = 0; $across < 2; $across++) {
                $i = ($this->macroblock_y * 2 + $down) * $sum_two + $this
                    ->macroblock_x * 2 +
                    $across;
                $this->frame->blue_value_count[$i] = 0;
                $this->frame->red_value_count[$i] = 0;
            }
        }
    }
    /**
     * setNnzLuma records how many coefficients a luma block held. them
     *
     * @param int $block_in_order which block, in the order the format visits
     * @param int $value the value read
     */
    private function setNnzLuma(int $block_in_order, int $value): void
    {
        [$block_value_four, $block_row_four] =
            self::xyFromZ($block_in_order);
        $sum_four = 4 * $this->frame->macroblock_across;
        $this->frame->brightness_value_count[($this
            ->macroblock_y * 4 + $block_row_four) * $sum_four + $this
                ->macroblock_x *
            4 + $block_value_four]
            = $value;
    }
    /**
     * setNnzChroma records how many coefficients a chroma block held.
     *
     * @param int $plane zero for luma, one and two for the chroma planes
     * @param int $block_at which block within the macroblock
     * @param int $value the value read
     */
    private function setNnzChroma(int $plane, int $block_at, int $value): void
    {
        $sum_two = 2 * $this->frame->macroblock_across;
        $i = ($this->macroblock_y * 2 + ($block_at >> 1)) * $sum_two + $this
            ->macroblock_x * 2 +
            ($block_at & 1);
        if ($plane === 0) {
            $this->frame->blue_value_count[$i] = $value;
        } else {
            $this->frame->red_value_count[$i] = $value;
        }
    }
    /**
     * decodePcm reads a macroblock whose samples are stored as they are, with
     * no prediction and no transform.
     */
    private function decodePcm(): void
    {
        $this->frame->macroblock_kind[$this->macroblock_position] = 25;
        /* 8.7.2.2: an macroblocks whose samples are stored as they are
           macroblock contributes qP = 0 to the deblocking */
        /* filter, whatever the running slice QP happens to be */
        $this->frame->macroblock_quantizer_y[$this->macroblock_position] = 0;
        $this->frame->macroblock_coded_blocks_luma[$this
            ->macroblock_position] = 15;
        $this->frame->macroblock_coded_blocks_chroma[$this
            ->macroblock_position] = 2;
        if ($this->useCabac()) {
            $this->bits->position = $this->arithmetic_reader->pcmResyncBitPos();
        }
        $this->bits->alignToByte();
        $wide = $this->frame->coded_width;
        for ($down = 0; $down < 16; $down++) {
            for ($across = 0; $across < 16; $across++) {
                $this->frame->luma[($this->macroblock_y * 16 + $down) * $wide
                    + $this->macroblock_x * 16
                    + $across] = $this->bits->readBits(8);
            }
        }
        $code_word = $this->frame->color_width;
        foreach ([0, 1] as $plane) {
            for ($down = 0; $down < 8; $down++) {
                for ($across = 0; $across < 8; $across++) {
                    $value = $this->bits->readBits(8);
                    $i = ($this->macroblock_y * 8 + $down) * $code_word + $this
                        ->macroblock_x * 8
                        + $across;
                    if ($plane === 0) {
                        $this->frame->blue[$i] = $value;
                    } else {
                        $this->frame->red[$i] = $value;
                    }
                }
            }
        }
        for ($last = 0; $last < 16; $last++) {
            $this->setNnzLuma($last, 16);
        }
        for ($bits = 0; $bits < 4; $bits++) {
            $this->setNnzChroma(0, $bits, 16);
            $this->setNnzChroma(1, $bits, 16);
        }
        $this->frame->has_values_first_value_y[$this->macroblock_position] = 1;
        $this->frame->has_values_first_value_blue[$this
            ->macroblock_position] = 1;
        $this->frame->has_values_first_value_red[$this
            ->macroblock_position] = 1;
        $this->frame->macroblock_quantizer_blue[$this->macroblock_position]
            = H264Scan::CHROMA_QP[
                $this->holdQuantizerInRange($this->picture_settings
                    ->chroma_quantizer_index_offset)];
        $this->frame->macroblock_quantizer_red[$this->macroblock_position]
            = H264Scan::CHROMA_QP[
                $this->holdQuantizerInRange($this->picture_settings
                    ->second_chroma_quantizer_index_offset)];
        if ($this->useCabac()) {
            $this->arithmetic_reader->startAfresh();
        }
    }
    /**
     * parseResidual reads the coefficients of a macroblock.
     */
    private function parseResidual(): void
    {
        if ($this->is_sixteen_by_sixteen) {
            $neighbor_count = $this->lumaNcForBlock(0);
            if ($this->useCabac()) {
                [$levels, $number]
                    = $this->arithmetic_reader->residual($this, 0, 16, 0, 0, 0);
            } else {
                [$levels, $number] = H264Cavlc::residual($this->bits,
                    $neighbor_count, 16, 0);
            }
            $this->luma_first_value = $this->blockOrderToRows($levels);
            $this->frame->has_values_first_value_y[$this->macroblock_position] =
                $number > 0 ? 1 : 0;
        } else {
            $this->frame->has_values_first_value_y[$this
                ->macroblock_position] = 0;
        }
        if ($this->eight_by_eight_transform) {
            $this->parseLuma8x8();
        } else {
            $this->parseLuma4x4();
        }
        $this->parseChroma();
    }
    /**
     * lumaNcForBlock the neighbor count that picks the code table for one luma
     * block. them
     *
     * @param int $block_in_order which block, in the order the format visits
     * @return int what was read
     */
    private function lumaNcForBlock(int $block_in_order): int
    {
        [$block_value_four, $block_row_four] =
            self::xyFromZ($block_in_order);
        $pixel_x = $this->macroblock_x * 16 + $block_value_four * 4;
        $pixel_y = $this->macroblock_y * 16 + $block_row_four * 4;
        $left_values = $this->nnzLumaAt($pixel_x - 1, $pixel_y,
            $block_in_order);
        $above_values = $this->nnzLumaAt($pixel_x, $pixel_y - 1,
            $block_in_order);
        return self::combineNc($left_values, $above_values);
    }
    /**
     * parseLuma4x4 reads the coefficients of the sixteen small luma blocks.
     */
    private function parseLuma4x4(): void
    {
        $max_value = $this->is_sixteen_by_sixteen ? 15 : 16;
        $start = $this->is_sixteen_by_sixteen ? 1 : 0;
        for ($eight_by_eight = 0; $eight_by_eight < 4; $eight_by_eight++) {
            for ($four_by_four = 0; $four_by_four < 4; $four_by_four++) {
                $block_in_order = $eight_by_eight * 4 + $four_by_four;
                if (($this->coded_blocks_luma >> $eight_by_eight) & 1) {
                    $neighbor_count = $this->lumaNcForBlock($block_in_order);
                    if ($this->useCabac()) {
                        $block_kind = $this->is_sixteen_by_sixteen ? 1 : 2;
                        [$levels, $number] = $this->arithmetic_reader->residual(
                            $this, $block_kind, $max_value, $start,
                                $block_in_order, 0);
                    } else {
                        [$levels, $number] = H264Cavlc::residual(
                            $this->bits, $neighbor_count, $max_value, $start);
                    }
                    $this->luma_value[$block_in_order] =
                        $this->blockOrderToRows($levels);
                    $this->setNnzLuma($block_in_order, $number);
                } else {
                    $this->luma_value[$block_in_order] = array_fill(0, 16, 0);
                    $this->setNnzLuma($block_in_order, 0);
                }
            }
        }
    }
    /**
     * parseLuma8x8 reads the coefficients of the four large luma blocks.
     */
    private function parseLuma8x8(): void
    {
        for ($eight_by_eight = 0; $eight_by_eight < 4; $eight_by_eight++) {
            if (!(($this->coded_blocks_luma >> $eight_by_eight) & 1)) {
                $this->luma_value[$eight_by_eight] = array_fill(0, 64, 0);
                for ($k = 0; $k < 4; $k++) {
                    $this->setNnzLuma($eight_by_eight * 4 + $k, 0);
                }
                continue;
            }
            $scan = array_fill(0, 64, 0);
            if ($this->useCabac()) {
                [$levels, $number]
                    = $this->arithmetic_reader->residual($this, 5, 64, 0,
                        $eight_by_eight, 0);
                $scan = $levels;
                for ($k = 0; $k < 4; $k++) {
                    $this->setNnzLuma($eight_by_eight * 4 + $k, $number);
                }
            } else {
                for ($four_by_four = 0; $four_by_four < 4; $four_by_four++) {
                    $block_in_order = $eight_by_eight * 4 + $four_by_four;
                    $neighbor_count = $this->lumaNcForBlock($block_in_order);
                    [$levels, $number]
                        = H264Cavlc::residual($this->bits, $neighbor_count,
                            16, 0);
                    for ($k = 0; $k < 16; $k++) {
                        $scan[4 * $k + $four_by_four] = $levels[$k];
                    }
                    $this->setNnzLuma($block_in_order, $number);
                }
            }
            $raster = array_fill(0, 64, 0);
            foreach (H264Scan::ZZ8 as $block_order => $run) {
                $raster[$run] = $scan[$block_order];
            }
            $this->luma_value[$eight_by_eight] = $raster;
        }
    }
    /**
     * parseChroma reads the chroma coefficients, whose flat terms are coded
     * separately from the rest.
     */
    private function parseChroma(): void
    {
        for ($plane = 0; $plane < 2; $plane++) {
            $this->chroma_value[$plane] = [];
            for ($bits = 0; $bits < 4; $bits++) {
                $this->chroma_value[$plane][$bits] = array_fill(0, 16, 0);
            }
        }
        if ($this->coded_blocks_chroma === 0) {
            for ($plane = 0; $plane < 2; $plane++) {
                for ($bits = 0; $bits < 4; $bits++) {
                    $this->setNnzChroma($plane, $bits, 0);
                }
            }
            $this->frame->has_values_first_value_blue[$this
                ->macroblock_position] = 0;
            $this->frame->has_values_first_value_red[$this
                ->macroblock_position] = 0;
            return;
        }
        for ($plane = 0; $plane < 2; $plane++) {
            if ($this->useCabac()) {
                [$levels, $number]
                    = $this->arithmetic_reader->residual($this, 3, 4, 0, 0,
                        $plane);
            } else {
                [$levels, $number] = H264Cavlc::residual($this->bits, -1, 4, 0);
            }
            $this->chroma_first_value[$plane] = $levels;
            if ($plane === 0) {
                $this->frame->has_values_first_value_blue[$this
                    ->macroblock_position] = $number > 0 ? 1 : 0;
            } else {
                $this->frame->has_values_first_value_red[$this
                    ->macroblock_position] = $number > 0 ? 1 : 0;
            }
        }
        for ($plane = 0; $plane < 2; $plane++) {
            for ($bits = 0; $bits < 4; $bits++) {
                if ($this->coded_blocks_chroma === 2) {
                    $pixel_x = $this->macroblock_x * 8 + ($bits & 1) * 4;
                    $pixel_y = $this->macroblock_y * 8 + ($bits >> 1) * 4;
                    $left_values = $this->nnzChromaAt($plane, $pixel_x - 1,
                        $pixel_y);
                    $above_values = $this->nnzChromaAt($plane, $pixel_x,
                        $pixel_y - 1);
                    $neighbor_count = self::combineNc($left_values,
                        $above_values);
                    if ($this->useCabac()) {
                        [$levels, $number] = $this->arithmetic_reader->residual(
                            $this, 4, 15, 1, $bits, $plane);
                    } else {
                        [$levels, $number]
                            = H264Cavlc::residual($this->bits, $neighbor_count,
                                15, 1);
                    }
                    $this->chroma_value[$plane][$bits]
                        = $this->blockOrderToRows($levels);
                    $this->setNnzChroma($plane, $bits, $number);
                } else {
                    $this->setNnzChroma($plane, $bits, 0);
                }
            }
        }
    }
    /**
     * blockOrderToRows puts the sixteen values of a four by four block
     * back into rows. A stream writes them in the order the format
     * visits a block's places, which runs corner to corner, so they
     * have to be put back before the block is drawn.
     *
     * @param array $levels scan-order coefficients
     * @return array what was read
     */
    private function blockOrderToRows(array $levels): array
    {
        $written = array_fill(0, 16, 0);
        foreach (H264Scan::ZZ4 as $block_order => $run) {
            $written[$run] = $levels[$block_order] ?? 0;
        }
        return $written;
    }
    /**
     * reconstruct turns the prediction and the coefficients of a macroblock
     * into samples in the frame.
     */
    private function reconstruct(): void
    {
        if ($this->is_sixteen_by_sixteen) {
            $this->reconstructI16();
        } elseif ($this->eight_by_eight_transform) {
            $this->reconstructI8x8();
        } else {
            $this->reconstructI4x4();
        }
        $this->reconstructChroma();
    }
    /**
     * bypassAccumulate adds up the stored differences for a block whose
     * samples were kept as they are, with no transform. The standard
     * calls this clause 8.5.15. With the transform bypassed, vertically and
     * horizontally predicted blocks carry storing each value as a difference
     * from the one beside it residuals that have to be accumulated along the
     * prediction direction. Other prediction modes leave the residual
     * untouched.
     *
     * @param array $run how many values are skipped
     * @param int $number which one
     * @param int $mode which way the block is guessed from its neighbors
     * @param bool $chroma whether this is a color plane
     * @return array what was read
     */
    private static function bypassAccumulate(array $run, int $number, int $mode,
        bool $chroma): array
    {
        if ($chroma) {
            if ($mode === 1) {
                $across = true;
            } elseif ($mode === 2) {
                $across = false;
            } else {
                return $run;
            }
        } else {
            if ($mode === 1) {
                $across = true;
            } elseif ($mode === 0) {
                $across = false;
            } else {
                return $run;
            }
        }
        if ($across) {
            for ($i = 0; $i < $number; $i++) {
                for ($j = 1; $j < $number; $j++) {
                    $run[$i * $number + $j] += $run[$i * $number + $j - 1];
                }
            }
        } else {
            for ($j = 0; $j < $number; $j++) {
                for ($i = 1; $i < $number; $i++) {
                    $run[$i * $number + $j] += $run[($i - 1) * $number + $j];
                }
            }
        }
        return $run;
    }
    /**
     * holdInsideByte holds a sample inside the range a byte can carry.
     *
     * @param int $value the value read
     * @return int what was read
     */
    private static function holdInsideByte(int $value): int
    {
        return $value < 0 ? 0 : ($value > 255 ? 255 : $value);
    }
    /**
     * reconstructI4x4 rebuilds a macroblock predicted in four sample squares.
     */
    private function reconstructI4x4(): void
    {
        $wide = $this->frame->coded_width;
        $stride = 4 * $this->frame->macroblock_across;
        for ($last = 0; $last < 16; $last++) {
            [$block_value_four, $block_row_four] =
                self::xyFromZ($last);
            $pixel_x = $this->macroblock_x * 16 + $block_value_four * 4;
            $pixel_y = $this->macroblock_y * 16 + $block_row_four * 4;
            $mode = $this->frame->four_by_four_guess[($pixel_y >> 2) * $stride +
                ($pixel_x >> 2)];
            $predicted = H264Intra::pred4x4(
                $mode, $this->frame->luma, $wide, $pixel_x, $pixel_y,
                $this->availLuma($pixel_x - 1, $pixel_y, $last),
                $this->availLuma($pixel_x, $pixel_y - 1, $last),
                $this->availLuma($pixel_x - 1, $pixel_y - 1, $last),
                $this->availLuma($pixel_x + 4, $pixel_y - 1, $last)
            );
            $value = $this->luma_value[$last] ?? array_fill(0, 16, 0);
            $run = null;
            if ($this->bypass) {
                $run = self::bypassAccumulate($value, 4, $mode, false);
            } elseif ($this->hasNonZero($value)) {
                $payload = H264Transform::dequant4x4(
                    $value, $this->ls_block_row_four, $this->quantizer_y,
                        false);
                $run = H264Transform::inverse4x4($payload);
            }
            for ($down = 0; $down < 4; $down++) {
                $row = ($pixel_y + $down) * $wide + $pixel_x;
                for ($across = 0; $across < 4; $across++) {
                    $value = $predicted[$down * 4 + $across]
                        + ($run === null ? 0 : $run[$down * 4 + $across]);
                    $this->frame->luma[$row
                        + $across] = self::holdInsideByte($value);
                }
            }
        }
    }
    /**
     * reconstructI8x8 rebuilds a macroblock predicted in eight sample squares.
     */
    private function reconstructI8x8(): void
    {
        $wide = $this->frame->coded_width;
        $stride = 4 * $this->frame->macroblock_across;
        for ($eight_by_eight = 0; $eight_by_eight < 4; $eight_by_eight++) {
            $value_eight = $eight_by_eight & 1;
            $row_eight = $eight_by_eight >> 1;
            $pixel_x = $this->macroblock_x * 16 + $value_eight * 8;
            $pixel_y = $this->macroblock_y * 16 + $row_eight * 8;
            $last = $eight_by_eight * 4;
            $mode = $this->frame->four_by_four_guess[($pixel_y >> 2) * $stride +
                ($pixel_x >> 2)];
            $predicted = H264Intra::pred8x8(
                $mode, $this->frame->luma, $wide, $pixel_x, $pixel_y,
                $this->availLuma($pixel_x - 1, $pixel_y, $last),
                $this->availLuma($pixel_x, $pixel_y - 1, $last),
                $this->availLuma($pixel_x - 1, $pixel_y - 1, $last),
                $this->availLuma($pixel_x + 8, $pixel_y - 1, $last)
            );
            $value = $this->luma_value[$eight_by_eight] ?? array_fill(0, 64, 0);
            $run = null;
            if ($this->bypass) {
                $run = self::bypassAccumulate($value, 8, $mode, false);
            } elseif ($this->hasNonZero($value)) {
                $payload = H264Transform::dequant8x8(
                    $value, $this->level_scale_eight_by_eight, $this
                        ->quantizer_y);
                $run = H264Transform::inverse8x8($payload);
            }
            for ($down = 0; $down < 8; $down++) {
                $row = ($pixel_y + $down) * $wide + $pixel_x;
                for ($across = 0; $across < 8; $across++) {
                    $value = $predicted[$down * 8 + $across]
                        + ($run === null ? 0 : $run[$down * 8 + $across]);
                    $this->frame->luma[$row
                        + $across] = self::holdInsideByte($value);
                }
            }
        }
    }
    /**
     * reconstructI16 rebuilds a macroblock predicted as one sixteen sample
     * square, whose flat terms are transformed together.
     */
    private function reconstructI16(): void
    {
        $wide = $this->frame->coded_width;
        $pixel_start_x = $this->macroblock_x * 16;
        $pixel_start_y = $this->macroblock_y * 16;
        $predicted = H264Intra::pred16x16(
            $this->whole_macroblock_guess, $this->frame->luma, $wide,
                $pixel_start_x, $pixel_start_y,
            $this->mbAvailable($this->macroblock_x - 1, $this->macroblock_y),
            $this->mbAvailable($this->macroblock_x, $this->macroblock_y - 1),
            $this->mbAvailable($this->macroblock_x - 1, $this->macroblock_y - 1)
        );
        $first_value = $this->bypass
            ? $this->luma_first_value
            : H264Transform::brightnessFirstValues($this->luma_first_value,
                $this
                ->ls_block_row_four, $this->quantizer_y);
        if ($this->bypass) {
            $result = array_fill(0, 256, 0);
            for ($last = 0; $last < 16; $last++) {
                [$block_value_four, $block_row_four] =
                    self::xyFromZ($last);
                $value = $this->luma_value[$last] ?? array_fill(0, 16, 0);
                $value[0] = $first_value[$block_row_four * 4 +
                    $block_value_four];
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        $result[($block_row_four * 4 + $down) * 16 +
                            $block_value_four * 4 +
                            $across]
                            = $value[$down * 4 + $across];
                    }
                }
            }
            $result = self::bypassAccumulate($result, 16, $this
                ->whole_macroblock_guess,
                false);
            for ($down = 0; $down < 16; $down++) {
                $row = ($pixel_start_y + $down) * $wide + $pixel_start_x;
                for ($across = 0; $across < 16; $across++) {
                    $spot = $down * 16 + $across;
                    $this->frame->luma[$row + $across]
                        = self::holdInsideByte($predicted[$spot] + $result
                            [$spot]);
                }
            }
            return;
        }
        for ($last = 0; $last < 16; $last++) {
            [$block_value_four, $block_row_four] =
                self::xyFromZ($last);
            $value = $this->luma_value[$last] ?? array_fill(0, 16, 0);
            $payload = H264Transform::dequant4x4(
                $value, $this->ls_block_row_four, $this->quantizer_y, true);
            $payload[0] = $first_value[$block_row_four * 4 +
                $block_value_four];
            $run = H264Transform::inverse4x4($payload);
            $pixel_x = $pixel_start_x + $block_value_four * 4;
            $pixel_y = $pixel_start_y + $block_row_four * 4;
            for ($down = 0; $down < 4; $down++) {
                $row = ($pixel_y + $down) * $wide + $pixel_x;
                $prow =
                    ($block_row_four * 4 + $down) * 16 +
                        $block_value_four * 4;
                for ($across = 0; $across < 4; $across++) {
                    $this->frame->luma[$row + $across]
                        = self::holdInsideByte(
                            $predicted[$prow + $across] + $run[$down * 4 +
                                $across]);
                }
            }
        }
    }
    /**
     * reconstructChroma rebuilds the two chroma planes of a macroblock.
     */
    private function reconstructChroma(): void
    {
        $code_word = $this->frame->color_width;
        $pixel_start_x = $this->macroblock_x * 8;
        $pixel_start_y = $this->macroblock_y * 8;
        $left_there = $this->mbAvailable($this->macroblock_x - 1, $this
            ->macroblock_y);
        $above_there = $this->mbAvailable($this->macroblock_x, $this
            ->macroblock_y - 1);
        $above_left_there = $this->mbAvailable($this->macroblock_x - 1, $this
            ->macroblock_y - 1);
        for ($plane = 0; $plane < 2; $plane++) {
            $quantizer = $plane === 0 ? $this->quantizer_blue : $this
                ->quantizer_red;
            $level_scale = $plane === 0 ? $this->ls_blue : $this->ls_red;
            $predicted = ($plane === 0)
                ? H264Intra::predChroma(
                    $this->chroma_mode, $this->frame->blue, $code_word,
                        $pixel_start_x,
                        $pixel_start_y, $left_there,
                    $above_there,
                    $above_left_there)
                : H264Intra::predChroma(
                    $this->chroma_mode, $this->frame->red, $code_word,
                        $pixel_start_x,
                        $pixel_start_y, $left_there,
                        $above_there,
                    $above_left_there);
            if ($this->coded_blocks_chroma === 0) {
                $first_value = [0, 0, 0, 0];
            } elseif ($this->bypass) {
                $first_value = $this->chroma_first_value[$plane];
            } else {
                $first_value = H264Transform::chromaDc(
                    $this->chroma_first_value[$plane], $level_scale,
                        $quantizer);
            }
            $written = [];
            if ($this->bypass) {
                $result = array_fill(0, 64, 0);
                for ($bits = 0; $bits < 4; $bits++) {
                    $value
                        = $this->chroma_value[$plane][$bits]
                            ?? array_fill(0, 16, 0);
                    $value[0] = $first_value[$bits];
                    $block_x = ($bits & 1) * 4;
                    $block_y = ($bits >> 1) * 4;
                    for ($down = 0; $down < 4; $down++) {
                        for ($across = 0; $across < 4; $across++) {
                            $result[($block_y + $down) * 8 + $block_x + $across]
                                = $value[$down * 4 + $across];
                        }
                    }
                }
                $result = self::bypassAccumulate($result, 8, $this->chroma_mode,
                    true);
                for ($i = 0; $i < 64; $i++) {
                    $written[$i] =
                        self::holdInsideByte($predicted[$i] + $result[$i]);
                }
            } else {
            for ($bits = 0; $bits < 4; $bits++) {
                $value
                    = $this->chroma_value[$plane][$bits] ?? array_fill(0, 16,
                        0);
                $payload = H264Transform::dequant4x4($value, $level_scale,
                    $quantizer, true);
                $payload[0] = $first_value[$bits];
                $run = H264Transform::inverse4x4($payload);
                $block_x = ($bits & 1) * 4;
                $block_y = ($bits >> 1) * 4;
                for ($down = 0; $down < 4; $down++) {
                    for ($across = 0; $across < 4; $across++) {
                        $written[($block_y + $down) * 8 + $block_x + $across] =
                            self::holdInsideByte($predicted
                                [($block_y + $down) * 8
                                + $block_x + $across]
                                + $run[$down * 4 + $across]);
                    }
                }
            }
            }
            for ($down = 0; $down < 8; $down++) {
                $row = ($pixel_start_y + $down) * $code_word + $pixel_start_x;
                for ($across = 0; $across < 8; $across++) {
                    if ($plane === 0) {
                        $this->frame->blue[$row + $across]
                            = $written[$down * 8 + $across];
                    } else {
                        $this->frame->red[$row + $across]
                            = $written[$down * 8 + $across];
                    }
                }
            }
        }
    }
    /**
     * hasNonZero says whether a block holds any coefficient at all.
     *
     * @param array $amount how much
     * @return bool what was read
     */
    private function hasNonZero(array $amount): bool
    {
        foreach ($amount as $value) {
            if ($value !== 0) {
                return true;
            }
        }
        return false;
    }
    /**
     * frameAt the frame being decoded.
     *
     * @return H264Frame what was read
     */
    public function frameAt(): H264Frame { return $this->frame; }
    /**
     * macroblockAcross column of the macroblock being decoded.
     *
     * @return int what was read
     */
    public function macroblockAcross(): int { return $this->macroblock_x; }
    /**
     * macroblockDown row of the macroblock being decoded.
     *
     * @return int what was read
     */
    public function macroblockDown(): int { return $this->macroblock_y; }
    /**
     * mbAvail says whether a neighboring macroblock can be used.
     *
     * @param int $across how far across the block
     * @param int $down how far down the block
     * @param mixed $down how far down the block
     * @return bool what was read
     */
    public function mbAvail(int $across,
        int $down): bool { return $this->mbAvailable($across, $down); }
    /**
     * nnzLumaBlk coefficient count of a luma block, for a neighbor to use.
     *
     * @param int $pixel_x how far across the frame the pixel is
     * @param int $pixel_y how far down the frame the pixel is
     * @param int $order_position where in that order the reading is
     * @param mixed $pixel_y how far down the frame the pixel is
     * @param mixed $order_position where in that order the reading is
     * @return int what was read
     */
    public function nnzLumaBlk(int $pixel_x, int $pixel_y,
        int $order_position): int { return $this->nnzLumaAt($pixel_x, $pixel_y,
            $order_position); }
    /**
     * nnzChromaBlk coefficient count of a chroma block, for a neighbor to use.
     *
     * @param int $plane zero for luma, one and two for the chroma planes
     * @param int $pixel_x how far across the frame the pixel is
     * @param int $pixel_y how far down the frame the pixel is
     * @param mixed $pixel_x how far across the frame the pixel is
     * @param mixed $pixel_y how far down the frame the pixel is
     * @return int what was read
     */
    public function nnzChromaBlk(int $plane, int $pixel_x,
        int $pixel_y): int { return $this->nnzChromaAt($plane, $pixel_x,
            $pixel_y); }
}

/**
 * H264Deblock deblocking filter, clause 8.7. This decoder only handles I
 * slices, so every macroblock is intra: the boundary strength is 4 on
 * macroblock edges and 3 on internal edges, and no motion-vector or reference-
 * index comparison is needed.
 */
final class H264Deblock
{
    /**
     * holdBetween holds a value between a lower and an upper bound.
     *
     * @param int $lo the lower end
     * @param int $hi the upper end
     * @param int $value the value read
     * @return int what was read
     */
    private static function holdBetween(int $lo, int $hi, int $value): int
    {
        return $value < $lo ? $lo : ($value > $hi ? $hi : $value);
    }
    /**
     * holdInsideSample holds a sample inside the range a byte can carry.
     *
     * @param int $value the value read
     * @return int what was read
     */
    private static function holdInsideSample(int $value): int
    {
        return $value < 0 ? 0 : ($value > 255 ? 255 : $value);
    }
    /**
     * applyOffsets smooths the sample values either side of every block edge of
     * a frame.
     *
     * @param H264Frame $frame the stored bytes of one frame
     */
    public static function applyOffsets(H264Frame $frame): void
    {
        for ($macroblock_y = 0; $macroblock_y < $frame
            ->macroblock_down; $macroblock_y++) {
            for ($macroblock_x = 0; $macroblock_x < $frame
                ->macroblock_across; $macroblock_x++) {
                $position = $macroblock_y * $frame
                    ->macroblock_across + $macroblock_x;
                if ($frame->macroblock_slice[$position] < 0) {
                    continue;
                }
                $setting = $frame->macroblock_smoothing_off[$position];
                if ($setting === 1) {
                    continue;
                }
                $same_slice_only = ($setting === 2);
                $held_eight = $frame
                    ->macroblock_larger_transform[$position] === 1;
                /* vertical edges, left to right */
                for ($entry = 0; $entry < 4; $entry++) {
                    if ($held_eight && ($entry === 1 || $entry === 3)) {
                        continue;
                    }
                    if ($entry === 0) {
                        if ($macroblock_x === 0) {
                            continue;
                        }
                        $left = $position - 1;
                        if ($frame->macroblock_slice[$left] < 0) {
                            continue;
                        }
                        if ($same_slice_only
                            && $frame->macroblock_slice[$left]
                            !== $frame->macroblock_slice[$position]) {
                            continue;
                        }
                        self::edgeLuma(
                            $frame, $position, $left, $macroblock_x * 16,
                                $macroblock_y * 16,
                                true, 0,
                            4);
                        self::edgeChroma(
                            $frame, $position, $left, $macroblock_x * 8,
                                $macroblock_y * 8, true, 0,
                            4);
                    } else {
                        self::edgeLuma($frame, $position, $position,
                            $macroblock_x * 16
                            + $entry * 4, $macroblock_y * 16, true, 0, 3);
                        if ($entry === 2) {
                            self::edgeChroma($frame, $position, $position,
                                $macroblock_x * 8
                                + 4, $macroblock_y * 8, true, 0, 3);
                        }
                    }
                }
                /* horizontal edges, top to bottom */
                for ($entry = 0; $entry < 4; $entry++) {
                    if ($held_eight && ($entry === 1 || $entry === 3)) {
                        continue;
                    }
                    if ($entry === 0) {
                        if ($macroblock_y === 0) {
                            continue;
                        }
                        $up = $position - $frame->macroblock_across;
                        if ($frame->macroblock_slice[$up] < 0) {
                            continue;
                        }
                        if ($same_slice_only
                            && $frame->macroblock_slice[$up]
                            !== $frame->macroblock_slice[$position]) {
                            continue;
                        }
                        self::edgeLuma(
                            $frame, $position, $up, $macroblock_x * 16,
                                $macroblock_y * 16, false,
                                0,
                            4);
                        self::edgeChroma(
                            $frame, $position, $up, $macroblock_x * 8,
                                $macroblock_y * 8, false, 0,
                            4);
                    } else {
                        self::edgeLuma($frame, $position, $position,
                            $macroblock_x * 16,
                            $macroblock_y * 16
                            + $entry * 4, false, 0, 3);
                        if ($entry === 2) {
                            self::edgeChroma(
                                $frame, $position, $position,
                                    $macroblock_x * 8, $macroblock_y * 8 + 4,
                                false,
                                0, 3);
                        }
                    }
                }
            }
        }
    }
    /**
     * edgeLuma horizontally)
     *
     * @param bool $vertical true filters a vertical edge (samples run
     * @param H264Frame $frame the frame being built
     * @param int $q_position where the block on the other side sits
     * @param int $p_position where the block on one side of the edge sits
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param int $unused not read; kept so the shape of the call is unchanged
     * @param int $smooth_strength how strongly the edge is smoothed
     */
    private static function edgeLuma(
        H264Frame $frame, int $q_position, int $p_position, int $block_x,
            int $block_y,
        bool $vertical, int $unused, int $smooth_strength
    ): void {
        $quantizer = ($frame->macroblock_quantizer_y[$q_position] + $frame
            ->macroblock_quantizer_y[$p_position] +
            1) >> 1;
        $index_a = self::holdBetween(0, 51, $quantizer + $frame
            ->macroblock_alpha_off[$q_position]);
        $index_b = self::holdBetween(0, 51, $quantizer + $frame
            ->macroblock_beta_off[$q_position]);
        $alpha = H264Tables::DEBLOCK_ALPHA[$index_a];
        $beta = H264Tables::DEBLOCK_BETA[$index_b];
        if ($alpha === 0 || $beta === 0) {
            return;
        }
        $strength = ($smooth_strength >= 4) ? 3 : $smooth_strength;
        $move_limit = H264Tables::EDGE_MOVE_LIMITS[$index_a][$strength];
        $wide = $frame->coded_width;
        for ($i = 0; $i < 16; $i++) {
            if ($vertical) {
                $base = ($block_y + $i) * $wide + $block_x;
                $step = 1;
            } else {
                $base = ($block_y) * $wide + $block_x + $i;
                $step = $wide;
            }
            self::filterLine(
                $frame->luma, $base, $step, $alpha, $beta, $move_limit,
                    $smooth_strength, true);
        }
    }
    /**
     * edgeChroma filters one edge of the chroma planes.
     *
     * @param H264Frame $frame the stored bytes of one frame
     * @param int $q_position where the block on the other side sits
     * @param int $p_position where the block on one side of the edge sits
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param bool $vertical whether the edge runs up and down
     * @param int $unused not read; kept so the shape of the call is unchanged
     * @param int $smooth_strength how strongly the edge is smoothed
     */
    private static function edgeChroma(
        H264Frame $frame, int $q_position, int $p_position, int $block_x,
            int $block_y,
        bool $vertical, int $unused, int $smooth_strength
    ): void {
        $code_word = $frame->color_width;
        foreach ([0, 1] as $plane) {
            $quantizer_after = $plane === 0 ? $frame
                ->macroblock_quantizer_blue[$q_position]
                : $frame->macroblock_quantizer_red[$q_position];
            $quantizer_before = $plane === 0 ? $frame
                ->macroblock_quantizer_blue[$p_position]
                : $frame->macroblock_quantizer_red[$p_position];
            $quantizer = ($quantizer_after + $quantizer_before + 1) >> 1;
            $index_a = self::holdBetween(0, 51, $quantizer + $frame
                ->macroblock_alpha_off[$q_position]);
            $index_b = self::holdBetween(0, 51, $quantizer + $frame
                ->macroblock_beta_off[$q_position]);
            $alpha = H264Tables::DEBLOCK_ALPHA[$index_a];
            $beta = H264Tables::DEBLOCK_BETA[$index_b];
            if ($alpha === 0 || $beta === 0) {
                continue;
            }
            $strength = ($smooth_strength === 4) ? 3 : $smooth_strength;
            $move_limit =
                H264Tables::EDGE_MOVE_LIMITS[$index_a][$strength];
            for ($i = 0; $i < 8; $i++) {
                if ($vertical) {
                    $base = ($block_y + $i) * $code_word + $block_x;
                    $step = 1;
                } else {
                    $base = $block_y * $code_word + $block_x + $i;
                    $step = $code_word;
                }
                if ($plane === 0) {
                    self::filterLine(
                        $frame->blue, $base, $step, $alpha, $beta,
                            $move_limit, $smooth_strength,
                        false);
                } else {
                    self::filterLine(
                        $frame->red, $base, $step, $alpha, $beta,
                            $move_limit, $smooth_strength,
                        false);
                }
            }
        }
    }
    /**
     * filterLine smooths one line of samples that runs across a block edge,
     * moving the values on either side toward each other by as much as the
     * thresholds allow. The standard calls this clauses 8.7.2.3 and 8.7.2.4.
     * The line is read from the value the rest are measured from, with the
     * sample on the other side of the edge one step back.
     *
     * @param array $plane which of the picture's planes, brightness or color
     * @param int $base the value the rest are measured from
     * @param int $step how far to move each time
     * @param int $alpha the threshold an edge is smoothed above
     * @param int $beta the threshold a neighbor is smoothed above
     * @param int $move_limit how far a value may be moved while smoothing
     * @param int $smooth_strength how strongly the edge is smoothed
     * @param bool $luma whether this is the brightness plane
     */
    private static function filterLine(
        array &$plane, int $base, int $step, int $alpha, int $beta,
            int $move_limit,
        int $smooth_strength, bool $luma
    ): void {
        $after_edge = $plane[$base];
        $after_edge_one = $plane[$base + $step];
        $after_edge_two = $plane[$base + 2 * $step];
        $after_edge_three = $plane[$base + 3 * $step];
        $before_edge = $plane[$base - $step];
        $before_edge_one = $plane[$base - 2 * $step];
        $before_edge_two = $plane[$base - 3 * $step];
        $before_edge_three = $plane[$base - 4 * $step];
        if (abs($before_edge - $after_edge) >= $alpha ||
            abs($before_edge_one - $before_edge) >= $beta
            || abs($after_edge_one - $after_edge) >= $beta) {
            return;
        }
        $above_limit = abs($before_edge_two - $before_edge);
        $below_limit = abs($after_edge_two - $after_edge);
        if ($smooth_strength < 4) {
            if ($luma) {
                $move_limit = $move_limit + ($above_limit < $beta ? 1 : 0) +
                    ($below_limit < $beta ? 1 : 0);
            } else {
                $move_limit = $move_limit + 1;
            }
            $delta = self::holdBetween(-$move_limit, $move_limit,
                ((($after_edge -
                $before_edge) << 2) + ($before_edge_one - $after_edge_one)
                + 4) >> 3);
            $plane[$base - $step] = self::holdInsideSample($before_edge +
                $delta);
            $plane[$base] = self::holdInsideSample($after_edge - $delta);
            if ($luma && $above_limit < $beta) {
                $plane[$base - 2 * $step] =
                    $before_edge_one + self::holdBetween(-$move_limit,
                        $move_limit,
                        ($before_edge_two + (($before_edge + $after_edge +
                            1) >> 1) -
                            ($before_edge_one << 1)) >> 1);
            }
            if ($luma && $below_limit < $beta) {
                $plane[$base + $step] =
                    $after_edge_one + self::holdBetween(-$move_limit,
                        $move_limit,
                        ($after_edge_two + (($before_edge + $after_edge +
                            1) >> 1) -
                            ($after_edge_one << 1)) >> 1);
            }
            return;
        }
        /* bS == 4 */
        $strong_p = $luma && $above_limit < $beta && abs($before_edge -
            $after_edge) < (($alpha >> 2) +
            2);
        $strong_q = $luma && $below_limit < $beta && abs($before_edge -
            $after_edge) < (($alpha >> 2) +
            2);
        if ($strong_p) {
            $plane[$base - $step]
                = ($before_edge_two + 2 * $before_edge_one + 2 *
                    $before_edge + 2 * $after_edge + $after_edge_one +
                    4) >> 3;
            $plane[$base - 2 * $step] = ($before_edge_two +
                $before_edge_one + $before_edge + $after_edge +
                2) >> 2;
            $plane[$base - 3 * $step]
                = (2 * $before_edge_three + 3 * $before_edge_two +
                    $before_edge_one + $before_edge + $after_edge + 4) >> 3;
        } else {
            $plane[$base - $step] = (2 * $before_edge_one + $before_edge +
                $after_edge_one + 2) >> 2;
        }
        if ($strong_q) {
            $plane[$base]
                = ($after_edge_two + 2 * $after_edge_one + 2 * $after_edge +
                    2 * $before_edge + $before_edge_one +
                    4) >> 3;
            $plane[$base + $step]     = ($after_edge_two + $after_edge_one +
                $after_edge + $before_edge +
                2) >> 2;
            $plane[$base + 2 * $step]
                = (2 * $after_edge_three + 3 * $after_edge_two +
                    $after_edge_one + $after_edge + $before_edge + 4) >> 3;
        } else {
            $plane[$base] = (2 * $after_edge_one + $after_edge +
                $before_edge_one + 2) >> 2;
        }
    }
}

/**
 * H264Picture the picture an H.264 keyframe decodes to.
 */
final class H264Picture extends VideoPicture
{
}

/**
 * The H264Decoder class decodes one self contained H.264 picture in
 * pure PHP.
 *
 * It reads slices that stand on their own, brightness with color at
 * half width and half height, eight bits to a sample, either of the two
 * codings the format offers, blocks guessed in four by four or eight by
 * eight pieces or as one whole macroblock, macroblocks whose samples
 * are stored as they are, the tables a stream may carry for scaling its
 * values, several slices to a picture, and the smoothing of block
 * edges.
 *
 * Not supported (each raises H264Exception rather than producing wrong pixels):
 * P and B slices, half-pictures, or frames that mix whole and half pictures,
 * 4:2:2 / 4:4:4 / monochrome,
 * bit depths above 8, slices written out of order or in groups, and data
 * partitioning.
 */
final class H264Decoder
{
    /**
     * $sequence_settings_map stores the sequence settings the stream has
     * carried so far, kept
     * by their number so a slice can name the one it uses.
     * @var array
     */
    private array $sequence_settings_map = [];
    /**
     * $picture_settings_map stores the picture settings the stream has carried
     * so
     * far, kept by the number each set gives itself, so a slice can
     * name the set it was coded with.
     * @var array
     */
    private array $picture_settings_map = [];
    /**
     * addSettingsUnit feed a parameter set NAL (a unit an H.264 or HEVC stream
     * is cut into) (as stored in avcC (the box holding an MP4's H.264
     * settings), including its header byte)
     *
     * @param string $stream_unit_unit the unit read out of the stream
     */
    public function addSettingsUnit(string $stream_unit_unit): void
    {
        $total = H264Nal::parseUnit($stream_unit_unit);
        $this->consume($total);
    }
    /**
     * consume takes the next unit off the stream and hands it to whichever
     * reader deals with that kind.
     *
     * @param array $total how many bits
     */
    private function consume(array $total): void
    {
        if ($total['type'] === 7) {
            $sequence_settings =
                H264ParamParser::readSequenceSettings($total['rbsp']);
            $this->sequence_settings_map[$sequence_settings->id] =
                $sequence_settings;
        } elseif ($total['type'] === 8) {
            $picture_settings
                = H264ParamParser::readPictureSettings($total['rbsp'],
                    $this->sequenceSettingsFor($total['rbsp']));
            $this->picture_settings_map[$picture_settings->id] =
                $picture_settings;
        }
    }
    /**
     * sequenceSettingsFor the PPS references an SPS id; peek at it before the
     * full parse unpacking
     *
     * @param string $picture_settings_unpacked the picture settings as they
     *     arrived, before
     * @return H264Sps what was read
     */
    private function sequenceSettingsFor(string $picture_settings_unpacked):
        H264Sps
    {
        $bits = new H264Bits($picture_settings_unpacked);
        /* pic_parameter_set_id */
        $bits->readWholeNumber();
        $sequence_settings_id = $bits->readWholeNumber();
        if (!isset($this->sequence_settings_map[$sequence_settings_id])) {
            throw new H264Exception("picture settings name sequence "
                . "settings $sequence_settings_id, which the stream "
                . "has not carried");
        }
        return $this->sequence_settings_map[$sequence_settings_id];
    }
    /**
     * decodePlainStream decode one picture from an Annex-B byte stream (start-
     * code delimited). Parameter sets present in the stream are picked up
     * automatically.
     *
     * @param string $stream which stream of the file
     * @return H264Picture what was read
     */
    public function decodePlainStream(string $stream): H264Picture
    {
        return $this->decodeUnits(H264Nal::unitsFromStream($stream));
    }
    /**
     * decodeUnits read out of the stream
     *
     * @param array $stream_units the units
     * @return H264Picture what was read
     */
    public function decodeUnits(array $stream_units): H264Picture
    {
        foreach ($stream_units as $total) {
            if ($total['type'] === 7 || $total['type'] === 8) {
                $this->consume($total);
            }
        }
        $frame = null;
        $sequence_settings = null;
        $slice_id = 0;
        $decoded_any = false;
        foreach ($stream_units as $total) {
            if ($total['type'] !== 1 && $total['type'] !== 5) {
                continue;
            }
            if ($total['type'] === 2 || $total['type'] === 3
                || $total['type'] === 4) {
                throw new H264Exception('data partitioning is not supported');
            }
            $bits = new H264Bits($total['rbsp']);
            /* peek the PPS id to pick the parameter sets before the full parse
              */
            $probe = new H264Bits($total['rbsp']);
            $probe->readWholeNumber();
            $probe->readWholeNumber();
            $picture_settings_id = $probe->readWholeNumber();
            if (!isset($this->picture_settings_map[$picture_settings_id])) {
                throw new H264Exception("a slice names picture "
                    . "settings $picture_settings_id, which the "
                    . "stream has not carried");
            }
            $picture_settings = $this
                ->picture_settings_map[$picture_settings_id];
            $sequence_settings = $this
                ->sequence_settings_map[$picture_settings
                    ->sequence_settings_id];
            if ($sequence_settings->chroma_format_setting !== 1) {
                throw new H264Exception('only 4:2:0 chroma is supported');
            }
            if ($sequence_settings->bit_depth_luma !== 8 ||
                $sequence_settings->bit_depth_chroma !== 8) {
                throw new H264Exception('only 8-bit video is supported');
            }
            if (!$sequence_settings->frame_macroblocks_only) {
                throw new H264Exception(
                    'half-picture coding is not read');
            }
            $header = H264SliceHeader::readSettings(
                $bits, $total['type'], $total['refIdc'], $sequence_settings,
                    $picture_settings);
            if ($frame === null) {
                $macroblock_down = $sequence_settings
                    ->picture_height_in_map_units;
                $frame = new H264Frame($sequence_settings
                    ->picture_width_in_macroblocks,
                    $macroblock_down);
            }
            $decoder = new H264SliceDecoder(
                $frame, $sequence_settings, $picture_settings, $header, $bits,
                    $slice_id++);
            $decoder->decodePicture();
            $decoded_any = true;
        }
        if (!$decoded_any || $frame === null || $sequence_settings === null) {
            throw new H264Exception('no decodable I slice found');
        }
        H264Deblock::applyOffsets($frame);
        $crop_x = $sequence_settings->frame_cropping ? $sequence_settings
            ->crop_left * 2 : 0;
        $crop_y = $sequence_settings->frame_cropping ? $sequence_settings
            ->crop_top * 2 : 0;
        return new H264Picture(
            $frame->luma, $frame->blue, $frame->red,
            $sequence_settings->croppedWidth(),
                $sequence_settings->croppedHeight(),
            $frame->coded_width, $frame->color_width,
            $crop_x, $crop_y
        );
    }
    /**
     * toImage turns the decoded planes into a picture the image library can
     * work with, scaling it down where a width was asked for.
     *
     * @return GdImage what was read
     * @param VideoPicture $position where the reading is
     */
    public static function toImage(VideoPicture $position)
    {
        return $position->toImage();
    }
}
