//! LZX decompressor for Xbox 360 XEX2 "normal compression".
//! Ported from libmspack lzxd.c (C) 2003-2013 Stuart Caie, LGPL 2.1.

use std::fmt;

// ── LZX constants ───────────────────────────────────────────────────────────

const LZX_MIN_MATCH: usize = 2;
const LZX_NUM_CHARS: usize = 256;
const LZX_BLOCKTYPE_VERBATIM: u8 = 1;
const LZX_BLOCKTYPE_ALIGNED: u8 = 2;
const LZX_BLOCKTYPE_UNCOMPRESSED: u8 = 3;
const LZX_NUM_PRIMARY_LENGTHS: usize = 7;
const LZX_NUM_SECONDARY_LENGTHS: usize = 249;
const LZX_FRAME_SIZE: usize = 32768;
const HUFF_MAXBITS: usize = 16;

const PRETREE_MAXSYMS: usize = 20;
const PRETREE_TABLEBITS: usize = 6;
const MAINTREE_MAXSYMS: usize = LZX_NUM_CHARS + 290 * 8; // 2576
const MAINTREE_TABLEBITS: usize = 12;
const LENGTH_MAXSYMS: usize = LZX_NUM_SECONDARY_LENGTHS + 1; // 250
const LENGTH_TABLEBITS: usize = 12;
const ALIGNED_MAXSYMS: usize = 8;
const ALIGNED_TABLEBITS: usize = 7;
const LENTABLE_SAFETY: usize = 64;

const BITBUF_WIDTH: u32 = 32;

// ── Static tables ───────────────────────────────────────────────────────────

static POSITION_SLOTS: [u32; 11] = [30, 32, 34, 36, 38, 42, 50, 66, 98, 162, 290];

static EXTRA_BITS: [u8; 36] = [
    0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
    7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14,
    15, 15, 16, 16,
];

#[rustfmt::skip]
static POSITION_BASE: [u32; 290] = [
    0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512,
    768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576, 32768,
    49152, 65536, 98304, 131072, 196608, 262144, 393216, 524288, 655360,
    786432, 917504, 1048576, 1179648, 1310720, 1441792, 1572864, 1703936,
    1835008, 1966080, 2097152, 2228224, 2359296, 2490368, 2621440, 2752512,
    2883584, 3014656, 3145728, 3276800, 3407872, 3538944, 3670016, 3801088,
    3932160, 4063232, 4194304, 4325376, 4456448, 4587520, 4718592, 4849664,
    4980736, 5111808, 5242880, 5373952, 5505024, 5636096, 5767168, 5898240,
    6029312, 6160384, 6291456, 6422528, 6553600, 6684672, 6815744, 6946816,
    7077888, 7208960, 7340032, 7471104, 7602176, 7733248, 7864320, 7995392,
    8126464, 8257536, 8388608, 8519680, 8650752, 8781824, 8912896, 9043968,
    9175040, 9306112, 9437184, 9568256, 9699328, 9830400, 9961472, 10092544,
    10223616, 10354688, 10485760, 10616832, 10747904, 10878976, 11010048,
    11141120, 11272192, 11403264, 11534336, 11665408, 11796480, 11927552,
    12058624, 12189696, 12320768, 12451840, 12582912, 12713984, 12845056,
    12976128, 13107200, 13238272, 13369344, 13500416, 13631488, 13762560,
    13893632, 14024704, 14155776, 14286848, 14417920, 14548992, 14680064,
    14811136, 14942208, 15073280, 15204352, 15335424, 15466496, 15597568,
    15728640, 15859712, 15990784, 16121856, 16252928, 16384000, 16515072,
    16646144, 16777216, 16908288, 17039360, 17170432, 17301504, 17432576,
    17563648, 17694720, 17825792, 17956864, 18087936, 18219008, 18350080,
    18481152, 18612224, 18743296, 18874368, 19005440, 19136512, 19267584,
    19398656, 19529728, 19660800, 19791872, 19922944, 20054016, 20185088,
    20316160, 20447232, 20578304, 20709376, 20840448, 20971520, 21102592,
    21233664, 21364736, 21495808, 21626880, 21757952, 21889024, 22020096,
    22151168, 22282240, 22413312, 22544384, 22675456, 22806528, 22937600,
    23068672, 23199744, 23330816, 23461888, 23592960, 23724032, 23855104,
    23986176, 24117248, 24248320, 24379392, 24510464, 24641536, 24772608,
    24903680, 25034752, 25165824, 25296896, 25427968, 25559040, 25690112,
    25821184, 25952256, 26083328, 26214400, 26345472, 26476544, 26607616,
    26738688, 26869760, 27000832, 27131904, 27262976, 27394048, 27525120,
    27656192, 27787264, 27918336, 28049408, 28180480, 28311552, 28442624,
    28573696, 28704768, 28835840, 28966912, 29097984, 29229056, 29360128,
    29491200, 29622272, 29753344, 29884416, 30015488, 30146560, 30277632,
    30408704, 30539776, 30670848, 30801920, 30932992, 31064064, 31195136,
    31326208, 31457280, 31588352, 31719424, 31850496, 31981568, 32112640,
    32243712, 32374784, 32505856, 32636928, 32768000, 32899072, 33030144,
    33161216, 33292288, 33423360,
];

