from __future__ import annotations

import argparse
import json
import re
from collections.abc import Iterable, Mapping
from pathlib import Path
from typing import Any

from .formatting import h16, label_for
from .serial_semantics import DIRECT_TABLE_TO_LOGICAL_OFFSET, LOGICAL_TABLES


JsonObject = dict[str, Any]


TABLES: tuple[JsonObject, ...] = (
    {
        "name": "primary_value_table_candidate",
        "logical_base_address": 0xE000,
        "logical_range_end": 0xE3FF,
        "negative_offset": 0x2000,
        "element_candidate": "word_value",
        "direct_addresses": [0xF900],
        "direct_range_end": 0xF91F,
    },
    {
        "name": "secondary_value_table_candidate",
        "logical_base_address": 0xE400,
        "logical_range_end": 0xE7FF,
        "negative_offset": 0x1C00,
        "element_candidate": "word_value",
        "direct_addresses": [0xF940],
        "direct_range_end": 0xF95F,
    },
    {
        "name": "current_value_table_candidate",
        "logical_base_address": 0xE800,
        "logical_range_end": 0xEBFF,
        "negative_offset": 0x1800,
        "element_candidate": "word_value",
        "direct_addresses": [0xF920],
        "direct_range_end": 0xF93F,
    },
    {
        "name": "flag_table_candidate",
        "logical_base_address": 0xEC00,
        "logical_range_end": 0xEFFF,
        "negative_offset": 0x1400,
        "element_candidate": "bit_flags",
        "direct_addresses": [0xF980],
        "direct_range_end": 0xF99F,
    },
)

_TABLE_BY_NEGATIVE_OFFSET = {int(item["negative_offset"]): item for item in TABLES}
_TABLE_BY_DIRECT_ADDRESS = {
    address: item
    for item in TABLES
    for address in item["direct_addresses"]
}
LCD_CORRELATION_TERMS = (
    "CONNECT",
    "CONNECT: OK",
    "CONNECT: NOT ACT",
    "NOT ACT",
    "COMM LINK",
    "COMPLETED",
)


def load_table_xref_input(path: Path) -> JsonObject:
    with path.open("r", encoding="utf-8") as handle:
        payload = json.load(handle)
    if not isinstance(payload, dict) or "instructions" not in payload:
        raise ValueError(f"{path} does not look like h8536_decompiler JSON output")
    return payload


def analyze_table_xrefs(payload: Mapping[str, Any]) -> JsonObject:
    instructions = _instruction_sequence(payload.get("instructions"))
    functions = _function_ranges(payload)
    semantic_accesses = _semantic_access_locations(payload)
    accesses_by_table = {str(table["name"]): [] for table in TABLES}

    for index, ins in enumerate(instructions):
        for access in _logical_operand_accesses(instructions, index, functions, semantic_accesses):
            accesses_by_table.setdefault(str(access["table"]), []).append(access)
        for access in _direct_address_accesses(ins, functions, semantic_accesses):
            accesses_by_table.setdefault(str(access["table"]), []).append(access)

    tables: list[JsonObject] = []
    for table in TABLES:
        name = str(table["name"])
        accesses = sorted(accesses_by_table.get(name, []), key=lambda item: int(item["instruction_address"]))
        reads = sum(1 for access in accesses if access["access"] == "read")
        writes = sum(1 for access in accesses if access["access"] == "write")
        read_write = sum(1 for access in accesses if access["access"] == "read_write_candidate")
        dynamic = sum(1 for access in accesses if access.get("index") == "dynamic")
        static_offsets = sorted(
            {
                int(access["offset"])
                for access in accesses
                if isinstance(access.get("offset"), int)
            }
        )
        tables.append(
            {
                "name": name,
                "logical_base_address": table["logical_base_address"],
                "logical_base_address_hex": h16(int(table["logical_base_address"])),
                "logical_range_end": table["logical_range_end"],
                "logical_range_end_hex": h16(int(table["logical_range_end"])),
                "negative_offset": table["negative_offset"],
                "negative_offset_hex": h16(int(table["negative_offset"])),
                "element_candidate": table["element_candidate"],
                "direct_addresses": table["direct_addresses"],
                "direct_addresses_hex": [h16(int(address)) for address in table["direct_addresses"]],
                "direct_range_end": table["direct_range_end"],
                "direct_range_end_hex": h16(int(table["direct_range_end"])),
                "access_count": len(accesses),
                "read_count": reads,
                "write_count": writes,
                "read_write_candidate_count": read_write,
                "dynamic_index_count": dynamic,
                "static_offsets": static_offsets,
                "static_offsets_hex": [h16(offset) for offset in static_offsets],
                "functions": _summarize_functions(accesses),
                "accesses": accesses,
            }
        )

    return {
        "kind": "table_xrefs",
        "tables": tables,
        "summary": {
            "table_count": len(tables),
            "access_count": sum(int(table["access_count"]) for table in tables),
            "dynamic_index_count": sum(int(table["dynamic_index_count"]) for table in tables),
            "source_instruction_count": len(instructions),
        },
        "lcd_correlation": _lcd_correlation_hints(payload),
        "caveat": (
            "Static offsets are emitted only when an index register value can be derived from "
            "nearby immediate loads in the current JSON. Other indexed accesses are dynamic."
        ),
    }


