use crate::cpu::{AddressingMode, Opcode};
use crate::state::{AppState, BlockType};
use std::collections::BTreeMap;

use crate::state::LabelType;

/// Result of the analysis pass.
pub struct AnalysisResult {
    pub labels: BTreeMap<u16, Vec<crate::state::Label>>,
    pub cross_refs: BTreeMap<u16, Vec<u16>>,
    pub hints: BTreeMap<u16, String>,
}

type UsageData = (
    BTreeMap<LabelType, usize>,
    Vec<u16>,
    LabelType,         // first overall
    Option<LabelType>, // first ZeroPage style (2-digit)
    Option<LabelType>, // first Absolute style (4-digit)
);

fn is_zp_style(t: LabelType) -> bool {
    matches!(
        t,
        LabelType::ZeroPageAbsoluteAddress | LabelType::ZeroPageField | LabelType::ZeroPagePointer
    )
}

pub fn analyze(state: &AppState) -> AnalysisResult {
    // We want to track ALL usages, illegal or not, and then pick the best ones.
    // Map: Address -> Set of used LabelTypes
    // We also need ref counts.
    // Map: Address -> UsageData
    // storing (counts, refs, first_seen_type)
    let mut usage_map: BTreeMap<u16, UsageData> = BTreeMap::new();
    let mut pc = 0;
    let data_len = state.raw_data.len();
    let origin = state.origin;

    while pc < data_len {
        let current_type = state
            .block_types
            .get(pc)
            .copied()
            .unwrap_or(BlockType::Code);

        if current_type == BlockType::Code {
            let opcode_byte = state.raw_data[pc];
            if let Some(opcode) = &state.disassembler.opcodes[opcode_byte as usize] {
                if opcode.illegal && !state.settings.use_illegal_opcodes {
                    // TBD, but we can skip 1 byte, or the entire opcode.
                    // Let's be safe, and skip 1 byte.
                    // pc += opcode.size as usize;
                    pc += 1;
                    continue;
                }
                // Check if we have enough bytes
                if pc + opcode.size as usize <= data_len {
                    // Extract operands
                    let mut operands = Vec::new();
                    for i in 1..opcode.size {
                        operands.push(state.raw_data[pc + i as usize]);
                    }

                    analyze_instruction(
                        state,
                        opcode,
                        &operands,
                        origin.wrapping_add(pc as u16),
                        &mut usage_map,
                    );

                    pc += opcode.size as usize;
                } else {
                    pc += 1;
                }
            } else {
                pc += 1;
            }
        } else {
            // Data skip
            if current_type == BlockType::Address {
                if pc + 2 <= data_len {
                    let low = state.raw_data[pc];
                    let high = state.raw_data[pc + 1];
                    let val = (high as u16) << 8 | (low as u16);
                    update_usage(
                        &mut usage_map,
                        val,
                        LabelType::AbsoluteAddress,
                        origin.wrapping_add(pc as u16),
                    );
                    pc += 2;
                } else {
                    pc += 1;
                }
            } else if current_type == BlockType::DataWord {
                pc += 2;
            } else if current_type == BlockType::LoHiAddress {
                // Determine the full length of this LoHi block
                let mut count = 0;
                while pc + count < data_len
                    && state.block_types.get(pc + count) == Some(&BlockType::LoHiAddress)
                {
                    count += 1;
                }

                let pair_count = count / 2;
                if pair_count > 0 {
                    let split_offset = pair_count;
                    for i in 0..pair_count {
                        if pc + i < data_len && pc + split_offset + i < data_len {
                            let lo = state.raw_data[pc + i];
                            let hi = state.raw_data[pc + split_offset + i];
                            let val = (hi as u16) << 8 | (lo as u16);
                            update_usage(
                                &mut usage_map,
                                val,
                                LabelType::AbsoluteAddress, // Or External? Analyzer figures out external vs internal later in loop.
                                origin.wrapping_add((pc + i) as u16),
                            );
                        }
                    }
                }
                pc += count;
            } else if current_type == BlockType::HiLoAddress {
                // Determine the full length of this HiLo block
                let mut count = 0;
                while pc + count < data_len
                    && state.block_types.get(pc + count) == Some(&BlockType::HiLoAddress)
                {
                    count += 1;
                }

                let pair_count = count / 2;
                if pair_count > 0 {
                    let split_offset = pair_count;
                    for i in 0..pair_count {
                        if pc + i < data_len && pc + split_offset + i < data_len {
                            let hi = state.raw_data[pc + i];
                            let lo = state.raw_data[pc + split_offset + i];
                            let val = (hi as u16) << 8 | (lo as u16);
                            update_usage(
                                &mut usage_map,
                                val,
                                LabelType::AbsoluteAddress,
                                origin.wrapping_add((pc + i) as u16),
                            );
                        }
                    }
                }
                pc += count;
            } else if current_type == BlockType::LoHiWord || current_type == BlockType::HiLoWord {
                // Skip the whole block
                let mut count = 0;
                while pc + count < data_len
                    && state.block_types.get(pc + count) == Some(&current_type)
                {
                    count += 1;
                }
                pc += count;
            } else {
                pc += 1;
            }
        }
    }

    // Generate labels
    let mut labels: BTreeMap<u16, Vec<crate::state::Label>> = BTreeMap::new();
    let mut cross_refs: BTreeMap<u16, Vec<u16>> = BTreeMap::new();

    // 0. Pre-populate labels from immediate_value_formats (LoHi/HiLo)
    for (addr, fmt) in &state.immediate_value_formats {
        let target = match fmt {
            crate::state::ImmediateFormat::LowByte(val) => Some(*val),
            crate::state::ImmediateFormat::HighByte(val) => Some(*val),
            _ => None,
        };

        if let Some(target_addr) = target {
            // Add to usage map to ensure it's tracked
            update_usage(&mut usage_map, target_addr, LabelType::Pointer, *addr);
        }
    }

    // 1. Process all used addresses
    for (addr, (_types_map, refs, first_type, first_zp, first_abs)) in usage_map {
        let mut addr_labels = Vec::new();

        // Populate cross_refs
        if !refs.is_empty() {
            cross_refs.insert(addr, refs);
        }
        if let Some(existing_vec) = state.labels.get(&addr) {
            for existing in existing_vec {
                let should_preserve = existing.kind == crate::state::LabelKind::User
                    || existing.kind == crate::state::LabelKind::System;

                if should_preserve {
                    addr_labels.push(crate::state::Label {
                        name: existing.name.clone(),
                        label_type: existing.label_type,
                        kind: existing.kind.clone(), // User or System
                    });
                }
            }
        }

        // If we have User labels for *all* needed contexts, we might skip Auto generation?
        // But for now, let's just generate Auto labels if needed.

        let is_ext = state.is_external(addr);

        if is_ext {
            // External: up to TWO labels based on first usage in each category
            let mut candidates = Vec::new();
            if let Some(t) = first_zp {
                candidates.push(t);
            }
            if let Some(t) = first_abs {
                let canonical =
                    if t == LabelType::Jump || t == LabelType::Subroutine || t == LabelType::Branch
                    {
                        LabelType::ExternalJump
                    } else {
                        t
                    };
                candidates.push(canonical);
            }

            for l_type in candidates {
                // Check if we already have this type in addr_labels (User defined or already added)
                if !addr_labels
                    .iter()
                    .any(|l| l.label_type == l_type || l.label_type == LabelType::UserDefined)
                    && !state.excluded_addresses.contains(&addr)
                {
                    addr_labels.push(crate::state::Label {
                        name: l_type.format_label(addr),
                        label_type: l_type,
                        kind: crate::state::LabelKind::Auto,
                    });
                }
            }
        } else {
            // Internal: Single canonical label (first_type or best?)
            // If User label exists, do we add Auto label? No, User label supersedes.
            if addr_labels.is_empty() {
                // Match ignored types logic from earlier or just rely on first found?
                // Logic: if usage contains Subroutine, promote to 's'.
                // Internal: Single canonical label (first_wins)
                let final_type = first_type;
                let name = final_type.format_label(addr);

                addr_labels.push(crate::state::Label {
                    name,
                    label_type: final_type,
                    kind: crate::state::LabelKind::Auto,
                });
            } else {
                // User label exists, nothing to do on labels.
            }
        }

        if !addr_labels.is_empty() {
            labels.insert(addr, addr_labels);
        }
    }

    // 2. Preserve strictly unused User labels
    for (addr, label_vec) in &state.labels {
        if !labels.contains_key(addr) {
            let mut preserved = Vec::new();
            for label in label_vec {
                if label.kind == crate::state::LabelKind::User {
                    preserved.push(crate::state::Label {
                        name: label.name.clone(),
                        label_type: label.label_type,
                        kind: crate::state::LabelKind::User,
                    });
                }
            }
            if !preserved.is_empty() {
                labels.insert(*addr, preserved);
            }
        }
    }

    // === Advanced analysis passes ===
    let mut hints: BTreeMap<u16, String> = BTreeMap::new();

    // Pass 1: Follow JMP (Indirect) through jump tables
    // Always run this pass since it also generates labels and cross-refs
    follow_indirect_jumps(state, &mut labels, &mut cross_refs, &mut hints);

    if state.settings.show_analysis_hints {
        // Pass 2: Detect common patterns (platform-aware)
        detect_patterns(state, &mut hints);

        // Pass 3: Self-modifying code detection
        detect_self_modifying_code(state, &mut hints);
    } else {
        // Discard hints from pass 1 when hints are disabled
        hints.clear();
    }

    AnalysisResult {
        labels,
        cross_refs,
        hints,
    }
}