// ── Error type ──────────────────────────────────────────────────────────────

#[derive(Debug)]
pub enum LzxError {
    BadHuffmanTable,
    Decrunch(String),
}

impl fmt::Display for LzxError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::BadHuffmanTable => write!(f, "failed to build Huffman table"),
            Self::Decrunch(msg) => write!(f, "LZX decrunch error: {msg}"),
        }
    }
}

impl std::error::Error for LzxError {}

// ── Bit reader (MSB order, 16-bit LE pairs) ────────────────────────────────

struct BitReader<'a> {
    data: &'a [u8],
    pos: usize,
    buf: u32,
    left: i32,
}

impl<'a> BitReader<'a> {
    fn new(data: &'a [u8]) -> Self {
        Self { data, pos: 0, buf: 0, left: 0 }
    }

    /// Inject one 16-bit little-endian pair into MSB bit buffer.
    fn fill(&mut self) {
        let b0 = if self.pos < self.data.len() {
            let b = self.data[self.pos]; self.pos += 1; b as u32
        } else { 0 };
        let b1 = if self.pos < self.data.len() {
            let b = self.data[self.pos]; self.pos += 1; b as u32
        } else { 0 };
        let word = (b1 << 8) | b0;
        self.buf |= word << (16 - self.left as u32);
        self.left += 16;
    }

    #[inline]
    fn ensure(&mut self, n: i32) {
        while self.left < n { self.fill(); }
    }

    #[inline]
    fn peek(&self, n: u32) -> u32 {
        self.buf >> (BITBUF_WIDTH - n)
    }

    #[inline]
    fn remove(&mut self, n: u32) {
        self.buf <<= n;
        self.left -= n as i32;
    }

    #[inline]
    fn read(&mut self, n: u32) -> u32 {
        self.ensure(n as i32);
        let v = self.peek(n);
        self.remove(n);
        v
    }

    /// Read a raw byte directly (for UNCOMPRESSED blocks).
    fn raw_byte(&mut self) -> u8 {
        if self.pos < self.data.len() {
            let b = self.data[self.pos]; self.pos += 1; b
        } else { 0 }
    }

    /// Re-align the bitstream at a frame boundary.
    fn align_frame(&mut self) {
        if self.left > 0 { self.ensure(16); }
        let r = self.left & 15;
        if r != 0 { self.remove(r as u32); }
    }
}

// ── Huffman table builder (MSB order) ───────────────────────────────────────