def generate_table_xref_report(payload: Mapping[str, Any], *, source_name: str = "") -> str:
    analysis = analyze_table_xrefs(payload)
    lines: list[str] = []
    suffix = f" for {source_name}" if source_name else ""
    lines.append(f"Table/Index Cross-Reference Report{suffix}")
    lines.append("=" * len(lines[0]))
    lines.append("")
    lines.append(str(analysis["caveat"]))
    lines.append("")
    lines.extend(_format_lcd_correlation_lines(analysis.get("lcd_correlation")))
    if lines[-1] != "":
        lines.append("")

    for table in analysis["tables"]:
        name = str(table["name"])
        direct = ", ".join(str(item) for item in table["direct_addresses_hex"])
        lines.append(
            f"{name} {table['logical_base_address_hex']}-{table['logical_range_end_hex']} "
            f"(negative {table['negative_offset_hex']}; direct {direct}-{table['direct_range_end_hex']})"
        )
        lines.append(
            f"  accesses={table['access_count']} reads={table['read_count']} "
            f"writes={table['write_count']} dynamic={table['dynamic_index_count']}"
        )
        offsets = table.get("static_offsets_hex") or []
        if offsets:
            lines.append(f"  static offsets: {', '.join(str(item) for item in offsets[:16])}")
        function_summaries = table.get("functions") or []
        if function_summaries:
            joined = ", ".join(
                f"{item['label']}:{item['access_count']}" for item in function_summaries[:12]
            )
            lines.append(f"  functions: {joined}")
        accesses = table.get("accesses")
        if isinstance(accesses, list) and accesses:
            for access in accesses[:80]:
                lines.append(f"  - {_format_access_line(access)}")
            if len(accesses) > 80:
                lines.append(f"  - ... {len(accesses) - 80} more accesses omitted")
        else:
            lines.append("  no references found in current JSON")
        lines.append("")
    return "\n".join(lines).rstrip() + "\n"


def _format_lcd_correlation_lines(value: Any) -> list[str]:
    if not isinstance(value, Mapping):
        return []
    lines = ["LCD correlation hints"]
    for hit in value.get("term_hits", []):
        if not isinstance(hit, Mapping):
            continue
        term = hit.get("term")
        count = int(hit.get("hit_count", 0))
        if count:
            samples = ", ".join(
                f"{item['address_hex']} {item['trimmed']!r}"
                for item in hit.get("hits", [])[:4]
                if isinstance(item, Mapping)
            )
            lines.append(f"  term {term!r}: {count} candidate hit(s): {samples}")
        else:
            lines.append(f"  term {term!r}: no LCD/text candidate hits in current decompile")
    builders = value.get("display_builder_targets", [])
    if isinstance(builders, list) and builders:
        parts = [
            f"{item['target_hex']}:{item['xref_count']}"
            for item in builders[:8]
            if isinstance(item, Mapping)
        ]
        lines.append(f"  display builder xrefs: {', '.join(parts)}")
    routines = value.get("lcd_driver_routines", [])
    if isinstance(routines, list) and routines:
        parts = [
            f"{item['start_hex']} {item['role_hint']}"
            for item in routines[:4]
            if isinstance(item, Mapping)
        ]
        lines.append(f"  LCD driver routines: {', '.join(parts)}")
    lines.append(
        "  caveat: LCD strings can be builder/script output; absence of a literal term does not disprove runtime composition."
    )
    return lines