fn analyze_instruction(
    _state: &AppState,
    opcode: &Opcode,
    operands: &[u8],
    address: u16,
    usage_map: &mut BTreeMap<u16, UsageData>,
) {
    match opcode.mode {
        AddressingMode::Implied | AddressingMode::Accumulator | AddressingMode::Immediate => {}
        AddressingMode::ZeroPage => {
            if !operands.is_empty() {
                let addr = operands[0] as u16;
                // "a: Zero Page Address"
                update_usage(usage_map, addr, LabelType::ZeroPageAbsoluteAddress, address);
            }
        }
        AddressingMode::ZeroPageX | AddressingMode::ZeroPageY => {
            if !operands.is_empty() {
                let addr = operands[0] as u16;
                // Indexed zero page often used for arrays/fields
                update_usage(usage_map, addr, LabelType::ZeroPageField, address);
            }
        }
        AddressingMode::Relative => {
            if !operands.is_empty() {
                let offset = operands[0] as i8;
                let target = address.wrapping_add(2).wrapping_add(offset as u16);
                // "b: ... branch opcodes"
                update_usage(usage_map, target, LabelType::Branch, address);
            }
        }
        AddressingMode::Absolute => {
            if operands.len() >= 2 {
                let target = (operands[1] as u16) << 8 | (operands[0] as u16);

                if opcode.mnemonic == "JSR" {
                    update_usage(usage_map, target, LabelType::Subroutine, address);
                } else if opcode.mnemonic == "JMP" {
                    update_usage(usage_map, target, LabelType::Jump, address);
                } else {
                    // "a: ... absolute address"
                    update_usage(usage_map, target, LabelType::AbsoluteAddress, address);
                }
            }
        }
        AddressingMode::AbsoluteX | AddressingMode::AbsoluteY => {
            if operands.len() >= 2 {
                let target = (operands[1] as u16) << 8 | (operands[0] as u16);
                // Indexed absolute is also "absolute address" usage
                update_usage(usage_map, target, LabelType::Field, address);
            }
        }
        AddressingMode::Indirect => {
            if operands.len() >= 2 {
                let pointer_addr = (operands[1] as u16) << 8 | (operands[0] as u16);
                // "p: if this is a pointer"
                // The address `pointer_addr` is BEING USED a pointer.
                update_usage(usage_map, pointer_addr, LabelType::Pointer, address);
            }
        }
        AddressingMode::IndirectX => {
            if !operands.is_empty() {
                let base = operands[0] as u16;
                // (base, X) -> points to a table of pointers in ZP? Or just ZP pointer?
                // It is "Indirect" X. The address `base` (and base+1) holds the address.
                // So `base` is a pointer.
                update_usage(usage_map, base, LabelType::ZeroPagePointer, address);
            }
        }
        AddressingMode::IndirectY => {
            if !operands.is_empty() {
                let base = operands[0] as u16;
                // (base), Y -> base is a ZP pointer.
                update_usage(usage_map, base, LabelType::ZeroPagePointer, address);
            }
        }
        AddressingMode::Unknown => {}
    }
}