fn make_decode_table(
    nsyms: usize,
    nbits: usize,
    length: &[u8],
    table: &mut [u16],
) -> bool {
    let mut pos: usize = 0;
    let table_mask = 1usize << nbits;
    let mut bit_mask = table_mask >> 1;

    // Short codes: direct mapping
    for bit_num in 1..=nbits {
        for sym in 0..nsyms {
            if length[sym] as usize != bit_num { continue; }
            let leaf = pos;
            pos += bit_mask;
            if pos > table_mask { return true; }
            for i in leaf..leaf + bit_mask {
                table[i] = sym as u16;
            }
        }
        bit_mask >>= 1;
    }

    if pos == table_mask { return false; }

    // Mark remaining entries as unused
    for i in pos..table_mask {
        table[i] = 0xFFFF;
    }

    let mut next_symbol = if (table_mask >> 1) < nsyms { nsyms } else { table_mask >> 1 };

    let mut pos32 = (pos as u32) << 16;
    let table_mask32 = (table_mask as u32) << 16;
    let mut bit_mask32: u32 = 1 << 15;

    // Long codes: tree traversal
    for bit_num in (nbits + 1)..=HUFF_MAXBITS {
        for sym in 0..nsyms {
            if length[sym] as usize != bit_num { continue; }
            if pos32 >= table_mask32 { return true; }

            let mut leaf = (pos32 >> 16) as usize;

            for fill in 0..(bit_num - nbits) {
                if table[leaf] == 0xFFFF {
                    table[next_symbol << 1] = 0xFFFF;
                    table[(next_symbol << 1) + 1] = 0xFFFF;
                    table[leaf] = next_symbol as u16;
                    next_symbol += 1;
                }
                leaf = (table[leaf] as usize) << 1;
                if (pos32 >> (15 - fill as u32)) & 1 != 0 {
                    leaf += 1;
                }
            }
            table[leaf] = sym as u16;
            pos32 += bit_mask32;
        }
        bit_mask32 >>= 1;
    }

    pos32 != table_mask32
}

// ── Huffman symbol decoder ──────────────────────────────────────────────────

fn read_huffsym(
    br: &mut BitReader,
    table: &[u16],
    lens: &[u8],
    tablebits: usize,
    maxsyms: usize,
) -> Result<usize, LzxError> {
    br.ensure(HUFF_MAXBITS as i32);
    let mut sym = table[br.peek(tablebits as u32) as usize] as usize;
    if sym >= maxsyms {
        let mut i: u32 = 1 << (BITBUF_WIDTH - tablebits as u32);
        loop {
            i >>= 1;
            if i == 0 { return Err(LzxError::BadHuffmanTable); }
            sym = table[(sym << 1) | if br.buf & i != 0 { 1 } else { 0 }] as usize;
            if sym < maxsyms { break; }
        }
    }
    br.remove(lens[sym] as u32);
    Ok(sym)
}

// ── LZX decoder state ───────────────────────────────────────────────────────

pub struct LzxDecoder {
    window: Vec<u8>,
    window_size: usize,
    window_posn: usize,
    frame_posn: usize,
    frame: usize,
    num_offsets: usize,

    r0: u32,
    r1: u32,
    r2: u32,

    block_type: u8,
    block_length: usize,
    block_remaining: usize,

    header_read: bool,
    intel_filesize: i32,
    intel_curpos: i32,
    intel_started: bool,

    // Huffman code lengths
    pretree_len: Vec<u8>,
    maintree_len: Vec<u8>,
    length_len: Vec<u8>,
    aligned_len: Vec<u8>,

    // Huffman decode tables
    pretree_table: Vec<u16>,
    maintree_table: Vec<u16>,
    length_table: Vec<u16>,
    aligned_table: Vec<u16>,

    length_empty: bool,
}