def write_table_xrefs(input_path: Path, output_path: Path, *, as_json: bool = False) -> None:
    payload = load_table_xref_input(input_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    if as_json:
        analysis = analyze_table_xrefs(payload)
        analysis["source"] = str(input_path)
        output_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8")
    else:
        output_path.write_text(generate_table_xref_report(payload, source_name=str(input_path)), encoding="utf-8")


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="Generate table/index cross-references for candidate serial protocol data tables.",
    )
    parser.add_argument(
        "input",
        nargs="?",
        type=Path,
        default=Path("build/rom_decompiled.json"),
        help="structured JSON emitted by h8536_decompiler.py",
    )
    parser.add_argument(
        "--out",
        type=Path,
        default=Path("build/rom_table_xrefs.txt"),
        help="table cross-reference report output path",
    )
    parser.add_argument("--json", action="store_true", help="write structured JSON instead of text")
    args = parser.parse_args(argv)

    write_table_xrefs(args.input, args.out, as_json=args.json)
    print(f"wrote {args.out}")
    return 0


def _logical_operand_accesses(
    instructions: list[JsonObject],
    index: int,
    functions: list[JsonObject],
    semantic_accesses: Mapping[int, list[JsonObject]],
) -> list[JsonObject]:
    ins = instructions[index]
    accesses: list[JsonObject] = []
    operands = str(ins.get("operands", ""))
    for operand in _negative_indexed_operands(operands):
        table = _TABLE_BY_NEGATIVE_OFFSET.get(int(operand["negative_offset"]))
        if table is None:
            continue
        register = str(operand["index_register"])
        known = _nearby_register_value(instructions, index, register)
        offset: int | str = known if known is not None else "dynamic"
        logical_address: int | None = None
        if isinstance(offset, int):
            logical_address = (int(table["logical_base_address"]) + offset) & 0xFFFF
        access = _base_access(ins, functions, semantic_accesses)
        access.update(
            {
                "table": table["name"],
                "table_base_address": table["logical_base_address"],
                "table_base_address_hex": h16(int(table["logical_base_address"])),
                "kind": "logical_negative_indexed_access",
                "operand": operand["operand"],
                "negative_offset": operand["negative_offset"],
                "negative_offset_hex": h16(int(operand["negative_offset"])),
                "index_register": register,
                "index": offset,
                "offset": offset,
                "access": _operand_access_kind(ins, str(operand["operand"])),
            }
        )
        if logical_address is not None:
            access["logical_address"] = logical_address
            access["logical_address_hex"] = h16(logical_address)
        accesses.append(access)
    return accesses


def _direct_address_accesses(
    ins: Mapping[str, Any],
    functions: list[JsonObject],
    semantic_accesses: Mapping[int, list[JsonObject]],
) -> list[JsonObject]:
    accesses: list[JsonObject] = []
    refs = _references(ins)
    for address in refs:
        logical_table = _table_for_logical_address(address)
        if logical_table is not None:
            accesses.append(
                _direct_logical_address_access(ins, logical_table, address, functions, semantic_accesses),
            )
            continue

        direct_table = _table_for_direct_candidate_address(address)
        if direct_table is not None:
            accesses.append(
                _direct_candidate_address_access(ins, direct_table, address, functions, semantic_accesses),
            )
    return accesses