fn update_usage(
    map: &mut BTreeMap<u16, UsageData>,
    addr: u16,
    priority: LabelType,
    from_addr: u16,
) {
    map.entry(addr)
        .and_modify(|(types, refs, _, first_zp, first_abs)| {
            *types.entry(priority).or_insert(0) += 1;
            refs.push(from_addr);
            if is_zp_style(priority) {
                if first_zp.is_none() {
                    *first_zp = Some(priority);
                }
            } else if first_abs.is_none() {
                *first_abs = Some(priority);
            }
        })
        .or_insert_with(|| {
            let mut types = BTreeMap::new();
            types.insert(priority, 1);
            let refs = vec![from_addr];
            let zp = if is_zp_style(priority) {
                Some(priority)
            } else {
                None
            };
            let abs = if !is_zp_style(priority) {
                Some(priority)
            } else {
                None
            };
            (types, refs, priority, zp, abs)
        });
}

// =============================================================================
// Pass 1: Follow JMP (Indirect) through jump tables
// =============================================================================

/// When we see `JMP ($xxxx)`, check if `$xxxx` points to an Address-typed block
/// inside our binary. If so, read the 16-bit pointer stored there and register
/// it as a jump target label + cross-reference.
fn follow_indirect_jumps(
    state: &AppState,
    labels: &mut BTreeMap<u16, Vec<crate::state::Label>>,
    cross_refs: &mut BTreeMap<u16, Vec<u16>>,
    hints: &mut BTreeMap<u16, String>,
) {
    let data = &state.raw_data;
    let origin = state.origin;
    let data_len = data.len();
    let end_addr = origin.wrapping_add(data_len as u16);

    let mut pc = 0;
    while pc < data_len {
        let current_type = state
            .block_types
            .get(pc)
            .copied()
            .unwrap_or(BlockType::Code);

        if current_type != BlockType::Code {
            pc += 1;
            continue;
        }

        let opcode_byte = data[pc];
        // JMP Indirect = 0x6C
        if opcode_byte != 0x6C {
            if let Some(opcode) = &state.disassembler.opcodes[opcode_byte as usize] {
                if opcode.illegal && !state.settings.use_illegal_opcodes {
                    pc += 1;
                } else if pc + opcode.size as usize <= data_len {
                    pc += opcode.size as usize;
                } else {
                    pc += 1;
                }
            } else {
                pc += 1;
            }
            continue;
        }

        // JMP ($xxxx) - 3 bytes: 6C lo hi
        if pc + 3 > data_len {
            pc += 1;
            continue;
        }

        let jmp_addr = origin.wrapping_add(pc as u16);
        let pointer_addr = (data[pc + 2] as u16) << 8 | (data[pc + 1] as u16);

        // Check if pointer_addr is inside our binary
        let is_internal = if origin < end_addr {
            pointer_addr >= origin && pointer_addr < end_addr
        } else {
            pointer_addr >= origin || pointer_addr < end_addr
        };

        if is_internal {
            let ptr_offset = pointer_addr.wrapping_sub(origin) as usize;

            // Check if the pointer location is an Address block
            let is_address_block = ptr_offset + 1 < data_len
                && state.block_types.get(ptr_offset) == Some(&BlockType::Address);

            if is_address_block {
                // Read the target address from the jump table
                let target = (data[ptr_offset + 1] as u16) << 8 | (data[ptr_offset] as u16);

                // Add label for the target
                let is_target_external = state.is_external(target);
                let label_type = if is_target_external {
                    LabelType::ExternalJump
                } else {
                    LabelType::Jump
                };

                let label_name = label_type.format_label(target);

                // Only add if no user label already exists at target
                let existing = labels.get(&target);
                let has_user_label = existing
                    .map(|v| v.iter().any(|l| l.kind == crate::state::LabelKind::User))
                    .unwrap_or(false);

                if !has_user_label && !state.excluded_addresses.contains(&target) {
                    labels.entry(target).or_default().push(crate::state::Label {
                        name: label_name,
                        label_type,
                        kind: crate::state::LabelKind::Auto,
                    });
                }

                // Add cross-reference
                cross_refs.entry(target).or_default().push(jmp_addr);

                // Add hint at the JMP instruction
                hints.insert(
                    jmp_addr,
                    format!(
                        "[Jump Table] Indirect JMP via ${:04X} -> target ${:04X}",
                        pointer_addr, target
                    ),
                );
            } else {
                // Pointer is in binary but not an Address block - still note it
                hints.insert(
                    jmp_addr,
                    format!(
                        "[Jump Table] JMP (${:04X}) - pointer in binary, consider marking as Address",
                        pointer_addr
                    ),
                );
            }
        }

        pc += 3;
    }
}

// =============================================================================
// Pass 2: Detect common C64/6502 patterns
// =============================================================================

