"""Embodied BF interpreter — 7 instructions on a unified tape.

The key modification from standard BF:
- No separate I/O. Data and code share one tape.
- '.' copies tape[DP] to tape[WP] and advances WP (the replication primitive).
- Three pointers: IP (instruction), DP (data), WP (write).

Instruction encoding (ASCII byte values):
  > (62)  Move DP right
  < (60)  Move DP left
  + (43)  Increment tape[DP]
  - (45)  Decrement tape[DP]
  . (46)  Copy tape[DP] → tape[WP]; WP++
  [ (91)  If tape[DP]==0, jump past matching ]
  ] (93)  If tape[DP]!=0, jump back to matching [

All other byte values are no-ops (skipped, not counted toward ops).
"""

# Byte values for the 7 instructions
OP_RIGHT = 62   # >
OP_LEFT  = 60   # <
OP_INC   = 43   # +
OP_DEC   = 45   # -
OP_COPY  = 46   # .
OP_OPEN  = 91   # [
OP_CLOSE = 93   # ]

INSTRUCTION_SET = frozenset((OP_RIGHT, OP_LEFT, OP_INC, OP_DEC, OP_COPY, OP_OPEN, OP_CLOSE))


def _build_bracket_map(tape: bytearray) -> dict[int, int]:
    """Pre-compute matching bracket positions.

    Returns a dict mapping each matched '[' index to its matching ']' and
    vice versa. Unmatched brackets are silently ignored — they become no-ops
    at runtime. This allows partial execution of tapes with imperfect bracket
    structure, which is critical for symbiogenesis (two partial tapes fusing
    into a working program).
    """
    bracket_map = {}
    stack = []
    for i, b in enumerate(tape):
        if b == OP_OPEN:
            stack.append(i)
        elif b == OP_CLOSE:
            if stack:
                j = stack.pop()
                bracket_map[j] = i
                bracket_map[i] = j
            # else: unmatched ] — ignore, will be no-op at runtime
    # any remaining unmatched [ on stack — ignore, will be no-op at runtime
    return bracket_map


def execute(tape: bytearray, max_steps: int = 10_000,
            dp_offset: int | None = None,
            wp_offset: int | None = None) -> tuple[bytearray, int]:
    """Execute tape as embodied BF.

    Args:
        tape: The unified code/data tape (modified in place AND returned).
        max_steps: Maximum instruction executions before halting.
        dp_offset: Initial data pointer offset from start. Default: len//3.
        wp_offset: Initial write pointer offset from start. Default: 2*len//3.

    Returns:
        (modified_tape, ops_executed) — ops_executed counts only actual
        instructions, not no-ops.
    """
    length = len(tape)
    if length == 0:
        return tape, 0

    bracket_map = _build_bracket_map(tape)

    ip = 0
    dp = dp_offset if dp_offset is not None else length // 3
    wp = wp_offset if wp_offset is not None else (2 * length) // 3
    ops = 0

    while ops < max_steps and ip < length:
        opcode = tape[ip]

        if opcode == OP_RIGHT:
            dp = (dp + 1) % length
            ops += 1
        elif opcode == OP_LEFT:
            dp = (dp - 1) % length
            ops += 1
        elif opcode == OP_INC:
            tape[dp] = (tape[dp] + 1) & 0xFF
            ops += 1
        elif opcode == OP_DEC:
            tape[dp] = (tape[dp] - 1) & 0xFF
            ops += 1
        elif opcode == OP_COPY:
            tape[wp % length] = tape[dp]
            wp = (wp + 1) % length
            ops += 1
        elif opcode == OP_OPEN:
            if ip in bracket_map:
                ops += 1
                if tape[dp] == 0:
                    ip = bracket_map[ip]  # jump past matching ]
            # else: unmatched [ — skip (no-op)
        elif opcode == OP_CLOSE:
            if ip in bracket_map:
                ops += 1
                if tape[dp] != 0:
                    ip = bracket_map[ip] - 1
            # else: unmatched ] — skip (no-op)
        # else: no-op, just advance IP (not counted)

        ip += 1

    return tape, ops


def execute_traced(tape: bytearray, max_steps: int = 10_000,
                   dp_offset: int | None = None,
                   wp_offset: int | None = None) -> tuple[bytearray, int, set[int], set[int]]:
    """Like execute(), but also tracks which positions were executed and written.

    Returns:
        (modified_tape, ops_executed, executed_positions, written_positions)
    """
    length = len(tape)
    if length == 0:
        return tape, 0, set(), set()

    bracket_map = _build_bracket_map(tape)

    ip = 0
    dp = dp_offset if dp_offset is not None else length // 3
    wp = wp_offset if wp_offset is not None else (2 * length) // 3
    ops = 0
    executed_positions: set[int] = set()
    written_positions: set[int] = set()

    while ops < max_steps and ip < length:
        opcode = tape[ip]

        if opcode == OP_RIGHT:
            executed_positions.add(ip)
            dp = (dp + 1) % length
            ops += 1
        elif opcode == OP_LEFT:
            executed_positions.add(ip)
            dp = (dp - 1) % length
            ops += 1
        elif opcode == OP_INC:
            executed_positions.add(ip)
            tape[dp] = (tape[dp] + 1) & 0xFF
            ops += 1
        elif opcode == OP_DEC:
            executed_positions.add(ip)
            tape[dp] = (tape[dp] - 1) & 0xFF
            ops += 1
        elif opcode == OP_COPY:
            executed_positions.add(ip)
            w_pos = wp % length
            tape[w_pos] = tape[dp]
            written_positions.add(w_pos)
            wp = (wp + 1) % length
            ops += 1
        elif opcode == OP_OPEN:
            if ip in bracket_map:
                executed_positions.add(ip)
                ops += 1
                if tape[dp] == 0:
                    ip = bracket_map[ip]
        elif opcode == OP_CLOSE:
            if ip in bracket_map:
                executed_positions.add(ip)
                ops += 1
                if tape[dp] != 0:
                    ip = bracket_map[ip] - 1

        ip += 1

    return tape, ops, executed_positions, written_positions