def _direct_logical_address_access(
    ins: Mapping[str, Any],
    table: Mapping[str, Any],
    address: int,
    functions: list[JsonObject],
    semantic_accesses: Mapping[int, list[JsonObject]],
) -> JsonObject:
    base = int(table["logical_base_address"])
    offset = address - base
    access = _base_access(ins, functions, semantic_accesses)
    access.update(
        {
            "table": table["name"],
            "table_base_address": base,
            "table_base_address_hex": h16(base),
            "kind": "direct_logical_address_access",
            "direct_address": address,
            "direct_address_hex": h16(address),
            "logical_address": address,
            "logical_address_hex": h16(address),
            "index": offset,
            "offset": offset,
            "offset_hex": h16(offset),
            "access": _access_direction(ins, address) or "read_write_candidate",
        }
    )
    return access


def _direct_candidate_address_access(
    ins: Mapping[str, Any],
    table: Mapping[str, Any],
    address: int,
    functions: list[JsonObject],
    semantic_accesses: Mapping[int, list[JsonObject]],
) -> JsonObject:
    base = min(int(item) for item in table["direct_addresses"])
    offset = address - base
    access = _base_access(ins, functions, semantic_accesses)
    logical_offset = DIRECT_TABLE_TO_LOGICAL_OFFSET.get(base)
    access.update(
        {
            "table": table["name"],
            "table_base_address": table["logical_base_address"],
            "table_base_address_hex": h16(int(table["logical_base_address"])),
            "kind": "direct_candidate_address_access",
            "direct_address": address,
            "direct_address_hex": h16(address),
            "direct_base_address": base,
            "direct_base_address_hex": h16(base),
            "index": offset,
            "offset": offset,
            "offset_hex": h16(offset),
            "access": _access_direction(ins, address) or "read_write_candidate",
        }
    )
    if logical_offset is not None:
        access["semantic_negative_offset"] = logical_offset
        access["semantic_negative_offset_hex"] = h16(logical_offset)
    return access


def _lcd_correlation_hints(payload: Mapping[str, Any]) -> JsonObject:
    lcd_text = payload.get("lcd_text")
    strings = []
    if isinstance(lcd_text, Mapping) and isinstance(lcd_text.get("strings"), list):
        strings = [item for item in lcd_text["strings"] if isinstance(item, Mapping)]

    term_hits = []
    for term in LCD_CORRELATION_TERMS:
        hits = []
        upper_term = term.upper()
        for item in strings:
            text = f"{item.get('text', '')} {item.get('trimmed', '')}".upper()
            if upper_term not in text:
                continue
            hits.append(_lcd_string_summary(item))
        term_hits.append(
            {
                "term": term,
                "hit_count": len(hits),
                "hits": hits[:24],
                "status": "candidate_hits" if hits else "not_found",
            }
        )

    builder_targets: dict[int, JsonObject] = {}
    for item in strings:
        for xref in item.get("xrefs", []):
            if not isinstance(xref, Mapping):
                continue
            following = xref.get("following_bsr")
            if not isinstance(following, Mapping) or not isinstance(following.get("target"), int):
                continue
            target = int(following["target"])
            record = builder_targets.setdefault(
                target,
                {
                    "target": target,
                    "target_hex": h16(target),
                    "xref_count": 0,
                    "examples": [],
                },
            )
            record["xref_count"] = int(record["xref_count"]) + 1
            examples = record["examples"]
            if isinstance(examples, list) and len(examples) < 8:
                examples.append(
                    {
                        "text_address": item.get("address"),
                        "text_address_hex": h16(int(item["address"])) if isinstance(item.get("address"), int) else None,
                        "trimmed": item.get("trimmed"),
                        "xref_address": xref.get("address"),
                        "xref_address_hex": h16(int(xref["address"])) if isinstance(xref.get("address"), int) else None,
                    }
                )

    lcd_driver = payload.get("lcd_driver")
    routines = []
    if isinstance(lcd_driver, Mapping) and isinstance(lcd_driver.get("routines"), list):
        for routine in lcd_driver["routines"]:
            if not isinstance(routine, Mapping) or not isinstance(routine.get("start"), int):
                continue
            routines.append(
                {
                    "start": routine["start"],
                    "start_hex": h16(int(routine["start"])),
                    "end": routine.get("end"),
                    "end_hex": h16(int(routine["end"])) if isinstance(routine.get("end"), int) else None,
                    "role_hint": routine.get("role_hint"),
                    "roles": routine.get("roles", []),
                }
            )

    return {
        "terms": list(LCD_CORRELATION_TERMS),
        "term_hits": term_hits,
        "display_builder_targets": sorted(
            builder_targets.values(),
            key=lambda item: (-int(item["xref_count"]), int(item["target"])),
        ),
        "lcd_driver_routines": routines,
        "caveat": (
            "This is a static correlation helper. It reports text/script candidates and LCD driver "
            "routines in the same decompile; it does not prove a protocol field directly causes a string."
        ),
    }


