"""
Peephole Optimizer for 8080/Z80.

Performs pattern-based optimizations on generated assembly code.
This runs after code generation to clean up inefficient sequences.

For Z80 target:
1. First applies universal patterns
2. Then translates 8080 mnemonics to Z80
3. Then applies Z80-specific patterns (JR relative jumps, etc.)
"""

import re
from dataclasses import dataclass
from enum import Enum, auto
from typing import Callable


class Target(Enum):
    """Target processor."""

    I8080 = auto()
    Z80 = auto()


class InputSyntax(Enum):
    """Input assembly syntax.

    I8080: Input uses 8080 mnemonics (MOV, MVI, LXI, etc.)
           Will be translated to Z80 if target is Z80.
    Z80: Input already uses Z80 mnemonics (LD, JP, etc.)
         Skips 8080 pattern phases and translation.
    """

    I8080 = auto()
    Z80 = auto()


# 8080 to Z80 mnemonic translations
Z80_TRANSLATIONS: dict[str, str] = {
    # Arithmetic
    "ADI": "ADD A,",
    "ACI": "ADC A,",
    "SUI": "SUB",
    "SBI": "SBC A,",
    "ANI": "AND",
    "ORI": "OR",
    "XRI": "XOR",
    "CPI": "CP",
    # Register operations
    "MOV": "LD",
    "MVI": "LD",
    "LXI": "LD",
    "LDA": "LD A,",
    "STA": "LD",
    "LHLD": "LD HL,",
    "SHLD": "LD",
    "LDAX": "LD A,",
    "STAX": "LD",
    # Arithmetic with accumulator
    "ADD": "ADD A,",
    "ADC": "ADC A,",
    "SUB": "SUB",
    "SBB": "SBC A,",
    "ANA": "AND",
    "ORA": "OR",
    "XRA": "XOR",
    "CMP": "CP",
    # Increment/Decrement
    "INR": "INC",
    "DCR": "DEC",
    "INX": "INC",
    "DCX": "DEC",
    "DAD": "ADD HL,",
    # Jumps and calls
    "JMP": "JP",
    "JZ": "JP Z,",
    "JNZ": "JP NZ,",
    "JC": "JP C,",
    "JNC": "JP NC,",
    "JP": "JP P,",
    "JM": "JP M,",
    "JPE": "JP PE,",
    "JPO": "JP PO,",
    "CZ": "CALL Z,",
    "CNZ": "CALL NZ,",
    "CC": "CALL C,",
    "CNC": "CALL NC,",
    "CP": "CALL P,",
    "CM": "CALL M,",
    "CPE": "CALL PE,",
    "CPO": "CALL PO,",
    "RZ": "RET Z",
    "RNZ": "RET NZ",
    "RC": "RET C",
    "RNC": "RET NC",
    "RP": "RET P",
    "RM": "RET M",
    "RPE": "RET PE",
    "RPO": "RET PO",
    # Stack
    "PUSH": "PUSH",
    "POP": "POP",
    "XTHL": "EX (SP),HL",
    "SPHL": "LD SP,HL",
    # Misc
    "XCHG": "EX DE,HL",
    "PCHL": "JP (HL)",
    "CMA": "CPL",
    "CMC": "CCF",
    "STC": "SCF",
    "RAL": "RLA",
    "RAR": "RRA",
    "RLC": "RLCA",
    "RRC": "RRCA",
    "DAA": "DAA",
    "NOP": "NOP",
    "HLT": "HALT",
    "DI": "DI",
    "EI": "EI",
    # I/O
    "IN": "IN A,",
    "OUT": "OUT",
    "INP": "IN A,(C)",  # Variable port
    "OUTP": "OUT (C),A",  # Variable port
}

# Z80 register pair translations
Z80_REG_PAIRS: dict[str, str] = {
    "B": "BC",
    "D": "DE",
    "H": "HL",
    "SP": "SP",
    "PSW": "AF",
}


@dataclass
class PeepholePattern:
    """A peephole optimization pattern."""

    name: str
    # Pattern: list of (opcode, operands) tuples, or regex strings
    pattern: list[tuple[str, str | None]]
    # Replacement: list of (opcode, operands) tuples, or None to delete
    replacement: list[tuple[str, str]] | None
    # Optional condition function
    condition: Callable[[list[tuple[str, str]]], bool] | None = None
    # Target-specific (None = both, Target.Z80 = Z80 only, Target.I8080 = 8080 only)
    target: Target | None = None


