from __future__ import annotations

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

from .emulator.peripherals.x24164 import (
    X24164_FACTORY_DEFAULT_BASE,
    X24164_FACTORY_DEFAULT_BYTES,
    X24164_LOGICAL_PAGE_COUNT,
    X24164_LOGICAL_PAGE_SIZE,
    factory_default_words_from_rom,
)
from .formatting import h16, label_for
from .rom import Rom


JsonObject = dict[str, Any]

DEFAULT_INPUT = Path("build/rom_decompiled.json")
DEFAULT_ROM = Path("ROM/M27C512@DIP28_1.BIN")

SHADOW_BASE = 0xF400
SHADOW_SIZE = 0x0100
SELECTOR_MAP_BASE = 0xC564
SELECTOR_MAP_COUNT = 0x0200
RECORD_RAM_BASE = 0xF7B0
RECORD_BYTES = 8

STATE_BYTES: tuple[tuple[int, str], ...] = (
    (0xF402, "factory_signature_word"),
    (0xF404, "feature_or_option_flags_candidate"),
    (0xF730, "connect_display_state_candidate"),
    (0xF731, "session_latch_candidate"),
    (0xF732, "display_dispatch_selector_candidate"),
    (0xF76E, "eeprom_page_and_persist_flags"),
    (0xF790, "connection_latch_shadow_candidate"),
    (0xF791, "feature_flag_gate_candidate"),
    (0xFB03, "report_bridge_suppress_candidate"),
)


def load_eeprom_layout_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_eeprom_layout(payload: Mapping[str, Any], *, rom_path: Path | None = DEFAULT_ROM) -> JsonObject:
    rom = _load_rom(rom_path)
    factory_entries = _factory_entries(rom)
    selector_map = _selector_map_entries(rom, factory_entries)
    xrefs = _xref_summary(payload)

    return {
        "kind": "eeprom_layout",
        "summary": {
            "confidence": "medium-high",
            "model": (
                "The ROM treats the traced P9 bus as two X24164-style EEPROM banks, "
                "mirrors a 0x100-byte factory/default block into F400-F4FF, and loads "
                "sixteen 8-byte persistent records into F7B0-F82F at boot."
            ),
            "factory_default_count": len(factory_entries),
            "selector_persistence_mapping_count": len(selector_map),
            "persistent_record_count": X24164_LOGICAL_PAGE_COUNT,
        },
        "physical_model": _physical_model(),
        "boot_flow": _boot_flow(),
        "factory_defaults": {
            "rom_base": X24164_FACTORY_DEFAULT_BASE,
            "rom_base_hex": h16(X24164_FACTORY_DEFAULT_BASE),
            "byte_count": X24164_FACTORY_DEFAULT_BYTES,
            "shadow_base": SHADOW_BASE,
            "shadow_base_hex": h16(SHADOW_BASE),
            "shadow_range_hex": f"{h16(SHADOW_BASE)}-{h16(SHADOW_BASE + SHADOW_SIZE - 1)}",
            "entries": factory_entries,
            "notable_entries": _notable_factory_entries(factory_entries, xrefs, selector_map),
        },
        "persistent_records": _persistent_records(factory_entries),
        "serial_persistence_mapping": {
            "table_base": SELECTOR_MAP_BASE,
            "table_base_hex": h16(SELECTOR_MAP_BASE),
            "entry_count": SELECTOR_MAP_COUNT,
            "mapped_entry_count": len(selector_map),
            "entries": selector_map,
            "offset_histogram": _offset_histogram(selector_map),
            "high_byte_histogram": _high_byte_histogram(selector_map),
            "address_formula": (
                "command 4 persists to EEPROM address "
                "(((F76E & 0x0F) << 8) | (mapping_low_byte & 0xFE)) when F76E.7 is set"
            ),
        },
        "xrefs": xrefs,
        "state_byte_hints": _state_byte_hints(factory_entries, xrefs),
        "bench_implications": [
            "A live EEPROM dump should first compare F400-F4FF shadow-equivalent offsets against the ROM factory table, especially F402/F404 and mapped offsets.",
            "Pages 0x0-0xF offset 0x00-0x07 are loaded into F7B0-F82F; page 0 carries the signature/options header and pages 1-F default to spaces, so non-space bytes there are high-value identity/config data.",
            "Serial command 0/4 can mirror values into F400 offsets selected by the ROM mapping table, but command 4 only persists when the continuation path reaches BD2B and F76E.7 is set.",
            "F76E is not just a page byte: bit7 gates EEPROM persistence, bit6 suppresses the 48FA dispatch path, and only its low nibble survives into the EEPROM page address.",
            "CONNECT OK is still likely gated by volatile table/session state as well as EEPROM-backed defaults; EEPROM differences may explain why bench and emulator diverge, but are unlikely to be the whole protocol by themselves.",
        ],
        "caveats": [
            "The selector map proves where firmware mirrors/persists serial values, not what every field means to the panel UI.",
            "High bytes in the C564 selector map look structured, but the observed command-0/command-4 paths only use the low byte for F400/EEPROM offsets.",
            "Indexed F7B0-F82F record consumers can be missed by a static xref pass; dynamic emulator traces should be used once interesting record bytes are found.",
        ],
    }