/// Hardware addresses used for cross-instruction pattern detection.
const IRQ_VECTOR_LO: u16 = 0x0314;
const IRQ_VECTOR_HI: u16 = 0x0315;
const NMI_VECTOR_LO: u16 = 0x0318;
const NMI_VECTOR_HI: u16 = 0x0319;
const HW_IRQ_LO: u16 = 0xFFFE;
const HW_IRQ_HI: u16 = 0xFFFF;
const SID_BASE: u16 = 0xD400;
const SID_END: u16 = 0xD418;

/// Scans code for well-known patterns:
/// - HW IRQ setup (STA $FFFE/$FFFF) — universal for all 6502 platforms
/// - IRQ setup (STA $0314/$0315) — C64/C128 only
/// - NMI setup (STA $0318/$0319) — C64/C128 only
/// - Raster interrupt setup (STA $D012, STA $D01A) — C64/C128 only
/// - SID player init/play (JSR to routines that touch $D400-$D418) — C64/C128 only
fn detect_patterns(state: &AppState, hints: &mut BTreeMap<u16, String>) {
    let data = &state.raw_data;
    let origin = state.origin;
    let data_len = data.len();

    let is_c64_c128 =
        state.settings.platform == "Commodore 64" || state.settings.platform == "Commodore 128";

    // First pass: find SID-touching subroutines (C64/C128 only)
    let sid_routines = if is_c64_c128 {
        find_sid_routines(state)
    } else {
        BTreeMap::new()
    };

    let mut pc = 0;
    while pc < data_len {
        let current_type = state
            .block_types
            .get(pc)
            .copied()
            .unwrap_or(BlockType::Code);

        if current_type != BlockType::Code {
            pc += 1;
            continue;
        }

        let opcode_byte = data[pc];
        let Some(opcode) = &state.disassembler.opcodes[opcode_byte as usize] else {
            pc += 1;
            continue;
        };

        if opcode.illegal && !state.settings.use_illegal_opcodes {
            pc += 1;
            continue;
        }

        if pc + opcode.size as usize > data_len {
            pc += 1;
            continue;
        }

        let addr = origin.wrapping_add(pc as u16);

        // Check for JSR to SID-touching routines (C64/C128 only)
        // This is a pattern-level insight: identifies subroutines that are likely
        // SID init/play routines based on aggregated register access patterns.
        // System comments only annotate individual register addresses, not routine roles.
        if is_c64_c128 && opcode.mnemonic == "JSR" && opcode.size == 3 {
            let target = (data[pc + 2] as u16) << 8 | (data[pc + 1] as u16);
            if let Some(role) = sid_routines.get(&target) {
                hints
                    .entry(addr)
                    .or_insert_with(|| format!("[SID Player] JSR to {} routine", role));
            }
        }

        // Check for SEI (0x78) which often precedes IRQ setup.
        // This is a cross-instruction pattern: SEI followed by vector writes nearby.
        // System comments can't detect this because they only annotate individual addresses.
        if opcode_byte == 0x78 && has_irq_setup_nearby(state, pc, is_c64_c128) {
            hints
                .entry(addr)
                .or_insert_with(|| "[IRQ Setup] SEI before interrupt vector change".to_string());
        }

        pc += opcode.size as usize;
    }
}

/// Look for SID-touching subroutines. A subroutine that writes to $D400-$D418
/// is likely a SID init or play routine.
///
/// Returns a map from subroutine address to a role description.
fn find_sid_routines(state: &AppState) -> BTreeMap<u16, String> {
    let data = &state.raw_data;
    let origin = state.origin;
    let data_len = data.len();
    let mut result = BTreeMap::new();

    // Find all JSR targets (subroutine entry points within our binary)
    let mut jsr_targets: Vec<u16> = Vec::new();
    let mut pc = 0;
    while pc < data_len {
        let current_type = state
            .block_types
            .get(pc)
            .copied()
            .unwrap_or(BlockType::Code);
        if current_type == BlockType::Code {
            if let Some(opcode) = &state.disassembler.opcodes[data[pc] as usize] {
                if !opcode.illegal || state.settings.use_illegal_opcodes {
                    if opcode.mnemonic == "JSR" && opcode.size == 3 && pc + 3 <= data_len {
                        let target = (data[pc + 2] as u16) << 8 | (data[pc + 1] as u16);
                        if !state.is_external(target) {
                            jsr_targets.push(target);
                        }
                    }
                    pc += opcode.size as usize;
                } else {
                    pc += 1;
                }
            } else {
                pc += 1;
            }
        } else {
            pc += 1;
        }
    }

    jsr_targets.sort_unstable();
    jsr_targets.dedup();

    // For each subroutine, scan its body for SID register writes
    for &entry in &jsr_targets {
        let entry_offset = entry.wrapping_sub(origin) as usize;
        if entry_offset >= data_len {
            continue;
        }

        let mut sid_write_count = 0;
        let mut scan_pc = entry_offset;
        let scan_limit = (entry_offset + 256).min(data_len); // Scan up to 256 bytes

        while scan_pc < scan_limit {
            let bt = state
                .block_types
                .get(scan_pc)
                .copied()
                .unwrap_or(BlockType::Code);
            if bt != BlockType::Code {
                break;
            }

            let ob = data[scan_pc];
            if let Some(op) = &state.disassembler.opcodes[ob as usize] {
                if op.mnemonic == "RTS" || op.mnemonic == "RTI" {
                    break; // End of subroutine
                }
                if matches!(op.mnemonic, "STA" | "STX" | "STY")
                    && op.mode == AddressingMode::Absolute
                    && op.size == 3
                    && scan_pc + 3 <= data_len
                {
                    let t = (data[scan_pc + 2] as u16) << 8 | (data[scan_pc + 1] as u16);
                    if (SID_BASE..=SID_END).contains(&t) {
                        sid_write_count += 1;
                    }
                }
                if scan_pc + op.size as usize <= data_len {
                    scan_pc += op.size as usize;
                } else {
                    break;
                }
            } else {
                break;
            }
        }

        if sid_write_count >= 3 {
            // Heuristic: many SID writes = likely init or play
            // init typically writes more registers (full reset)
            let role = if sid_write_count >= 10 {
                "init"
            } else {
                "play"
            };
            result.insert(entry, role.to_string());
        }
    }

    result
}