class PeepholeOptimizer:
    """
    Peephole optimizer that applies pattern-based transformations.

    Patterns are applied repeatedly until no more changes are made.
    """

    def __init__(
        self,
        target: Target = Target.Z80,
        input_syntax: InputSyntax | None = None,
    ) -> None:
        self.target = target
        # Default: input syntax matches target (Z80 target expects Z80 input now)
        if input_syntax is None:
            input_syntax = InputSyntax.Z80 if target == Target.Z80 else InputSyntax.I8080
        self.input_syntax = input_syntax
        self.patterns = self._init_patterns()
        self.z80_patterns = self._init_z80_patterns()  # Native Z80 patterns
        self.stats: dict[str, int] = {}
        # Track label positions for relative jump optimization
        self.label_positions: dict[str, int] = {}

    def _init_patterns(self) -> list[PeepholePattern]:
        """Initialize peephole optimization patterns."""
        return [
            # Push/Pop elimination: PUSH r; POP r -> (nothing)
            PeepholePattern(
                name="push_pop_same",
                pattern=[("PUSH", None), ("POP", None)],
                replacement=[],
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),
            # POP r; PUSH r -> just peek, keep value on stack
            # Actually this is a "XTHL" for HL but simpler to just keep both
            # POP H; PUSH H copies TOS to HL - can we use XTHL? No, different semantics
            # Actually POP H; PUSH H loads TOS into HL AND keeps it on stack
            # Can't optimize this without more context... skip for now

            # Redundant MOV: MOV A,r; MOV r,A -> MOV A,r
            PeepholePattern(
                name="redundant_mov",
                pattern=[("MOV", "A,*"), ("MOV", "*,A")],
                replacement=None,  # Keep first, handled specially
                condition=lambda ops: ops[0][1].split(",")[1] == ops[1][1].split(",")[0],
            ),
            # Jump to next: JMP L; L: -> L:
            # This is handled specially in _optimize_pass, not as a standard pattern
            # Removed - was incorrectly deleting ALL JMP instructions!
            # Zero A: MVI A,0 -> XRA A (smaller, faster)
            PeepholePattern(
                name="zero_a_mvi",
                pattern=[("MVI", "A,0")],
                replacement=[("XRA", "A")],
            ),
            # Compare to zero: CPI 0 -> ORA A
            PeepholePattern(
                name="cpi_zero",
                pattern=[("CPI", "0")],
                replacement=[("ORA", "A")],
            ),
            # Load then store same: LDA x; STA x -> LDA x
            PeepholePattern(
                name="load_store_same",
                pattern=[("LDA", None), ("STA", None)],
                replacement=None,  # Keep first only
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),
            # Double INX: INX H; INX H -> LXI D,2; DAD D (if no flags needed)
            # This is actually worse, skip it

            # Redundant PUSH/POP around no-change: PUSH H; ... ; POP H
            # (complex pattern, skip for now)

            # MOV A,A -> (nothing)
            PeepholePattern(
                name="mov_a_a",
                pattern=[("MOV", "A,A")],
                replacement=[],
            ),
            # Duplicate MOV: MOV X,Y; MOV X,Y -> MOV X,Y
            PeepholePattern(
                name="duplicate_mov",
                pattern=[("MOV", None), ("MOV", None)],
                replacement=None,  # Keep first only
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),
            # Duplicate LD (Z80): LD X,Y; LD X,Y -> LD X,Y
            PeepholePattern(
                name="duplicate_ld",
                pattern=[("LD", None), ("LD", None)],
                replacement=None,  # Keep first only
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),
            # Wasteful byte extension before byte op: LD L,A; LD H,0; SUB x -> SUB x
            # (Also for CP, AND, OR, XOR, ADD byte ops)
            PeepholePattern(
                name="useless_extend_before_sub",
                pattern=[("LD", "L,A"), ("LD", "H,0"), ("SUB", None)],
                replacement=None,  # Keep last only
                condition=lambda ops: True,  # Always apply
            ),
            PeepholePattern(
                name="useless_extend_before_cp",
                pattern=[("LD", "L,A"), ("LD", "H,0"), ("CP", None)],
                replacement=None,  # Keep last only
                condition=lambda ops: True,
            ),
            # Redundant byte extension: MOV L,A; MVI H,0; MOV L,A; MVI H,0 -> MOV L,A; MVI H,0
            PeepholePattern(
                name="double_byte_extend",
                pattern=[("MOV", "L,A"), ("MVI", "H,0"), ("MOV", "L,A"), ("MVI", "H,0")],
                replacement=[("MOV", "L,A"), ("MVI", "H,0")],
            ),
            # Redundant load to L: MOV L,A; MVI H,0; PUSH H; MOV L,A -> MOV L,A; MVI H,0; PUSH H
            # (The second MOV L,A before further ops is redundant if we just pushed)
            PeepholePattern(
                name="redundant_mov_l_after_push",
                pattern=[("MOV", "L,A"), ("MVI", "H,0"), ("PUSH", "H"), ("MOV", "L,A")],
                replacement=[("MOV", "L,A"), ("MVI", "H,0"), ("PUSH", "H")],
            ),
            # MOV to self: MOV r,r -> (nothing)
            PeepholePattern(
                name="mov_self",
                pattern=[("MOV", None)],
                replacement=[],
                condition=lambda ops: ops[0][1].split(",")[0] == ops[0][1].split(",")[1],
            ),
            # Sequential INX: INX H; INX H; INX H -> LXI D,3; DAD D (for 3+)
            # Skip for now - complex

            # LXI H,0 followed by DAD -> just use the other reg pair
            # Skip - complex

            # XCHG; XCHG -> (nothing)
            PeepholePattern(
                name="double_xchg",
                pattern=[("XCHG", ""), ("XCHG", "")],
                replacement=[],
            ),
            # XTHL; XTHL -> (nothing)
            PeepholePattern(
                name="double_xthl",
                pattern=[("XTHL", ""), ("XTHL", "")],
                replacement=[],
            ),
            # CMC; CMC -> (nothing) - complement carry twice
            PeepholePattern(
                name="double_cmc",
                pattern=[("CMC", ""), ("CMC", "")],
                replacement=[],
            ),
            # CMA; CMA -> (nothing) - complement A twice
            PeepholePattern(
                name="double_cma",
                pattern=[("CMA", ""), ("CMA", "")],
                replacement=[],
            ),
            # POP H; PUSH H; LXI H,x -> LXI H,x
            # The POP/PUSH just peeks at TOS (leaves stack unchanged), then HL is overwritten
            PeepholePattern(
                name="pop_push_lxi",
                pattern=[("POP", "H"), ("PUSH", "H"), ("LXI", None)],
                replacement=None,  # Handled specially - keep only LXI
                condition=lambda ops: ops[2][1].startswith("H,"),
            ),
            # PUSH PSW; STA addr; POP PSW -> STA addr
            # Saving/restoring A around a store of A is pointless
            PeepholePattern(
                name="push_sta_pop",
                pattern=[("PUSH", "PSW"), ("STA", None), ("POP", "PSW")],
                replacement=None,  # Keep only STA
            ),
            # RAL; RAR -> (effectively nothing, but changes flags)
            # Skip - affects flags

            # Conditional jump followed by unconditional to same place
            # JZ L; JMP L -> JMP L
            PeepholePattern(
                name="cond_uncond_same",
                pattern=[("JZ", None), ("JMP", None)],
                replacement=None,  # Keep second only
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),
            PeepholePattern(
                name="cond_uncond_same_jnz",
                pattern=[("JNZ", None), ("JMP", None)],
                replacement=None,
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),
            PeepholePattern(
                name="cond_uncond_same_jc",
                pattern=[("JC", None), ("JMP", None)],
                replacement=None,
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),
            PeepholePattern(
                name="cond_uncond_same_jnc",
                pattern=[("JNC", None), ("JMP", None)],
                replacement=None,
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),

            # LXI H,x; XCHG; POP H -> LXI D,x; POP H
            # (Moving constant to DE before popping can avoid XCHG)
            PeepholePattern(
                name="lxi_xchg_pop",
                pattern=[("LXI", None), ("XCHG", ""), ("POP", "H")],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1].startswith("H,"),
            ),

            # LXI H,x; XCHG; CALL y -> LXI D,x; CALL y
            # (Loading constant into DE directly saves the XCHG)
            PeepholePattern(
                name="lxi_xchg_call",
                pattern=[("LXI", None), ("XCHG", ""), ("CALL", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1].startswith("H,"),
            ),

            # LXI H,x; XCHG; JMP y -> LXI D,x; JMP y
            PeepholePattern(
                name="lxi_xchg_jmp",
                pattern=[("LXI", None), ("XCHG", ""), ("JMP", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1].startswith("H,"),
            ),

            # LXI H,x; XCHG; LDA y -> LXI D,x; LDA y
            PeepholePattern(
                name="lxi_xchg_lda",
                pattern=[("LXI", None), ("XCHG", ""), ("LDA", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1].startswith("H,"),
            ),

            # LXI H,x; XCHG; STA y -> LXI D,x; STA y
            PeepholePattern(
                name="lxi_xchg_sta",
                pattern=[("LXI", None), ("XCHG", ""), ("STA", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1].startswith("H,"),
            ),

            # LXI H,x; XCHG; LHLD y -> LXI D,x; LHLD y
            PeepholePattern(
                name="lxi_xchg_lhld",
                pattern=[("LXI", None), ("XCHG", ""), ("LHLD", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1].startswith("H,"),
            ),

            # PUSH H; LXI H,x; XCHG; POP H -> LXI D,x
            # (Constant goes to DE directly, PUSH/POP eliminated entirely)
            PeepholePattern(
                name="push_lxi_xchg_pop",
                pattern=[("PUSH", "H"), ("LXI", None), ("XCHG", ""), ("POP", "H")],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[1][1].startswith("H,"),
            ),

            # MOV L,A; MVI H,0; XCHG; POP H -> MOV E,A; MVI D,0; POP H
            # (Putting byte value in DE directly, saves XCHG)
            PeepholePattern(
                name="mov_la_mvi_h0_xchg_pop",
                pattern=[("MOV", "L,A"), ("MVI", "H,0"), ("XCHG", ""), ("POP", "H")],
                replacement=[("MOV", "E,A"), ("MVI", "D,0"), ("POP", "H")],
            ),

            # MOV L,A; MVI H,0; DCX H; MOV A,L -> DCR A; MOV L,A
            # (16-bit decrement of byte value - just decrement A directly)
            PeepholePattern(
                name="mov_la_mvi_h0_dcx_mov_al",
                pattern=[("MOV", "L,A"), ("MVI", "H,0"), ("DCX", "H"), ("MOV", "A,L")],
                replacement=[("DCR", "A"), ("MOV", "L,A")],
            ),

            # LXI H,0; DAD SP -> LXI H,0; DAD SP (reading SP, can't optimize easily)

            # CALL x; POP D -> CALL x; POP D (can't optimize, stack cleanup)

            # LXI H,const; PUSH H; LXI H,const; PUSH H -> LXI H,const; PUSH H; PUSH H
            # (If same constant pushed twice)
            PeepholePattern(
                name="double_push_same_const",
                pattern=[("LXI", None), ("PUSH", "H"), ("LXI", None), ("PUSH", "H")],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1] == ops[2][1],
            ),

            # LXI H,0FFFFH; MOV A,L; ORA H -> LXI H,0FFFFH (since 0xFFFF is always true)
            # The test is redundant
            PeepholePattern(
                name="test_true_const",
                pattern=[("LXI", "H,0FFFFH"), ("MOV", "A,L"), ("ORA", "H")],
                replacement=[("LXI", "H,0FFFFH"), ("ORA", "A")],  # Just set flags from A=L=FF
            ),

            # LXI H,1; MOV A,L; ORA H -> MVI A,1; ORA A (smaller, 1 is also true)
            PeepholePattern(
                name="test_true_const_1",
                pattern=[("LXI", "H,1"), ("MOV", "A,L"), ("ORA", "H")],
                replacement=[("MVI", "A,1"), ("ORA", "A")],
            ),

            # LXI H,1; MOV C,L -> MVI C,1 (for shift count)
            PeepholePattern(
                name="lxi_h1_mov_cl",
                pattern=[("LXI", "H,1"), ("MOV", "C,L")],
                replacement=[("MVI", "C,1")],
            ),

            # MOV A,L; MVI H,0; STA x -> MOV A,L; STA x (MVI H,0 is useless before STA)
            PeepholePattern(
                name="mov_al_mvi_h0_sta",
                pattern=[("MOV", "A,L"), ("MVI", "H,0"), ("STA", None)],
                replacement=None,  # Keep MOV A,L and STA, remove MVI H,0
                condition=lambda ops: True,
            ),

            # LXI H,0; MOV A,L; ORA H -> XRA A (sets Z, clears A)
            PeepholePattern(
                name="test_false_const",
                pattern=[("LXI", "H,0"), ("MOV", "A,L"), ("ORA", "H")],
                replacement=[("XRA", "A")],  # Sets Z flag and clears HL conceptually
            ),

            # PUSH H; SHLD x; POP H -> SHLD x (SHLD doesn't modify HL)
            PeepholePattern(
                name="push_shld_pop",
                pattern=[("PUSH", "H"), ("SHLD", None), ("POP", "H")],
                replacement=None,  # Handled specially - keep only SHLD
            ),

            # PUSH H; MVI A,x; MOV E,A; MVI D,0; POP H -> MVI E,x; MVI D,0
            # (HL not modified, PUSH/POP is wasteful)
            PeepholePattern(
                name="push_mvi_a_mov_e_mvi_d_pop",
                pattern=[("PUSH", "H"), ("MVI", None), ("MOV", "E,A"), ("MVI", "D,0"), ("POP", "H")],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[1][1].startswith("A,"),
            ),

            # PUSH H; LDA x; MOV B,A; LDA y; SUB B; MOV E,A; MVI D,0; POP H
            # -> LDA x; MOV B,A; LDA y; SUB B; MOV E,A; MVI D,0
            # (HL not modified, PUSH/POP is wasteful)
            PeepholePattern(
                name="push_lda_sub_to_de_pop",
                pattern=[("PUSH", "H"), ("LDA", None), ("MOV", "B,A"), ("LDA", None),
                         ("SUB", "B"), ("MOV", "E,A"), ("MVI", "D,0"), ("POP", "H")],
                replacement=None,  # Handled specially - remove PUSH/POP
            ),

            # PUSH H; LHLD x; POP D -> XCHG; LHLD x; XCHG (Z80: use LD DE,(x) directly)
            # Gets (x) into HL and old HL into DE
            PeepholePattern(
                name="push_lhld_pop_d",
                pattern=[("PUSH", "H"), ("LHLD", None), ("POP", "D")],
                replacement=None,  # Handled specially
            ),

            # PUSH H; MOV E,A; MVI D,0; POP H -> MOV E,A; MVI D,0
            # (HL not modified, PUSH/POP is wasteful)
            PeepholePattern(
                name="push_mov_ea_mvi_d0_pop",
                pattern=[("PUSH", "H"), ("MOV", "E,A"), ("MVI", "D,0"), ("POP", "H")],
                replacement=[("MOV", "E,A"), ("MVI", "D,0")],
            ),

            # LHLD x; PUSH H; LDED y; LHLD z; CALL ??SUBDE; XCHG; POP H; CALL ??SUBDE
            # -> LDED y; LHLD z; CALL ??SUBDE; XCHG; LHLD x; CALL ??SUBDE
            # (Delay loading x until it's actually needed)
            PeepholePattern(
                name="early_load_push_subde",
                pattern=[("LHLD", None), ("PUSH", "H"), ("LDED", None), ("LHLD", None),
                         ("CALL", "??SUBDE"), ("XCHG", ""), ("POP", "H"), ("CALL", "??SUBDE")],
                replacement=None,  # Handled specially
            ),

            # ============================================================
            # Additional 8080/Z80 patterns
            # ============================================================

            # INX H; DCX H -> (nothing)
            PeepholePattern(
                name="inx_dcx_h",
                pattern=[("INX", "H"), ("DCX", "H")],
                replacement=[],
            ),
            # DCX H; INX H -> (nothing)
            PeepholePattern(
                name="dcx_inx_h",
                pattern=[("DCX", "H"), ("INX", "H")],
                replacement=[],
            ),
            # INR A; DCR A -> (nothing) - but affects flags differently
            # Skip - affects flags

            # LXI H,0; DAD SP -> LXI H,0; DAD SP (can't optimize, SP access)

            # PUSH PSW; POP PSW -> (nothing if no interrupt)
            PeepholePattern(
                name="push_pop_psw",
                pattern=[("PUSH", "PSW"), ("POP", "PSW")],
                replacement=[],
            ),

            # SHLD x; LHLD x -> SHLD x (store then load same = just store, keep HL)
            PeepholePattern(
                name="shld_lhld_same",
                pattern=[("SHLD", None), ("LHLD", None)],
                replacement=None,  # Keep first only
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),

            # SHLD x; CALL y; LHLD x -> PUSH H; CALL y; POP H
            # (Save/restore HL around a call using stack instead of memory)
            PeepholePattern(
                name="shld_call_lhld_same",
                pattern=[("SHLD", None), ("CALL", None), ("LHLD", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1] == ops[2][1],
            ),

            # SHLD x; CALL y; LHLD x; JMP z -> PUSH H; CALL y; POP H; JMP z
            # (Same pattern but with tail optimization)
            PeepholePattern(
                name="shld_call_lhld_jmp_same",
                pattern=[("SHLD", None), ("CALL", None), ("LHLD", None), ("JMP", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1] == ops[2][1],
            ),

            # SHLD x; MVI r,n; LHLD x -> MVI r,n (MVI doesn't touch HL)
            PeepholePattern(
                name="shld_mvi_lhld_same",
                pattern=[("SHLD", None), ("MVI", None), ("LHLD", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1] == ops[2][1],
            ),

            # SHLD x; MVI r,n; LHLD x; XCHG -> MVI r,n; XCHG
            PeepholePattern(
                name="shld_mvi_lhld_xchg_same",
                pattern=[("SHLD", None), ("MVI", None), ("LHLD", None), ("XCHG", "")],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1] == ops[2][1],
            ),

            # STA x; CALL y; LDA x -> PUSH PSW; CALL y; POP PSW
            # (Save/restore A around a call using stack instead of memory)
            PeepholePattern(
                name="sta_call_lda_same",
                pattern=[("STA", None), ("CALL", None), ("LDA", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1] == ops[2][1],
            ),

            # STA x; MVI r,n; LDA x -> MVI r,n (MVI doesn't touch A, when r != A)
            PeepholePattern(
                name="sta_mvi_lda_same",
                pattern=[("STA", None), ("MVI", None), ("LDA", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1] == ops[2][1] and not ops[1][1].startswith("A,"),
            ),

            # PUSH H; LXI H,const; MOV C,L; POP H -> MVI C,const
            # Loading a constant into C while preserving HL
            PeepholePattern(
                name="push_lxi_mov_cl_pop",
                pattern=[("PUSH", "H"), ("LXI", None), ("MOV", "C,L"), ("POP", "H")],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[1][1].startswith("H,"),
            ),

            # PUSH H; MOV A,L; STA x; POP H; MVI H,0 -> MOV A,L; STA x; MVI H,0
            # The PUSH/POP is pointless since we're about to overwrite H anyway
            PeepholePattern(
                name="push_mov_sta_pop_mvi_h0",
                pattern=[("PUSH", "H"), ("MOV", "A,L"), ("STA", None), ("POP", "H"), ("MVI", "H,0")],
                replacement=None,  # Handled specially
            ),

            # MVI H,0; MVI L,x -> LXI H,x (smaller on Z80)
            # Complex - skip for now

            # ORA A; RZ -> RZ (ORA A sets Z based on A, RZ checks Z)
            # Only valid if we want to return if A==0
            # Skip - context dependent

            # ANI 0FFH -> ORA A (same effect, smaller)
            PeepholePattern(
                name="ani_ff",
                pattern=[("ANI", "0FFH")],
                replacement=[("ORA", "A")],
            ),

            # ORI 0 -> ORA A (same effect)
            PeepholePattern(
                name="ori_0",
                pattern=[("ORI", "0")],
                replacement=[("ORA", "A")],
            ),

            # XRI 0 -> ORA A (same effect, sets flags)
            PeepholePattern(
                name="xri_0",
                pattern=[("XRI", "0")],
                replacement=[("ORA", "A")],
            ),

            # ADI 0 -> ORA A (same effect on Z flag, but not on C)
            # Skip - different carry behavior

            # LDA x; ADI 1; STA x -> LXI H,x; INR M (in-place increment)
            # Saves 3 bytes when result not needed in A
            PeepholePattern(
                name="lda_adi1_sta_same",
                pattern=[("LDA", None), ("ADI", "1"), ("STA", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1] == ops[2][1],
            ),
            # LDA x; SUI 1; STA x -> LXI H,x; DCR M (in-place decrement)
            PeepholePattern(
                name="lda_sui1_sta_same",
                pattern=[("LDA", None), ("SUI", "1"), ("STA", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1] == ops[2][1],
            ),

            # SUI 0 -> ORA A (same effect on Z flag, but not on C)
            # Skip - different carry behavior

            # DAD H -> DAD H (shift HL left, can't optimize)

            # CALL x; RET -> JMP x (tail call optimization)
            PeepholePattern(
                name="tail_call",
                pattern=[("CALL", None), ("RET", "")],
                replacement=None,  # Replaced specially
                condition=lambda ops: True,
            ),

            # RET; RET -> RET (unreachable code)
            PeepholePattern(
                name="double_ret",
                pattern=[("RET", ""), ("RET", "")],
                replacement=[("RET", "")],
            ),

            # LDA x; CPI y; JZ z; LDA x -> LDA x; CPI y; JZ z
            # (A unchanged after CPI/Jcond, so redundant reload)
            PeepholePattern(
                name="lda_cpi_jz_lda_same",
                pattern=[("LDA", None), ("CPI", None), ("JZ", None), ("LDA", None)],
                replacement=None,  # Keep first 3 only
                condition=lambda ops: ops[0][1] == ops[3][1],
            ),
            PeepholePattern(
                name="lda_cpi_jnz_lda_same",
                pattern=[("LDA", None), ("CPI", None), ("JNZ", None), ("LDA", None)],
                replacement=None,
                condition=lambda ops: ops[0][1] == ops[3][1],
            ),
            PeepholePattern(
                name="lda_cpi_jc_lda_same",
                pattern=[("LDA", None), ("CPI", None), ("JC", None), ("LDA", None)],
                replacement=None,
                condition=lambda ops: ops[0][1] == ops[3][1],
            ),
            PeepholePattern(
                name="lda_cpi_jnc_lda_same",
                pattern=[("LDA", None), ("CPI", None), ("JNC", None), ("LDA", None)],
                replacement=None,
                condition=lambda ops: ops[0][1] == ops[3][1],
            ),

            # LDA x; ORA A; JZ z; LDA x -> LDA x; ORA A; JZ z
            # (A unchanged after ORA A/Jcond)
            PeepholePattern(
                name="lda_ora_jz_lda_same",
                pattern=[("LDA", None), ("ORA", "A"), ("JZ", None), ("LDA", None)],
                replacement=None,
                condition=lambda ops: ops[0][1] == ops[3][1],
            ),
            PeepholePattern(
                name="lda_ora_jnz_lda_same",
                pattern=[("LDA", None), ("ORA", "A"), ("JNZ", None), ("LDA", None)],
                replacement=None,
                condition=lambda ops: ops[0][1] == ops[3][1],
            ),

            # MOV B,A; MOV A,B -> MOV B,A
            PeepholePattern(
                name="mov_ba_ab",
                pattern=[("MOV", "B,A"), ("MOV", "A,B")],
                replacement=[("MOV", "B,A")],
            ),
            PeepholePattern(
                name="mov_ca_ac",
                pattern=[("MOV", "C,A"), ("MOV", "A,C")],
                replacement=[("MOV", "C,A")],
            ),
            PeepholePattern(
                name="mov_da_ad",
                pattern=[("MOV", "D,A"), ("MOV", "A,D")],
                replacement=[("MOV", "D,A")],
            ),
            PeepholePattern(
                name="mov_ea_ae",
                pattern=[("MOV", "E,A"), ("MOV", "A,E")],
                replacement=[("MOV", "E,A")],
            ),
            PeepholePattern(
                name="mov_ha_ah",
                pattern=[("MOV", "H,A"), ("MOV", "A,H")],
                replacement=[("MOV", "H,A")],
            ),
            PeepholePattern(
                name="mov_la_al",
                pattern=[("MOV", "L,A"), ("MOV", "A,L")],
                replacement=[("MOV", "L,A")],
            ),

            # MOV A,M; MOV E,A -> MOV E,M (load byte into E directly)
            PeepholePattern(
                name="mov_am_mov_ea",
                pattern=[("MOV", "A,M"), ("MOV", "E,A")],
                replacement=[("MOV", "E,M")],
            ),
            # MOV A,M; MOV D,A -> MOV D,M
            PeepholePattern(
                name="mov_am_mov_da",
                pattern=[("MOV", "A,M"), ("MOV", "D,A")],
                replacement=[("MOV", "D,M")],
            ),
            # MOV A,M; MOV C,A -> MOV C,M
            PeepholePattern(
                name="mov_am_mov_ca",
                pattern=[("MOV", "A,M"), ("MOV", "C,A")],
                replacement=[("MOV", "C,M")],
            ),
            # MOV A,M; MOV B,A -> MOV B,M
            PeepholePattern(
                name="mov_am_mov_ba",
                pattern=[("MOV", "A,M"), ("MOV", "B,A")],
                replacement=[("MOV", "B,M")],
            ),

            # LDA x; ORA A; JZ -> load and test combined
            # Skip - context dependent

            # PUSH H; XCHG; POP H -> MOV D,H; MOV E,L
            # The XCHG swaps HL<->DE, then POP restores HL, so DE = original HL
            PeepholePattern(
                name="push_xchg_pop",
                pattern=[("PUSH", "H"), ("XCHG", ""), ("POP", "H")],
                replacement=[("MOV", "D,H"), ("MOV", "E,L")],
            ),

            # MVI H,0; MOV D,H; MOV E,L -> MVI D,0; MOV E,L
            # D = H = 0, so just load D directly with 0
            PeepholePattern(
                name="mvi_h0_mov_dh_mov_el",
                pattern=[("MVI", "H,0"), ("MOV", "D,H"), ("MOV", "E,L")],
                replacement=[("MVI", "D,0"), ("MOV", "E,L")],
            ),

            # PUSH H; LXI D,x; POP H; DAD D -> LXI D,x; DAD D
            # The PUSH/POP is unnecessary since we're just adding D to H
            PeepholePattern(
                name="push_lxi_d_pop_dad",
                pattern=[("PUSH", "H"), ("LXI", None), ("POP", "H"), ("DAD", "D")],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[1][1].startswith("D,"),
            ),

            # MOV E,A; MVI D,0; POP H; XCHG; MOV M,E -> POP D; XCHG; MOV M,A
            # When storing a byte (in A) to a stacked address, we can use A directly
            # instead of copying to E then storing from E
            PeepholePattern(
                name="store_byte_via_stack",
                pattern=[("MOV", "E,A"), ("MVI", "D,0"), ("POP", "H"), ("XCHG", ""), ("MOV", "M,E")],
                replacement=[("POP", "D"), ("XCHG", ""), ("MOV", "M,A")],
            ),

            # MOV B,A; ... ; MOV A,B; SUB C -> remove MOV A,B if A==B already
            # This is context dependent, skip

            # LXI H,addr; MOV A,M -> LDA addr (direct memory access)
            PeepholePattern(
                name="lxi_mov_am_to_lda",
                pattern=[("LXI", None), ("MOV", "A,M")],
                replacement=None,  # Handled specially - convert to LDA
                condition=lambda ops: ops[0][1].startswith("H,") and not ops[0][1].startswith("H,0"),
            ),

            # LHLD x; MOV A,L; MVI H,0 -> LDA x; MOV L,A; MVI H,0
            # Only if we just need the low byte
            # Skip - complex pattern

            # STA x; LDA x -> STA x (redundant reload)
            PeepholePattern(
                name="sta_lda_same",
                pattern=[("STA", None), ("LDA", None)],
                replacement=None,  # Keep first only
                condition=lambda ops: ops[0][1] == ops[1][1],
            ),

            # LXI H,const; MOV A,L; STA x -> MVI A,const; STA x
            # (When we only need the low byte of a constant)
            PeepholePattern(
                name="lxi_mov_al_sta",
                pattern=[("LXI", None), ("MOV", "A,L"), ("STA", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: ops[0][1].startswith("H,"),
            ),

            # LXI H,0; MOV L,A; MVI H,0 -> MOV L,A; MVI H,0
            # (LXI H,0 is redundant if we're about to set L from A)
            PeepholePattern(
                name="lxi_h0_mov_la",
                pattern=[("LXI", "H,0"), ("MOV", "L,A"), ("MVI", "H,0")],
                replacement=[("MOV", "L,A"), ("MVI", "H,0")],
            ),

            # MOV A,L; MVI H,0; MOV A,L -> MOV A,L; MVI H,0
            # (Second MOV A,L is redundant - A already has L)
            PeepholePattern(
                name="mov_al_mvi_h0_mov_al",
                pattern=[("MOV", "A,L"), ("MVI", "H,0"), ("MOV", "A,L")],
                replacement=[("MOV", "A,L"), ("MVI", "H,0")],
            ),

            # MOV L,A; MVI H,0; STA x -> STA x
            # (If we're just storing A, no need to extend to HL first)
            PeepholePattern(
                name="mov_la_mvi_h0_sta",
                pattern=[("MOV", "L,A"), ("MVI", "H,0"), ("STA", None)],
                replacement=None,  # Keep only STA
                condition=lambda ops: True,
            ),

            # MOV A,L; MVI H,0; ORA H -> MOV A,L; ORA A
            # (H is 0, so ORA H is same as ORA A but ORA A is 1 byte vs 2)
            # Actually: MVI H,0 then ORA H - since H was just set to 0, we can skip MVI and do ORA A
            PeepholePattern(
                name="mov_al_mvi_h0_ora_h",
                pattern=[("MOV", "A,L"), ("MVI", "H,0"), ("ORA", "H")],
                replacement=[("MOV", "A,L"), ("ORA", "A")],
            ),

            # MVI H,0; ORA H -> ORA A
            # (H is 0, so ORA H tests if A is 0 - same as ORA A)
            PeepholePattern(
                name="mvi_h0_ora_h",
                pattern=[("MVI", "H,0"), ("ORA", "H")],
                replacement=[("MVI", "H,0"), ("ORA", "A")],
            ),

            # SBB D; MOV H,A; ORA A; JM x -> SBB D; MOV H,A; JM x
            # (After 16-bit subtract, sign flag is set by SBB D, MOV doesn't affect flags)
            PeepholePattern(
                name="sbb_mov_ora_jm",
                pattern=[("SBB", "D"), ("MOV", "H,A"), ("ORA", "A"), ("JM", None)],
                replacement=None,  # Handled specially - remove ORA A
                condition=lambda ops: True,
            ),

            # SBB D; MOV H,A; ORA A; JP x -> SBB D; MOV H,A; JP x
            # (Same optimization for JP - checking if non-negative)
            PeepholePattern(
                name="sbb_mov_ora_jp",
                pattern=[("SBB", "D"), ("MOV", "H,A"), ("ORA", "A"), ("JP", None)],
                replacement=None,  # Handled specially - remove ORA A
                condition=lambda ops: True,
            ),

            # MOV B,A; STA x; MOV A,B -> STA x
            # (STA doesn't modify A, so A still equals B after the sequence)
            PeepholePattern(
                name="mov_ba_sta_mov_ab",
                pattern=[("MOV", "B,A"), ("STA", None), ("MOV", "A,B")],
                replacement=None,  # Keep only STA - handled specially
                condition=lambda ops: True,
            ),
            PeepholePattern(
                name="mov_ca_sta_mov_ac",
                pattern=[("MOV", "C,A"), ("STA", None), ("MOV", "A,C")],
                replacement=None,
                condition=lambda ops: True,
            ),
            PeepholePattern(
                name="mov_da_sta_mov_ad",
                pattern=[("MOV", "D,A"), ("STA", None), ("MOV", "A,D")],
                replacement=None,
                condition=lambda ops: True,
            ),
            PeepholePattern(
                name="mov_ea_sta_mov_ae",
                pattern=[("MOV", "E,A"), ("STA", None), ("MOV", "A,E")],
                replacement=None,
                condition=lambda ops: True,
            ),

            # SUB E; MOV L,A; MOV A,H; SBB D; MOV H,A; JM x -> SUB E; MOV A,H; SBB D; JM x
            # (When just checking sign, we don't need to store result in HL)
            PeepholePattern(
                name="sub_16bit_sign_jm",
                pattern=[("SUB", "E"), ("MOV", "L,A"), ("MOV", "A,H"), ("SBB", "D"), ("MOV", "H,A"), ("JM", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: True,
            ),
            # Same for JP (non-negative)
            PeepholePattern(
                name="sub_16bit_sign_jp",
                pattern=[("SUB", "E"), ("MOV", "L,A"), ("MOV", "A,H"), ("SBB", "D"), ("MOV", "H,A"), ("JP", None)],
                replacement=None,  # Handled specially
                condition=lambda ops: True,
            ),

            # MOV B,A; LHLD x; MOV A,B; MOV M,A -> MOV B,A; LHLD x; MOV M,B
            # (B already has the value, use it directly)
            PeepholePattern(
                name="mov_ba_lhld_mov_ab_mov_ma",
                pattern=[("MOV", "B,A"), ("LHLD", None), ("MOV", "A,B"), ("MOV", "M,A")],
                replacement=None,  # Handled specially
                condition=lambda ops: True,
            ),
            PeepholePattern(
                name="mov_ca_lhld_mov_ac_mov_ma",
                pattern=[("MOV", "C,A"), ("LHLD", None), ("MOV", "A,C"), ("MOV", "M,A")],
                replacement=None,
                condition=lambda ops: True,
            ),

            # ============================================================
            # Z80-specific patterns (applied after 8080->Z80 translation)
            # ============================================================

            # LD A,0 -> XOR A (smaller: 1 byte vs 2)
            PeepholePattern(
                name="z80_ld_a_0",
                pattern=[("LD", "A,0")],
                replacement=[("XOR", "A")],
                target=Target.Z80,
            ),

            # LD HL,0 -> LD HL,0 (can't improve, 3 bytes)

            # INC HL; INC HL -> LD DE,2; ADD HL,DE only if we can use DE
            # Skip - register pressure

            # JP Z,x; JP y where y is next instruction -> JP NZ,x
            # Complex - skip for now
        ]

    def _init_z80_patterns(self) -> list[PeepholePattern]:
        """Initialize Z80-native peephole patterns.

        These patterns work directly on Z80 mnemonics (LD, JP, etc.)
        for compilers that generate native Z80 assembly.
        """
        return [
            # Push/Pop elimination: PUSH rr; POP rr -> (nothing)
            PeepholePattern(
                name="z80_push_pop_same",
                pattern=[("PUSH", None), ("POP", None)],
                replacement=[],
                condition=lambda ops: ops[0][1].upper() == ops[1][1].upper(),
            ),
            # Redundant LD: LD A,r; LD r,A -> LD A,r
            PeepholePattern(
                name="z80_redundant_ld",
                pattern=[("LD", "A,*"), ("LD", "*,A")],
                replacement=None,  # Keep first only
                condition=lambda ops: ops[0][1].split(",")[1].upper() == ops[1][1].split(",")[0].upper(),
            ),
            # Zero A: LD A,0 -> XOR A (smaller, faster)
            PeepholePattern(
                name="z80_zero_a_ld",
                pattern=[("LD", "A,0")],
                replacement=[("XOR", "A")],
            ),
            # Compare to zero: CP 0 -> OR A (sets Z flag, smaller)
            PeepholePattern(
                name="z80_cp_zero",
                pattern=[("CP", "0")],
                replacement=[("OR", "A")],
            ),
            # Load then store same address: LD A,(addr); LD (addr),A -> LD A,(addr)
            PeepholePattern(
                name="z80_load_store_same",
                pattern=[("LD", "A,(*)")],  # Will need special handling
                replacement=None,
            ),
            # Double INC: INC HL; INC HL (keep as-is, 2 bytes vs 4 for LD+ADD)
            # Zero register pair: LD HL,0 -> (can't improve, 3 bytes)

            # Redundant duplicate LD: LD X,Y; LD X,Y -> LD X,Y
            PeepholePattern(
                name="z80_duplicate_ld",
                pattern=[("LD", None), ("LD", None)],
                replacement=None,  # Keep first only
                condition=lambda ops: ops[0][1].upper() == ops[1][1].upper(),
            ),
            # LD A,A -> (nothing, useless)
            PeepholePattern(
                name="z80_ld_a_a",
                pattern=[("LD", "A,A")],
                replacement=[],
            ),
            # LD B,B, LD C,C, etc. -> (nothing)
            PeepholePattern(
                name="z80_ld_r_r",
                pattern=[("LD", None)],
                replacement=[],
                condition=lambda ops: len(ops[0][1].split(",")) == 2 and
                                      ops[0][1].split(",")[0].strip().upper() ==
                                      ops[0][1].split(",")[1].strip().upper() and
                                      ops[0][1].split(",")[0].strip().upper() in
                                      ("A", "B", "C", "D", "E", "H", "L"),
            ),
            # INC A; DEC A -> (nothing)
            PeepholePattern(
                name="z80_inc_dec_a",
                pattern=[("INC", "A"), ("DEC", "A")],
                replacement=[],
            ),
            # DEC A; INC A -> (nothing)
            PeepholePattern(
                name="z80_dec_inc_a",
                pattern=[("DEC", "A"), ("INC", "A")],
                replacement=[],
            ),
            # INC HL; DEC HL -> (nothing)
            PeepholePattern(
                name="z80_inc_dec_hl",
                pattern=[("INC", "HL"), ("DEC", "HL")],
                replacement=[],
            ),
            # DEC HL; INC HL -> (nothing)
            PeepholePattern(
                name="z80_dec_inc_hl",
                pattern=[("DEC", "HL"), ("INC", "HL")],
                replacement=[],
            ),
            # ADD A,0 -> (nothing, doesn't change A, but clears carry - keep if carry matters)
            # SUB 0 -> (nothing, but sets flags - keep for flag side effects)

            # OR A; OR A -> OR A
            PeepholePattern(
                name="z80_double_or_a",
                pattern=[("OR", "A"), ("OR", "A")],
                replacement=[("OR", "A")],
            ),
            # AND A; AND A -> AND A
            PeepholePattern(
                name="z80_double_and_a",
                pattern=[("AND", "A"), ("AND", "A")],
                replacement=[("AND", "A")],
            ),
            # XOR A; XOR A -> XOR A (still zero)
            PeepholePattern(
                name="z80_double_xor_a",
                pattern=[("XOR", "A"), ("XOR", "A")],
                replacement=[("XOR", "A")],
            ),
            # EX DE,HL; EX DE,HL -> (nothing)
            PeepholePattern(
                name="z80_double_ex",
                pattern=[("EX", "DE,HL"), ("EX", "DE,HL")],
                replacement=[],
            ),
            # PUSH HL; POP DE -> LD D,H; LD E,L (faster if registers free)
            # Actually PUSH/POP is 11+10=21 cycles, LD D,H; LD E,L is 4+4=8 cycles!
            PeepholePattern(
                name="z80_push_pop_copy_hl_de",
                pattern=[("PUSH", "HL"), ("POP", "DE")],
                replacement=[("LD", "D,H"), ("LD", "E,L")],
            ),
            # PUSH DE; POP HL -> LD H,D; LD L,E
            PeepholePattern(
                name="z80_push_pop_copy_de_hl",
                pattern=[("PUSH", "DE"), ("POP", "HL")],
                replacement=[("LD", "H,D"), ("LD", "L,E")],
            ),
            # PUSH BC; POP DE -> LD D,B; LD E,C
            PeepholePattern(
                name="z80_push_pop_copy_bc_de",
                pattern=[("PUSH", "BC"), ("POP", "DE")],
                replacement=[("LD", "D,B"), ("LD", "E,C")],
            ),
            # PUSH BC; POP HL -> LD H,B; LD L,C
            PeepholePattern(
                name="z80_push_pop_copy_bc_hl",
                pattern=[("PUSH", "BC"), ("POP", "HL")],
                replacement=[("LD", "H,B"), ("LD", "L,C")],
            ),
            # JP to RET: JP label; ... label: RET -> RET (if unconditional)
            # This is handled specially in jump threading

            # SCF; CCF -> reset carry (OR A also works but affects other flags)
            # CCF; SCF -> set carry (SCF alone works)
            PeepholePattern(
                name="z80_ccf_scf",
                pattern=[("CCF", None), ("SCF", None)],
                replacement=[("SCF", "")],
            ),
        ]

    def optimize(self, asm_text: str) -> str:
        """Optimize assembly text."""
        lines = asm_text.split("\n")
        changed = True
        passes = 0
        max_passes = 10

        # For native Z80 input, skip 8080 phases entirely
        if self.input_syntax == InputSyntax.Z80:
            # Go directly to Z80 optimization phases
            return self._optimize_z80_native(lines)

        # Phase 1: Apply universal 8080 patterns
        while changed and passes < max_passes:
            changed = False
            passes += 1
            lines, did_change = self._optimize_pass(lines)
            if did_change:
                changed = True

        # Phase 1.5: Register tracking optimization (eliminate redundant loads)
        lines, did_change = self._register_tracking_pass(lines)
        if did_change:
            # Run pattern matching again after register tracking
            changed = True
            passes = 0
            while changed and passes < max_passes:
                changed = False
                passes += 1
                lines, did_change = self._optimize_pass(lines)
                if did_change:
                    changed = True

        # Phase 1.6: Eliminate useless PUSH/POP pairs where register isn't modified
        lines, did_change = self._eliminate_useless_push_pop(lines)
        if did_change:
            # Run pattern matching again
            changed = True
            passes = 0
            while changed and passes < max_passes:
                changed = False
                passes += 1
                lines, did_change = self._optimize_pass(lines)
                if did_change:
                    changed = True

        # Phase 2: For Z80, translate to Z80 mnemonics
        if self.target == Target.Z80:
            lines = self._translate_to_z80(lines)

            # Phase 3: Apply Z80-specific patterns
            changed = True
            passes = 0
            while changed and passes < max_passes:
                changed = False
                passes += 1
                lines, did_change = self._optimize_z80_pass(lines)
                if did_change:
                    changed = True

            # Phase 4: Jump threading - JP to label that is just JP -> thread through
            changed = True
            passes = 0
            while changed and passes < max_passes:
                changed = False
                passes += 1
                lines, did_change = self._jump_threading_pass(lines)
                if did_change:
                    changed = True

            # Phase 5: Convert long jumps to relative jumps where possible
            lines = self._convert_to_relative_jumps(lines)

            # Phase 6: Apply Z80-specific patterns again (for DJNZ after JR conversion)
            lines, _ = self._optimize_z80_pass(lines)

            # Phase 7: Dead store elimination at procedure entry
            lines, _ = self._dead_store_elimination(lines)

        return "\n".join(lines)

    def _optimize_z80_native(self, lines: list[str]) -> str:
        """
        Optimize native Z80 assembly.

        This is the optimization path for compilers that generate Z80 mnemonics
        directly (LD, JP, etc.) rather than 8080 mnemonics. It skips the 8080
        pattern matching and translation phases.

        Phases:
        1. Apply native Z80 patterns (LD A,0 -> XOR A, PUSH/POP elimination, etc.)
        2. Apply existing Z80-specific patterns (from _optimize_z80_pass)
        3. Jump threading (JP to JP -> direct JP)
        4. Convert long jumps to relative jumps (JP -> JR where possible)
        5. Apply Z80 patterns again (for DJNZ opportunities after JR conversion)
        6. Dead store elimination
        """
        max_passes = 10

        # Phase 1: Apply native Z80 patterns
        changed = True
        passes = 0
        while changed and passes < max_passes:
            changed = False
            passes += 1
            lines, did_change = self._apply_z80_native_patterns(lines)
            if did_change:
                changed = True

        # Phase 2: Apply existing Z80-specific patterns
        changed = True
        passes = 0
        while changed and passes < max_passes:
            changed = False
            passes += 1
            lines, did_change = self._optimize_z80_pass(lines)
            if did_change:
                changed = True

        # Phase 3: Jump threading
        changed = True
        passes = 0
        while changed and passes < max_passes:
            changed = False
            passes += 1
            lines, did_change = self._jump_threading_pass(lines)
            if did_change:
                changed = True

        # Phase 4: Convert long jumps to relative jumps where possible
        lines = self._convert_to_relative_jumps(lines)

        # Phase 5: Apply Z80-specific patterns again (for DJNZ after JR conversion)
        lines, _ = self._optimize_z80_pass(lines)

        # Phase 6: Dead store elimination at procedure entry
        lines, _ = self._dead_store_elimination(lines)

        return "\n".join(lines)

    def _apply_z80_native_patterns(self, lines: list[str]) -> tuple[list[str], bool]:
        """
        Apply native Z80 peephole patterns.

        Similar to _optimize_pass but uses self.z80_patterns for native Z80 code.
        """
        result: list[str] = []
        changed = False
        i = 0

        while i < len(lines):
            line = lines[i]
            stripped = line.strip()

            # Skip empty lines, comments, labels, directives
            if not stripped or stripped.startswith(';') or stripped.endswith(':'):
                result.append(line)
                i += 1
                continue

            if stripped.startswith('.') or stripped.upper().startswith(('ORG', 'EQU', 'DB', 'DW', 'DS')):
                result.append(line)
                i += 1
                continue

            # Try to match each pattern
            matched = False
            for pattern in self.z80_patterns:
                match_len = len(pattern.pattern)
                if i + match_len > len(lines):
                    continue

                # Extract instructions for pattern matching
                instrs: list[tuple[str, str]] = []
                valid = True
                for j in range(match_len):
                    instr_line = lines[i + j].strip()
                    if not instr_line or instr_line.startswith(';') or instr_line.endswith(':'):
                        valid = False
                        break
                    parts = instr_line.split(None, 1)
                    if not parts:
                        valid = False
                        break
                    opcode = parts[0].upper()
                    operands = parts[1].strip() if len(parts) > 1 else ""
                    # Remove inline comments
                    if ';' in operands:
                        operands = operands.split(';')[0].strip()
                    instrs.append((opcode, operands))

                if not valid or len(instrs) != match_len:
                    continue

                # Check if pattern matches
                if self._z80_pattern_matches(pattern, instrs):
                    # Apply condition if present
                    if pattern.condition and not pattern.condition(instrs):
                        continue

                    # Pattern matched!
                    self.stats[pattern.name] = self.stats.get(pattern.name, 0) + 1
                    changed = True
                    matched = True

                    # Apply replacement
                    if pattern.replacement is None:
                        # Keep first instruction only (for redundant patterns)
                        result.append(lines[i])
                        i += match_len
                    elif pattern.replacement == []:
                        # Delete all matched instructions
                        i += match_len
                    else:
                        # Replace with new instructions
                        for opcode, operands in pattern.replacement:
                            if operands:
                                result.append(f"    {opcode} {operands}")
                            else:
                                result.append(f"    {opcode}")
                        i += match_len
                    break

            if not matched:
                result.append(line)
                i += 1

        return result, changed

    def _z80_pattern_matches(
        self, pattern: PeepholePattern, instrs: list[tuple[str, str]]
    ) -> bool:
        """Check if instructions match a Z80 pattern."""
        if len(instrs) != len(pattern.pattern):
            return False

        for (actual_op, actual_operands), (pat_op, pat_operands) in zip(
            instrs, pattern.pattern
        ):
            # Check opcode
            if actual_op != pat_op:
                return False

            # Check operands
            if pat_operands is None:
                # None means any operands
                continue
            elif pat_operands.endswith("*)"):
                # Pattern like "A,(*)" - match A,<anything in parens>
                prefix = pat_operands[:-2]
                if not actual_operands.upper().startswith(prefix.upper()):
                    return False
                rest = actual_operands[len(prefix):]
                if not (rest.startswith("(") and rest.endswith(")")):
                    return False
            elif "*" in pat_operands:
                # Wildcard pattern like "A,*"
                parts = pat_operands.split("*")
                if len(parts) == 2:
                    prefix, suffix = parts
                    if not actual_operands.upper().startswith(prefix.upper()):
                        return False
                    if suffix and not actual_operands.upper().endswith(suffix.upper()):
                        return False
                else:
                    # Complex pattern, just check prefix
                    if not actual_operands.upper().startswith(parts[0].upper()):
                        return False
            else:
                # Exact match
                if actual_operands.upper() != pat_operands.upper():
                    return False

        return True

    def _eliminate_useless_push_pop(self, lines: list[str]) -> tuple[list[str], bool]:
        """
        Eliminate PUSH H / POP H pairs when HL isn't modified between them.

        This handles the general case where we save HL, do operations that don't
        touch HL, then restore it - the save/restore is wasteful.
        """
        result: list[str] = []
        changed = False
        i = 0

        # Instructions that modify H or L (or HL as a pair)
        hl_modifying_opcodes = {
            'LHLD', 'LXI', 'INX', 'DCX', 'DAD', 'XTHL', 'SPHL', 'PCHL',
            'POP',  # POP H modifies HL
            'MOV',  # MOV H,x or MOV L,x modifies HL
            'MVI',  # MVI H,x or MVI L,x modifies HL
            'INR', 'DCR',  # INR H, DCR L etc
            'ADD', 'ADC', 'SUB', 'SBB', 'ANA', 'XRA', 'ORA', 'CMP',  # Don't modify HL but check operand
            'XCHG',  # Swaps HL with DE
            'CALL', 'RST',  # Could modify anything
        }

        def modifies_hl(line: str) -> bool:
            """Check if an instruction modifies H or L registers."""
            stripped = line.strip()
            if not stripped or stripped.startswith(';') or stripped.endswith(':'):
                return False

            parts = stripped.split(None, 1)
            if not parts:
                return False
            opcode = parts[0].upper()
            operands = parts[1] if len(parts) > 1 else ""

            # CALL modifies everything (conservatively)
            if opcode in ('CALL', 'RST'):
                return True

            # XCHG swaps HL with DE
            if opcode == 'XCHG':
                return True

            # These always modify HL
            if opcode in ('LHLD', 'XTHL', 'SPHL', 'PCHL'):
                return True

            # LXI H modifies HL
            if opcode == 'LXI' and operands.upper().startswith('H'):
                return True

            # INX H, DCX H modify HL
            if opcode in ('INX', 'DCX') and operands.upper() == 'H':
                return True

            # DAD modifies HL
            if opcode == 'DAD':
                return True

            # POP H modifies HL
            if opcode == 'POP' and operands.upper() == 'H':
                return True

            # MOV H,x or MOV L,x modifies H or L
            if opcode == 'MOV':
                dest = operands.split(',')[0].upper().strip() if ',' in operands else ""
                if dest in ('H', 'L', 'M'):  # M uses HL as pointer but doesn't modify it
                    if dest != 'M':
                        return True

            # MVI H,x or MVI L,x
            if opcode == 'MVI':
                dest = operands.split(',')[0].upper().strip() if ',' in operands else ""
                if dest in ('H', 'L'):
                    return True

            # INR H, DCR L, etc
            if opcode in ('INR', 'DCR'):
                if operands.upper().strip() in ('H', 'L'):
                    return True

            return False

        while i < len(lines):
            line = lines[i]
            stripped = line.strip()
            parts = stripped.split(None, 1)

            # Look for PUSH H
            if parts and parts[0].upper() == 'PUSH' and len(parts) > 1 and parts[1].upper() == 'H':
                # Found PUSH H - scan forward for matching POP H
                push_idx = i
                j = i + 1
                hl_modified = False
                pop_idx = -1

                while j < len(lines):
                    inner_line = lines[j]
                    inner_stripped = inner_line.strip()

                    # Skip empty lines and comments
                    if not inner_stripped or inner_stripped.startswith(';'):
                        j += 1
                        continue

                    # Check for label - can't optimize across labels
                    if inner_stripped.endswith(':'):
                        break

                    inner_parts = inner_stripped.split(None, 1)
                    if not inner_parts:
                        j += 1
                        continue

                    opcode = inner_parts[0].upper()
                    operands = inner_parts[1].upper() if len(inner_parts) > 1 else ""

                    # Found POP H?
                    if opcode == 'POP' and operands == 'H':
                        pop_idx = j
                        break

                    # Another PUSH H? Nested, can't optimize simply
                    if opcode == 'PUSH' and operands == 'H':
                        break

                    # Check if this instruction modifies HL
                    if modifies_hl(inner_line):
                        hl_modified = True
                        break

                    j += 1

                # If we found a matching POP H and HL wasn't modified, eliminate both
                if pop_idx > 0 and not hl_modified:
                    # Skip the PUSH H, copy everything in between, skip the POP H
                    for k in range(push_idx + 1, pop_idx):
                        result.append(lines[k])
                    i = pop_idx + 1
                    changed = True
                    continue

            result.append(line)
            i += 1

        return result, changed

    def _register_tracking_pass(self, lines: list[str]) -> tuple[list[str], bool]:
        """
        Track register contents and eliminate redundant loads.

        Tracks what value is in each register and removes loads that
        would load the same value that's already there.
        """
        result: list[str] = []
        changed = False

        # Track register contents: reg -> value (string describing the value)
        # None means unknown, a string like "??AUTO+5" means that memory location
        regs: dict[str, str | None] = {
            'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'H': None, 'L': None
        }
        # Track memory locations loaded into HL as a pair
        hl_value: str | None = None

        def invalidate_all():
            nonlocal hl_value
            for r in regs:
                regs[r] = None
            hl_value = None

        def invalidate_reg(r: str):
            nonlocal hl_value
            regs[r] = None
            if r in ('H', 'L'):
                hl_value = None

        def invalidate_hl():
            nonlocal hl_value
            regs['H'] = None
            regs['L'] = None
            hl_value = None

        for line in lines:
            parsed = self._parse_line(line)

            # Labels and control flow invalidate tracking
            stripped = line.strip()
            if stripped and ':' in stripped and not stripped.startswith('\t'):
                # This is a label - invalidate all (could be jump target)
                invalidate_all()
                result.append(line)
                continue

            if parsed is None:
                result.append(line)
                continue

            opcode, operands = parsed

            # Control flow instructions invalidate tracking
            if opcode in ('JMP', 'JZ', 'JNZ', 'JC', 'JNC', 'JP', 'JM', 'JPE', 'JPO',
                          'CALL', 'CZ', 'CNZ', 'CC', 'CNC', 'RET', 'RZ', 'RNZ', 'RC', 'RNC',
                          'PCHL', 'RST'):
                invalidate_all()
                result.append(line)
                continue

            # Track LDA - A gets value from memory
            if opcode == 'LDA':
                addr = operands
                if regs['A'] == f"mem:{addr}":
                    # Already have this value in A - skip this instruction
                    changed = True
                    self.stats['redundant_load_eliminated'] = self.stats.get('redundant_load_eliminated', 0) + 1
                    continue
                regs['A'] = f"mem:{addr}"
                result.append(line)
                continue

            # Track STA - memory gets A, but A is unchanged
            if opcode == 'STA':
                # A is unchanged, memory now has A's value
                result.append(line)
                continue

            # Track LHLD - HL gets value from memory
            if opcode == 'LHLD':
                addr = operands
                if hl_value == f"mem:{addr}":
                    # Already have this value in HL - skip this instruction
                    changed = True
                    self.stats['redundant_load_eliminated'] = self.stats.get('redundant_load_eliminated', 0) + 1
                    continue
                hl_value = f"mem:{addr}"
                regs['H'] = None  # Individual regs unknown
                regs['L'] = None
                result.append(line)
                continue

            # Track SHLD - memory gets HL, but HL is unchanged
            # Also track that HL now contains the same value as mem:addr
            if opcode == 'SHLD':
                addr = operands
                hl_value = f"mem:{addr}"  # HL contains what was just stored
                result.append(line)
                continue

            # Track LXI H,const
            if opcode == 'LXI' and operands.startswith('H,'):
                const = operands[2:]
                if hl_value == f"const:{const}":
                    # Already have this constant in HL - skip
                    changed = True
                    self.stats['redundant_load_eliminated'] = self.stats.get('redundant_load_eliminated', 0) + 1
                    continue
                hl_value = f"const:{const}"
                regs['H'] = None
                regs['L'] = None
                result.append(line)
                continue

            # Track LXI for other register pairs
            if opcode == 'LXI':
                if operands.startswith('D,'):
                    regs['D'] = None
                    regs['E'] = None
                elif operands.startswith('B,'):
                    regs['B'] = None
                    regs['C'] = None
                result.append(line)
                continue

            # Track MVI
            if opcode == 'MVI':
                parts = operands.split(',')
                if len(parts) == 2:
                    reg, val = parts[0], parts[1]
                    if reg in regs:
                        if regs[reg] == f"const:{val}":
                            # Already have this constant - skip
                            changed = True
                            self.stats['redundant_load_eliminated'] = self.stats.get('redundant_load_eliminated', 0) + 1
                            continue
                        regs[reg] = f"const:{val}"
                        if reg in ('H', 'L'):
                            hl_value = None
                result.append(line)
                continue

            # Track MOV
            if opcode == 'MOV':
                parts = operands.split(',')
                if len(parts) == 2:
                    dst, src = parts[0], parts[1]
                    if dst in regs and src in regs:
                        if regs[dst] == regs[src] and regs[dst] is not None:
                            # Same value - skip
                            changed = True
                            self.stats['redundant_load_eliminated'] = self.stats.get('redundant_load_eliminated', 0) + 1
                            continue
                        regs[dst] = regs[src]
                        if dst in ('H', 'L'):
                            hl_value = None
                    elif dst in regs:
                        # Loading from memory via M or something else
                        invalidate_reg(dst)
                result.append(line)
                continue

            # Instructions that modify registers
            if opcode in ('ADD', 'ADC', 'SUB', 'SBB', 'ANA', 'ORA', 'XRA', 'CMP',
                          'ADI', 'ACI', 'SUI', 'SBI', 'ANI', 'ORI', 'XRI', 'CPI'):
                regs['A'] = None  # A is modified
                result.append(line)
                continue

            if opcode in ('INR', 'DCR'):
                if operands in regs:
                    invalidate_reg(operands)
                result.append(line)
                continue

            if opcode in ('INX', 'DCX'):
                if operands == 'H':
                    invalidate_hl()
                elif operands == 'D':
                    regs['D'] = None
                    regs['E'] = None
                elif operands == 'B':
                    regs['B'] = None
                    regs['C'] = None
                result.append(line)
                continue

            if opcode == 'DAD':
                invalidate_hl()
                result.append(line)
                continue

            if opcode == 'XCHG':
                # Swap HL and DE
                hl_value = None  # For simplicity, just invalidate
                regs['H'], regs['D'] = regs['D'], regs['H']
                regs['L'], regs['E'] = regs['E'], regs['L']
                result.append(line)
                continue

            if opcode in ('PUSH', 'POP'):
                if opcode == 'POP':
                    if operands == 'H':
                        invalidate_hl()
                    elif operands == 'D':
                        regs['D'] = None
                        regs['E'] = None
                    elif operands == 'B':
                        regs['B'] = None
                        regs['C'] = None
                    elif operands == 'PSW':
                        regs['A'] = None
                result.append(line)
                continue

            # Rotates modify A
            if opcode in ('RLC', 'RRC', 'RAL', 'RAR'):
                regs['A'] = None
                result.append(line)
                continue

            # Other instructions - be conservative and invalidate A
            if opcode in ('CMA', 'DAA'):
                regs['A'] = None

            result.append(line)

        return result, changed

    def _translate_to_z80(self, lines: list[str]) -> list[str]:
        """Translate 8080 mnemonics to Z80 equivalents."""
        result: list[str] = []
        for line in lines:
            translated = self._translate_line_to_z80(line)
            result.append(translated)
        return result

    def _translate_line_to_z80(self, line: str) -> str:
        """Translate a single line from 8080 to Z80 mnemonics."""
        stripped = line.strip()

        # Skip empty, comments, labels (without instructions)
        if not stripped or stripped.startswith(";"):
            return line

        # Handle labels with potential instruction after
        label_prefix = ""
        if ":" in stripped and not stripped.startswith("\t"):
            parts = stripped.split(":", 1)
            label_prefix = parts[0] + ":"
            if len(parts) > 1 and parts[1].strip():
                stripped = parts[1].strip()
            else:
                return line  # Just a label

        # Skip directives
        directives = {"ORG", "END", "DB", "DW", "DS", "EQU", "PUBLIC", "EXTRN"}
        parts = stripped.split(None, 1)
        if not parts:
            return line
        opcode = parts[0].upper()
        if opcode in directives:
            return line

        operands = parts[1].split(";")[0].strip() if len(parts) > 1 else ""
        comment = ""
        if ";" in line:
            comment = "\t;" + line.split(";", 1)[1]

        # Translate based on opcode
        z80_line = self._translate_instruction(opcode, operands)
        if z80_line:
            if label_prefix:
                return f"{label_prefix}\t{z80_line}{comment}"
            return f"\t{z80_line}{comment}"

        return line

    def _translate_instruction(self, opcode: str, operands: str) -> str | None:
        """Translate a single 8080 instruction to Z80."""
        # Special cases that need operand transformation

        # MOV r,r -> LD r,r
        if opcode == "MOV":
            return f"LD {operands}"

        # MVI r,n -> LD r,n
        if opcode == "MVI":
            return f"LD {operands}"

        # LXI rp,nn -> LD rp,nn (with register pair translation)
        if opcode == "LXI":
            parts = operands.split(",", 1)
            if len(parts) == 2:
                rp = Z80_REG_PAIRS.get(parts[0].upper(), parts[0])
                return f"LD {rp},{parts[1]}"

        # LDA addr -> LD A,(addr)
        if opcode == "LDA":
            return f"LD A,({operands})"

        # STA addr -> LD (addr),A
        if opcode == "STA":
            return f"LD ({operands}),A"

        # LHLD addr -> LD HL,(addr)
        if opcode == "LHLD":
            return f"LD HL,({operands})"

        # SHLD addr -> LD (addr),HL
        if opcode == "SHLD":
            return f"LD ({operands}),HL"

        # LDED addr -> LD DE,(addr) (custom pseudo-op for Z80)
        if opcode == "LDED":
            return f"LD DE,({operands})"

        # LDAX rp -> LD A,(rp)
        if opcode == "LDAX":
            rp = Z80_REG_PAIRS.get(operands.upper(), operands)
            return f"LD A,({rp})"

        # STAX rp -> LD (rp),A
        if opcode == "STAX":
            rp = Z80_REG_PAIRS.get(operands.upper(), operands)
            return f"LD ({rp}),A"

        # ADD r -> ADD A,r
        if opcode == "ADD" and not operands.startswith("A,"):
            return f"ADD A,{operands}"

        # ADC r -> ADC A,r
        if opcode == "ADC" and not operands.startswith("A,"):
            return f"ADC A,{operands}"

        # SUB r -> SUB r (no change needed, Z80 SUB doesn't use A prefix)
        if opcode == "SUB":
            return f"SUB {operands}"

        # SBB r -> SBC A,r
        if opcode == "SBB":
            return f"SBC A,{operands}"

        # ANA r -> AND r
        if opcode == "ANA":
            return f"AND {operands}"

        # ORA r -> OR r
        if opcode == "ORA":
            return f"OR {operands}"

        # XRA r -> XOR r
        if opcode == "XRA":
            return f"XOR {operands}"

        # CMP r -> CP r
        if opcode == "CMP":
            return f"CP {operands}"

        # INR r -> INC r
        if opcode == "INR":
            return f"INC {operands}"

        # DCR r -> DEC r
        if opcode == "DCR":
            return f"DEC {operands}"

        # INX rp -> INC rp
        if opcode == "INX":
            rp = Z80_REG_PAIRS.get(operands.upper(), operands)
            return f"INC {rp}"

        # DCX rp -> DEC rp
        if opcode == "DCX":
            rp = Z80_REG_PAIRS.get(operands.upper(), operands)
            return f"DEC {rp}"

        # DAD rp -> ADD HL,rp
        if opcode == "DAD":
            rp = Z80_REG_PAIRS.get(operands.upper(), operands)
            return f"ADD HL,{rp}"

        # Immediate arithmetic
        if opcode == "ADI":
            return f"ADD A,{operands}"
        if opcode == "ACI":
            return f"ADC A,{operands}"
        if opcode == "SUI":
            return f"SUB {operands}"
        if opcode == "SBI":
            return f"SBC A,{operands}"
        if opcode == "ANI":
            return f"AND {operands}"
        if opcode == "ORI":
            return f"OR {operands}"
        if opcode == "XRI":
            return f"XOR {operands}"
        if opcode == "CPI":
            return f"CP {operands}"

        # Jumps
        if opcode == "JMP":
            return f"JP {operands}"
        if opcode == "JZ":
            return f"JP Z,{operands}"
        if opcode == "JNZ":
            return f"JP NZ,{operands}"
        if opcode == "JC":
            return f"JP C,{operands}"
        if opcode == "JNC":
            return f"JP NC,{operands}"
        if opcode == "JM":
            return f"JP M,{operands}"
        if opcode == "JPE":
            return f"JP PE,{operands}"
        if opcode == "JPO":
            return f"JP PO,{operands}"

        # Calls
        if opcode == "CZ":
            return f"CALL Z,{operands}"
        if opcode == "CNZ":
            return f"CALL NZ,{operands}"
        if opcode == "CC":
            return f"CALL C,{operands}"
        if opcode == "CNC":
            return f"CALL NC,{operands}"
        if opcode == "CM":
            return f"CALL M,{operands}"
        if opcode == "CPE":
            return f"CALL PE,{operands}"
        if opcode == "CPO":
            return f"CALL PO,{operands}"

        # Returns
        if opcode == "RZ":
            return "RET Z"
        if opcode == "RNZ":
            return "RET NZ"
        if opcode == "RC":
            return "RET C"
        if opcode == "RNC":
            return "RET NC"
        if opcode == "RM":
            return "RET M"
        if opcode == "RPE":
            return "RET PE"
        if opcode == "RPO":
            return "RET PO"

        # PUSH/POP with register pair translation
        if opcode == "PUSH":
            rp = Z80_REG_PAIRS.get(operands.upper(), operands)
            return f"PUSH {rp}"
        if opcode == "POP":
            rp = Z80_REG_PAIRS.get(operands.upper(), operands)
            return f"POP {rp}"

        # Misc
        if opcode == "XTHL":
            return "EX (SP),HL"
        if opcode == "SPHL":
            return "LD SP,HL"
        if opcode == "XCHG":
            return "EX DE,HL"
        if opcode == "PCHL":
            return "JP (HL)"
        if opcode == "CMA":
            return "CPL"
        if opcode == "CMC":
            return "CCF"
        if opcode == "STC":
            return "SCF"
        if opcode == "RAL":
            return "RLA"
        if opcode == "RAR":
            return "RRA"
        if opcode == "RLC":
            return "RLCA"
        if opcode == "RRC":
            return "RRCA"
        if opcode == "HLT":
            return "HALT"

        # I/O
        if opcode == "IN":
            return f"IN A,({operands})"
        if opcode == "OUT":
            return f"OUT ({operands}),A"
        if opcode == "INP":
            return "IN A,(C)"
        if opcode == "OUTP":
            return "OUT (C),A"

        # Instructions that don't change
        if opcode in ("CALL", "RET", "DAA", "NOP", "DI", "EI", "RST"):
            if operands:
                return f"{opcode} {operands}"
            return opcode

        return None

    def _optimize_z80_pass(self, lines: list[str]) -> tuple[list[str], bool]:
        """Apply Z80-specific optimizations."""
        changed = False
        result: list[str] = []
        i = 0

        # Build label_lines map for range checking (used by DJNZ optimization)
        label_lines: dict[str, int] = {}
        for line_num, line in enumerate(lines):
            stripped = line.strip()
            if ":" in stripped and not stripped.startswith("\t"):
                label = stripped.split(":")[0].strip()
                label_lines[label] = line_num

        while i < len(lines):
            line = lines[i].strip()
            parsed = self._parse_z80_line(line)

            if parsed:
                opcode, operands = parsed

                # LD A,0 -> XOR A (1 byte vs 2)
                if opcode == "LD" and operands == "A,0":
                    result.append("\tXOR A")
                    changed = True
                    self.stats["z80_xor_a"] = self.stats.get("z80_xor_a", 0) + 1
                    i += 1
                    continue

                # LD A,(addr); INC A; LD (addr),A -> LD HL,addr; INC (HL)
                # Saves 2 bytes: 3+1+3=7 bytes -> 3+1=4 bytes (actually 6->4 for direct addr)
                if opcode == "LD" and operands.startswith("A,(") and operands.endswith(")"):
                    addr = operands[3:-1]  # Extract address
                    if i + 2 < len(lines):
                        p1 = self._parse_z80_line(lines[i + 1].strip())
                        p2 = self._parse_z80_line(lines[i + 2].strip())
                        if (p1 and p1[0] == "INC" and p1[1] == "A" and
                            p2 and p2[0] == "LD" and p2[1] == f"({addr}),A"):
                            result.append(f"\tLD HL,{addr}")
                            result.append("\tINC (HL)")
                            changed = True
                            self.stats["z80_inc_mem"] = self.stats.get("z80_inc_mem", 0) + 1
                            i += 3
                            continue
                        # Also check for DEC A
                        if (p1 and p1[0] == "DEC" and p1[1] == "A" and
                            p2 and p2[0] == "LD" and p2[1] == f"({addr}),A"):
                            result.append(f"\tLD HL,{addr}")
                            result.append("\tDEC (HL)")
                            changed = True
                            self.stats["z80_dec_mem"] = self.stats.get("z80_dec_mem", 0) + 1
                            i += 3
                            continue

                # LD HL,const; PUSH HL; ... ; POP DE; ADD HL,DE
                # -> ... ; LD DE,const; ADD HL,DE
                # Saves PUSH/POP (2 bytes) by deferring the constant load
                # The value saved by PUSH HL is restored to DE, not HL, so we can defer loading it
                if opcode == "LD" and operands.startswith("HL,") and not operands.startswith("HL,("):
                    const_val = operands[3:]
                    if i + 1 < len(lines):
                        p1 = self._parse_z80_line(lines[i + 1].strip())
                        if p1 and p1[0] == "PUSH" and p1[1] == "HL":
                            # Look for POP DE followed by ADD HL,DE
                            j = i + 2
                            middle_ops = []
                            found_pop_de = False
                            while j < len(lines) and len(middle_ops) < 15:
                                pj = self._parse_z80_line(lines[j].strip())
                                if not pj:
                                    middle_ops.append(lines[j])
                                    j += 1
                                    continue
                                if pj[0] == "POP" and pj[1] == "DE":
                                    # Found POP DE - check if next is ADD HL,DE
                                    if j + 1 < len(lines):
                                        pk = self._parse_z80_line(lines[j + 1].strip())
                                        if pk and pk[0] == "ADD" and pk[1] == "HL,DE":
                                            # Can optimize! Remove LD HL,const; PUSH HL and POP DE
                                            # Emit middle ops, then LD DE,const; ADD HL,DE
                                            for op in middle_ops:
                                                result.append(op)
                                            result.append(f"\tLD DE,{const_val}")
                                            result.append("\tADD HL,DE")
                                            changed = True
                                            self.stats["z80_defer_const_add"] = self.stats.get("z80_defer_const_add", 0) + 1
                                            i = j + 2
                                            found_pop_de = True
                                    break
                                # Check for control flow that would break the pattern
                                if pj[0] in ("JP", "JR", "CALL", "RET", "DJNZ"):
                                    break
                                # Check for another PUSH/POP that would unbalance the stack
                                if pj[0] in ("PUSH", "POP"):
                                    break
                                middle_ops.append(lines[j])
                                j += 1
                            if found_pop_de:
                                continue

                # LD r,0 -> LD r,0 can sometimes use XOR for A
                # OR A -> OR A (can't improve)

                # EX DE,HL; EX DE,HL -> (nothing)
                if opcode == "EX" and operands == "DE,HL" and i + 1 < len(lines):
                    next_parsed = self._parse_z80_line(lines[i + 1].strip())
                    if next_parsed and next_parsed[0] == "EX" and next_parsed[1] == "DE,HL":
                        changed = True
                        self.stats["z80_double_ex"] = self.stats.get("z80_double_ex", 0) + 1
                        i += 2
                        continue

                # INC HL; DEC HL -> (nothing)
                if opcode == "INC" and operands == "HL" and i + 1 < len(lines):
                    next_parsed = self._parse_z80_line(lines[i + 1].strip())
                    if next_parsed and next_parsed[0] == "DEC" and next_parsed[1] == "HL":
                        changed = True
                        self.stats["z80_inc_dec_hl"] = self.stats.get("z80_inc_dec_hl", 0) + 1
                        i += 2
                        continue

                # DEC HL; INC HL -> (nothing)
                if opcode == "DEC" and operands == "HL" and i + 1 < len(lines):
                    next_parsed = self._parse_z80_line(lines[i + 1].strip())
                    if next_parsed and next_parsed[0] == "INC" and next_parsed[1] == "HL":
                        changed = True
                        self.stats["z80_dec_inc_hl"] = self.stats.get("z80_dec_inc_hl", 0) + 1
                        i += 2
                        continue

                # LD (addr),HL; LD HL,(addr) -> LD (addr),HL (same address)
                if opcode == "LD" and operands.startswith("(") and operands.endswith("),HL"):
                    addr = operands[1:-4]
                    if i + 1 < len(lines):
                        next_parsed = self._parse_z80_line(lines[i + 1].strip())
                        if next_parsed and next_parsed[0] == "LD" and next_parsed[1] == f"HL,({addr})":
                            result.append(lines[i])
                            changed = True
                            self.stats["z80_ld_hl_same"] = self.stats.get("z80_ld_hl_same", 0) + 1
                            i += 2
                            continue

                # DEC B; JR/JP NZ,label -> DJNZ label (saves 1-2 bytes)
                # Only convert if target is within DJNZ range (±128 bytes)
                if opcode == "DEC" and operands == "B" and i + 1 < len(lines):
                    next_parsed = self._parse_z80_line(lines[i + 1].strip())
                    if next_parsed and next_parsed[0] in ("JR", "JP") and next_parsed[1].startswith("NZ,"):
                        target = next_parsed[1][3:]  # Remove "NZ,"
                        # Check range - find target label
                        if target in label_lines:
                            # Target is earlier in the code (backward jump)
                            # DJNZ range is -126 to +129 bytes
                            # Use conservative estimate: ~2.5 bytes per line on average
                            distance = label_lines[target] - i
                            # ~50 lines is roughly 125 bytes
                            if -50 < distance < 50:
                                result.append(f"\tDJNZ {target}")
                                changed = True
                                self.stats["z80_djnz"] = self.stats.get("z80_djnz", 0) + 1
                                i += 2
                                continue

                # CP 1; JP Z/NZ -> DEC A; JP Z/NZ (saves 1 byte: CP 1 is 2 bytes, DEC A is 1 byte)
                # Only valid when A is not needed afterward (comparison just sets flags)
                # DISABLED: This optimization breaks DO CASE sequential comparisons where
                # multiple CP instructions test the same A value. The DEC A modifies A,
                # breaking subsequent comparisons.
                # if opcode == "CP" and operands == "1" and i + 1 < len(lines):
                #     next_parsed = self._parse_z80_line(lines[i + 1].strip())
                #     if next_parsed and next_parsed[0] in ("JP", "JR") and next_parsed[1].startswith(("Z,", "NZ,")):
                #         result.append("\tDEC A")
                #         changed = True
                #         self.stats["z80_cp1_dec"] = self.stats.get("z80_cp1_dec", 0) + 1
                #         i += 1
                #         continue

                # Skip trick: JP label; LD A,0FFH; label: -> DB 21H; LD A,0FFH; label:
                # The 21H byte is the opcode for LD HL,nn which "eats" the next 2 bytes
                # This saves 2 bytes (JP is 3 bytes, DB 21H is 1 byte)
                if opcode == "JP" and "," not in operands and operands != "(HL)" and i + 2 < len(lines):
                    target = operands.strip()
                    next_line = lines[i + 1].strip()
                    next_parsed = self._parse_z80_line(next_line)
                    third_line = lines[i + 2].strip()
                    # Check if next instruction is LD A,0FFH (2 bytes) and third is the target label
                    if (next_parsed and next_parsed[0] == "LD" and next_parsed[1] == "A,0FFH" and
                        third_line.startswith(target + ":")):
                        result.append("\tDB 21H\t; skip next 2 bytes (LD HL,nn opcode)")
                        changed = True
                        self.stats["z80_skip_trick"] = self.stats.get("z80_skip_trick", 0) + 1
                        i += 1
                        continue

                # LD HL,(addr1); PUSH HL; LD HL,(addr2); EX DE,HL; POP HL
                # -> LD DE,(addr2); LD HL,(addr1)
                # Saves 3 bytes (10 -> 7) by using Z80's LD DE,(nn) instruction
                if opcode == "LD" and operands.startswith("HL,(") and operands.endswith(")"):
                    addr1 = operands[3:]  # Keep the (addr) part
                    if i + 4 < len(lines):
                        p1 = self._parse_z80_line(lines[i + 1].strip())
                        p2 = self._parse_z80_line(lines[i + 2].strip())
                        p3 = self._parse_z80_line(lines[i + 3].strip())
                        p4 = self._parse_z80_line(lines[i + 4].strip())
                        if (p1 and p1[0] == "PUSH" and p1[1] == "HL" and
                            p2 and p2[0] == "LD" and p2[1].startswith("HL,(") and
                            p3 and p3[0] == "EX" and p3[1] == "DE,HL" and
                            p4 and p4[0] == "POP" and p4[1] == "HL"):
                            addr2 = p2[1][3:]  # Get (addr2)
                            result.append(f"\tLD DE,{addr2}")
                            result.append(f"\tLD HL,{addr1}")
                            changed = True
                            self.stats["z80_ld_de_nn"] = self.stats.get("z80_ld_de_nn", 0) + 1
                            i += 5
                            continue

                # LD HL,const; PUSH HL; LD HL,(addr); LD E,(HL); LD D,0; POP HL
                # -> LD HL,(addr); LD E,(HL); LD D,0; LD HL,const
                # Defer loading constant until after memory access, saves PUSH/POP (2 bytes)
                if opcode == "LD" and operands.startswith("HL,") and not operands.startswith("HL,("):
                    const_val = operands[3:]  # Get the constant
                    if i + 5 < len(lines):
                        p1 = self._parse_z80_line(lines[i + 1].strip())
                        p2 = self._parse_z80_line(lines[i + 2].strip())
                        p3 = self._parse_z80_line(lines[i + 3].strip())
                        p4 = self._parse_z80_line(lines[i + 4].strip())
                        p5 = self._parse_z80_line(lines[i + 5].strip())
                        if (p1 and p1[0] == "PUSH" and p1[1] == "HL" and
                            p2 and p2[0] == "LD" and p2[1].startswith("HL,(") and
                            p3 and p3[0] == "LD" and p3[1] in ("E,(HL)", "E,M") and
                            p4 and p4[0] == "LD" and p4[1] in ("D,0", "D,00H") and
                            p5 and p5[0] == "POP" and p5[1] == "HL"):
                            addr = p2[1][3:]  # Get (addr)
                            result.append(f"\tLD HL,{addr}")
                            result.append("\tLD E,(HL)")
                            result.append("\tLD D,0")
                            result.append(f"\tLD HL,{const_val}")
                            changed = True
                            self.stats["z80_defer_const_load"] = self.stats.get("z80_defer_const_load", 0) + 1
                            i += 6
                            continue

                # LD HL,0; PUSH HL; LD A,L; LD (addr),A; POP HL
                # -> XOR A; LD (addr),A; LD HL,0
                # Using HL just to get 0 into A is wasteful. XOR A is 1 byte.
                # Saves PUSH/POP (2 bytes) and LXI->XOR saves 2 more bytes
                if opcode == "LD" and operands == "HL,0":
                    if i + 4 < len(lines):
                        p1 = self._parse_z80_line(lines[i + 1].strip())
                        p2 = self._parse_z80_line(lines[i + 2].strip())
                        p3 = self._parse_z80_line(lines[i + 3].strip())
                        p4 = self._parse_z80_line(lines[i + 4].strip())
                        if (p1 and p1[0] == "PUSH" and p1[1] == "HL" and
                            p2 and p2[0] == "LD" and p2[1] == "A,L" and
                            p3 and p3[0] == "LD" and p3[1].startswith("(") and p3[1].endswith("),A") and
                            p4 and p4[0] == "POP" and p4[1] == "HL"):
                            addr = p3[1][:-2]  # Get (addr) part
                            result.append("\tXOR A")
                            result.append(f"\tLD {addr},A")
                            result.append("\tLD HL,0")
                            changed = True
                            self.stats["z80_xor_a_store"] = self.stats.get("z80_xor_a_store", 0) + 1
                            i += 5
                            continue

                # LD HL,0; LD A,L; LD (addr),A -> XOR A; LD (addr),A; LD HL,0
                # Loading 0 via HL just to get it into A is wasteful
                # XOR A is 1 byte vs LD A,L which is 1 byte, but we eliminate the dependency
                # Keep LD HL,0 at end in case subsequent code needs it
                if opcode == "LD" and operands == "HL,0":
                    if i + 2 < len(lines):
                        p1 = self._parse_z80_line(lines[i + 1].strip())
                        p2 = self._parse_z80_line(lines[i + 2].strip())
                        if (p1 and p1[0] == "LD" and p1[1] == "A,L" and
                            p2 and p2[0] == "LD" and p2[1].startswith("(") and p2[1].endswith("),A")):
                            addr = p2[1][:-2]  # Get (addr) part
                            result.append("\tXOR A")
                            result.append(f"\tLD {addr},A")
                            result.append("\tLD HL,0")
                            changed = True
                            self.stats["z80_xor_a_store_simple"] = self.stats.get("z80_xor_a_store_simple", 0) + 1
                            i += 3
                            continue

                # POP HL; PUSH HL; LD HL,x -> LD HL,x
                # POP/PUSH just peeks at TOS (no stack change), then HL is overwritten
                if opcode == "POP" and operands == "HL" and i + 2 < len(lines):
                    p1 = self._parse_z80_line(lines[i + 1].strip())
                    p2 = self._parse_z80_line(lines[i + 2].strip())
                    if (p1 and p1[0] == "PUSH" and p1[1] == "HL" and
                        p2 and p2[0] == "LD" and p2[1].startswith("HL,")):
                        result.append(lines[i + 2])  # Keep only LD HL,x
                        changed = True
                        self.stats["z80_pop_push_ld"] = self.stats.get("z80_pop_push_ld", 0) + 1
                        i += 3
                        continue

                # PUSH HL; LD HL,(addr); EX DE,HL; POP HL -> LD DE,(addr)
                # Z80 has direct LD DE,(addr) which 8080 doesn't have
                if opcode == "PUSH" and operands == "HL" and i + 3 < len(lines):
                    p1 = self._parse_z80_line(lines[i + 1].strip())
                    p2 = self._parse_z80_line(lines[i + 2].strip())
                    p3 = self._parse_z80_line(lines[i + 3].strip())
                    if (p1 and p1[0] == "LD" and p1[1].startswith("HL,(") and p1[1].endswith(")") and
                        p2 and p2[0] == "EX" and p2[1] == "DE,HL" and
                        p3 and p3[0] == "POP" and p3[1] == "HL"):
                        addr = p1[1][3:]  # Get (addr) including parens
                        result.append(f"\tLD DE,{addr}")
                        changed = True
                        self.stats["z80_ld_de_addr"] = self.stats.get("z80_ld_de_addr", 0) + 1
                        i += 4
                        continue

                # PUSH AF; LD (addr),A; POP AF -> LD (addr),A
                # Saving/restoring A around a store of A is pointless
                if opcode == "PUSH" and operands == "AF" and i + 2 < len(lines):
                    p1 = self._parse_z80_line(lines[i + 1].strip())
                    p2 = self._parse_z80_line(lines[i + 2].strip())
                    if (p1 and p1[0] == "LD" and p1[1].startswith("(") and p1[1].endswith("),A") and
                        p2 and p2[0] == "POP" and p2[1] == "AF"):
                        result.append(lines[i + 1])  # Keep only LD (addr),A
                        changed = True
                        self.stats["z80_push_sta_pop"] = self.stats.get("z80_push_sta_pop", 0) + 1
                        i += 3
                        continue

                # LD HL,const; LD r,L -> LD r,const (when const fits in byte)
                # Loading constant via HL just to move low byte is wasteful
                if opcode == "LD" and operands.startswith("HL,") and not operands.startswith("HL,("):
                    const_val = operands[3:]
                    if i + 1 < len(lines):
                        p1 = self._parse_z80_line(lines[i + 1].strip())
                        if p1 and p1[0] == "LD" and p1[1].endswith(",L"):
                            dest_reg = p1[1][:-2]  # Get destination register
                            if dest_reg in ("A", "B", "C", "D", "E"):
                                result.append(f"\tLD {dest_reg},{const_val}")
                                changed = True
                                self.stats["z80_ld_via_hl"] = self.stats.get("z80_ld_via_hl", 0) + 1
                                i += 2
                                continue

                # PUSH HL; LD HL,const; <ops not using HL>; POP HL -> <ops not using HL>
                # If HL is saved, loaded with constant, then restored without using it
                if opcode == "PUSH" and operands == "HL" and i + 2 < len(lines):
                    p1 = self._parse_z80_line(lines[i + 1].strip())
                    # Check for LD HL,const (not memory load)
                    if p1 and p1[0] == "LD" and p1[1].startswith("HL,") and not p1[1].startswith("HL,("):
                        # Look for operations that don't use HL, followed by POP HL
                        j = i + 2
                        middle_ops = []
                        hl_used = False
                        while j < len(lines):
                            pj = self._parse_z80_line(lines[j].strip())
                            if not pj:
                                middle_ops.append(lines[j])
                                j += 1
                                continue
                            if pj[0] == "POP" and pj[1] == "HL":
                                # Found the matching POP - can eliminate PUSH/LD/POP
                                if not hl_used:
                                    for op in middle_ops:
                                        result.append(op)
                                    changed = True
                                    self.stats["z80_push_ld_pop_unused"] = self.stats.get("z80_push_ld_pop_unused", 0) + 1
                                    i = j + 1
                                break
                            # Check if this op uses HL
                            op_str = pj[1] if pj[1] else ""
                            if "HL" in pj[0] or "HL" in op_str or "(HL)" in op_str or "H" in op_str.split(",")[0] or "L" in op_str.split(",")[0]:
                                hl_used = True
                                break
                            middle_ops.append(lines[j])
                            j += 1
                            # Limit search depth
                            if len(middle_ops) > 5:
                                break
                        if not hl_used and j < len(lines) and changed:
                            continue

            result.append(lines[i])
            i += 1

        return result, changed

    def _parse_z80_line(self, line: str) -> tuple[str, str] | None:
        """Parse a Z80 assembly line."""
        if not line or line.startswith(";"):
            return None
        if ":" in line and not line.startswith("\t"):
            parts = line.split(":", 1)
            if len(parts) > 1 and parts[1].strip():
                line = parts[1].strip()
            else:
                return None

        parts = line.split(None, 1)
        if not parts:
            return None
        opcode = parts[0].upper()
        operands = parts[1].split(";")[0].strip() if len(parts) > 1 else ""
        return (opcode, operands)

    def _convert_to_relative_jumps(self, lines: list[str]) -> list[str]:
        """Convert JP to JR where the jump is within range (-126 to +129 bytes)."""
        # First pass: find all label positions (approximate by line number)
        # This is a simple approximation - actual byte distances would need
        # proper assembly
        label_lines: dict[str, int] = {}
        for i, line in enumerate(lines):
            stripped = line.strip()
            if ":" in stripped and not stripped.startswith("\t"):
                label = stripped.split(":")[0].strip()
                label_lines[label] = i

        # Second pass: convert jumps where target is close
        result: list[str] = []
        for i, line in enumerate(lines):
            parsed = self._parse_z80_line(line.strip())

            if parsed:
                opcode, operands = parsed

                # Check for convertible jumps (JP, JP Z, JP NZ, JP C, JP NC)
                convert_map = {
                    "JP": ("JR", None),
                    "JP Z,": ("JR Z,", 5),
                    "JP NZ,": ("JR NZ,", 6),
                    "JP C,": ("JR C,", 5),
                    "JP NC,": ("JR NC,", 6),
                }

                for jp_prefix, (jr_prefix, prefix_len) in convert_map.items():
                    if prefix_len:
                        if opcode == "JP" and operands.startswith(jp_prefix[3:]):
                            # Conditional jump
                            target = operands[prefix_len - 3:].strip()
                            if target in label_lines:
                                distance = label_lines[target] - i
                                # Rough estimate: each line ~2-3 bytes on average
                                # JR range is -126 to +129 bytes
                                # Use conservative estimate of ~40 lines
                                if -40 < distance < 40:
                                    result.append(f"\t{jr_prefix}{target}")
                                    self.stats["z80_jr_convert"] = self.stats.get("z80_jr_convert", 0) + 1
                                    break
                    else:
                        if opcode == "JP" and "," not in operands and operands != "(HL)":
                            # Unconditional JP to label
                            target = operands.strip()
                            if target in label_lines:
                                distance = label_lines[target] - i
                                if -40 < distance < 40:
                                    result.append(f"\tJR {target}")
                                    self.stats["z80_jr_convert"] = self.stats.get("z80_jr_convert", 0) + 1
                                    break
                else:
                    result.append(line)
                    continue
                continue

            result.append(line)

        return result

    def _jump_threading_pass(self, lines: list[str]) -> tuple[list[str], bool]:
        """
        Jump threading optimization.

        If a jump targets a label whose only content is another unconditional jump,
        thread through to the final destination.

        Also removes labels that:
        1. Cannot have fall-through execution (preceded by unconditional jump/ret)
        2. Only contain an unconditional jump
        3. Are not referenced elsewhere after threading
        """
        changed = False

        # Build map of label -> (line index, first instruction after label)
        label_info: dict[str, tuple[int, str | None]] = {}
        for i, line in enumerate(lines):
            stripped = line.strip()
            if ":" in stripped and not stripped.startswith("\t"):
                label = stripped.split(":")[0].strip()
                # Find first instruction after this label
                first_instr = None
                for j in range(i + 1, len(lines)):
                    next_line = lines[j].strip()
                    if not next_line or next_line.startswith(";"):
                        continue
                    if ":" in next_line and not next_line.startswith("\t"):
                        # Another label - no instruction
                        break
                    first_instr = next_line
                    break
                label_info[label] = (i, first_instr)

        # Build map of label -> final destination (for jump chains)
        label_target: dict[str, str] = {}
        for label, (_, first_instr) in label_info.items():
            if first_instr:
                parsed = self._parse_z80_line(first_instr)
                if parsed and parsed[0] in ("JP", "JR") and "," not in parsed[1] and parsed[1] != "(HL)":
                    target = parsed[1].strip()
                    # Follow the chain
                    visited = {label}
                    while target in label_info and target not in visited:
                        visited.add(target)
                        _, target_instr = label_info[target]
                        if target_instr:
                            target_parsed = self._parse_z80_line(target_instr)
                            if target_parsed and target_parsed[0] in ("JP", "JR") and "," not in target_parsed[1] and target_parsed[1] != "(HL)":
                                target = target_parsed[1].strip()
                            else:
                                break
                        else:
                            break
                    if target != label:
                        label_target[label] = target

        # Track which labels are referenced
        label_refs: dict[str, int] = {label: 0 for label in label_info}

        # First pass: rewrite jumps AND DW references to use final destinations
        result: list[str] = []
        for i, line in enumerate(lines):
            stripped = line.strip()
            parsed = self._parse_z80_line(stripped)

            if parsed and parsed[0] in ("JP", "JR", "CALL", "DJNZ"):
                operands = parsed[1]
                # Handle conditional jumps like "Z,label" or "NZ,label"
                if "," in operands:
                    parts = operands.split(",", 1)
                    target = parts[1].strip()
                    prefix = parts[0] + ","
                else:
                    target = operands.strip()
                    prefix = ""

                # Thread through for unconditional jumps only
                if parsed[0] in ("JP", "JR") and not prefix and target in label_target:
                    new_target = label_target[target]
                    if parsed[0] == "JP":
                        result.append(f"\tJP {new_target}")
                    else:
                        result.append(f"\tJR {new_target}")
                    changed = True
                    self.stats["jump_thread"] = self.stats.get("jump_thread", 0) + 1
                    label_refs[new_target] = label_refs.get(new_target, 0) + 1
                else:
                    result.append(line)
                    if target in label_refs:
                        label_refs[target] += 1
            elif parsed and parsed[0] == "DW":
                # Thread DW references through jump-only labels
                target = parsed[1].strip()
                if target in label_target:
                    new_target = label_target[target]
                    result.append(f"\tDW\t{new_target}")
                    changed = True
                    self.stats["dw_thread"] = self.stats.get("dw_thread", 0) + 1
                    label_refs[new_target] = label_refs.get(new_target, 0) + 1
                else:
                    result.append(line)
                    if target in label_refs:
                        label_refs[target] += 1
            else:
                result.append(line)
                # Count label references in other contexts (e.g., LXI)
                # But don't count the label definition itself
                if ":" in stripped and not stripped.startswith("\t"):
                    # This is a label definition line, skip it
                    pass
                else:
                    for label in label_info:
                        if label in stripped:
                            label_refs[label] = label_refs.get(label, 0) + 1

        # Second pass: remove unreferenced labels that just jump
        # (Only if they can't have fall-through)
        final_result: list[str] = []
        i = 0
        while i < len(result):
            line = result[i]
            stripped = line.strip()

            # Check if this is a label
            if ":" in stripped and not stripped.startswith("\t"):
                label = stripped.split(":")[0].strip()

                # Check if label is unreferenced and just contains a jump
                if label in label_refs and label_refs[label] == 0 and label in label_target:
                    # Check if previous instruction prevents fall-through
                    can_fallthrough = True
                    for j in range(len(final_result) - 1, -1, -1):
                        prev = final_result[j].strip()
                        if not prev or prev.startswith(";"):
                            continue
                        if ":" in prev and not prev.startswith("\t"):
                            # Another label - fall-through possible
                            break
                        prev_parsed = self._parse_z80_line(prev)
                        if prev_parsed:
                            if prev_parsed[0] in ("JP", "JR", "RET") and "," not in prev_parsed[1]:
                                can_fallthrough = False
                            break

                    if not can_fallthrough:
                        # Skip this label and its jump instruction
                        changed = True
                        self.stats["dead_label_removed"] = self.stats.get("dead_label_removed", 0) + 1
                        i += 1
                        # Skip the jump instruction too
                        while i < len(result):
                            next_line = result[i].strip()
                            if not next_line or next_line.startswith(";"):
                                i += 1
                                continue
                            next_parsed = self._parse_z80_line(next_line)
                            if next_parsed and next_parsed[0] in ("JP", "JR"):
                                i += 1
                                break
                            break
                        continue

            final_result.append(line)
            i += 1

        return final_result, changed

    def _dead_store_elimination(self, lines: list[str]) -> tuple[list[str], bool]:
        """
        Eliminate dead stores at procedure entry.

        Pattern: A procedure stores a register parameter to memory at entry,
        but uses the register directly without ever loading from that memory.
        The store is dead and can be removed.

        Example:
            LD (??AUTO+38),A   ; Store param
            LD L,A             ; Use A directly
            ...                ; ??AUTO+38 never loaded before RET
        ->
            LD L,A
            ...
        """
        result: list[str] = []
        changed = False
        i = 0

        # Process one procedure at a time
        while i < len(lines):
            line = lines[i]
            stripped = line.strip()

            # Look for procedure entry (label followed by LD (addr),A)
            if ":" in stripped and not stripped.startswith("\t") and not stripped.startswith(";"):
                label = stripped.split(":")[0].strip()
                # This might be a procedure entry
                if i + 1 < len(lines):
                    next_stripped = lines[i + 1].strip()
                    parsed = self._parse_z80_line(next_stripped)
                    # Check for LD (addr),A pattern
                    if (parsed and parsed[0] == "LD" and
                        parsed[1].startswith("(") and parsed[1].endswith("),A")):
                        addr = parsed[1][1:-3]  # Extract addr from (addr),A
                        # Find end of procedure (next procedure label or end)
                        proc_end = i + 2
                        while proc_end < len(lines):
                            end_stripped = lines[proc_end].strip()
                            if (":" in end_stripped and
                                not end_stripped.startswith("\t") and
                                not end_stripped.startswith(";") and
                                end_stripped.split(":")[0].strip().startswith("@")):
                                # Another procedure label
                                break
                            proc_end += 1

                        # Check if addr is ever loaded within this procedure
                        addr_loaded = False
                        for j in range(i + 2, proc_end):
                            check_line = lines[j].strip()
                            # Check for LD A,(addr) or LD r,(addr) patterns
                            if f"({addr})" in check_line:
                                p = self._parse_z80_line(check_line)
                                if p and p[0] == "LD":
                                    # Check if it's a load (not store)
                                    # Load: LD r,(addr) - addr is in second part
                                    # Store: LD (addr),r - addr is in first part
                                    if not p[1].startswith("("):
                                        # It's LD r,(addr) - a load
                                        addr_loaded = True
                                        break

                        if not addr_loaded:
                            # Store is dead - skip it
                            result.append(line)  # Keep the label
                            i += 2  # Skip the store instruction
                            changed = True
                            self.stats["dead_store_elim"] = self.stats.get("dead_store_elim", 0) + 1
                            continue

            result.append(line)
            i += 1

        return result, changed

    def _optimize_pass(self, lines: list[str]) -> tuple[list[str], bool]:
        """Single optimization pass."""
        changed = False
        result: list[str] = []
        i = 0

        while i < len(lines):
            # Special case: JMP/JR to immediately following label
            parsed = self._parse_line(lines[i])
            if parsed and parsed[0] in ("JMP", "JR"):
                target = parsed[1]
                # Look ahead for the target label (skip comments/empty lines)
                j = i + 1
                found_target = False
                while j < len(lines):
                    next_line = lines[j].strip()
                    if not next_line or next_line.startswith(";"):
                        j += 1
                        continue
                    # Check if this is a label line
                    if ":" in next_line and not next_line.startswith("\t"):
                        label = next_line.split(":")[0].strip()
                        if label == target:
                            # JMP to next label - remove the JMP, keep going
                            self.stats["jump_to_next"] = self.stats.get("jump_to_next", 0) + 1
                            changed = True
                            found_target = True
                    break
                if found_target:
                    i += 1
                    continue

            # Special case: ORA A; JZ label where label: XRA A; RET -> ORA A; RZ
            # This handles the common IF condition THEN ... RETURN TRUE; RETURN FALSE pattern
            # Also handles: ORA A; JNZ label where label: MVI A,1; RET -> ORA A; RNZ
            if parsed and parsed[0] == "ORA" and parsed[1] == "A":
                # Check if next instruction is JZ or JNZ
                next_parsed = None
                j = i + 1
                while j < len(lines):
                    next_line = lines[j].strip()
                    if not next_line or next_line.startswith(";"):
                        j += 1
                        continue
                    next_parsed = self._parse_line(lines[j])
                    break

                if next_parsed and next_parsed[0] in ("JZ", "JNZ"):
                    is_jz = next_parsed[0] == "JZ"
                    target = next_parsed[1]
                    # Search for the target label and check pattern
                    for k in range(j + 1, min(j + 100, len(lines))):
                        line_k = lines[k].strip()
                        if line_k.startswith(target + ":"):
                            # Found the label, check next instructions
                            m = k + 1
                            while m < len(lines):
                                line_m = lines[m].strip()
                                if not line_m or line_m.startswith(";"):
                                    m += 1
                                    continue
                                parsed_m = self._parse_line(lines[m])

                                # For JZ: look for XRA A; RET (return 0)
                                if is_jz and parsed_m and parsed_m[0] == "XRA" and parsed_m[1] == "A":
                                    # Check for RET after XRA A
                                    n = m + 1
                                    while n < len(lines):
                                        line_n = lines[n].strip()
                                        if not line_n or line_n.startswith(";"):
                                            n += 1
                                            continue
                                        parsed_n = self._parse_line(lines[n])
                                        if parsed_n and parsed_n[0] == "RET" and parsed_n[1] == "":
                                            # Pattern matched! Replace ORA A; JZ label with ORA A; RZ
                                            result.append(lines[i])  # Keep ORA A
                                            result.append("\tRZ")    # Replace JZ with RZ
                                            self.stats["jz_xra_ret_to_rz"] = self.stats.get("jz_xra_ret_to_rz", 0) + 1
                                            changed = True
                                            i = j + 1  # Skip past JZ
                                            break
                                        break

                                # For JNZ: look for MVI A,1; RET (return 1/TRUE)
                                elif not is_jz and parsed_m and parsed_m[0] == "MVI" and parsed_m[1] == "A,1":
                                    # Check for RET after MVI A,1
                                    n = m + 1
                                    while n < len(lines):
                                        line_n = lines[n].strip()
                                        if not line_n or line_n.startswith(";"):
                                            n += 1
                                            continue
                                        parsed_n = self._parse_line(lines[n])
                                        if parsed_n and parsed_n[0] == "RET" and parsed_n[1] == "":
                                            # Pattern matched! Replace ORA A; JNZ label with ORA A; RNZ
                                            result.append(lines[i])  # Keep ORA A
                                            result.append("\tRNZ")   # Replace JNZ with RNZ
                                            self.stats["jnz_mvi_ret_to_rnz"] = self.stats.get("jnz_mvi_ret_to_rnz", 0) + 1
                                            changed = True
                                            i = j + 1  # Skip past JNZ
                                            break
                                        break
                                break
                            break

            # Try to match patterns starting at current position
            matched = False

            for pattern in self.patterns:
                match_len = len(pattern.pattern)
                if i + match_len > len(lines):
                    continue

                # Extract instructions for potential match
                instructions = []
                instruction_lines = []  # Line indices of actual instructions
                skip_indices = []

                j = i
                instr_count = 0
                while instr_count < match_len and j < len(lines):
                    line = lines[j].strip()
                    parsed = self._parse_line(lines[j])
                    if parsed is None:
                        # Check if this is a label (not just a comment)
                        # Labels break pattern matching - code can jump to labels
                        if line and ':' in line and not line.startswith(';'):
                            # This is a label - stop pattern matching here
                            break
                        # Comment or empty line - include but don't count
                        skip_indices.append(j - i)
                        j += 1
                        continue
                    instructions.append(parsed)
                    instruction_lines.append(j)
                    instr_count += 1
                    j += 1

                if len(instructions) < match_len:
                    continue

                # Check if pattern matches
                if self._matches_pattern(pattern, instructions):
                    # Check condition if present
                    if pattern.condition and not pattern.condition(instructions):
                        continue

                    # Apply replacement
                    self.stats[pattern.name] = self.stats.get(pattern.name, 0) + 1
                    changed = True
                    matched = True

                    # First, preserve any labels/comments that were skipped during matching
                    # These should appear BEFORE any replacement instructions
                    for offset in skip_indices:
                        result.append(lines[i + offset])

                    # Then apply the replacement
                    if pattern.replacement is not None:
                        for opcode, operands in pattern.replacement:
                            result.append(f"\t{opcode}\t{operands}" if operands else f"\t{opcode}")
                    elif pattern.name.startswith("cond_uncond"):
                        # Keep second instruction only
                        result.append(lines[instruction_lines[-1]])
                    elif pattern.name in ("redundant_mov", "duplicate_mov", "duplicate_ld"):
                        # Keep first instruction only
                        result.append(lines[instruction_lines[0]])
                    elif pattern.name in ("load_store_same", "shld_lhld_same"):
                        # Keep first instruction only
                        result.append(lines[instruction_lines[0]])
                    elif pattern.name in ("useless_extend_before_sub", "useless_extend_before_cp"):
                        # Keep last instruction only (SUB or CP)
                        result.append(lines[instruction_lines[-1]])
                    elif pattern.name == "tail_call":
                        # CALL x; RET -> JMP x
                        call_target = instructions[0][1]
                        result.append(f"\tJMP\t{call_target}")
                    elif pattern.name == "pop_push_lxi":
                        # POP H; PUSH H; LXI H,x -> LXI H,x
                        # POP/PUSH peeks TOS (no stack change), then HL overwritten
                        result.append(lines[instruction_lines[2]])  # Keep only LXI H,x
                    elif pattern.name == "push_sta_pop":
                        # PUSH PSW; STA addr; POP PSW -> STA addr
                        result.append(lines[instruction_lines[1]])  # Keep only STA
                    elif pattern.name == "lxi_xchg_pop":
                        # LXI H,x; XCHG; POP H -> LXI D,x; POP H
                        operand = instructions[0][1][2:]  # Remove "H," prefix
                        result.append(f"\tLXI\tD,{operand}")
                        result.append("\tPOP\tH")
                    elif pattern.name == "lxi_xchg_call":
                        # LXI H,x; XCHG; CALL y -> LXI D,x; CALL y
                        operand = instructions[0][1][2:]  # Remove "H," prefix
                        result.append(f"\tLXI\tD,{operand}")
                        result.append(lines[instruction_lines[2]])  # CALL y
                    elif pattern.name == "lxi_xchg_jmp":
                        # LXI H,x; XCHG; JMP y -> LXI D,x; JMP y
                        operand = instructions[0][1][2:]  # Remove "H," prefix
                        result.append(f"\tLXI\tD,{operand}")
                        result.append(lines[instruction_lines[2]])  # JMP y
                    elif pattern.name in ("lxi_xchg_lda", "lxi_xchg_sta", "lxi_xchg_lhld"):
                        # LXI H,x; XCHG; LDA/STA/LHLD y -> LXI D,x; LDA/STA/LHLD y
                        operand = instructions[0][1][2:]  # Remove "H," prefix
                        result.append(f"\tLXI\tD,{operand}")
                        result.append(lines[instruction_lines[2]])  # LDA/STA/LHLD y
                    elif pattern.name == "push_lxi_xchg_pop":
                        # PUSH H; LXI H,x; XCHG; POP H -> LXI D,x
                        # The PUSH is no longer needed since we're not saving/restoring HL
                        operand = instructions[1][1][2:]  # Remove "H," prefix
                        result.append(f"\tLXI\tD,{operand}")
                    elif pattern.name == "double_push_same_const":
                        # LXI H,x; PUSH H; LXI H,x; PUSH H -> LXI H,x; PUSH H; PUSH H
                        result.append(lines[instruction_lines[0]])
                        result.append("\tPUSH\tH")
                        result.append("\tPUSH\tH")
                    elif pattern.name == "push_lxi_d_pop_dad":
                        # PUSH H; LXI D,x; POP H; DAD D -> LXI D,x; DAD D
                        result.append(lines[instruction_lines[1]])  # LXI D,x
                        result.append(lines[instruction_lines[3]])  # DAD D
                    elif pattern.name == "lxi_mov_am_to_lda":
                        # LXI H,addr; MOV A,M -> LDA addr
                        addr = instructions[0][1][2:]  # Remove "H," prefix
                        result.append(f"\tLDA\t{addr}")
                    elif pattern.name == "sta_lda_same":
                        # STA x; LDA x -> STA x
                        result.append(lines[instruction_lines[0]])

                    elif pattern.name in ("lda_cpi_jz_lda_same", "lda_cpi_jnz_lda_same",
                                          "lda_cpi_jc_lda_same", "lda_cpi_jnc_lda_same",
                                          "lda_ora_jz_lda_same", "lda_ora_jnz_lda_same"):
                        # LDA x; CPI/ORA; Jcond; LDA x -> LDA x; CPI/ORA; Jcond
                        # Keep first 3 instructions, drop the redundant reload
                        result.append(lines[instruction_lines[0]])  # LDA
                        result.append(lines[instruction_lines[1]])  # CPI/ORA
                        result.append(lines[instruction_lines[2]])  # Jcond

                    elif pattern.name == "lda_adi1_sta_same":
                        # LDA x; ADI 1; STA x -> LXI H,x; INR M
                        addr = instructions[0][1]
                        result.append(f"\tLXI\tH,{addr}")
                        result.append(f"\tINR\tM")
                    elif pattern.name == "lda_sui1_sta_same":
                        # LDA x; SUI 1; STA x -> LXI H,x; DCR M
                        addr = instructions[0][1]
                        result.append(f"\tLXI\tH,{addr}")
                        result.append(f"\tDCR\tM")

                    elif pattern.name == "lxi_mov_al_sta":
                        # LXI H,const; MOV A,L; STA x -> MVI A,const; STA x
                        const = instructions[0][1][2:]  # Remove "H," prefix
                        sta_addr = instructions[2][1]
                        result.append(f"\tMVI\tA,{const}")
                        result.append(f"\tSTA\t{sta_addr}")
                    elif pattern.name == "mov_la_mvi_h0_sta":
                        # MOV L,A; MVI H,0; STA x -> STA x
                        result.append(lines[instruction_lines[2]])

                    elif pattern.name == "mov_al_mvi_h0_sta":
                        # MOV A,L; MVI H,0; STA x -> MOV A,L; STA x
                        result.append(lines[instruction_lines[0]])  # MOV A,L
                        result.append(lines[instruction_lines[2]])  # STA x

                    elif pattern.name == "push_shld_pop":
                        # PUSH H; SHLD x; POP H -> SHLD x
                        # SHLD doesn't modify HL, so save/restore is unnecessary
                        result.append(lines[instruction_lines[1]])  # Keep only SHLD

                    elif pattern.name == "push_mvi_a_mov_e_mvi_d_pop":
                        # PUSH H; MVI A,x; MOV E,A; MVI D,0; POP H -> MVI E,x; MVI D,0
                        # HL not modified, PUSH/POP is wasteful
                        const = instructions[1][1][2:]  # Get constant from "A,x"
                        result.append(f"\tMVI\tE,{const}")
                        result.append(f"\tMVI\tD,0")

                    elif pattern.name == "push_lda_sub_to_de_pop":
                        # PUSH H; LDA x; MOV B,A; LDA y; SUB B; MOV E,A; MVI D,0; POP H
                        # -> just the middle instructions without PUSH/POP
                        # HL not modified, PUSH/POP is wasteful
                        for idx in range(1, 7):  # Instructions 1-6 (skip PUSH at 0 and POP at 7)
                            result.append(lines[instruction_lines[idx]])

                    elif pattern.name == "push_lhld_pop_d":
                        # PUSH H; LHLD x; POP D -> XCHG; LHLD x
                        # Old HL goes to DE, new value from (x) into HL
                        # This is 1 byte smaller (5 -> 4 bytes)
                        addr = instructions[1][1]  # Get address from LHLD
                        result.append(f"\tXCHG")
                        result.append(f"\tLHLD\t{addr}")

                    elif pattern.name == "early_load_push_subde":
                        # LHLD x; PUSH H; LDED y; LHLD z; CALL ??SUBDE; XCHG; POP H; CALL ??SUBDE
                        # -> LDED y; LHLD z; CALL ??SUBDE; XCHG; LHLD x; CALL ??SUBDE
                        # Delay loading x until needed, eliminate PUSH/POP
                        addr_x = instructions[0][1]  # First LHLD operand
                        addr_y = instructions[2][1]  # LDED operand
                        addr_z = instructions[3][1]  # Second LHLD operand
                        result.append(f"\tLDED\t{addr_y}")
                        result.append(f"\tLHLD\t{addr_z}")
                        result.append(f"\tCALL\t??SUBDE")
                        result.append(f"\tXCHG")
                        result.append(f"\tLHLD\t{addr_x}")
                        result.append(f"\tCALL\t??SUBDE")

                    elif pattern.name in ("mov_ba_sta_mov_ab", "mov_ca_sta_mov_ac",
                                          "mov_da_sta_mov_ad", "mov_ea_sta_mov_ae"):
                        # MOV x,A; STA y; MOV A,x -> STA y
                        # A is unchanged by STA, so the save/restore is unnecessary
                        result.append(lines[instruction_lines[1]])

                    elif pattern.name == "mov_ba_lhld_mov_ab_mov_ma":
                        # MOV B,A; LHLD x; MOV A,B; MOV M,A -> MOV B,A; LHLD x; MOV M,B
                        result.append(lines[instruction_lines[0]])  # MOV B,A
                        result.append(lines[instruction_lines[1]])  # LHLD x
                        result.append(f"\tMOV\tM,B")

                    elif pattern.name == "mov_ca_lhld_mov_ac_mov_ma":
                        # MOV C,A; LHLD x; MOV A,C; MOV M,A -> MOV C,A; LHLD x; MOV M,C
                        result.append(lines[instruction_lines[0]])  # MOV C,A
                        result.append(lines[instruction_lines[1]])  # LHLD x
                        result.append(f"\tMOV\tM,C")

                    elif pattern.name in ("sbb_mov_ora_jm", "sbb_mov_ora_jp"):
                        # SBB D; MOV H,A; ORA A; JM/JP x -> SBB D; MOV H,A; JM/JP x
                        # ORA A is redundant - sign flag already set by SBB D
                        result.append(lines[instruction_lines[0]])  # SBB D
                        result.append(lines[instruction_lines[1]])  # MOV H,A
                        result.append(lines[instruction_lines[3]])  # JM/JP x (skip ORA A)

                    elif pattern.name in ("sub_16bit_sign_jm", "sub_16bit_sign_jp"):
                        # SUB E; MOV L,A; MOV A,H; SBB D; MOV H,A; JM/JP x
                        # -> SUB E; MOV A,H; SBB D; JM/JP x
                        # When just checking sign, skip MOV L,A and MOV H,A
                        result.append(lines[instruction_lines[0]])  # SUB E
                        result.append(lines[instruction_lines[2]])  # MOV A,H
                        result.append(lines[instruction_lines[3]])  # SBB D
                        result.append(lines[instruction_lines[5]])  # JM/JP x

                    elif pattern.name == "shld_call_lhld_same":
                        # SHLD x; CALL y; LHLD x -> PUSH H; CALL y; POP H
                        call_target = instructions[1][1]
                        result.append("\tPUSH\tH")
                        result.append(f"\tCALL\t{call_target}")
                        result.append("\tPOP\tH")

                    elif pattern.name == "shld_call_lhld_jmp_same":
                        # SHLD x; CALL y; LHLD x; JMP z -> PUSH H; CALL y; POP H; JMP z
                        call_target = instructions[1][1]
                        jmp_target = instructions[3][1]
                        result.append("\tPUSH\tH")
                        result.append(f"\tCALL\t{call_target}")
                        result.append("\tPOP\tH")
                        result.append(f"\tJMP\t{jmp_target}")

                    elif pattern.name == "sta_call_lda_same":
                        # STA x; CALL y; LDA x -> PUSH PSW; CALL y; POP PSW
                        call_target = instructions[1][1]
                        result.append("\tPUSH\tPSW")
                        result.append(f"\tCALL\t{call_target}")
                        result.append("\tPOP\tPSW")

                    elif pattern.name == "shld_mvi_lhld_same":
                        # SHLD x; MVI r,n; LHLD x -> SHLD x; MVI r,n
                        # Keep the SHLD (variable may be needed), remove LHLD (HL unchanged)
                        result.append(lines[instruction_lines[0]])  # SHLD x
                        result.append(lines[instruction_lines[1]])  # MVI r,n

                    elif pattern.name == "sta_mvi_lda_same":
                        # STA x; MVI r,n; LDA x -> STA x; MVI r,n
                        # Keep the STA (variable may be needed), remove LDA (A unchanged by MVI r)
                        result.append(lines[instruction_lines[0]])  # STA x
                        result.append(lines[instruction_lines[1]])  # MVI r,n

                    elif pattern.name == "push_lxi_mov_cl_pop":
                        # PUSH H; LXI H,const; MOV C,L; POP H -> MVI C,const
                        const = instructions[1][1][2:]  # Remove "H," prefix
                        result.append(f"\tMVI\tC,{const}")

                    elif pattern.name == "push_mov_sta_pop_mvi_h0":
                        # PUSH H; MOV A,L; STA x; POP H; MVI H,0 -> MOV A,L; STA x; MVI H,0
                        sta_addr = instructions[2][1]
                        result.append("\tMOV\tA,L")
                        result.append(f"\tSTA\t{sta_addr}")
                        result.append("\tMVI\tH,0")

                    elif pattern.name == "shld_mvi_lhld_xchg_same":
                        # SHLD x; MVI r,n; LHLD x; XCHG -> SHLD x; MVI r,n; XCHG
                        # Keep the SHLD (variable may be needed), remove LHLD (HL unchanged)
                        result.append(lines[instruction_lines[0]])  # SHLD x
                        result.append(lines[instruction_lines[1]])  # MVI r,n
                        result.append("\tXCHG")

                    i = j
                    break

            if not matched:
                result.append(lines[i])
                i += 1

        return result, changed

    def _parse_line(self, line: str) -> tuple[str, str] | None:
        """Parse an assembly line into (opcode, operands). Returns None for non-instructions."""
        line = line.strip()

        # Skip empty lines and comments
        if not line or line.startswith(";"):
            return None

        # Skip labels (but they might have instructions after)
        if ":" in line and not line.startswith("\t"):
            # Label line
            parts = line.split(":", 1)
            if len(parts) > 1 and parts[1].strip():
                line = parts[1].strip()
            else:
                return None

        # Skip directives
        directives = {"ORG", "END", "DB", "DW", "DS", "EQU", "PUBLIC", "EXTRN"}

        # Parse instruction
        parts = line.split(None, 1)
        if not parts:
            return None

        opcode = parts[0].upper()
        if opcode in directives:
            return None

        operands = parts[1].split(";")[0].strip() if len(parts) > 1 else ""

        return (opcode, operands)

    def _matches_pattern(
        self, pattern: PeepholePattern, instructions: list[tuple[str, str]]
    ) -> bool:
        """Check if instructions match the pattern."""
        if len(instructions) != len(pattern.pattern):
            return False

        for (pat_op, pat_operands), (inst_op, inst_operands) in zip(
            pattern.pattern, instructions
        ):
            if pat_op != inst_op:
                return False

            if pat_operands is not None:
                # Check operands
                if "*" in pat_operands:
                    # Wildcard match
                    pat_re = pat_operands.replace("*", ".*")
                    if not re.match(pat_re, inst_operands):
                        return False
                elif pat_operands != inst_operands:
                    return False

        return True


def optimize_peephole(asm_text: str) -> str:
    """Convenience function to apply peephole optimization.

    By default, this now expects native Z80 assembly (LD, JP, etc.)
    since that's the common use case for Z80 compilers.
    """
    optimizer = PeepholeOptimizer()
    return optimizer.optimize(asm_text)


def optimize_z80(asm_text: str) -> str:
    """Optimize native Z80 assembly.

    Use this for compilers that generate Z80 mnemonics directly
    (LD, JP, JR, IX, IY, etc.) rather than 8080 mnemonics.

    This is the preferred function for modern Z80 compilers.
    """
    optimizer = PeepholeOptimizer(
        target=Target.Z80,
        input_syntax=InputSyntax.Z80,
    )
    return optimizer.optimize(asm_text)


def optimize_8080(asm_text: str, target: Target = Target.Z80) -> str:
    """Optimize 8080 assembly, optionally translating to Z80.

    Use this for compilers that generate 8080 mnemonics (MOV, MVI, etc.).
    If target is Z80, the output will be translated to Z80 mnemonics.

    Args:
        asm_text: Assembly text using 8080 mnemonics
        target: Target processor (I8080 or Z80)
    """
    optimizer = PeepholeOptimizer(
        target=target,
        input_syntax=InputSyntax.I8080,
    )
    return optimizer.optimize(asm_text)


# Alias for consistency with library API
optimize_asm = optimize_peephole
