from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass

from .formatting import h16
from .model import Instruction


MANUAL_REFERENCES = [
    "Manual/0900766b802125d0.md:12185 FRT FRC/OCRA/OCRB/ICR use TEMP for 16-bit CPU access",
    "Manual/0900766b802125d0.md:12193 FRT byte access order is upper byte then lower byte",
    "Manual/0900766b802125d0.md:12212 OCRA/OCRB reads are direct; writes still use TEMP",
    "Manual/0900766b802125d0.md:17546 A/D ADDRA-ADDRD lower byte is accessed through TEMP",
    "Manual/0900766b802125d0.md:17556 A/D full-result byte reads must be upper byte then lower byte",
]


@dataclass(frozen=True)
class TempRegisterPair:
    name: str
    high: int
    low: int
    module: str
    read_only: bool = False
    high_only_read_ok: bool = False
    direct_read_without_temp: bool = False


TEMP_REGISTER_PAIRS: tuple[TempRegisterPair, ...] = (
    TempRegisterPair("FRT1_FRC", 0xFE92, 0xFE93, "FRT TEMP"),
    TempRegisterPair("FRT1_OCRA", 0xFE94, 0xFE95, "FRT TEMP", direct_read_without_temp=True),
    TempRegisterPair("FRT1_OCRB", 0xFE96, 0xFE97, "FRT TEMP", direct_read_without_temp=True),
    TempRegisterPair("FRT1_ICR", 0xFE98, 0xFE99, "FRT TEMP", read_only=True),
    TempRegisterPair("FRT2_FRC", 0xFEA2, 0xFEA3, "FRT TEMP"),
    TempRegisterPair("FRT2_OCRA", 0xFEA4, 0xFEA5, "FRT TEMP", direct_read_without_temp=True),
    TempRegisterPair("FRT2_OCRB", 0xFEA6, 0xFEA7, "FRT TEMP", direct_read_without_temp=True),
    TempRegisterPair("FRT2_ICR", 0xFEA8, 0xFEA9, "FRT TEMP", read_only=True),
    TempRegisterPair("FRT3_FRC", 0xFEB2, 0xFEB3, "FRT TEMP"),
    TempRegisterPair("FRT3_OCRA", 0xFEB4, 0xFEB5, "FRT TEMP", direct_read_without_temp=True),
    TempRegisterPair("FRT3_OCRB", 0xFEB6, 0xFEB7, "FRT TEMP", direct_read_without_temp=True),
    TempRegisterPair("FRT3_ICR", 0xFEB8, 0xFEB9, "FRT TEMP", read_only=True),
    TempRegisterPair("ADDRA", 0xFEE0, 0xFEE1, "A/D TEMP", read_only=True, high_only_read_ok=True),
    TempRegisterPair("ADDRB", 0xFEE2, 0xFEE3, "A/D TEMP", read_only=True, high_only_read_ok=True),
    TempRegisterPair("ADDRC", 0xFEE4, 0xFEE5, "A/D TEMP", read_only=True, high_only_read_ok=True),
    TempRegisterPair("ADDRD", 0xFEE6, 0xFEE7, "A/D TEMP", read_only=True, high_only_read_ok=True),
)

TEMP_PAIR_BY_ADDRESS = {
    address: (pair, byte_name)
    for pair in TEMP_REGISTER_PAIRS
    for address, byte_name in ((pair.high, "high"), (pair.low, "low"))
}

READ_ONLY_ROOTS = {"BTST", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}
WRITE_ROOTS = {"BCLR", "BNOT", "BSET", "CLR", "MOV:G", "MOV:S", "MOVTPE", "NEG", "NOT"}


def analyze_peripheral_access(instructions: Mapping[int, Instruction]) -> dict[str, object]:
    annotations: dict[int, list[str]] = {}
    warnings: list[dict[str, object]] = []
    instruction_metadata: dict[int, list[dict[str, object]]] = {}
    pending: dict[tuple[str, str], dict[str, object]] = {}

    for address in sorted(instructions):
        ins = instructions[address]
        accesses = _instruction_accesses(ins)
        if not accesses:
            continue
        instruction_metadata[address] = [_public_access(access) for access in accesses]
        for access in accesses:
            note, warning = _analyze_access(access, pending)
            if note:
                annotations.setdefault(address, []).append(note)
            if warning:
                warnings.append(warning)

    for item in pending.values():
        pair = item["pair"]
        assert isinstance(pair, TempRegisterPair)
        if _pending_high_byte_is_suspicious(pair, str(item["direction"])):
            warnings.append(
                {
                    "address": item["address"],
                    "register": pair.name,
                    "severity": "warning",
                    "message": (
                        f"{pair.name} high byte access was not followed by low byte; "
                        "manual recommends word access or high-byte then low-byte"
                    ),
                    "manual": MANUAL_REFERENCES,
                },
            )

    return {
        "manual_references": MANUAL_REFERENCES,
        "warnings": warnings,
        "annotations": {
            address: "; ".join(parts)
            for address, parts in sorted(annotations.items())
        },
        "instructions": instruction_metadata,
    }