/// Check if there are IRQ vector writes within ~20 instructions after the given PC.
/// When `is_c64_c128` is false, only checks universal HW vectors ($FFFE/$FFFF).
fn has_irq_setup_nearby(state: &AppState, start_pc: usize, is_c64_c128: bool) -> bool {
    let data = &state.raw_data;
    let data_len = data.len();
    let mut pc = start_pc;
    let limit = (start_pc + 40).min(data_len); // ~20 instructions max

    while pc < limit {
        let bt = state
            .block_types
            .get(pc)
            .copied()
            .unwrap_or(BlockType::Code);
        if bt != BlockType::Code {
            break;
        }
        let ob = data[pc];
        if let Some(op) = &state.disassembler.opcodes[ob as usize] {
            if matches!(op.mnemonic, "STA" | "STX" | "STY")
                && op.mode == AddressingMode::Absolute
                && op.size == 3
                && pc + 3 <= data_len
            {
                let t = (data[pc + 2] as u16) << 8 | (data[pc + 1] as u16);
                // Universal: HW IRQ vectors
                if t == HW_IRQ_LO || t == HW_IRQ_HI {
                    return true;
                }
                // C64/C128-specific: Kernal IRQ/NMI vectors
                if is_c64_c128
                    && (t == IRQ_VECTOR_LO
                        || t == IRQ_VECTOR_HI
                        || t == NMI_VECTOR_LO
                        || t == NMI_VECTOR_HI)
                {
                    return true;
                }
            }
            if pc + op.size as usize <= data_len {
                pc += op.size as usize;
            } else {
                break;
            }
        } else {
            break;
        }
    }
    false
}

// =============================================================================
// Pass 3: Self-modifying code detection
// =============================================================================

