from __future__ import annotations

from collections.abc import Mapping

from .formatting import parse_int
from .model import Instruction


SCI_REGISTERS: dict[str, dict[str, int]] = {
    "SCI1": {
        "SMR": 0xFED8,
        "BRR": 0xFED9,
        "SCR": 0xFEDA,
        "TDR": 0xFEDB,
        "SSR": 0xFEDC,
        "RDR": 0xFEDD,
    },
    "SCI2": {
        "SMR": 0xFEF0,
        "BRR": 0xFEF1,
        "SCR": 0xFEF2,
        "TDR": 0xFEF3,
        "SSR": 0xFEF4,
        "RDR": 0xFEF5,
    },
}

SCI_REGISTER_BY_ADDRESS = {
    address: (channel, register)
    for channel, registers in SCI_REGISTERS.items()
    for register, address in registers.items()
}

SYSCR2_ADDRESS = 0xFEFD
SYSCR2_INITIAL = 0x80
P9SCI2E_BIT = 0
SCR_INITIAL = 0x0C

MANUAL_REFERENCES = [
    "Manual/0900766b802125d0.md:2417 FP-80 H8/536 pin 66 is P95/TXD",
    "Manual/0900766b802125d0.md:2418 FP-80 H8/536 pin 67 is P96/RXD",
    "Manual/0900766b802125d0.md:11192 Port 9 carries SCI1 and SCI2 serial signals",
    "Manual/0900766b802125d0.md:11201 P96 is RXD1 input",
    "Manual/0900766b802125d0.md:11202 P95 is TXD1 output",
    "Manual/0900766b802125d0.md:15725 SCI1 RXD input pin",
    "Manual/0900766b802125d0.md:15726 SCI1 TXD output pin",
    "Manual/0900766b802125d0.md:15750 SCI register table starts with SCI1 RDR/TDR/SMR/SCR/SSR/BRR",
    "Manual/0900766b802125d0.md:15758 SCI register table lists SCI2 RDR/TDR/SMR/SCR/SSR/BRR",
    "Manual/0900766b802125d0.md:15794 RDR receive data register",
    "Manual/0900766b802125d0.md:15823 TDR transmit data register",
    "Manual/0900766b802125d0.md:15969 SCR enables and disables SCI functions",
    "Manual/0900766b802125d0.md:16009 SCR.TE makes the TXD pin output",
    "Manual/0900766b802125d0.md:16029 SCR.RE makes the RXD pin input",
    "Manual/0900766b802125d0.md:16090 SSR contains transmit/receive status flags",
    "Manual/0900766b802125d0.md:10560 SYSCR2 controls port 9 pin functions",
    "Manual/0900766b802125d0.md:10631 SYSCR2.P9SCI2E controls the SCI2 functions of P92-P94",
]

BOARD_PROFILES = {
    "sony_rcp_tx7": {
        "name": "Sony RCP-TX7",
        "summary": "Board trace ties the H8/536 SCI1 pins to a MAX202 RS232 transceiver.",
        "manual_references": MANUAL_REFERENCES,
        "traces": [
            {
                "channel": "SCI1",
                "signal": "TXD",
                "h8_pin": 66,
                "h8_pin_name": "P95/TXD",
                "h8_function": "TXD1",
                "max202_pin": 11,
                "evidence": "MAX202 pin 11 traces to H8 pin 66",
            },
            {
                "channel": "SCI1",
                "signal": "RXD",
                "h8_pin": 67,
                "h8_pin_name": "P96/RXD",
                "h8_function": "RXD1",
                "max202_pin": 12,
                "evidence": "MAX202 pin 12 traces to H8 pin 67",
            },
        ],
    },
}

_READ_ONLY_ROOTS = {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}
_BIT_WRITE_ROOTS = {"BCLR", "BNOT", "BSET"}


