use super::super::core;
use super::{MATCH_CONTEXT_WORDS, boxed_tag, heap_slice, jump_label, read_word, write_word};
use crate::atom::Atom;
use crate::error::ExecError;
use crate::gc;
use crate::interpreter::InstructionOutcome;
use crate::loader::decode::Literal;
use crate::loader::decode::compact::Operand;
use crate::module::Module;
use crate::process::Process;
use crate::term::Term;
use crate::term::binary_ref::BinaryRef;
use crate::term::boxed::{BoxedHeader, BoxedTag, ProcBin, write_float};
use crate::term::shared_binary::{alloc_binary, alloc_binary_word_count};
use crate::term::sub_binary::{SUB_BINARY_WORDS, write_sub_binary};
type GetOperands<'a> = (
    &'a Operand,
    &'a Operand,
    &'a Operand,
    &'a Operand,
    &'a Operand,
    &'a Operand,
);
pub(super) fn bs_start_match(
    process: &mut Process,
    module: &Module,
    op: super::BinaryOp,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (fail, source, destination) = match (op, operands) {
        // `{bs_start_match4, Fail, Live, Src, Dst}` puts Live before Src, and
        // Fail may be the atom `no_fail` or `resume` when the compiler proved
        // the operand is always matchable.
        (super::BinaryOp::BsStartMatch4, [fail, _live, source, destination]) => {
            (fail, source, destination)
        }
        (_, [fail, source, destination]) => (fail, source, destination),
        (_, [fail, source, _live, destination]) => (fail, source, destination),
        _ => return Err(ExecError::InvalidOperand("bs_start_match operands")),
    };
    // Reserve before reading the source: GC moves heap terms, and registers
    // are the only roots, so no term may be held across a collection.
    gc::ensure_space(process, MATCH_CONTEXT_WORDS, 256).map_err(core::gc_error_to_exec)?;
    let source = core::read_term(process, module, source)?;
    // `resume` hands back a term that already carries a match context.
    if MatchContext::new(source).is_some() {
        core::write_term(process, destination, source)?;
        return Ok(InstructionOutcome::Continue);
    }
    let Some(binary) = BinaryRef::new(source) else {
        return match fail {
            // With `no_fail`/`resume` there is no fail label to jump to.
            Operand::Atom(Some(_)) => Err(ExecError::Badarg),
            _ => jump_label(module, fail),
        };
    };
    let total_bits = binary
        .len()
        .checked_mul(u8::BITS as usize)
        .ok_or(ExecError::Badarg)?;
    let ptr = process
        .heap_mut()
        .alloc(MATCH_CONTEXT_WORDS)
        .map_err(ExecError::from)?;
    let heap = heap_slice(ptr, MATCH_CONTEXT_WORDS);
    heap[0] = BoxedHeader::new(BoxedTag::MatchContext, MATCH_CONTEXT_WORDS - 1);
    heap[1] = 0;
    heap[2] = total_bits as u64;
    heap[3] = source.raw();
    core::write_term(process, destination, Term::boxed_ptr(heap.as_ptr()))?;
    Ok(InstructionOutcome::Continue)
}
pub(super) fn bs_get_integer(
    process: &mut Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (fail, context, size, unit, flags, destination) =
        parse_get_operands(operands, "bs_get_integer2")?;
    let context = read_context(process, module, context)?;
    match get_integer_value(context, size, unit, flags)? {
        Some((value, bits)) => {
            let term = Term::try_small_int(value).ok_or(ExecError::Badarg)?;
            core::write_term(process, destination, term)?;
            context.set_position_bits(context.position_bits() + bits);
            Ok(InstructionOutcome::Continue)
        }
        None => jump_label(module, fail),
    }
}
pub(super) fn bs_get_float(
    process: &mut Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (fail, context, size, unit, flags, destination) =
        parse_get_operands(operands, "bs_get_float2")?;
    gc::ensure_space(process, 2, 256).map_err(core::gc_error_to_exec)?;
    let context = read_context(process, module, context)?;
    match get_float_value(context, size, unit, flags)? {
        Some((value, bits)) => {
            let ptr = process.heap_mut().alloc(2).map_err(ExecError::from)?;
            let term = write_float(heap_slice(ptr, 2), value).ok_or(ExecError::Badarg)?;
            core::write_term(process, destination, term)?;
            context.set_position_bits(context.position_bits() + bits);
            Ok(InstructionOutcome::Continue)
        }
        None => jump_label(module, fail),
    }
}
pub(super) fn bs_get_binary(
    process: &mut Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (fail, context, size, unit, flags, destination) =
        parse_get_operands(operands, "bs_get_binary2")?;
    // Worst case across both extraction shapes: a sub-binary into a ProcBin
    // source or a full heap copy of the matched bytes.
    let bits = segment_bits(size, unit)?;
    let words = SUB_BINARY_WORDS.max(alloc_binary_word_count(bits / u8::BITS as usize));
    gc::ensure_space(process, words, 256).map_err(core::gc_error_to_exec)?;
    let context = read_context(process, module, context)?;
    match get_binary_bytes(context, size, unit, flags)? {
        Some((bytes, bits)) => {
            let binary = allocate_extracted_binary(process, context, bytes, bits)?;
            core::write_term(process, destination, binary)?;
            context.set_position_bits(context.position_bits() + bits);
            Ok(InstructionOutcome::Continue)
        }
        None => jump_label(module, fail),
    }
}
pub(super) fn bs_skip_bits(
    process: &mut Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (fail, context, size, unit) = match operands {
        [fail, context, size, unit, _flags] => (fail, context, size, unit),
        _ => return Err(ExecError::InvalidOperand("bs_skip_bits2 operands")),
    };
    let bits = segment_bits(size, unit)?;
    let context = read_context(process, module, context)?;
    if !context.has_bits(bits) {
        return jump_label(module, fail);
    }
    context.set_position_bits(context.position_bits() + bits);
    Ok(InstructionOutcome::Continue)
}
pub(super) fn bs_match_string(
    process: &mut Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (fail, context, bit_len, literal) = match operands {
        [fail, context, bit_len, literal] => (fail, context, bit_len, literal),
        _ => return Err(ExecError::InvalidOperand("bs_match_string operands")),
    };
    let bit_len = core::operand_usize(bit_len, "bs_match_string bit length")?;
    let expected = literal_bytes(module, literal, bit_len / u8::BITS as usize)?;
    let context = read_context(process, module, context)?;
    if match_bytes(context, bit_len, expected)? {
        context.set_position_bits(context.position_bits() + bit_len);
        Ok(InstructionOutcome::Continue)
    } else {
        jump_label(module, fail)
    }
}
pub(super) fn bs_test_tail(
    process: &Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (fail, context, expected) = match operands {
        [fail, context, expected] => (fail, context, expected),
        _ => return Err(ExecError::InvalidOperand("bs_test_tail2 operands")),
    };
    let expected = core::operand_usize(expected, "bs_test_tail2 remaining bits")?;
    let context = read_context(process, module, context)?;
    if context.remaining_bits() == expected {
        Ok(InstructionOutcome::Continue)
    } else {
        jump_label(module, fail)
    }
}
pub(super) fn bs_test_unit(
    process: &Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (fail, context, unit) = match operands {
        [fail, context, unit] => (fail, context, unit),
        _ => return Err(ExecError::InvalidOperand("bs_test_unit operands")),
    };
    let unit = core::operand_usize(unit, "bs_test_unit unit")?;
    if unit == 0 {
        return Err(ExecError::Badarg);
    }
    let context = read_context(process, module, context)?;
    if context.remaining_bits().is_multiple_of(unit) {
        Ok(InstructionOutcome::Continue)
    } else {
        jump_label(module, fail)
    }
}
pub(super) fn bs_get_tail(
    process: &mut Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    // `{bs_get_tail, Ctx, Dst, Live}` — no fail operand.
    let (context_operand, destination) = match operands {
        [context, destination, _live] => (context, destination),
        _ => return Err(ExecError::InvalidOperand("bs_get_tail operands")),
    };
    let context = read_context(process, module, context_operand)?;
    if !context.position_bits().is_multiple_of(u8::BITS as usize) {
        return Err(ExecError::Badarg);
    }
    let bits = context.remaining_bits();
    if !bits.is_multiple_of(u8::BITS as usize) {
        return Err(ExecError::Badarg);
    }
    // The tail size depends on the context, so reserve after sizing and then
    // re-read the (possibly moved) context before touching its bytes.
    let words = alloc_binary_word_count(bits / u8::BITS as usize);
    gc::ensure_space(process, words, 256).map_err(core::gc_error_to_exec)?;
    let context = read_context(process, module, context_operand)?;
    let bytes = context.slice(bits).ok_or(ExecError::Badarg)?;
    let binary = allocate_binary(process, bytes)?;
    core::write_term(process, destination, binary)?;
    context.set_position_bits(context.total_bits());
    Ok(InstructionOutcome::Continue)
}
pub(super) fn bs_get_position(
    process: &mut Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (context, destination) = match operands {
        [context, destination, _live] => (context, destination),
        _ => return Err(ExecError::InvalidOperand("bs_get_position operands")),
    };
    let context = read_context(process, module, context)?;
    let term = Term::try_small_int(context.position_bits() as i64).ok_or(ExecError::Badarg)?;
    core::write_term(process, destination, term)?;
    Ok(InstructionOutcome::Continue)
}
pub(super) fn bs_set_position(
    process: &mut Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (context, source) = match operands {
        [context, source] => (context, source),
        _ => return Err(ExecError::InvalidOperand("bs_set_position operands")),
    };
    let context = read_context(process, module, context)?;
    let position = core::read_term(process, module, source)?
        .as_small_int()
        .ok_or(ExecError::Badarg)?;
    if position < 0 || position as usize > context.total_bits() {
        return Err(ExecError::Badarg);
    }
    context.set_position_bits(position as usize);
    Ok(InstructionOutcome::Continue)
}
pub(super) fn bs_match(
    process: &mut Process,
    module: &Module,
    operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
    let (fail, context_operand, commands) = match operands {
        [fail, context, Operand::List(commands)] => (fail, context, commands.as_slice()),
        [fail, context, rest @ ..] => (fail, context, rest),
        _ => return Err(ExecError::InvalidOperand("bs_match operands")),
    };
    // Reserve the worst-case heap need of the whole command list up front:
    // the command loop holds the raw match context across commands, so no
    // collection may run once it starts.
    let context = read_context(process, module, context_operand)?;
    let needed = match_commands_heap_need(context, commands)?;
    if needed > 0 {
        gc::ensure_space(process, needed, 256).map_err(core::gc_error_to_exec)?;
    }
    let context = read_context(process, module, context_operand)?;
    let saved = context.position_bits();
    let result = run_match_commands(process, module, context, commands);
    match result {
        Ok(true) => Ok(InstructionOutcome::Continue),
        Ok(false) => {
            context.set_position_bits(saved);
            jump_label(module, fail)
        }
        Err(error) => {
            context.set_position_bits(saved);
            Err(error)
        }
    }
}
fn run_match_commands(
    process: &mut Process,
    module: &Module,
    context: MatchContext,
    commands: &[Operand],
) -> Result<bool, ExecError> {
    if commands
        .iter()
        .all(|command| matches!(command, Operand::List(_)))
    {
        for command in commands {
            if !run_one_nested_command(process, module, context, command)? {
                return Ok(false);
            }
        }
        return Ok(true);
    }
    let mut index = 0;
    while index < commands.len() {
        let tag = command_name(&commands[index])?;
        index += 1;
        if !run_flat_command(process, module, context, tag, commands, &mut index)? {
            return Ok(false);
        }
    }
    Ok(true)
}
fn run_one_nested_command(
    process: &mut Process,
    module: &Module,
    context: MatchContext,
    command: &Operand,
) -> Result<bool, ExecError> {
    let Operand::List(items) = command else {
        return Err(ExecError::InvalidOperand("bs_match command"));
    };
    let Some((tag, args)) = items.split_first() else {
        return Err(ExecError::InvalidOperand("bs_match command"));
    };
    run_command_args(process, module, context, command_name(tag)?, args)
}
fn run_flat_command(
    process: &mut Process,
    module: &Module,
    context: MatchContext,
    tag: &str,
    commands: &[Operand],
    index: &mut usize,
) -> Result<bool, ExecError> {
    let args = flat_command_args(tag, commands, index)?;
    run_command_args(process, module, context, tag, args)
}