/// Detects STA/STX/STY instructions whose target address falls inside a region
/// marked as Code. This is a strong indicator of self-modifying code (SMC).
fn detect_self_modifying_code(state: &AppState, hints: &mut BTreeMap<u16, String>) {
    let data = &state.raw_data;
    let origin = state.origin;
    let data_len = data.len();
    let end_addr = origin.wrapping_add(data_len as u16);

    let mut pc = 0;
    while pc < data_len {
        let current_type = state
            .block_types
            .get(pc)
            .copied()
            .unwrap_or(BlockType::Code);

        if current_type != BlockType::Code {
            pc += 1;
            continue;
        }

        let opcode_byte = data[pc];
        let Some(opcode) = &state.disassembler.opcodes[opcode_byte as usize] else {
            pc += 1;
            continue;
        };

        if opcode.illegal && !state.settings.use_illegal_opcodes {
            pc += 1;
            continue;
        }

        if pc + opcode.size as usize > data_len {
            pc += 1;
            continue;
        }

        // Only check store instructions with absolute addressing
        if matches!(opcode.mnemonic, "STA" | "STX" | "STY")
            && matches!(
                opcode.mode,
                AddressingMode::Absolute | AddressingMode::AbsoluteX | AddressingMode::AbsoluteY
            )
            && opcode.size == 3
        {
            let target = (data[pc + 2] as u16) << 8 | (data[pc + 1] as u16);

            // Check if target is inside our binary
            let is_internal = if origin < end_addr {
                target >= origin && target < end_addr
            } else {
                target >= origin || target < end_addr
            };

            if is_internal {
                let target_offset = target.wrapping_sub(origin) as usize;
                if target_offset < data_len {
                    let target_block = state
                        .block_types
                        .get(target_offset)
                        .copied()
                        .unwrap_or(BlockType::Code);

                    if target_block == BlockType::Code {
                        let addr = origin.wrapping_add(pc as u16);
                        hints.entry(addr).or_insert_with(|| {
                            format!(
                                "[SMC] {} writes to code region at ${:04X}",
                                opcode.mnemonic, target
                            )
                        });
                    }
                }
            }
        }

        pc += opcode.size as usize;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::state::{AppState, BlockType};

    #[test]
    fn test_analyze_simple() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        // JMP $1005 (4C 05 10)
        // JSR $1008 (20 08 10)
        // NOP (EA)
        // LDA $1000 (AD 00 10)
        let data = vec![0x4C, 0x05, 0x10, 0x20, 0x08, 0x10, 0xEA, 0xAD, 0x00, 0x10];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; state.raw_data.len()];

        let result = analyze(&state);
        let labels = result.labels;

        // $1005 is JMP target -> j1005
        assert_eq!(
            labels
                .get(&0x1005)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("j1005")
        );
        // $1008 is JSR target -> s1008
        assert_eq!(
            labels
                .get(&0x1008)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("s1008")
        );
        // $1000 is accessed via LDA (Absolute) -> a1000
        assert_eq!(
            labels
                .get(&0x1000)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("a1000")
        );
    }

    #[test]
    fn test_priority_override() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        // JMP $2000 (4C 00 20) -> usage j2000 -> external e2000
        // JSR $2000 (20 00 20) -> usage s2000 -> external e2000
        // Since both are external, and both allow external prefix, result is e2000.
        let data = vec![0x4C, 0x00, 0x20, 0x20, 0x00, 0x20];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; state.raw_data.len()];

        let result = analyze(&state);
        let labels = result.labels;
        // Changed expectations: explicit ExternalJump type logic
        assert_eq!(
            labels
                .get(&0x2000)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("e2000")
        );
    }

    #[test]
    fn test_access_types_zp() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        // LDA $10 (ZP) -> A5 10
        let data = vec![0xA5, 0x10];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; 2];

        let result = analyze(&state);
        let labels = result.labels;
        // ZP access -> ZeroPage Priority -> a10
        assert_eq!(
            labels
                .get(&0x0010)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("a10")
        );
    }

    #[test]
    fn test_zp_field() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        // LDA $50, X (B5 50) -> Field usage, ZP address
        let data = vec![0xB5, 0x50];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; 2];

        let result = analyze(&state);
        let labels = result.labels;
        // Field usage in ZP -> f50
        assert_eq!(
            labels
                .get(&0x0050)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("f50")
        );
    }

    #[test]
    fn test_external() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        // JMP $0010 (External, out of range [1000..1003])
        let data = vec![0x4C, 0x10, 0x00];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; 3];

        let result = analyze(&state);
        let labels = result.labels;
        // External Jump -> e0010
        // (Note: Jumps use 4 digits usually, unless we want e10?
        // User said "Only for external jumps... not for data".
        // Standard external handling logic still uses 4 digits for 'e' prefix in my code:
        // format!("{}{:04X}", prefix, addr)
        // ZeroPage special formatting is only for ZP priority or ZP Field.
        // Jump is Jump priority. So e0010 is correct.)
        assert_eq!(
            labels
                .get(&0x0010)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("e0010")
        );
    }

    #[test]
    fn test_data_word_vs_address() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        // $1000: DataWord ($2000) -> 00 20
        // $1002: Address ($1000) -> 00 10 (Internal)
        let data = vec![0x00, 0x20, 0x00, 0x10];
        state.raw_data = data;
        state.block_types = vec![
            BlockType::DataWord,
            BlockType::DataWord,
            BlockType::Address,
            BlockType::Address,
        ];

        // Re-analyze reference counts and labels
        let result = analyze(&state);
        let labels = result.labels;

        // DataWord at $1000 should NOT generate label for ITSELF ($1000)
        // BUT $1002 IS Reference to $1000. So $1000 SHOULD have a label now.
        // Address type usage at 1002 -> Absolute priority -> a1000
        assert_eq!(
            labels
                .get(&0x1000)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("a1000")
        );

        // And content of DataWord ($2000) should still be None (assuming it's external/ignored)
        assert_eq!(labels.get(&0x2000), None);
    }
    #[test]
    fn test_branch_offsets() {
        let mut state = AppState::new();
        state.origin = 0x1000;

        // BNE instructions
        let data = vec![0xD0, 0x00, 0xD0, 0x7F, 0xD0, 0x80, 0xD0, 0xFF, 0xD0, 0xFE];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; state.raw_data.len()];

        let result = analyze(&state);
        let labels = result.labels;

        // Case 1: $1000 -> jump to $1002. Usage: b1002 (Internal)
        assert_eq!(
            labels
                .get(&0x1002)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("b1002")
        );

        // Case 2: $1002 -> jump to $1083. Usage: e1083 (External logic applies to Branch too now)
        assert_eq!(
            labels
                .get(&0x1083)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("e1083")
        );

        // Case 3: $1004 -> jump to $0F86. Usage: e0F86
        assert_eq!(
            labels
                .get(&0x0F86)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("e0F86")
        );

        // Case 4: $1006 -> jump to $1007. Usage: b1007
        assert_eq!(
            labels
                .get(&0x1007)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("b1007")
        );

        // Case 5: $1008 -> jump to $1008. Usage: b1008
        assert_eq!(
            labels
                .get(&0x1008)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("b1008")
        );
    }

    #[test]
    fn test_new_pointer_field_types() {
        let mut state = AppState::new();
        state.origin = 0x1000;

        // Indirect JMP (JMP ($1000)) -> 6C 00 10
        // LDA ($10, X) -> A1 10
        // LDA ($10), Y -> B1 10
        // LDA $1000, X -> BD 00 10
        // LDA $1000, Y -> B9 00 10
        // LDA $10, X -> B5 10
        let data = vec![
            0x6C, 0x00, 0x10, // JMP ($1000) -> Indirect -> p1000
            0xA1, 0x10, // LDA ($10, X) -> Indirect X -> p10
            0xB1, 0x20, // LDA ($20), Y -> Indirect Y -> p20
            0xBD, 0x50, 0x10, // LDA $1050, X -> Absolute X -> f1050
            0xB9, 0x60, 0x10, // LDA $1060, Y -> Absolute Y -> f1060
            0xB5, 0x30, // LDA $30, X -> ZeroPage X -> f30
        ];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; state.raw_data.len()];

        let result = analyze(&state);
        let labels = result.labels;

        // Indirect JMP -> p1000
        assert_eq!(
            labels
                .get(&0x1000)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("p1000")
        );

        // Indirect X -> p10
        assert_eq!(
            labels
                .get(&0x0010)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("p10")
        );

        // Indirect Y -> p20
        assert_eq!(
            labels
                .get(&0x0020)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("p20")
        );

        // Absolute X -> f1050
        assert_eq!(
            labels
                .get(&0x1050)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("f1050")
        );

        // Absolute Y -> f1060
        assert_eq!(
            labels
                .get(&0x1060)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("f1060")
        );

        // ZeroPage X -> f30
        assert_eq!(
            labels
                .get(&0x0030)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("f30")
        );
    }

    #[test]
    fn test_internal_label_priority() {
        let mut state = AppState::new();
        state.origin = 0x1000;

        // Scenario: Internal address $1005 accessed via:
        // 1. Branch (BNE $1005) -> D0 03 (assuming PC is 1000 + 2 = 1002. 1002+3=1005)
        // 2. Jump (JMP $1005) -> 4C 05 10
        // 3. Subroutine (JSR $1005) -> 20 05 10
        //
        // Priority should be Subroutine ("s") > Jump ("j") > Branch ("b").
        // Result should be b1005 (First usage wins).

        // $1000: BNE $1005 (D0 03)
        // $1002: JMP $1005 (4C 05 10)
        // $1005: JSR $1005 (20 05 10) (recursive call just for usage)

        let data = vec![
            0xD0, 0x03, // 1000: BNE +3 -> 1005
            0x4C, 0x05, 0x10, // 1002: JMP $1005
            0x20, 0x05, 0x10, // 1005: JSR $1005
        ];

        state.raw_data = data;
        state.block_types = vec![BlockType::Code; state.raw_data.len()];

        let result = analyze(&state);
        let labels = result.labels;

        // Check 1005
        // Expect 'b' because it was referenced by Branch FIRST.
        assert_eq!(
            labels
                .get(&0x1005)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("b1005")
        );

        // Also verify usage map contains all types?
        // We can't easily check usage map here as it's internal to analyze, but we check the result name.
    }

    #[test]
    fn test_internal_label_first_wins() {
        let mut state = AppState::new();
        state.origin = 0x1000;

        // BEQ $1005 (D0 03) -> Branch type
        // JMP $1005 (4C 05 10) -> Jump type
        // Internal address $1005.
        // BEQ appears first, so it should define the label as 'b1005'.
        let data = vec![
            0xD0, 0x03, // 1000: BEQ $1005
            0x4C, 0x05, 0x10, // 1002: JMP $1005
            0xEA, // 1005: NOP (make it valid internal address)
        ];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; state.raw_data.len()];

        let result = analyze(&state);
        let labels = result.labels;

        // Expectation: b1005 (Branch) because it was first.
        // (Before fix, this would likely be j1005 because Jump > Branch in priority)
        assert_eq!(
            labels
                .get(&0x1005)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("b1005")
        );
    }

    #[test]
    fn test_external_label_still_prioritized() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        let len = 100;
        state.raw_data = vec![0; len]; // Just dummy data placeholder
        state.block_types = vec![BlockType::Code; len];

        // We construct a specific scenario:
        // LDA $E000 (AD 00 E0) -> Absolute usage
        // JMP $E000 (4C 00 E0) -> Jump usage
        // Address $E000 is external.
        // LDA appears first.
        // WE apply "first wins" to external.
        // LDA appears first, so we expect aE000.
        // (Previously it was eE000 because ExternalJump > Absolute priority).

        let data = [
            0xAD, 0x00, 0xE0, // 1000: LDA $E000
            0x4C, 0x00, 0xE0, // 1003: JMP $E000
        ];
        // replace beginning of dummy data
        for (i, b) in data.iter().enumerate() {
            state.raw_data[i] = *b;
        }

        let result = analyze(&state);
        let labels = result.labels;

        // Result should be aE000
        assert_eq!(
            labels
                .get(&0xE000)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("aE000")
        );
    }

    #[test]
    fn test_excluded_addresses() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        // JMP $E500 (4C 00 E5) -> normally eE500
        // But we exclude E500.
        let data = vec![0x4C, 0x00, 0xE5];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; 3];

        state.excluded_addresses.insert(0xE500);

        let result = analyze(&state);
        let labels = result.labels;

        // Should be None
        assert_eq!(labels.get(&0xE500), None);

        // Verification: if we remove it from excludes, it should appear
        state.excluded_addresses.remove(&0xE500);
        let result = analyze(&state);
        let labels = result.labels;
        assert_eq!(
            labels
                .get(&0xE500)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("eE500")
        );
    }

    #[test]
    fn test_external_branch() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        // BNE target that is out of range
        // Origin 1000. Data len 2. Range 1000..1002.
        // 1000: D0 7F (BNE +$7F) -> Target: 1002 + 7F = 1081.
        // 1081 is >= origin+len (1002), so it is external.

        let data = vec![0xD0, 0x7F];
        state.raw_data = data;
        state.block_types = vec![BlockType::Code; 2];

        let result = analyze(&state);
        let labels = result.labels;
        // Should be e1081 (ExternalJump type), NOT b1081 (Branch type).
        assert_eq!(
            labels
                .get(&0x1081)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("e1081")
        );
    }

    #[test]
    fn test_absolute_addressing_on_zp_formatting() {
        let mut app_state = AppState::new();
        // 8D A0 00 -> STA $00A0 (Answer to Life, Universe, and Everything)
        // Absolute addressing mode targeting a ZP address.
        app_state.raw_data = vec![0x8D, 0xA0, 0x00];
        app_state.origin = 0x1000;
        app_state.block_types = vec![BlockType::Code; 3];
        // Fill opcodes
        app_state.disassembler.opcodes[0x8D] = Some(crate::cpu::Opcode {
            mnemonic: "STA",
            mode: AddressingMode::Absolute,
            size: 3,
            cycles: 4,
            description: "Store Accumulator",
            illegal: false,
        });

        let result = analyze(&app_state);
        let labels_map = result.labels;
        let labels = labels_map
            .get(&0x00A0)
            .expect("Should have a label at $00A0");
        let label = labels.first().expect("Label vector should not be empty");

        // User wants "a00A0" because it was forced absolute / accessed absolutely.
        // Current bug: "aA0"
        assert_eq!(label.name, "a00A0");
    }

    #[test]
    fn test_field_formatting_on_zp() {
        let mut app_state = AppState::new();
        // 9D A0 00 -> STA $00A0, X (Absolute, X)
        // Absolute indexed addressing mode targeting a ZP address.
        // Should generate "f00A0" (Field, 4 digits) NOT "fA0" (ZP Field, 2 digits).
        app_state.raw_data = vec![0x9D, 0xA0, 0x00];
        app_state.origin = 0x1000;
        app_state.block_types = vec![BlockType::Code; 3];
        // Fill opcodes
        app_state.disassembler.opcodes[0x9D] = Some(crate::cpu::Opcode {
            mnemonic: "STA",
            mode: AddressingMode::AbsoluteX,
            size: 3,
            cycles: 5,
            description: "Store Accumulator",
            illegal: false,
        });

        let result = analyze(&app_state);
        let labels_map = result.labels;
        let labels = labels_map
            .get(&0x00A0)
            .expect("Should have a label at $00A0");
        let label = labels.first().expect("Label vector should not be empty");

        assert_eq!(
            label.label_type,
            LabelType::Field,
            "Should be Field type (Absolute Indexed)"
        );
        assert_eq!(label.name, "f00A0");
    }

    #[test]
    fn test_dual_pointer_labels() {
        let mut app_state = AppState::new();
        // Scenario: Two instructions using address $00FB.
        // 1. Indirect JMP ($00FB) -> JMP ($00FB) -> 6C FB 00
        //    - This is AddressingMode::Indirect.
        //    - It targets $FB but the operands are $FB $00.
        //    - It should generate LabelType::Pointer ("p")
        //    - The USER wants this to be "p00FB" (4 digits) because it's used as a 16-bit pointer.

        // 2. LDA ($FB), Y -> B1 FB
        //    - This is AddressingMode::IndirectY.
        //    - It targets $FB.
        //    - It should generate LabelType::ZeroPagePointer ("p")
        //    - The USER wants this to be "pFB" (2 digits).

        app_state.origin = 0x1000;
        let data = vec![
            0x6C, 0xFB, 0x00, // JMP ($00FB)
            0xB1, 0xFB, // LDA ($FB), Y
        ];
        app_state.raw_data = data;
        app_state.block_types = vec![BlockType::Code; 5];

        // Mock Opcode info
        // JMP Indirect
        app_state.disassembler.opcodes[0x6C] = Some(crate::cpu::Opcode {
            mnemonic: "JMP",
            mode: AddressingMode::Indirect,
            size: 3,
            cycles: 5,
            description: "Jump Indirect",
            illegal: false,
        });
        // LDA IndirectY
        app_state.disassembler.opcodes[0xB1] = Some(crate::cpu::Opcode {
            mnemonic: "LDA",
            mode: AddressingMode::IndirectY,
            size: 2,
            cycles: 5,
            description: "Load Accumulator Indirect Y",
            illegal: false,
        });

        let result = analyze(&app_state);
        let labels_map = result.labels;
        let label_vec = labels_map
            .get(&0x00FB)
            .expect("Should have labels at $00FB");
        // We expect BOTH labels now: "p00FB" (Absolute style) and "pFB" (ZeroPage style).

        let has_p00fb = label_vec
            .iter()
            .any(|l| l.name == "p00FB" && l.label_type == LabelType::Pointer);
        let has_pfb = label_vec
            .iter()
            .any(|l| l.name == "pFB" && l.label_type == LabelType::ZeroPagePointer);

        assert!(has_p00fb, "Should have p00FB label for JMP ($00FB)");
        assert!(has_pfb, "Should have pFB label for LDA ($FB),Y");
        assert_eq!(label_vec.len(), 2, "Should have TWO labels for $00FB");
    }

    #[test]
    fn test_dual_external_zp_abs_labels() {
        let mut app_state = AppState::new();
        // 1000: LDA $A0    -> A5 A0    (ZP Addressing Mode) -> ZeroPageAbsoluteAddress
        // 1002: LDA $00A0  -> AD A0 00 (Absolute Addressing Mode) -> AbsoluteAddress
        // Address $A0 is external (origin is 1000).

        app_state.origin = 0x1000;
        app_state.raw_data = vec![
            0xA5, 0xA0, // LDA $A0
            0xAD, 0xA0, 0x00, // LDA $00A0
        ];
        app_state.block_types = vec![BlockType::Code; 5];

        // LDA ZP
        app_state.disassembler.opcodes[0xA5] = Some(crate::cpu::Opcode {
            mnemonic: "LDA",
            mode: AddressingMode::ZeroPage,
            size: 2,
            cycles: 3,
            description: "Load Accumulator ZP",
            illegal: false,
        });
        // LDA Abs
        app_state.disassembler.opcodes[0xAD] = Some(crate::cpu::Opcode {
            mnemonic: "LDA",
            mode: AddressingMode::Absolute,
            size: 3,
            cycles: 4,
            description: "Load Accumulator Abs",
            illegal: false,
        });

        let result = analyze(&app_state);
        let labels_map = result.labels;
        let label_vec = labels_map
            .get(&0x00A0)
            .expect("Should have labels at $00A0");

        let names: Vec<_> = label_vec.iter().map(|l| l.name.as_str()).collect();
        assert!(names.contains(&"aA0"), "Should have aA0 label");
        assert!(names.contains(&"a00A0"), "Should have a00A0 label");
        assert_eq!(
            label_vec.len(),
            2,
            "Should have TWO labels for external $00A0"
        );
    }
    #[test]
    fn test_hilo_analysis() {
        let mut state = AppState::new();
        state.origin = 0x1000;
        // HiLo Block: 4 bytes
        // Hi: $C0, $D0
        // Lo: $00, $01
        // Addresses: $C000, $D001 (External)
        let data = vec![0xC0, 0xD0, 0x00, 0x01];
        state.raw_data = data;
        state.block_types = vec![BlockType::HiLoAddress; 4];

        let result = analyze(&state);
        let labels = result.labels;

        // Check $C000 -> aC000 (AbsoluteAddress usage from HiLo)
        assert_eq!(
            labels
                .get(&0xC000)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("aC000")
        );

        // Check $D001 -> aD001
        assert_eq!(
            labels
                .get(&0xD001)
                .and_then(|v| v.first())
                .map(|l| l.name.as_str()),
            Some("aD001")
        );
    }
}