def format_text_report(analysis: Mapping[str, Any]) -> str:
    summary = analysis["summary"]
    lines = [
        "H8/536 EEPROM Layout Report",
        "",
        f"Summary: {summary['model']}",
        f"Confidence: {summary['confidence']}",
        "",
        "Physical / Logical Model:",
    ]
    physical = analysis.get("physical_model", {})
    for row in physical.get("banks", []):
        lines.append(
            f"- {row['name']}: logical {row['logical_range_hex']} control {row['control_write_hex']}/{row['control_read_hex']}"
        )
    lines.append(f"- bus: {physical.get('bus', 'unknown')}")
    lines.append(f"- page model: {physical.get('page_model', 'unknown')}")

    lines.extend(["", "Boot Flow:"])
    for step in analysis.get("boot_flow", []):
        lines.append(f"- {step['address_hex']} {step['name']}: {step['summary']}")

    defaults = analysis.get("factory_defaults", {})
    lines.extend(["", "Factory Shadow Block:"])
    lines.append(
        f"- ROM {defaults.get('rom_base_hex')} length 0x{int(defaults.get('byte_count', 0)):02X} "
        f"mirrors to {defaults.get('shadow_range_hex')}"
    )
    for entry in defaults.get("notable_entries", [])[:32]:
        details = []
        if entry.get("ascii"):
            details.append(f"ascii={entry['ascii']!r}")
        if entry.get("mapped_selectors_hex"):
            details.append(f"selectors={', '.join(entry['mapped_selectors_hex'][:8])}")
        if entry.get("xref_count"):
            details.append(f"xrefs={entry['xref_count']}")
        suffix = f" ({'; '.join(details)})" if details else ""
        lines.append(
            f"- {entry['shadow_address_hex']} offset {entry['offset_hex']} "
            f"default {entry['factory_word_hex']}{suffix}"
        )

    lines.extend(["", "Persistent 8-Byte Records:"])
    records = analysis.get("persistent_records", [])
    if records:
        first = records[0]
        last = records[-1]
        lines.append(
            f"- {len(records)} records: EEPROM {first['eeprom_range_hex']} .. {last['eeprom_range_hex']} "
            f"load into RAM {first['ram_range_hex']} .. {last['ram_range_hex']}"
        )
    for record in records[:16]:
        lines.append(
            f"  - record {record['record_index_hex']}: EEPROM {record['eeprom_range_hex']} "
            f"-> RAM {record['ram_range_hex']} default {record['default_text']!r}"
        )

    mapping = analysis.get("serial_persistence_mapping", {})
    lines.extend(["", "Serial Selector -> Shadow/EEPROM Mapping:"])
    lines.append(
        f"- table {mapping.get('table_base_hex')} has {mapping.get('mapped_entry_count', 0)} nonzero low-byte mappings"
    )
    lines.append(f"- formula: {mapping.get('address_formula')}")
    for entry in mapping.get("entries", [])[:80]:
        lines.append(
            f"  - selector {entry['selector_hex']} map_word={entry['mapping_word_hex']} "
            f"-> {entry['shadow_address_hex']} page+{entry['eeprom_word_offset_hex']} "
            f"default={entry.get('factory_word_hex', 'unknown')}"
        )
    omitted = int(mapping.get("mapped_entry_count", 0)) - min(80, len(mapping.get("entries", [])))
    if omitted > 0:
        lines.append(f"  - ... {omitted} more mapped selectors omitted")

    lines.extend(["", "State Byte Hints:"])
    for hint in analysis.get("state_byte_hints", []):
        lines.append(
            f"- {hint['address_hex']} {hint['name']}: {hint['summary']} "
            f"(xrefs={hint['xref_count']})"
        )

    lines.extend(["", "Bench Implications:"])
    for item in analysis.get("bench_implications", []):
        lines.append(f"- {item}")

    lines.extend(["", "Caveats:"])
    for item in analysis.get("caveats", []):
        lines.append(f"- {item}")
    return "\n".join(lines).rstrip() + "\n"


