#!/usr/bin/env python3
"""
Generates an AV1 bitstream with two temporal units whose sequence headers
share identical {profile, max_frame_size, bitdepth, chroma sampling, color
config} but differ in non-dimensional flags (use_128x128_superblock,
enable_cdef, enable_restoration, film_grain_params_present).

When fed through media::AV1Decoder, TU1 must yield kConfigChange (first
sequence header). TU2 must NOT yield kConfigChange (the gate at
av1_decoder.cc:367-383 only checks size/profile/bitdepth/colorspace), yet
SubmitDecode() will be invoked with the *new* sequence header carrying the
flipped flags.

Each TU is a lossless 256x256 4:2:0 8-bit shown key frame so that libgav1's
OBU parser short-circuits loop-filter / CDEF / LR / tx-mode parsing
regardless of the seq-header enable_* flags. Tile data is a single dummy
byte; libgav1's ObuParser only records tile buffer offsets without
entropy-decoding them.
"""
import struct
import sys


class BitWriter:
    def __init__(self):
        self.bits = []

    def f(self, value, n):
        for i in range(n - 1, -1, -1):
            self.bits.append((value >> i) & 1)

    def b(self, value):
        self.f(1 if value else 0, 1)

    def trailing_bits(self):
        self.bits.append(1)
        while len(self.bits) % 8:
            self.bits.append(0)

    def align_zero(self):
        while len(self.bits) % 8:
            self.bits.append(0)

    def bytes(self):
        assert len(self.bits) % 8 == 0
        out = bytearray()
        for i in range(0, len(self.bits), 8):
            byte = 0
            for j in range(8):
                byte = (byte << 1) | self.bits[i + j]
            out.append(byte)
        return bytes(out)


def leb128(v):
    out = bytearray()
    while True:
        b = v & 0x7F
        v >>= 7
        if v:
            out.append(b | 0x80)
        else:
            out.append(b)
            return bytes(out)


def obu(obu_type, payload):
    # forbidden=0, type(4), extension=0, has_size=1, reserved=0
    header = bytes([(obu_type << 3) | 0x02])
    return header + leb128(len(payload)) + payload


def sequence_header_obu(use_128x128_superblock, enable_cdef,
                        enable_restoration, film_grain_params_present):
    w = BitWriter()
    w.f(0, 3)         # seq_profile = Main
    w.b(False)        # still_picture
    w.b(False)        # reduced_still_picture_header
    w.b(False)        # timing_info_present_flag
    # decoder_model_info_present_flag: only when timing_info_present_flag.
    w.b(False)        # initial_display_delay_present_flag
    w.f(0, 5)         # operating_points_cnt_minus_1
    w.f(0, 12)        # operating_point_idc[0]
    w.f(0, 5)         # seq_level_idx[0]  (level 2.0, major<=3 -> no tier bit)
    # frame_width_bits_minus_1 / frame_height_bits_minus_1
    w.f(7, 4)         # 8 bits
    w.f(7, 4)         # 8 bits
    w.f(255, 8)       # max_frame_width_minus_1  -> 256
    w.f(255, 8)       # max_frame_height_minus_1 -> 256
    w.b(False)        # frame_id_numbers_present_flag
    w.b(use_128x128_superblock)
    w.b(False)        # enable_filter_intra
    w.b(False)        # enable_intra_edge_filter
    w.b(False)        # enable_interintra_compound
    w.b(False)        # enable_masked_compound
    w.b(False)        # enable_warped_motion
    w.b(False)        # enable_dual_filter
    w.b(False)        # enable_order_hint
    # (no enable_jnt_comp / enable_ref_frame_mvs)
    w.b(False)        # seq_choose_screen_content_tools
    w.b(False)        # seq_force_screen_content_tools = 0
    # force_screen_content_tools == 0 -> no integer_mv read
    # (no order_hint_bits_minus_1)
    w.b(False)        # enable_superres
    w.b(enable_cdef)
    w.b(enable_restoration)
    # color_config (profile 0, 8-bit 4:2:0)
    w.b(False)        # high_bitdepth
    w.b(False)        # mono_chrome
    w.b(False)        # color_description_present_flag
    w.b(False)        # color_range
    w.f(0, 2)         # chroma_sample_position
    w.b(False)        # separate_uv_delta_q
    w.b(film_grain_params_present)
    w.trailing_bits()
    return obu(1, w.bytes())  # OBU_SEQUENCE_HEADER