def analyze_board_profile(
    instructions: Mapping[int, Instruction],
    board: str = "sony_rcp_tx7",
) -> dict[str, object]:
    """Annotate board-specific serial-path accesses without changing disassembly output."""
    if board not in BOARD_PROFILES:
        raise ValueError(f"unsupported board profile: {board}")

    profile = BOARD_PROFILES[board]
    annotations: dict[int, str] = {}
    instruction_metadata: dict[int, dict[str, object]] = {}
    channels = _initial_channel_payload(profile)
    register_values: dict[int, int | None] = {
        SYSCR2_ADDRESS: SYSCR2_INITIAL,
        SCI_REGISTERS["SCI1"]["SCR"]: SCR_INITIAL,
        SCI_REGISTERS["SCI2"]["SCR"]: SCR_INITIAL,
    }

    for address in sorted(instructions):
        ins = instructions[address]
        access_kind = _access_kind(ins)
        access_records: list[dict[str, object]] = []
        comments: list[str] = []

        if SYSCR2_ADDRESS in _expanded_references(ins, access_kind):
            value = _write_value(ins, SYSCR2_ADDRESS, register_values.get(SYSCR2_ADDRESS), 1, 0)
            if access_kind == "write":
                register_values[SYSCR2_ADDRESS] = value
            record = _syscr2_access(ins, access_kind, register_values.get(SYSCR2_ADDRESS))
            access_records.append(record)
            comments.append(str(record["comment"]))

        for target in _sci_targets(ins, access_kind):
            register_address = int(target["register_address"])
            value = _write_value(
                ins,
                register_address,
                register_values.get(register_address),
                int(target["width"]),
                int(target["index"]),
            )
            if access_kind == "write":
                register_values[register_address] = value

            channel = str(target["channel"])
            register = str(target["register"])
            p9sci2e = _bit_state(register_values.get(SYSCR2_ADDRESS), P9SCI2E_BIT)
            record = _sci_access(
                ins,
                channel,
                register,
                register_address,
                access_kind,
                value,
                register_values.get(register_address),
                p9sci2e,
            )
            access_records.append(record)
            comments.append(str(record["comment"]))
            channel_payload = channels[channel]
            channel_accesses = channel_payload["accesses"]
            if isinstance(channel_accesses, list):
                channel_accesses.append(record)
            if register == "SCR":
                channel_payload["scr"] = _scr_metadata(register_values.get(register_address))

        if access_records:
            comment = "; ".join(_dedupe(comments))
            annotations[ins.address] = comment
            instruction_metadata[ins.address] = {
                "accesses": access_records,
                "comment": comment,
            }

        channels["SCI2"]["p9sci2e"] = _bit_state(register_values.get(SYSCR2_ADDRESS), P9SCI2E_BIT)

    return {
        "board": board,
        "name": profile["name"],
        "summary": profile["summary"],
        "manual_references": list(profile["manual_references"]),
        "traces": [dict(trace) for trace in profile["traces"]],
        "channels": channels,
        "annotations": annotations,
        "instructions": instruction_metadata,
        "state": {
            "SYSCR2": _register_state(register_values.get(SYSCR2_ADDRESS)),
            "P9SCI2E": _bit_state(register_values.get(SYSCR2_ADDRESS), P9SCI2E_BIT),
        },
    }


def board_comment_for_instruction(analysis: Mapping[str, object] | None, address: int) -> str:
    if not analysis:
        return ""
    annotations = analysis.get("annotations")
    if not isinstance(annotations, Mapping):
        return ""
    comment = annotations.get(address)
    if comment is None:
        comment = annotations.get(str(address))
    return str(comment) if comment else ""


def board_metadata_for_instruction(
    analysis: Mapping[str, object] | None,
    address: int,
) -> dict[str, object] | None:
    if not analysis:
        return None
    instructions = analysis.get("instructions")
    if not isinstance(instructions, Mapping):
        return None
    metadata = instructions.get(address)
    if metadata is None:
        metadata = instructions.get(str(address))
    return metadata if isinstance(metadata, dict) else None


def board_json_payload(analysis: Mapping[str, object] | None) -> dict[str, object]:
    if not analysis:
        return {
            "board": None,
            "name": None,
            "summary": None,
            "manual_references": [],
            "traces": [],
            "channels": {},
            "instructions": {},
        }
    return {
        "board": analysis.get("board"),
        "name": analysis.get("name"),
        "summary": analysis.get("summary"),
        "manual_references": analysis.get("manual_references", []),
        "traces": analysis.get("traces", []),
        "channels": analysis.get("channels", {}),
        "instructions": analysis.get("instructions", {}),
        "state": analysis.get("state", {}),
    }


def _initial_channel_payload(profile: Mapping[str, object]) -> dict[str, dict[str, object]]:
    traces = [dict(trace) for trace in profile["traces"] if isinstance(trace, Mapping)]
    return {
        "SCI1": {
            "traced_to_max202": True,
            "path": "RS232/MAX202",
            "pins": traces,
            "scr": _scr_metadata(SCR_INITIAL),
            "accesses": [],
        },
        "SCI2": {
            "traced_to_max202": False,
            "path": None,
            "note": "Sony RCP-TX7 MAX202 board traces are on SCI1 P95/P96, not SCI2 P92/P93.",
            "p9sci2e": False,
            "scr": _scr_metadata(SCR_INITIAL),
            "accesses": [],
        },
    }