def _lcd_string_summary(item: Mapping[str, Any]) -> JsonObject:
    address = item.get("address")
    return {
        "address": address,
        "address_hex": h16(int(address)) if isinstance(address, int) else None,
        "text": item.get("text"),
        "trimmed": item.get("trimmed"),
        "confidence": item.get("confidence"),
        "xref_count": item.get("xref_count", 0),
    }


def _base_access(
    ins: Mapping[str, Any],
    functions: list[JsonObject],
    semantic_accesses: Mapping[int, list[JsonObject]],
) -> JsonObject:
    address = int(ins["address"])
    function = _function_for_address(functions, address)
    access: JsonObject = {
        "instruction_address": address,
        "instruction_address_hex": h16(address),
        "mnemonic": str(ins.get("mnemonic", "")),
        "operands": str(ins.get("operands", "")),
        "instruction": str(ins.get("text") or _instruction_text(ins)),
        "references": _references(ins),
        "references_hex": [h16(ref) for ref in _references(ins)],
        "targets": _targets(ins),
        "targets_hex": [h16(target) for target in _targets(ins)],
        "label": _label_for_instruction(ins),
        "semantic_candidates": semantic_accesses.get(address, []),
    }
    if function:
        access["function_start"] = function["start"]
        access["function_start_hex"] = h16(int(function["start"]))
        access["function_label"] = function["label"]
    return access


def _semantic_access_locations(payload: Mapping[str, Any]) -> dict[int, list[JsonObject]]:
    locations: dict[int, list[JsonObject]] = {}
    semantics = payload.get("serial_semantics")
    if not isinstance(semantics, Mapping):
        return locations
    sources: list[Any] = []
    protocols = semantics.get("protocol_semantics")
    if isinstance(protocols, list):
        sources.extend(protocols)
    sources.append(semantics)
    for source in sources:
        if not isinstance(source, Mapping):
            continue
        for item in _table_candidate_items(source.get("table_map_candidates")):
            for access in _table_candidate_items(item.get("accesses")):
                address = access.get("instruction_address")
                if isinstance(address, int):
                    locations.setdefault(address, []).append(
                        {
                            "name_candidate": item.get("name_candidate"),
                            "kind": item.get("kind"),
                            "confidence": item.get("confidence"),
                        }
                    )
    return locations


def _table_candidate_items(value: Any) -> list[Mapping[str, Any]]:
    if isinstance(value, Mapping):
        return [item for item in value.values() if isinstance(item, Mapping)]
    if isinstance(value, list):
        return [item for item in value if isinstance(item, Mapping)]
    return []


def _format_access_line(access: Mapping[str, Any]) -> str:
    function = access.get("function_label") or "<no function>"
    operand = access.get("operand") or access.get("direct_address_hex")
    index = access.get("index")
    if index == "dynamic":
        index_text = f"index dynamic via {access.get('index_register')} operand {operand}"
    else:
        index_text = f"offset {h16(int(index or 0))}"
        if access.get("logical_address_hex"):
            index_text += f" -> {access['logical_address_hex']}"
        elif access.get("direct_address_hex"):
            index_text += f" at {access['direct_address_hex']}"
    return (
        f"{access['instruction_address_hex']} {access['access']} {index_text}; "
        f"{function}; {access['instruction']}"
    )