def write_eeprom_layout(
    input_path: Path,
    output_path: Path,
    *,
    rom_path: Path | None = DEFAULT_ROM,
    as_json: bool = False,
) -> JsonObject:
    analysis = analyze_eeprom_layout(load_eeprom_layout_input(input_path), rom_path=rom_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    if as_json:
        output_path.write_text(json.dumps(analysis, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    else:
        output_path.write_text(format_text_report(analysis), encoding="utf-8")
    return analysis


def main(argv: list[str] | None = None, stdout: Any | None = None) -> int:
    parser = argparse.ArgumentParser(description="Mine ROM-backed X24164 EEPROM layout and persistence hints.")
    parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT)
    parser.add_argument("--rom", type=Path, default=DEFAULT_ROM, help="ROM binary to mine")
    parser.add_argument("--json", action="store_true", help="emit structured JSON instead of readable text")
    parser.add_argument("--out", type=Path, default=None, help="write report to this path")
    args = parser.parse_args(argv)

    stream = stdout
    if stream is None:
        import sys

        stream = sys.stdout

    rom_path = args.rom if args.rom and args.rom.exists() else None
    analysis = analyze_eeprom_layout(load_eeprom_layout_input(args.input), rom_path=rom_path)
    rendered = json.dumps(analysis, indent=2, sort_keys=True) + "\n" if args.json else format_text_report(analysis)
    if args.out:
        args.out.parent.mkdir(parents=True, exist_ok=True)
        args.out.write_text(rendered, encoding="utf-8")
        print(f"wrote {args.out}", file=stream)
    else:
        print(rendered, end="", file=stream)
    return 0


def _load_rom(path: Path | None) -> Rom | None:
    if path is None or not path.exists():
        return None
    return Rom(path.read_bytes())


def _physical_model() -> JsonObject:
    return {
        "bus": "P91/SCL and P97/SDA bit-banged two-wire bus through ROM routines C121/C08B/C0DB/C10C/C142",
        "page_size": X24164_LOGICAL_PAGE_SIZE,
        "page_count": X24164_LOGICAL_PAGE_COUNT,
        "page_model": "16 logical pages of 0x100 bytes; low 8 address bits are sent as the EEPROM word address",
        "banks": [
            {
                "name": "lower_x24164_candidate",
                "logical_range": [0x0000, 0x07FF],
                "logical_range_hex": "0x000-0x7FF",
                "control_write": 0xA0,
                "control_write_hex": "A0",
                "control_read": 0xA1,
                "control_read_hex": "A1",
            },
            {
                "name": "upper_x24164_candidate",
                "logical_range": [0x0800, 0x0FFF],
                "logical_range_hex": "0x800-0xFFF",
                "control_write": 0xE0,
                "control_write_hex": "E0",
                "control_read": 0xE1,
                "control_read_hex": "E1",
            },
        ],
    }


def _boot_flow() -> list[JsonObject]:
    return [
        {
            "address": 0x40BB,
            "address_hex": h16(0x40BB),
            "name": "eeprom_boot_gate",
            "summary": "initializes queue/table scratch, checks P7DR.7, then checks F402 == H'6B6F before trusting persisted state",
        },
        {
            "address": 0x4103,
            "address_hex": h16(0x4103),
            "name": "factory_default_fill",
            "summary": "copies ROM C964-CA63 into F400-F4FF and writes the same 0x100-byte defaults to each EEPROM page",
        },
        {
            "address": 0x4187,
            "address_hex": h16(0x4187),
            "name": "record_header_blank",
            "summary": "overwrites page offsets 0x00-0x07 on pages 0x1-0xF with four H'2020 words after factory replication; page 0 keeps the signature/options header",
        },
        {
            "address": 0x41D2,
            "address_hex": h16(0x41D2),
            "name": "persistent_record_load",
            "summary": "reads page offsets 0x00-0x07 from pages 0x0-0xF into F7B0-F82F; record 0 is a signature/options header, records 1-F are label/identity-like slots",
        },
        {
            "address": 0xBD2B,
            "address_hex": h16(0xBD2B),
            "name": "serial_persist_path",
            "summary": "command-4 continuation writes the live value into F400+map[selector] and persists it through BFE0 when F76E.7 is set",
        },
    ]


def _factory_entries(rom: Rom | None) -> list[JsonObject]:
    if rom is None:
        return []
    entries = []
    for offset, word in factory_default_words_from_rom(rom.data):
        shadow_address = SHADOW_BASE + offset
        bytes_pair = [(word >> 8) & 0xFF, word & 0xFF]
        entries.append(
            {
                "offset": offset,
                "offset_hex": f"0x{offset:02X}",
                "rom_address": X24164_FACTORY_DEFAULT_BASE + offset,
                "rom_address_hex": h16(X24164_FACTORY_DEFAULT_BASE + offset),
                "shadow_address": shadow_address,
                "shadow_address_hex": h16(shadow_address),
                "factory_word": word,
                "factory_word_hex": f"0x{word:04X}",
                "bytes_hex": " ".join(f"{byte:02X}" for byte in bytes_pair),
                "ascii": _word_ascii(word),
                "default_eeprom_word_after_record_blank": 0x2020 if offset < RECORD_BYTES else word,
                "default_eeprom_word_after_record_blank_hex": "0x2020" if offset < RECORD_BYTES else f"0x{word:04X}",
            }
        )
    return entries


def _selector_map_entries(rom: Rom | None, factory_entries: list[JsonObject]) -> list[JsonObject]:
    if rom is None:
        return []
    defaults_by_offset = {int(entry["offset"]): entry for entry in factory_entries}
    entries: list[JsonObject] = []
    for selector in range(SELECTOR_MAP_COUNT):
        address = SELECTOR_MAP_BASE + selector * 2
        if not rom.contains(address, 2):
            break
        word = rom.u16(address)
        low = word & 0xFF
        if low == 0:
            continue
        aligned = low & 0xFE
        shadow_address = SHADOW_BASE + low
        aligned_shadow_address = SHADOW_BASE + aligned
        default = defaults_by_offset.get(aligned)
        entry: JsonObject = {
            "selector": selector,
            "selector_hex": f"0x{selector:03X}",
            "entry_address": address,
            "entry_address_hex": h16(address),
            "mapping_word": word,
            "mapping_word_hex": f"0x{word:04X}",
            "mapping_high_byte": (word >> 8) & 0xFF,
            "mapping_high_byte_hex": f"0x{(word >> 8) & 0xFF:02X}",
            "mapping_low_byte": low,
            "mapping_low_byte_hex": f"0x{low:02X}",
            "shadow_offset": low,
            "shadow_offset_hex": f"0x{low:02X}",
            "shadow_address": shadow_address,
            "shadow_address_hex": h16(shadow_address),
            "aligned_shadow_address": aligned_shadow_address,
            "aligned_shadow_address_hex": h16(aligned_shadow_address),
            "eeprom_word_offset": aligned,
            "eeprom_word_offset_hex": f"0x{aligned:02X}",
            "command0_shadow_mirror": True,
            "command4_shadow_mirror": True,
            "command4_persist_when_f76e_bit7": True,
        }
        if default is not None:
            entry["factory_word"] = default["factory_word"]
            entry["factory_word_hex"] = default["factory_word_hex"]
            entry["factory_ascii"] = default["ascii"]
        entries.append(entry)
    return entries


def _notable_factory_entries(
    factory_entries: list[JsonObject],
    xrefs: Mapping[str, Any],
    selector_map: list[JsonObject],
) -> list[JsonObject]:
    direct_counts: Counter[int] = Counter()
    for item in xrefs.get("direct_xrefs", []):
        if not isinstance(item, Mapping) or not isinstance(item.get("address"), int):
            continue
        address = int(item["address"])
        if SHADOW_BASE <= address < SHADOW_BASE + SHADOW_SIZE:
            direct_counts[address & 0xFF] += 1

    selectors_by_offset: dict[int, list[str]] = {}
    for entry in selector_map:
        selectors_by_offset.setdefault(int(entry["eeprom_word_offset"]), []).append(str(entry["selector_hex"]))

    notable = []
    for entry in factory_entries:
        offset = int(entry["offset"])
        word = int(entry["factory_word"])
        mapped = selectors_by_offset.get(offset, [])
        xref_count = direct_counts.get(offset, 0)
        if word in (0x0000, 0xFFFF) and not mapped and not xref_count and offset not in (0x02, 0x04):
            continue
        item = dict(entry)
        item["xref_count"] = xref_count
        item["mapped_selectors_hex"] = mapped[:24]
        notable.append(item)
    return notable


def _persistent_records(factory_entries: list[JsonObject]) -> list[JsonObject]:
    factory_by_offset = {int(entry["offset"]): int(entry["factory_word"]) for entry in factory_entries}
    records = []
    for index in range(X24164_LOGICAL_PAGE_COUNT):
        eeprom_base = index * X24164_LOGICAL_PAGE_SIZE
        ram_base = RECORD_RAM_BASE + index * RECORD_BYTES
        default_words = [
            factory_by_offset.get(offset, 0xFFFF) if index == 0 else 0x2020
            for offset in range(0, RECORD_BYTES, 2)
        ]
        default_bytes = bytearray()
        for word in default_words:
            default_bytes.extend([(word >> 8) & 0xFF, word & 0xFF])
        records.append(
            {
                "record_index": index,
                "record_index_hex": f"0x{index:X}",
                "eeprom_base": eeprom_base,
                "eeprom_base_hex": f"0x{eeprom_base:03X}",
                "eeprom_range_hex": f"0x{eeprom_base:03X}-0x{eeprom_base + RECORD_BYTES - 1:03X}",
                "ram_base": ram_base,
                "ram_base_hex": h16(ram_base),
                "ram_range_hex": f"{h16(ram_base)}-{h16(ram_base + RECORD_BYTES - 1)}",
                "word_offsets": [0, 2, 4, 6],
                "default_words": default_words,
                "default_words_hex": [f"0x{word:04X}" for word in default_words],
                "default_bytes_hex": default_bytes.hex(" ").upper(),
                "default_text": _ascii(default_bytes),
                "role_candidate": "page-0 signature/options header" if index == 0 else "8-byte persistent label/identity slot",
            }
        )
    return records


def _offset_histogram(entries: Iterable[Mapping[str, Any]]) -> list[JsonObject]:
    counts = Counter(int(entry["eeprom_word_offset"]) for entry in entries)
    return [
        {"offset": offset, "offset_hex": f"0x{offset:02X}", "selector_count": count}
        for offset, count in sorted(counts.items())
    ]


def _high_byte_histogram(entries: Iterable[Mapping[str, Any]]) -> list[JsonObject]:
    counts = Counter(int(entry["mapping_high_byte"]) for entry in entries)
    return [
        {"high_byte": value, "high_byte_hex": f"0x{value:02X}", "selector_count": count}
        for value, count in sorted(counts.items())
    ]


def _state_byte_hints(factory_entries: list[JsonObject], xrefs: Mapping[str, Any]) -> list[JsonObject]:
    defaults = {int(entry["shadow_address"]): entry for entry in factory_entries}
    refs_by_address: Counter[int] = Counter()
    examples_by_address: dict[int, list[JsonObject]] = {}
    for item in xrefs.get("direct_xrefs", []):
        if not isinstance(item, Mapping) or not isinstance(item.get("address"), int):
            continue
        address = int(item["address"])
        refs_by_address[address] += 1
        examples_by_address.setdefault(address, []).append(dict(item))

    hints = []
    for address, name in STATE_BYTES:
        default = defaults.get(address)
        summary = _state_summary(address, default)
        hints.append(
            {
                "address": address,
                "address_hex": h16(address),
                "name": name,
                "summary": summary,
                "factory_word_hex": default.get("factory_word_hex") if default else None,
                "xref_count": refs_by_address.get(address, 0),
                "xrefs": examples_by_address.get(address, [])[:12],
            }
        )
    return hints


def _state_summary(address: int, default: Mapping[str, Any] | None) -> str:
    factory = f"factory {default['factory_word_hex']}" if default else "volatile/no factory word"
    if address == 0xF402:
        return f"{factory}; boot accepts persisted state only when this word is H'6B6F"
    if address == 0xF404:
        return f"{factory}; bits 1-4 are tested with F791 gates in display/status routines"
    if address == 0xF76E:
        return "bit7 enables command-4 EEPROM persistence, bit6 suppresses 48FA dispatch, low nibble selects EEPROM page"
    if address == 0xF791:
        return "volatile gate tested alongside F404 option bits before setting report/display flags"
    if address == 0xF732:
        return "volatile display dispatch selector feeding the 493E pointer table and 48FA report bridge"
    return factory


def _xref_summary(payload: Mapping[str, Any]) -> JsonObject:
    instructions = _instruction_sequence(payload.get("instructions"))
    functions = _function_ranges(payload)
    direct = []
    dynamic = []
    for ins in instructions:
        for ref in _references(ins):
            if _is_interesting_address(ref):
                direct.append(_xref_item(ins, ref, functions))
        operands = str(ins.get("operands", ""))
        dynamic_kind = _dynamic_kind(operands)
        if dynamic_kind:
            item = _base_instruction_item(ins, functions)
            item["kind"] = dynamic_kind
            item["operand"] = operands
            dynamic.append(item)
    return {
        "direct_xref_count": len(direct),
        "dynamic_xref_count": len(dynamic),
        "direct_xrefs": direct,
        "dynamic_xrefs": dynamic,
    }


def _xref_item(ins: Mapping[str, Any], address: int, functions: list[JsonObject]) -> JsonObject:
    item = _base_instruction_item(ins, functions)
    item.update(
        {
            "address": address,
            "address_hex": h16(address),
            "region": _region_for_address(address),
            "access": _access_direction(ins, address) or "read_write_candidate",
        }
    )
    return item


def _base_instruction_item(ins: Mapping[str, Any], functions: list[JsonObject]) -> JsonObject:
    address = int(ins["address"])
    function = _function_for_address(functions, address)
    item: 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)),
    }
    if function:
        item["function_start"] = function["start"]
        item["function_start_hex"] = h16(int(function["start"]))
        item["function_label"] = function["label"]
    return item