impl LzxDecoder {
    pub fn new(window_bits: u32) -> Self {
        assert!((15..=21).contains(&window_bits));
        let window_size = 1usize << window_bits;
        let num_offsets = (POSITION_SLOTS[(window_bits - 15) as usize] as usize) << 3;

        Self {
            window: vec![0u8; window_size],
            window_size,
            window_posn: 0,
            frame_posn: 0,
            frame: 0,
            num_offsets,
            r0: 1, r1: 1, r2: 1,
            block_type: 0,
            block_length: 0,
            block_remaining: 0,
            header_read: false,
            intel_filesize: 0,
            intel_curpos: 0,
            intel_started: false,
            pretree_len:   vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY],
            maintree_len:  vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY],
            length_len:    vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY],
            aligned_len:   vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY],
            pretree_table:  vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2],
            maintree_table: vec![0u16; (1 << MAINTREE_TABLEBITS) + MAINTREE_MAXSYMS * 2],
            length_table:   vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2],
            aligned_table:  vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2],
            length_empty: false,
        }
    }

    fn build_table(
        lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
    ) -> Result<(), LzxError> {
        if make_decode_table(maxsyms, tablebits, lens, table) {
            Err(LzxError::BadHuffmanTable)
        } else {
            Ok(())
        }
    }

    fn build_table_maybe_empty(
        lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
    ) -> Result<bool, LzxError> {
        if make_decode_table(maxsyms, tablebits, lens, table) {
            // Check if table is simply empty (all lengths zero)
            for i in 0..maxsyms {
                if lens[i] > 0 {
                    return Err(LzxError::BadHuffmanTable);
                }
            }
            Ok(true) // empty
        } else {
            Ok(false) // not empty
        }
    }

    /// Read Huffman code lengths using the pretree (lzxd_read_lens).
    fn read_lens(
        br: &mut BitReader,
        lens: &mut [u8],
        pretree_len: &mut [u8],
        pretree_table: &mut [u16],
        first: usize,
        last: usize,
    ) -> Result<(), LzxError> {
        // Build pretree: 20 symbols, 4 bits each
        for i in 0..20 {
            pretree_len[i] = br.read(4) as u8;
        }
        Self::build_table(pretree_len, pretree_table, PRETREE_MAXSYMS, PRETREE_TABLEBITS)?;

        let mut x = first;
        while x < last {
            let z = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
            if z == 17 {
                // Run of zeros: [read 4 bits] + 4
                let mut y = br.read(4) as usize + 4;
                while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
            } else if z == 18 {
                // Run of zeros: [read 5 bits] + 20
                let mut y = br.read(5) as usize + 20;
                while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
            } else if z == 19 {
                // Run of same: [read 1 bit] + 4, then read symbol
                let mut y = br.read(1) as usize + 4;
                let z2 = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
                let mut val = lens[x] as i32 - z2 as i32;
                if val < 0 { val += 17; }
                while y > 0 && x < last { lens[x] = val as u8; x += 1; y -= 1; }
            } else {
                // Delta: code 0..16
                let mut val = lens[x] as i32 - z as i32;
                if val < 0 { val += 17; }
                lens[x] = val as u8;
                x += 1;
            }
        }
        Ok(())
    }

    /// Decompress the full LZX stream into the output buffer.
    pub fn decompress(&mut self, input: &[u8], output_len: usize) -> Result<Vec<u8>, LzxError> {
        let mut br = BitReader::new(input);
        let mut output = Vec::with_capacity(output_len);
        let mut offset: usize = 0;

        let end_frame = (output_len / LZX_FRAME_SIZE) + 1;

        while self.frame < end_frame {
            // Read header once
            if !self.header_read {
                let i_bit = br.read(1);
                let (hi, lo) = if i_bit != 0 {
                    (br.read(16), br.read(16))
                } else {
                    (0, 0)
                };
                self.intel_filesize = ((hi << 16) | lo) as i32;
                self.header_read = true;
            }

            // Frame size
            let frame_size = if output_len > 0 && (output_len - offset) < LZX_FRAME_SIZE {
                output_len - offset
            } else {
                LZX_FRAME_SIZE
            };

            let mut bytes_todo = (self.frame_posn + frame_size).wrapping_sub(self.window_posn) as i32;

            while bytes_todo > 0 {
                // New block?
                if self.block_remaining == 0 {
                    // Realign after odd UNCOMPRESSED block
                    if self.block_type == LZX_BLOCKTYPE_UNCOMPRESSED && (self.block_length & 1) != 0 {
                        br.raw_byte();
                    }
                    // Read block type (3 bits) and length (24 bits)
                    self.block_type = br.read(3) as u8;
                    let hi = br.read(16) as usize;
                    let lo = br.read(8) as usize;
                    self.block_length = (hi << 8) | lo;
                    self.block_remaining = self.block_length;

                    match self.block_type {
                        LZX_BLOCKTYPE_ALIGNED => {
                            for i in 0..8 { self.aligned_len[i] = br.read(3) as u8; }
                            Self::build_table(&self.aligned_len, &mut self.aligned_table, ALIGNED_MAXSYMS, ALIGNED_TABLEBITS)?;
                            // Fall through to verbatim tree reading
                            Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
                            Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
                            Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
                            if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
                            Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
                            self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
                        }
                        LZX_BLOCKTYPE_VERBATIM => {
                            Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
                            Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
                            Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
                            if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
                            Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
                            self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
                        }
                        LZX_BLOCKTYPE_UNCOMPRESSED => {
                            self.intel_started = true;
                            // Align to byte boundary
                            if br.left == 0 { br.ensure(16); }
                            br.left = 0;
                            br.buf = 0;
                            // Read R0, R1, R2 (12 bytes, little-endian u32s)
                            let mut buf = [0u8; 12];
                            for b in &mut buf { *b = br.raw_byte(); }
                            self.r0 = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
                            self.r1 = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
                            self.r2 = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
                        }
                        _ => return Err(LzxError::Decrunch("bad block type".into())),
                    }
                }

                let mut this_run = self.block_remaining as i32;
                if this_run > bytes_todo { this_run = bytes_todo; }
                bytes_todo -= this_run;
                self.block_remaining -= this_run as usize;

                let window_size = self.window_size;

                match self.block_type {
                    LZX_BLOCKTYPE_VERBATIM => {
                        while this_run > 0 {
                            let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
                            if main_element < LZX_NUM_CHARS {
                                self.window[self.window_posn] = main_element as u8;
                                self.window_posn += 1;
                                this_run -= 1;
                            } else {
                                let me = main_element - LZX_NUM_CHARS;
                                let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
                                if match_length == LZX_NUM_PRIMARY_LENGTHS {
                                    if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
                                    let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
                                    match_length += footer;
                                }
                                match_length += LZX_MIN_MATCH;

                                let mut match_offset = (me >> 3) as u32;
                                match match_offset {
                                    0 => match_offset = self.r0,
                                    1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
                                    2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
                                    3 => { match_offset = 1; self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
                                    _ => {
                                        let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
                                        let verbatim_bits = br.read(extra);
                                        match_offset = POSITION_BASE[match_offset as usize] - 2 + verbatim_bits;
                                        self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
                                    }
                                }

                                if self.window_posn + match_length > window_size {
                                    return Err(LzxError::Decrunch("match overrun".into()));
                                }
                                self.copy_match(match_offset as usize, match_length);
                                this_run -= match_length as i32;
                            }
                        }
                    }
                    LZX_BLOCKTYPE_ALIGNED => {
                        while this_run > 0 {
                            let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
                            if main_element < LZX_NUM_CHARS {
                                self.window[self.window_posn] = main_element as u8;
                                self.window_posn += 1;
                                this_run -= 1;
                            } else {
                                let me = main_element - LZX_NUM_CHARS;
                                let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
                                if match_length == LZX_NUM_PRIMARY_LENGTHS {
                                    if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
                                    let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
                                    match_length += footer;
                                }
                                match_length += LZX_MIN_MATCH;

                                let mut match_offset = (me >> 3) as u32;
                                match match_offset {
                                    0 => match_offset = self.r0,
                                    1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
                                    2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
                                    _ => {
                                        let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
                                        match_offset = POSITION_BASE[match_offset as usize] - 2;
                                        if extra > 3 {
                                            let verbatim_bits = br.read(extra - 3);
                                            match_offset += verbatim_bits << 3;
                                            let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
                                            match_offset += aligned as u32;
                                        } else if extra == 3 {
                                            let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
                                            match_offset += aligned as u32;
                                        } else if extra > 0 {
                                            let verbatim_bits = br.read(extra);
                                            match_offset += verbatim_bits;
                                        } else {
                                            match_offset = 1;
                                        }
                                        self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
                                    }
                                }

                                if self.window_posn + match_length > window_size {
                                    return Err(LzxError::Decrunch("match overrun".into()));
                                }
                                self.copy_match(match_offset as usize, match_length);
                                this_run -= match_length as i32;
                            }
                        }
                    }
                    LZX_BLOCKTYPE_UNCOMPRESSED => {
                        let run = this_run as usize;
                        for _ in 0..run {
                            self.window[self.window_posn] = br.raw_byte();
                            self.window_posn += 1;
                        }
                    }
                    _ => return Err(LzxError::Decrunch("bad block type in decode".into())),
                }

                // Overrun accounting
                if this_run < 0 {
                    let overrun = (-this_run) as usize;
                    if overrun > self.block_remaining {
                        return Err(LzxError::Decrunch("overrun past block end".into()));
                    }
                    self.block_remaining -= overrun;
                }
            }

            // Frame boundary check
            if (self.window_posn.wrapping_sub(self.frame_posn)) != frame_size {
                return Err(LzxError::Decrunch(format!(
                    "decode beyond frame: {} != {}", self.window_posn - self.frame_posn, frame_size
                )));
            }

            // Re-align bitstream
            br.align_frame();

            // Intel E8 postprocessing
            if self.intel_started && self.intel_filesize != 0
                && self.frame <= 32768 && frame_size > 10
            {
                let mut e8_buf = vec![0u8; frame_size];
                e8_buf.copy_from_slice(&self.window[self.frame_posn..self.frame_posn + frame_size]);

                let mut i = 0usize;
                let limit = frame_size - 10;
                let mut curpos = self.intel_curpos;
                let filesize = self.intel_filesize;

                while i < limit {
                    if e8_buf[i] != 0xE8 { i += 1; curpos += 1; continue; }
                    let abs_off = e8_buf[i+1] as i32
                        | (e8_buf[i+2] as i32) << 8
                        | (e8_buf[i+3] as i32) << 16
                        | (e8_buf[i+4] as i32) << 24;

                    if abs_off >= -curpos && abs_off < filesize {
                        let rel_off = if abs_off >= 0 { abs_off - curpos } else { abs_off + filesize };
                        e8_buf[i+1] = rel_off as u8;
                        e8_buf[i+2] = (rel_off >> 8) as u8;
                        e8_buf[i+3] = (rel_off >> 16) as u8;
                        e8_buf[i+4] = (rel_off >> 24) as u8;
                    }
                    i += 5;
                    curpos += 5;
                }
                self.intel_curpos += frame_size as i32;

                let to_write = frame_size.min(output_len - offset);
                output.extend_from_slice(&e8_buf[..to_write]);
                offset += to_write;
            } else {
                if self.intel_filesize != 0 { self.intel_curpos += frame_size as i32; }
                let to_write = frame_size.min(output_len - offset);
                output.extend_from_slice(&self.window[self.frame_posn..self.frame_posn + to_write]);
                offset += to_write;
            }

            // Advance frame
            self.frame_posn += frame_size;
            self.frame += 1;
            if self.window_posn == self.window_size { self.window_posn = 0; }
            if self.frame_posn == self.window_size { self.frame_posn = 0; }
        }

        Ok(output)
    }

    /// Copy a match from the window (handles wrap-around).
    fn copy_match(&mut self, match_offset: usize, match_length: usize) {
        let window_size = self.window_size;
        let mut remaining = match_length;

        if match_offset > self.window_posn {
            // Source wraps around window end
            let j = match_offset - self.window_posn;
            let mut src = window_size - j;
            if j < remaining {
                remaining -= j;
                for _ in 0..j {
                    self.window[self.window_posn] = self.window[src];
                    self.window_posn += 1;
                    src += 1;
                }
                src = 0; // wrap to start
            }
            for _ in 0..remaining {
                self.window[self.window_posn] = self.window[src];
                self.window_posn += 1;
                src += 1;
            }
        } else {
            let mut src = self.window_posn - match_offset;
            for _ in 0..remaining {
                self.window[self.window_posn] = self.window[src];
                self.window_posn += 1;
                src += 1;
            }
        }
    }
}