def _summarize_functions(accesses: Iterable[Mapping[str, Any]]) -> list[JsonObject]:
    summaries: dict[int, JsonObject] = {}
    for access in accesses:
        start = access.get("function_start")
        if not isinstance(start, int):
            start = -1
        summary = summaries.setdefault(
            start,
            {
                "start": start if start >= 0 else None,
                "start_hex": h16(start) if start >= 0 else None,
                "label": access.get("function_label") or "<no function>",
                "access_count": 0,
                "reads": 0,
                "writes": 0,
            },
        )
        summary["access_count"] = int(summary["access_count"]) + 1
        if access.get("access") == "read":
            summary["reads"] = int(summary["reads"]) + 1
        elif access.get("access") == "write":
            summary["writes"] = int(summary["writes"]) + 1
    return sorted(summaries.values(), key=lambda item: (-int(item["access_count"]), str(item["label"])))


def _function_ranges(payload: Mapping[str, Any]) -> list[JsonObject]:
    call_graph = payload.get("call_graph")
    if not isinstance(call_graph, Mapping):
        return []
    nodes = call_graph.get("nodes")
    if not isinstance(nodes, list):
        return []
    ranges: list[JsonObject] = []
    for node in nodes:
        if not isinstance(node, Mapping):
            continue
        start = node.get("start")
        end = node.get("end")
        if isinstance(start, int) and isinstance(end, int):
            ranges.append({"start": start, "end": end, "label": str(node.get("label") or label_for(start))})
    return sorted(ranges, key=lambda item: int(item["start"]))


def _function_for_address(functions: list[JsonObject], address: int) -> JsonObject | None:
    for function in functions:
        if int(function["start"]) <= address <= int(function["end"]):
            return function
    return None


def _nearby_register_value(instructions: list[JsonObject], index: int, register: str) -> int | None:
    register = register.upper()
    for prior_index in range(index - 1, max(-1, index - 10), -1):
        prior = instructions[prior_index]
        source, destination = _source_destination_operands(str(prior.get("operands", "")))
        if destination.upper() != register:
            continue
        value = _parse_immediate(source)
        if value is not None:
            return value
        if _writes_register(prior, register):
            return None
    return None


def _writes_register(ins: Mapping[str, Any], register: str) -> bool:
    _source, destination = _source_destination_operands(str(ins.get("operands", "")))
    return destination.upper() == register


def _instruction_sequence(value: object) -> list[JsonObject]:
    if isinstance(value, Mapping):
        values: Iterable[Any] = value.values()
    elif isinstance(value, list):
        values = value
    else:
        values = []
    return sorted(
        [item for item in values if isinstance(item, dict) and isinstance(item.get("address"), int)],
        key=lambda item: int(item["address"]),
    )


def _label_for_instruction(ins: Mapping[str, Any]) -> str | None:
    address = int(ins["address"])
    for key in ("label", "target_label"):
        value = ins.get(key)
        if isinstance(value, str) and value:
            return value
    if _targets(ins):
        return label_for(address)
    return None


def _instruction_text(ins: Mapping[str, Any]) -> str:
    operands = str(ins.get("operands", ""))
    return f"{ins.get('mnemonic', '')} {operands}".strip()


def _references(ins: Mapping[str, Any]) -> list[int]:
    refs = ins.get("references", [])
    if not isinstance(refs, list):
        return []
    output: list[int] = []
    for ref in refs:
        if isinstance(ref, Mapping) and isinstance(ref.get("address"), int):
            output.append(int(ref["address"]))
        elif isinstance(ref, int):
            output.append(ref)
    return output


def _targets(ins: Mapping[str, Any]) -> list[int]:
    targets = ins.get("targets", [])
    if not isinstance(targets, list):
        return []
    return [int(target) for target in targets if isinstance(target, int)]