def peripheral_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)
    return str(comment) if comment else ""


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


def peripheral_json_payload(analysis: Mapping[str, object] | None) -> dict[str, object]:
    if not analysis:
        return {"manual_references": MANUAL_REFERENCES, "warnings": []}
    return {
        "manual_references": analysis.get("manual_references", MANUAL_REFERENCES),
        "warnings": analysis.get("warnings", []),
    }


def _analyze_access(
    access: dict[str, object],
    pending: dict[tuple[str, str], dict[str, object]],
) -> tuple[str, dict[str, object] | None]:
    pair = access["pair"]
    assert isinstance(pair, TempRegisterPair)
    direction = str(access["direction"])
    byte = str(access["byte"])
    size = str(access["size"])
    key = (pair.name, direction)

    if size == "W" and byte == "high":
        pending.pop(key, None)
        return f"{pair.name} word {direction}; TEMP byte-order hazard avoided", None

    if byte == "high":
        pending[key] = access
        if direction == "read" and pair.high_only_read_ok:
            return f"{pair.name} high-byte read; valid 8-bit result, read low byte next for full 10-bit value", None
        if direction == "read" and pair.direct_read_without_temp:
            return f"{pair.name} high-byte read; OCRA/OCRB reads do not use TEMP", None
        return f"{pair.name} high-byte {direction}; next low-byte access completes TEMP transfer", None

    previous = pending.pop(key, None)
    if previous is not None:
        return f"{pair.name} low-byte {direction}; completes high->low TEMP transfer", None

    warning = {
        "address": access["address"],
        "register": pair.name,
        "severity": "warning",
        "message": (
            f"{pair.name} low byte {direction} before matching high byte; "
            "TEMP may contain stale or unrelated data"
        ),
        "manual": MANUAL_REFERENCES,
    }
    return f"{pair.name} low-byte {direction} before high byte; check TEMP ordering", warning


def _pending_high_byte_is_suspicious(pair: TempRegisterPair, direction: str) -> bool:
    if direction == "read" and pair.high_only_read_ok:
        return False
    if direction == "read" and pair.direct_read_without_temp:
        return False
    return True


def _instruction_accesses(ins: Instruction) -> list[dict[str, object]]:
    size = _mnemonic_size(ins.mnemonic)
    direction = _access_direction(ins)
    if direction is None:
        return []

    accesses: list[dict[str, object]] = []
    for ref in ins.references:
        pair_info = TEMP_PAIR_BY_ADDRESS.get(ref)
        if pair_info is None:
            continue
        pair, byte = pair_info
        if size == "W" and ref == pair.low:
            byte = "low_unaligned"
        accesses.append(
            {
                "address": ins.address,
                "instruction": ins.text,
                "register": pair.name,
                "pair": pair,
                "high_address": pair.high,
                "low_address": pair.low,
                "referenced_address": ref,
                "referenced_address_hex": h16(ref),
                "byte": byte,
                "size": size,
                "direction": direction,
            },
        )
    return accesses


def _public_access(access: dict[str, object]) -> dict[str, object]:
    return {
        key: value
        for key, value in access.items()
        if key != "pair"
    }


def _access_direction(ins: Instruction) -> str | None:
    root = _mnemonic_root(ins.mnemonic)
    if root in READ_ONLY_ROOTS:
        return "read"
    if root in {"BCLR", "BNOT", "BSET", "CLR", "NEG", "NOT"}:
        return "write"
    if root in {"ADD:Q", "ADD:G", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
        return "write"
    if root in {"MOV:G", "MOV:S", "MOVTPE"}:
        return "write" if _destination_operand(ins.operands).startswith("@") else "read"
    if root in {"MOV:L", "MOV:F", "MOVFPE"}:
        return "read"
    if root == "STC":
        return "write"
    if root == "LDC":
        return "read"
    return None


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


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


def _mnemonic_size(mnemonic: str) -> str:
    return "W" if mnemonic.endswith(".W") else "B"