/// Slice the argument operands of one flat-form `bs_match` command and
/// advance `index` past them.
fn flat_command_args<'a>(
    tag: &str,
    commands: &'a [Operand],
    index: &mut usize,
) -> Result<&'a [Operand], ExecError> {
    let arity = match tag {
        "ensure" | "ensure_at_least" => 2,
        // OTP encodes `{ensure_exactly, Stride}` — one argument.
        "ensure_exactly" => 1,
        "integer" | "float" | "binary" => 5,
        "skip" => 1,
        "get_tail" => {
            if commands.len().saturating_sub(*index) >= 3 {
                3
            } else {
                2
            }
        }
        "=:=" => 3,
        _ => return Err(ExecError::InvalidOperand("bs_match command")),
    };
    let end = index
        .checked_add(arity)
        .ok_or(ExecError::InvalidOperand("bs_match command"))?;
    let args = commands
        .get(*index..end)
        .ok_or(ExecError::InvalidOperand("bs_match command"))?;
    *index = end;
    Ok(args)
}

/// Upper bound on heap words the command list can allocate. Integer
/// extractions produce immediates, float extractions need a boxed float,
/// binary extractions need a sub-binary or a heap copy of the segment, and
/// `get_tail` copies at most the bits remaining when the instruction starts
/// (the position only ever advances).
fn match_commands_heap_need(
    context: MatchContext,
    commands: &[Operand],
) -> Result<usize, ExecError> {
    let mut needed = 0_usize;
    if commands
        .iter()
        .all(|command| matches!(command, Operand::List(_)))
    {
        for command in commands {
            let Operand::List(items) = command else {
                return Err(ExecError::InvalidOperand("bs_match command"));
            };
            let Some((tag, args)) = items.split_first() else {
                return Err(ExecError::InvalidOperand("bs_match command"));
            };
            needed = needed.saturating_add(command_heap_need(context, command_name(tag)?, args)?);
        }
        return Ok(needed);
    }
    let mut index = 0;
    while index < commands.len() {
        let tag = command_name(&commands[index])?;
        index += 1;
        let args = flat_command_args(tag, commands, &mut index)?;
        needed = needed.saturating_add(command_heap_need(context, tag, args)?);
    }
    Ok(needed)
}