def _syscr2_access(ins: Instruction, access: str, value: int | None) -> dict[str, object]:
    p9sci2e = _bit_state(value, P9SCI2E_BIT)
    comment = _syscr2_comment(access, p9sci2e)
    record: dict[str, object] = {
        "address": ins.address,
        "instruction": ins.text,
        "register": "SYSCR2",
        "register_address": SYSCR2_ADDRESS,
        "access": access,
        "p9sci2e": p9sci2e,
        "comment": comment,
    }
    if value is not None:
        record["value"] = value
        record["value_hex"] = _hex8(value)
    return record


def _sci_access(
    ins: Instruction,
    channel: str,
    register: str,
    register_address: int,
    access: str,
    value: int | None,
    state_value: int | None,
    p9sci2e: bool | None,
) -> dict[str, object]:
    comment = _sci_comment(channel, register, access, value if value is not None else state_value, p9sci2e)
    record: dict[str, object] = {
        "address": ins.address,
        "instruction": ins.text,
        "channel": channel,
        "register": register,
        "register_address": register_address,
        "access": access,
        "traced_to_max202": channel == "SCI1",
        "comment": comment,
    }
    if value is not None:
        record["value"] = value
        record["value_hex"] = _hex8(value)
    if register == "SCR":
        record["scr"] = _scr_metadata(value if value is not None else state_value)
    if channel == "SCI2":
        record["p9sci2e"] = p9sci2e
    return record


def _sci_comment(
    channel: str,
    register: str,
    access: str,
    value: int | None,
    p9sci2e: bool | None,
) -> str:
    if channel == "SCI1":
        return _sci1_comment(register, access, value)
    return _sci2_comment(register, access, p9sci2e)


def _sci1_comment(register: str, access: str, value: int | None) -> str:
    if register in {"SMR", "BRR"}:
        return (
            f"SCI1 {register} serial init for traced RS232/MAX202 path "
            "(H8 pin 66 P95/TXD to MAX202 pin 11; MAX202 pin 12 to H8 pin 67 P96/RXD)"
        )
    if register == "SCR":
        bits = _scr_bits_text(value)
        suffix = f" {bits}" if bits else ""
        return (
            f"SCI1 SCR {access}{suffix}; TE/RE select the traced RS232/MAX202 pins "
            "(P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12)"
        )
    if register == "TDR":
        verb = "transmits" if access == "write" else "accesses transmit buffer"
        return (
            f"SCI1 TDR {access} {verb} on traced RS232/MAX202 path: "
            "H8 pin 66 P95/TXD -> MAX202 pin 11"
        )
    if register == "RDR":
        verb = "receives" if access == "read" else "accesses receive buffer"
        return (
            f"SCI1 RDR {access} {verb} from traced RS232/MAX202 path: "
            "MAX202 pin 12 -> H8 pin 67 P96/RXD"
        )
    if register == "SSR":
        return "SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use"
    return f"SCI1 {register} {access} on traced RS232/MAX202 path"


def _sci2_comment(register: str, access: str, p9sci2e: bool | None) -> str:
    prefix = f"SCI2 {register} {access}; not the traced MAX202 path"
    if p9sci2e is False:
        return (
            f"{prefix}; P9SCI2E=0 disables SCI2 pins P92/P93/P94, "
            "while the board trace is SCI1 P95/P96"
        )
    if p9sci2e is True:
        return f"{prefix}; P9SCI2E=1 may route SCI2 pins, but the board trace is SCI1 P95/P96"
    return f"{prefix}; P9SCI2E state unknown, and the board trace is SCI1 P95/P96"


def _syscr2_comment(access: str, p9sci2e: bool | None) -> str:
    if p9sci2e is False:
        return (
            f"SYSCR2 {access} leaves P9SCI2E=0; SCI2 pins are disabled, "
            "so SCI2 is not the traced MAX202 path; traced RS232/MAX202 remains SCI1 P95/P96"
        )
    if p9sci2e is True:
        return (
            f"SYSCR2 {access} sets P9SCI2E=1; SCI2 pins may be enabled, "
            "but Sony RCP-TX7 MAX202 traces are SCI1 P95/P96"
        )
    return f"SYSCR2 {access}; P9SCI2E unknown, board trace remains SCI1 P95/P96"