def _negative_indexed_operands(operands: str) -> list[JsonObject]:
    matches: list[JsonObject] = []
    for match in re.finditer(r"@\(-H'([0-9A-Fa-f]+),\s*(R[0-7])\)", operands):
        offset = int(match.group(1), 16) & 0xFFFF
        if offset not in LOGICAL_TABLES:
            continue
        matches.append(
            {
                "operand": match.group(0),
                "negative_offset": offset,
                "index_register": match.group(2).upper(),
            }
        )
    return matches


def _table_for_logical_address(address: int) -> Mapping[str, Any] | None:
    for table in TABLES:
        if int(table["logical_base_address"]) <= address <= int(table["logical_range_end"]):
            return table
    return None


def _table_for_direct_candidate_address(address: int) -> Mapping[str, Any] | None:
    for table in TABLES:
        direct_addresses = [int(item) for item in table["direct_addresses"]]
        if min(direct_addresses) <= address <= int(table["direct_range_end"]):
            return table
    return None


def _operand_access_kind(ins: Mapping[str, Any], operand: str) -> str:
    root = _mnemonic_root(str(ins.get("mnemonic", "")))
    source, destination = _source_destination_operands(str(ins.get("operands", "")))
    if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "TST"}:
        return "read"
    if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}:
        return "write"
    if operand in destination and operand not in source:
        return "write"
    if operand in source and operand not in destination:
        return "read"
    if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
        return "write"
    return "read_write_candidate"


def _access_direction(ins: Mapping[str, Any], address: int) -> str | None:
    root = _mnemonic_root(str(ins.get("mnemonic", "")))
    if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}:
        return "read"
    if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}:
        return "write"
    if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
        return "write"
    if root in {"MOV:G", "MOV:S", "MOVTPE"}:
        source, destination = _source_destination_operands(str(ins.get("operands", "")))
        if _operand_mentions_address(destination, address):
            return "write"
        if _operand_mentions_address(source, address):
            return "read"
        if address in _references(ins):
            if destination.startswith("@") and not _operand_mentions_any_reference(source, _references(ins)):
                return "write"
            if source.startswith("@") and not _operand_mentions_any_reference(destination, _references(ins)):
                return "read"
    if root in {"MOV:L", "MOV:F"}:
        return "read"
    if root == "STC":
        return "write"
    if root == "LDC":
        return "read"
    return None


def _source_destination_operands(operands: str) -> tuple[str, str]:
    depth = 0
    split_at: int | None = None
    for index, char in enumerate(operands):
        if char in "({":
            depth += 1
        elif char in ")}" and depth:
            depth -= 1
        elif char == "," and depth == 0:
            split_at = index
    if split_at is None:
        operand = operands.strip()
        return "", operand
    return operands[:split_at].strip(), operands[split_at + 1 :].strip()


def _parse_immediate(operand: str) -> int | None:
    text = operand.strip()
    if text.startswith("#"):
        text = text[1:].strip()
    try:
        if text.upper().startswith("H'"):
            return int(text[2:], 16) & 0xFFFF
        if text.upper().startswith("0X"):
            return int(text, 16) & 0xFFFF
        if text.upper().startswith("$"):
            return int(text[1:], 16) & 0xFFFF
        return int(text, 10) & 0xFFFF
    except ValueError:
        return None


def _operand_mentions_any_reference(operand: str, references: list[int]) -> bool:
    return any(_operand_mentions_address(operand, address) for address in references)


def _operand_mentions_address(operand: str, address: int) -> bool:
    operand_upper = operand.upper().replace(" ", "")
    negative = (0x10000 - address) & 0xFFFF
    return (
        f"H'{address:04X}" in operand_upper
        or f"0X{address:04X}" in operand_upper
        or f"${address:04X}" in operand_upper
        or f"-H'{negative:04X}" in operand_upper
        or f"-0X{negative:04X}" in operand_upper
        or f"-${negative:04X}" in operand_upper
    )


def _mnemonic_root(mnemonic: str) -> str:
    return mnemonic.rsplit(".", 1)[0].upper()


__all__ = [
    "analyze_table_xrefs",
    "generate_table_xref_report",
    "load_table_xref_input",
    "main",
    "write_table_xrefs",
]