fn command_heap_need(
    context: MatchContext,
    tag: &str,
    args: &[Operand],
) -> Result<usize, ExecError> {
    Ok(match (tag, args) {
        ("float", _) => 2,
        ("binary", [_live, _flags, size, unit, _dst]) => {
            let bits = segment_bits(size, unit)?;
            SUB_BINARY_WORDS.max(alloc_binary_word_count(bits / u8::BITS as usize))
        }
        ("get_tail", _) => alloc_binary_word_count(context.remaining_bits() / u8::BITS as usize),
        _ => 0,
    })
}
fn run_command_args(
    process: &mut Process,
    module: &Module,
    context: MatchContext,
    tag: &str,
    args: &[Operand],
) -> Result<bool, ExecError> {
    match (tag, args) {
        // `{ensure_at_least, Stride, Unit}` requires at least Stride more
        // bits with the total remainder evenly divisible by Unit (e.g.
        // `<<H, T/binary>>` emits Stride=8, Unit=8: one byte plus a
        // whole-byte tail). Stride and Unit are independent operands, not
        // a Size*Unit product.
        ("ensure" | "ensure_at_least", [stride, unit]) => {
            let stride = core::operand_usize(stride, "bs_match ensure stride")?;
            let unit = core::operand_usize(unit, "bs_match ensure unit")?.max(1);
            let remaining = context.remaining_bits();
            Ok(remaining >= stride && remaining.is_multiple_of(unit))
        }
        ("ensure_exactly", [stride, _unit]) => {
            Ok(context.remaining_bits() == core::operand_usize(stride, "bs_match ensure exactly")?)
        }
        // OTP emits the nested form as `{ensure_exactly, Stride}` — one argument.
        ("ensure_exactly", [stride]) => {
            Ok(context.remaining_bits() == core::operand_usize(stride, "bs_match ensure exactly")?)
        }
        ("integer", [_live, flags, size, unit, dst]) => {
            let Some((value, bits)) = get_integer_value(context, size, unit, flags)? else {
                return Ok(false);
            };
            let term = Term::try_small_int(value).ok_or(ExecError::Badarg)?;
            core::write_term(process, dst, term)?;
            context.set_position_bits(context.position_bits() + bits);
            Ok(true)
        }
        ("float", [_live, flags, size, unit, dst]) => {
            let Some((value, bits)) = get_float_value(context, size, unit, flags)? else {
                return Ok(false);
            };
            // Space was reserved by `bs_match` before the command loop began.
            let ptr = process.heap_mut().alloc(2).map_err(ExecError::from)?;
            let term = write_float(heap_slice(ptr, 2), value).ok_or(ExecError::Badarg)?;
            core::write_term(process, dst, term)?;
            context.set_position_bits(context.position_bits() + bits);
            Ok(true)
        }
        ("binary", [_live, flags, size, unit, dst]) => {
            let Some((bytes, bits)) = get_binary_bytes(context, size, unit, flags)? else {
                return Ok(false);
            };
            let binary = allocate_extracted_binary(process, context, bytes, bits)?;
            core::write_term(process, dst, binary)?;
            context.set_position_bits(context.position_bits() + bits);
            Ok(true)
        }
        ("skip", [stride]) => {
            let bits = core::operand_usize(stride, "bs_match skip stride")?;
            if !context.has_bits(bits) {
                return Ok(false);
            }
            context.set_position_bits(context.position_bits() + bits);
            Ok(true)
        }
        ("get_tail", [_live, dst]) | ("get_tail", [_live, _, dst]) => {
            if !context.position_bits().is_multiple_of(u8::BITS as usize) {
                return Ok(false);
            }
            let bits = context.remaining_bits();
            let bytes = context.slice(bits).ok_or(ExecError::Badarg)?;
            let binary = allocate_binary(process, bytes)?;
            core::write_term(process, dst, binary)?;
            context.set_position_bits(context.total_bits());
            Ok(true)
        }
        ("=:=", [_live, bits, value]) => {
            let bits = core::operand_usize(bits, "bs_match exact bits")?;
            let expected = exact_value_bytes(module, value, bits)?;
            if match_bytes(context, bits, expected)? {
                context.set_position_bits(context.position_bits() + bits);
                Ok(true)
            } else {
                Ok(false)
            }
        }
        _ => Err(ExecError::InvalidOperand("bs_match command")),
    }
}
fn get_integer_value(
    context: MatchContext,
    size: &Operand,
    unit: &Operand,
    flags: &Operand,
) -> Result<Option<(i64, usize)>, ExecError> {
    let size_bits = segment_bits(size, unit)?;
    let flags = SegmentFlags::from_flags(flags);
    if !context.has_bits(size_bits) {
        return Ok(None);
    }
    if size_bits == 0 {
        return Ok(Some((0, 0)));
    }
    // Byte-aligned segments take the fast slice path; anything else (e.g.
    // `<<A:4, B:4>>` in gleam_stdlib's percent_encode) is read bitwise.
    if size_bits.is_multiple_of(u8::BITS as usize)
        && context.position_bits().is_multiple_of(u8::BITS as usize)
    {
        let bytes = context.slice(size_bits).ok_or(ExecError::Badarg)?;
        return Ok(Some((decode_integer(bytes, flags)?, size_bits)));
    }
    if matches!(flags.endian, Endian::Little) {
        // OTP only defines little-endian for whole-byte segments.
        return Err(ExecError::Badarg);
    }
    if size_bits > 63 {
        return Err(ExecError::Badarg);
    }
    let unsigned = context
        .read_bits_big_endian(context.position_bits(), size_bits)
        .ok_or(ExecError::Badarg)?;
    let value = if flags.signed && size_bits > 0 && (unsigned >> (size_bits - 1)) & 1 == 1 {
        (unsigned as i64) - (1_i64 << size_bits)
    } else {
        unsigned as i64
    };
    Ok(Some((value, size_bits)))
}
fn get_float_value(
    context: MatchContext,
    size: &Operand,
    unit: &Operand,
    flags: &Operand,
) -> Result<Option<(f64, usize)>, ExecError> {
    let bits = segment_bits(size, unit)?;
    if !matches!(bits, 32 | 64) || !context.position_bits().is_multiple_of(u8::BITS as usize) {
        return Err(ExecError::Badarg);
    }
    if !context.has_bits(bits) {
        return Ok(None);
    }
    let bytes = context.slice(bits).ok_or(ExecError::Badarg)?;
    let value = match (bits, Endian::from_flags(flags)) {
        (32, Endian::Big) => {
            f32::from_bits(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) as f64
        }
        (32, Endian::Little) => {
            f32::from_bits(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) as f64
        }
        (64, Endian::Big) => f64::from_bits(u64::from_be_bytes([
            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
        ])),
        (64, Endian::Little) => f64::from_bits(u64::from_le_bytes([
            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
        ])),
        _ => return Err(ExecError::Badarg),
    };
    Ok(Some((value, bits)))
}
fn get_binary_bytes(
    context: MatchContext,
    size: &Operand,
    unit: &Operand,
    _flags: &Operand,
) -> Result<Option<(&'static [u8], usize)>, ExecError> {
    let bits = segment_bits(size, unit)?;
    if !bits.is_multiple_of(u8::BITS as usize)
        || !context.position_bits().is_multiple_of(u8::BITS as usize)
    {
        return Err(ExecError::Badarg);
    }
    if !context.has_bits(bits) {
        return Ok(None);
    }
    Ok(Some((context.slice(bits).ok_or(ExecError::Badarg)?, bits)))
}
fn match_bytes(context: MatchContext, bits: usize, expected: &[u8]) -> Result<bool, ExecError> {
    if !bits.is_multiple_of(u8::BITS as usize) {
        return Err(ExecError::Badarg);
    }
    if !context.position_bits().is_multiple_of(u8::BITS as usize) || !context.has_bits(bits) {
        return Ok(false);
    }
    Ok(context.slice(bits).ok_or(ExecError::Badarg)? == expected)
}
/// Copy `bytes` into a fresh heap binary. Callers reserve heap space before
/// deriving `bytes` from the match context — no GC may run here because the
/// source slice points into the heap.
fn allocate_binary(process: &mut Process, bytes: &[u8]) -> Result<Term, ExecError> {
    let words = alloc_binary_word_count(bytes.len());
    let ptr = process.heap_mut().alloc(words).map_err(ExecError::from)?;
    alloc_binary(heap_slice(ptr, words), bytes).ok_or(ExecError::Badarg)
}
fn allocate_extracted_binary(
    process: &mut Process,
    context: MatchContext,
    bytes: &[u8],
    bits: usize,
) -> Result<Term, ExecError> {
    let source = context.source_term();
    if ProcBin::new(source).is_some() {
        let start = context.position_bits() / u8::BITS as usize;
        let length = bits / u8::BITS as usize;
        let ptr = process
            .heap_mut()
            .alloc(SUB_BINARY_WORDS)
            .map_err(ExecError::from)?;
        return write_sub_binary(heap_slice(ptr, SUB_BINARY_WORDS), source, start, length)
            .ok_or(ExecError::Badarg);
    }

    allocate_binary(process, bytes)
}
fn read_context(
    process: &Process,
    module: &Module,
    operand: &Operand,
) -> Result<MatchContext, ExecError> {
    MatchContext::new(core::read_term(process, module, operand)?).ok_or(ExecError::Badarg)
}
fn command_name(operand: &Operand) -> Result<&'static str, ExecError> {
    match operand {
        Operand::Atom(None) => Ok("=:="),
        Operand::Unsigned(0) | Operand::Integer(0) => Ok("ensure_at_least"),
        Operand::Unsigned(1) | Operand::Integer(1) => Ok("ensure_exactly"),
        Operand::Unsigned(2) | Operand::Integer(2) => Ok("integer"),
        Operand::Unsigned(3) | Operand::Integer(3) => Ok("float"),
        Operand::Unsigned(4) | Operand::Integer(4) => Ok("binary"),
        Operand::Unsigned(5) | Operand::Integer(5) => Ok("skip"),
        Operand::Unsigned(6) | Operand::Integer(6) => Ok("get_tail"),
        // Loaders that keep the command tag as a named atom (the form OTP's
        // assembler writes) resolve through the pre-interned common atoms.
        Operand::Atom(Some(atom)) if *atom == Atom::BS_ENSURE_AT_LEAST => Ok("ensure_at_least"),
        Operand::Atom(Some(atom)) if *atom == Atom::BS_ENSURE_EXACTLY => Ok("ensure_exactly"),
        Operand::Atom(Some(atom)) if *atom == Atom::BS_INTEGER => Ok("integer"),
        Operand::Atom(Some(atom)) if *atom == Atom::BS_FLOAT => Ok("float"),
        Operand::Atom(Some(atom)) if *atom == Atom::BS_BINARY => Ok("binary"),
        Operand::Atom(Some(atom)) if *atom == Atom::BS_SKIP => Ok("skip"),
        Operand::Atom(Some(atom)) if *atom == Atom::BS_GET_TAIL => Ok("get_tail"),
        Operand::Atom(Some(atom)) if *atom == Atom::BS_EQ_EXACT => Ok("=:="),
        _ => Err(ExecError::InvalidOperand("bs_match command")),
    }
}
pub(super) fn segment_bits(size: &Operand, unit: &Operand) -> Result<usize, ExecError> {
    let size = core::operand_usize(size, "segment size")?;
    let unit = core::operand_usize(unit, "segment unit")?;
    size.checked_mul(unit)
        .ok_or(ExecError::InvalidOperand("segment size"))
}
pub(super) fn literal_bytes<'a>(
    module: &'a Module,
    operand: &'a Operand,
    byte_len: usize,
) -> Result<&'a [u8], ExecError> {
    match operand {
        Operand::Literal(index) => match module.literals.get(*index) {
            Some(Literal::Binary(bytes) | Literal::String(bytes)) => bytes
                .get(..byte_len)
                .filter(|bytes| bytes.len() == byte_len)
                .ok_or(ExecError::Badarg),
            _ => Err(ExecError::Badarg),
        },
        offset => {
            let offset = core::operand_usize(offset, "string table offset")?;
            let end = offset.checked_add(byte_len).ok_or(ExecError::Badarg)?;
            module
                .string_table
                .get(offset..end)
                .ok_or(ExecError::Badarg)
        }
    }
}
fn exact_value_bytes<'a>(
    module: &'a Module,
    operand: &'a Operand,
    bits: usize,
) -> Result<&'a [u8], ExecError> {
    if !bits.is_multiple_of(u8::BITS as usize) {
        return Err(ExecError::Badarg);
    }
    literal_bytes(module, operand, bits / u8::BITS as usize)
}
#[derive(Copy, Clone)]
pub(crate) struct MatchContext {
    ptr: *mut u64,
}
impl MatchContext {
    pub(crate) fn new(term: Term) -> Option<Self> {
        let ptr = term.heap_ptr()? as *mut u64;
        (boxed_tag(ptr) == Some(BoxedTag::MatchContext)).then_some(Self { ptr })
    }
    pub(crate) fn position_bits(self) -> usize {
        read_word(self.ptr, 1) as usize
    }
    pub(crate) fn set_position_bits(self, bits: usize) {
        write_word(self.ptr, 1, bits as u64);
    }
    pub(crate) fn total_bits(self) -> usize {
        read_word(self.ptr, 2) as usize
    }
    fn source_term(self) -> Term {
        Term::from_raw(read_word(self.ptr, 3))
    }
    fn source(self) -> Option<BinaryRef> {
        BinaryRef::new(self.source_term())
    }
    pub(crate) fn remaining_bits(self) -> usize {
        self.total_bits().saturating_sub(self.position_bits())
    }
    pub(crate) fn has_bits(self, bits: usize) -> bool {
        self.position_bits()
            .checked_add(bits)
            .is_some_and(|end| end <= self.total_bits())
    }
    /// Read `size_bits` (1..=63) starting at absolute `pos_bits`, MSB-first.
    fn read_bits_big_endian(self, pos_bits: usize, size_bits: usize) -> Option<u64> {
        let end = pos_bits.checked_add(size_bits)?;
        if end > self.total_bits() {
            return None;
        }
        let bytes = self.source()?.as_bytes();
        let mut acc = 0_u64;
        for offset in pos_bits..end {
            let byte = *bytes.get(offset / u8::BITS as usize)?;
            let bit = (byte >> (7 - (offset % u8::BITS as usize))) & 1;
            acc = (acc << 1) | u64::from(bit);
        }
        Some(acc)
    }
    pub(crate) fn slice(self, bits: usize) -> Option<&'static [u8]> {
        if !bits.is_multiple_of(u8::BITS as usize)
            || !self.position_bits().is_multiple_of(u8::BITS as usize)
        {
            return None;
        }
        let start = self.position_bits() / u8::BITS as usize;
        let len = bits / u8::BITS as usize;
        let end = start.checked_add(len)?;
        self.source()?.as_bytes().get(start..end)
    }
}
#[derive(Copy, Clone)]
pub(crate) enum Endian {
    Big,
    Little,
}
impl Endian {
    pub(crate) fn from_flags(flags: &Operand) -> Self {
        match flags {
            Operand::Unsigned(1) | Operand::Integer(1) => Self::Little,
            Operand::List(items) if items.iter().any(is_little_flag) => Self::Little,
            Operand::Unsigned(v) if v & 0x02 != 0 => Self::Little,
            Operand::Integer(v) if v & 0x02 != 0 => Self::Little,
            _ => Self::Big,
        }
    }
}
#[derive(Copy, Clone)]
pub(crate) struct SegmentFlags {
    pub(crate) endian: Endian,
    pub(crate) signed: bool,
}
impl SegmentFlags {
    fn from_flags(flags: &Operand) -> Self {
        let signed = match flags {
            Operand::Unsigned(v) => v & 0x04 != 0,
            Operand::Integer(v) => v & 0x04 != 0,
            Operand::List(items) => items.iter().any(is_signed_flag),
            _ => false,
        };
        Self {
            endian: Endian::from_flags(flags),
            signed,
        }
    }
}
fn is_signed_flag(flag: &Operand) -> bool {
    matches!(flag, Operand::Unsigned(v) if v & 0x04 != 0)
        || matches!(flag, Operand::Integer(v) if v & 0x04 != 0)
}
fn is_little_flag(flag: &Operand) -> bool {
    matches!(flag, Operand::Unsigned(1) | Operand::Integer(1))
}
fn parse_get_operands<'a>(
    operands: &'a [Operand],
    context: &'static str,
) -> Result<GetOperands<'a>, ExecError> {
    match operands {
        [fail, match_context, _live, size, unit, flags, destination] => {
            Ok((fail, match_context, size, unit, flags, destination))
        }
        [fail, match_context, size, unit, flags, destination] => {
            Ok((fail, match_context, size, unit, flags, destination))
        }
        _ => Err(ExecError::InvalidOperand(context)),
    }
}
pub(crate) fn decode_integer(bytes: &[u8], flags: SegmentFlags) -> Result<i64, ExecError> {
    if bytes.len() > std::mem::size_of::<i64>() {
        return Err(ExecError::Badarg);
    }
    let msb = match flags.endian {
        Endian::Big => bytes.first(),
        Endian::Little => bytes.last(),
    };
    let negative = flags.signed && msb.is_some_and(|byte| byte & 0x80 != 0);
    let fill = if negative { 0xff_u8 } else { 0x00_u8 };
    let mut full = [fill; 8];
    match flags.endian {
        Endian::Big => full[8 - bytes.len()..].copy_from_slice(bytes),
        Endian::Little => full[..bytes.len()].copy_from_slice(bytes),
    }
    Ok(match flags.endian {
        Endian::Big => u64::from_be_bytes(full) as i64,
        Endian::Little => u64::from_le_bytes(full) as i64,
    })
}