def _sci_targets(ins: Instruction, access: str) -> list[dict[str, object]]:
    width = _mnemonic_width(ins.mnemonic)
    targets: list[dict[str, object]] = []
    seen: set[int] = set()
    for base_address in ins.references:
        target_width = width if _operand_references_memory(ins, access) else 1
        for index in range(target_width):
            register_address = base_address + index
            if register_address in seen:
                continue
            register_info = SCI_REGISTER_BY_ADDRESS.get(register_address)
            if register_info is None:
                continue
            seen.add(register_address)
            channel, register = register_info
            targets.append(
                {
                    "channel": channel,
                    "register": register,
                    "register_address": register_address,
                    "width": target_width,
                    "index": index,
                },
            )
    return targets


def _expanded_references(ins: Instruction, access: str) -> set[int]:
    width = _mnemonic_width(ins.mnemonic)
    target_width = width if _operand_references_memory(ins, access) else 1
    addresses: set[int] = set()
    for base_address in ins.references:
        for index in range(target_width):
            addresses.add(base_address + index)
    return addresses


def _operand_references_memory(ins: Instruction, access: str) -> bool:
    operand = _destination_operand(ins.operands) if access == "write" else _source_operand(ins.operands)
    return bool(operand and operand.startswith("@"))


def _access_kind(ins: Instruction) -> str:
    root = _mnemonic_root(ins.mnemonic)
    if root in _READ_ONLY_ROOTS:
        return "read"
    destination = _destination_operand(ins.operands)
    if destination and destination.startswith("@"):
        return "write"
    return "read"


def _write_value(
    ins: Instruction,
    register_address: int,
    current_value: int | None,
    width: int,
    index: int,
) -> int | None:
    if _access_kind(ins) != "write":
        return None

    root = _mnemonic_root(ins.mnemonic)
    if root == "CLR":
        return 0
    if root in _BIT_WRITE_ROOTS:
        bit = _immediate_bit(ins.operands)
        if bit is None or current_value is None:
            return None
        if root == "BSET":
            return (current_value | (1 << bit)) & 0xFF
        if root == "BCLR":
            return (current_value & ~(1 << bit)) & 0xFF
        return (current_value ^ (1 << bit)) & 0xFF

    value = _immediate_source_value(ins.operands)
    if value is None:
        return None
    return _byte_for_target(value, width, index)


def _destination_operand(operands: str) -> str | None:
    operands = operands.strip()
    if not operands:
        return None
    if "," not in operands:
        return operands
    return operands.rsplit(",", 1)[1].strip()


def _source_operand(operands: str) -> str | None:
    operands = operands.strip()
    if not operands:
        return None
    if "," not in operands:
        return operands
    return operands.rsplit(",", 1)[0].strip()


def _immediate_source_value(operands: str) -> int | None:
    source = _source_operand(operands) or ""
    if not source.startswith("#"):
        return None
    try:
        return parse_int(source[1:])
    except ValueError:
        return None


def _immediate_bit(operands: str) -> int | None:
    value = _immediate_source_value(operands)
    if value is None:
        return None
    return value if 0 <= value <= 7 else None


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


def _mnemonic_width(mnemonic: str) -> int:
    return 2 if mnemonic.endswith(".W") else 1


def _byte_for_target(value: int, width: int, index: int) -> int:
    shift = 8 * (width - index - 1)
    return (value >> shift) & 0xFF


def _scr_metadata(value: int | None) -> dict[str, object]:
    metadata = _register_state(value)
    metadata.update(
        {
            "tie": None if value is None else bool(value & 0x80),
            "rie": None if value is None else bool(value & 0x40),
            "tx_enabled": None if value is None else bool(value & 0x20),
            "rx_enabled": None if value is None else bool(value & 0x10),
        },
    )
    return metadata


def _register_state(value: int | None) -> dict[str, object]:
    return {
        "value": value,
        "value_hex": None if value is None else _hex8(value),
    }


def _scr_bits_text(value: int | None) -> str:
    if value is None:
        return ""
    return f"TE={1 if value & 0x20 else 0} RE={1 if value & 0x10 else 0}"


def _bit_state(value: int | None, bit: int) -> bool | None:
    if value is None:
        return None
    return bool(value & (1 << bit))


def _dedupe(items: list[str]) -> list[str]:
    seen: set[str] = set()
    output: list[str] = []
    for item in items:
        if item in seen:
            continue
        seen.add(item)
        output.append(item)
    return output


def _hex8(value: int) -> str:
    return f"H'{value & 0xFF:02X}"