def frame_obu(film_grain_params_present):
    w = BitWriter()
    # uncompressed_header()
    w.b(False)        # show_existing_frame
    w.f(0, 2)         # frame_type = KEY_FRAME
    w.b(True)         # show_frame
    # error_resilient_mode inferred = 1 for KEY+show_frame
    w.b(True)         # disable_cdf_update  (so no disable_frame_end_update_cdf)
    # allow_screen_content_tools inferred = 0 (seq_force_screen_content_tools=0)
    # frame_id_numbers_present = 0 -> no current_frame_id
    w.b(False)        # frame_size_override_flag
    # order_hint_bits = 0 -> no order_hint
    # primary_ref_frame inferred = NONE (intra)
    # decoder_model_info_present_flag = 0 -> no buffer_removal_time
    # refresh_frame_flags inferred = 0xff (KEY+show_frame)
    # IntraFrame && refresh==0xff -> no ref_order_hint loop
    # frame_size(): override=0 -> use max; superres disabled -> no bits
    w.b(False)        # render_and_frame_size_different
    # allow_screen_content_tools=0 -> no allow_intrabc
    # disable_cdf_update=1 -> no disable_frame_end_update_cdf bit
    # tile_info()
    w.b(True)         # uniform_tile_spacing_flag
    w.b(False)        # increment_tile_cols_log2 (stop)
    w.b(False)        # increment_tile_rows_log2 (stop)
    # tile_bits = 0 -> no context_update_id / tile_size_bytes
    # quantization_params()
    w.f(0, 8)         # base_q_idx = 0  (CodedLossless)
    w.b(False)        # DeltaQYDc.delta_coded
    # separate_uv_delta_q = 0 -> no diff_uv_delta bit
    w.b(False)        # DeltaQUDc.delta_coded
    w.b(False)        # DeltaQUAc.delta_coded
    w.b(False)        # using_qmatrix
    # segmentation_params()
    w.b(False)        # segmentation_enabled
    # delta_q_params(): base_q_idx==0 -> no bits
    # delta_lf_params(): delta_q_present==0 -> no bits
    # loop_filter / cdef / lr / tx_mode: skipped (CodedLossless / AllLossless)
    # frame_reference_mode(): intra -> no bits
    # skip_mode_params(): not allowed -> no bits
    # allow_warped_motion: intra -> no bits
    w.b(False)        # reduced_tx_set
    # global_motion_params(): intra -> no bits
    if film_grain_params_present:
        w.b(False)    # apply_grain = 0
    w.align_zero()    # byte_alignment() inside Frame OBU
    fh = w.bytes()
    tile_data = b'\x00'  # 1-byte dummy tile (parser only records offset/size)
    return obu(6, fh + tile_data)  # OBU_FRAME


def temporal_unit(sb128, cdef, lr, fg):
    td = obu(2, b'')  # OBU_TEMPORAL_DELIMITER
    return td + sequence_header_obu(sb128, cdef, lr, fg) + frame_obu(fg)


def ivf_header(num_frames, w=256, h=256):
    return (b'DKIF' + struct.pack('<HH', 0, 32) + b'AV01' +
            struct.pack('<HHIII', w, h, 30, 1, num_frames) +
            struct.pack('<I', 0))


def ivf_frame(data, pts):
    return struct.pack('<IQ', len(data), pts) + data


def main():
    # TU1: Seq A — superblock=64x64, no cdef, no restoration, no film grain.
    tu1 = temporal_unit(sb128=False, cdef=False, lr=False, fg=False)
    # TU2: Seq B — same dims/profile/bitdepth/color, flip sb128/cdef/lr/fg.
    tu2 = temporal_unit(sb128=True, cdef=True, lr=True, fg=True)

    raw = tu1 + tu2
    with open('seqhdr_flip.obu', 'wb') as f:
        f.write(raw)

    ivf = (ivf_header(2) + ivf_frame(tu1, 0) + ivf_frame(tu2, 1))
    with open('seqhdr_flip.ivf', 'wb') as f:
        f.write(ivf)

    print(f'TU1: {len(tu1)} bytes  {tu1.hex()}')
    print(f'TU2: {len(tu2)} bytes  {tu2.hex()}')
    print(f'Wrote seqhdr_flip.obu ({len(raw)} bytes), '
          f'seqhdr_flip.ivf ({len(ivf)} bytes)')


if __name__ == '__main__':
    main()