def _is_interesting_address(address: int) -> bool:
    return (
        SHADOW_BASE <= address < SHADOW_BASE + SHADOW_SIZE
        or RECORD_RAM_BASE <= address < RECORD_RAM_BASE + X24164_LOGICAL_PAGE_COUNT * RECORD_BYTES
        or any(address == item[0] for item in STATE_BYTES)
    )


def _region_for_address(address: int) -> str:
    if SHADOW_BASE <= address < SHADOW_BASE + SHADOW_SIZE:
        return "f400_shadow_defaults"
    if RECORD_RAM_BASE <= address < RECORD_RAM_BASE + X24164_LOGICAL_PAGE_COUNT * RECORD_BYTES:
        return "persistent_record_ram"
    return "state_or_gate_ram"


def _dynamic_kind(operands: str) -> str | None:
    patterns = (
        ("@(-H'0C00", "indexed_f400_shadow_access"),
        ("@(-H'0850", "indexed_persistent_record_store"),
        ("@(-H'084E", "indexed_persistent_record_store"),
        ("@(-H'084C", "indexed_persistent_record_store"),
        ("@(-H'084A", "indexed_persistent_record_store"),
        ("@(-H'3A9C", "selector_to_shadow_mapping_word"),
        ("@(-H'3A9B", "selector_to_shadow_mapping_low_byte"),
    )
    upper = operands.upper()
    for pattern, kind in patterns:
        if pattern in upper:
            return kind
    return None


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 _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 _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 _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"
    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 _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 _instruction_text(ins: Mapping[str, Any]) -> str:
    operands = str(ins.get("operands", ""))
    return f"{ins.get('mnemonic', '')} {operands}".strip()


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


def _word_ascii(word: int) -> str:
    chars = [(word >> 8) & 0xFF, word & 0xFF]
    if all(0x20 <= byte <= 0x7E for byte in chars):
        return "".join(chr(byte) for byte in chars)
    if any(0x20 <= byte <= 0x7E for byte in chars):
        return "".join(chr(byte) if 0x20 <= byte <= 0x7E else "." for byte in chars)
    return ""


def _ascii(data: bytes | bytearray) -> str:
    return "".join(chr(value) if 0x20 <= value <= 0x7E else "." for value in data)


__all__ = [
    "analyze_eeprom_layout",
    "format_text_report",
    "load_eeprom_layout_input",
    "main",
    "write_eeprom_layout",
]
