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},
    "SCI2": {"SMR": 0xFEF0, "BRR": 0xFEF1, "SCR": 0xFEF2},
}

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

FORMULAS = {
    "async": "B = clock_hz / (64 * 2^(2n) * (N + 1))",
    "sync": "B = clock_hz / (8 * 2^(2n) * (N + 1))",
}

MANUAL_REFERENCES = [
    "Manual/0900766b802125d0.md:15837 SMR selects SCI mode and CKS1/CKS0 internal clock source",
    "Manual/0900766b802125d0.md:16027 SCR.CKE1 selects internal or external clock source",
    "Manual/0900766b802125d0.md:16177 BRR and SMR.CKS determine the baud-rate generator",
    "Manual/0900766b802125d0.md:16303 asynchronous BRR formula",
    "Manual/0900766b802125d0.md:16379 synchronous BRR formula",
    "Manual/0900766b802125d0.md:16410 SCI clock source selection tables",
]

_READ_ONLY_ROOTS = {"BTST", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}


def analyze_sci(
    instructions: Mapping[int, Instruction],
    clock_hz: int | None = None,
) -> dict[str, object]:
    """Track SCI setup writes in listing order and infer conservative baud metadata."""
    state: dict[str, dict[str, int | None]] = {
        channel: {"SMR": None, "BRR": None, "SCR": None} for channel in SCI_REGISTERS
    }
    channels: dict[str, dict[str, list[dict[str, object]]]] = {
        channel: {"writes": [], "configurations": []} for channel in SCI_REGISTERS
    }
    annotations: dict[int, str] = {}
    instruction_metadata: dict[int, dict[str, object]] = {}
    seen_configurations: set[tuple[object, ...]] = set()

    for address in sorted(instructions):
        ins = instructions[address]
        writes = _apply_instruction(ins, state)
        if not writes:
            continue

        affected_channels = sorted({str(write["channel"]) for write in writes})
        instruction_entry: dict[str, object] = {"writes": writes}
        for write in writes:
            channel = str(write["channel"])
            channels[channel]["writes"].append(write)

        emitted: list[dict[str, object]] = []
        for channel in affected_channels:
            inference = _infer_channel(channel, state[channel], clock_hz)
            if inference is None:
                continue
            key = _configuration_key(channel, inference, clock_hz)
            if key in seen_configurations:
                continue
            seen_configurations.add(key)
            inference = {**inference, "address": ins.address, "instruction": ins.text}
            channels[channel]["configurations"].append(inference)
            emitted.append(inference)

        if emitted:
            annotation = "; ".join(str(item["comment"]) for item in emitted if item.get("comment"))
            if annotation:
                annotations[ins.address] = annotation
            instruction_entry["inferences"] = emitted
        instruction_metadata[ins.address] = instruction_entry

    return {
        "clock_hz": clock_hz,
        "formulas": FORMULAS,
        "manual_references": MANUAL_REFERENCES,
        "channels": channels,
        "annotations": annotations,
        "instructions": instruction_metadata,
    }


def sci_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 sci_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)
    return metadata if isinstance(metadata, dict) else None


def sci_json_payload(analysis: Mapping[str, object] | None) -> dict[str, object]:
    if not analysis:
        return {
            "clock_hz": None,
            "formulas": FORMULAS,
            "manual_references": MANUAL_REFERENCES,
            "channels": {channel: {"writes": [], "configurations": []} for channel in SCI_REGISTERS},
        }
    return {
        "clock_hz": analysis.get("clock_hz"),
        "formulas": analysis.get("formulas", FORMULAS),
        "manual_references": analysis.get("manual_references", MANUAL_REFERENCES),
        "channels": analysis.get("channels", {}),
    }


def _apply_instruction(ins: Instruction, state: dict[str, dict[str, int | None]]) -> list[dict[str, object]]:
    root = _mnemonic_root(ins.mnemonic)
    if root in _READ_ONLY_ROOTS:
        return []

    size = _mnemonic_size(ins.mnemonic)
    base_address = _destination_base_address(ins, size)
    if base_address is None:
        return []

    operation = root
    value: int | None
    if root in {"BCLR", "BNOT", "BSET"}:
        bit = _immediate_bit(ins.operands)
        value = None
        targets = list(_target_registers(base_address, "B"))
        if bit is not None and len(targets) == 1:
            channel, register, _address = targets[0]
            current = state[channel][register]
            if current is not None:
                if root == "BSET":
                    value = current | (1 << bit)
                elif root == "BCLR":
                    value = current & ~(1 << bit)
                else:
                    value = current ^ (1 << bit)
                value &= 0xFF
        return _record_writes(ins, targets, value, operation, state)

    if root == "CLR":
        value = 0
    else:
        value = _immediate_source_value(ins.operands)
        if value is None and root == "NOT":
            targets = list(_target_registers(base_address, size))
            if len(targets) == 1:
                channel, register, _address = targets[0]
                current = state[channel][register]
                value = None if current is None else (~current) & 0xFF

    targets = list(_target_registers(base_address, size))
    if not targets:
        return []
    if root in {"CMP", "CMP:E", "CMP:G", "CMP:I", "TST"}:
        return []
    return _record_writes(ins, targets, value, operation, state)


def _record_writes(
    ins: Instruction,
    targets: list[tuple[str, str, int]],
    value: int | None,
    operation: str,
    state: dict[str, dict[str, int | None]],
) -> list[dict[str, object]]:
    width = max(len(targets), 1)
    writes: list[dict[str, object]] = []
    for index, (channel, register, register_address) in enumerate(targets):
        byte_value = _byte_for_target(value, width, index)
        state[channel][register] = byte_value
        event: dict[str, object] = {
            "address": ins.address,
            "instruction": ins.text,
            "channel": channel,
            "register": register,
            "register_address": register_address,
            "operation": operation,
            "value": byte_value,
        }
        if byte_value is not None:
            event["value_hex"] = _hex8(byte_value)
        writes.append(event)
    return writes


def _target_registers(base_address: int, size: str) -> list[tuple[str, str, int]]:
    width = 2 if size == "W" else 1
    targets: list[tuple[str, str, int]] = []
    for offset in range(width):
        address = base_address + offset
        register = SCI_REGISTER_BY_ADDRESS.get(address)
        if register is None:
            continue
        channel, name = register
        targets.append((channel, name, address))
    return targets


def _destination_base_address(ins: Instruction, size: str) -> int | None:
    destination = _destination_operand(ins.operands)
    if destination is None or not destination.startswith("@"):
        return None
    width = 2 if size == "W" else 1
    for address in ins.references:
        if any((address + offset) in SCI_REGISTER_BY_ADDRESS for offset in range(width)):
            return address
    return None


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 _immediate_source_value(operands: str) -> int | None:
    source = operands.rsplit(",", 1)[0].strip() if "," in operands else ""
    if not source.startswith("#"):
        return None
    try:
        return parse_int(source[1:])
    except ValueError:
        return None


def _immediate_bit(operands: str) -> int | None:
    source = operands.rsplit(",", 1)[0].strip() if "," in operands else ""
    if not source.startswith("#"):
        return None
    try:
        bit = parse_int(source[1:])
    except ValueError:
        return None
    return bit if 0 <= bit <= 7 else None


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


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


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


def _infer_channel(
    channel: str,
    registers: Mapping[str, int | None],
    clock_hz: int | None,
) -> dict[str, object] | None:
    smr = registers.get("SMR")
    brr = registers.get("BRR")
    scr = registers.get("SCR")
    if smr is None or brr is None:
        return None

    mode = "sync" if smr & 0x80 else "async"
    n = smr & 0x03
    denominator = _baud_denominator(mode, n, brr)
    clock_source = "unknown" if scr is None else ("external" if scr & 0x02 else "internal")
    inference: dict[str, object] = {
        "channel": channel,
        "mode": mode,
        "mode_summary": _mode_summary(smr),
        "smr": smr,
        "smr_hex": _hex8(smr),
        "brr": brr,
        "brr_hex": _hex8(brr),
        "scr": scr,
        "scr_hex": _hex8(scr) if scr is not None else None,
        "cks_n": n,
        "cks_divisor": 1 << (2 * n),
        "denominator": denominator,
        "clock_source": clock_source,
        "formula": FORMULAS[mode],
    }

    if clock_hz is None:
        inference["baud_bps"] = None
        inference["confidence"] = "partial"
        inference["reason"] = "clock_hz_missing"
        inference["comment"] = (
            f"{channel} {_mode_summary(smr)} BRR N={brr} CKS n={n}; baud needs --clock-hz"
        )
        return inference

    if clock_source == "external":
        inference["baud_bps"] = None
        inference["confidence"] = "partial"
        inference["reason"] = "external_clock_selected"
        inference["comment"] = (
            f"{channel} {_mode_summary(smr)} external clock selected; "
            f"BRR N={brr} CKS n={n}, internal baud not inferred"
        )
        return inference

    baud = clock_hz / denominator
    inference["baud_bps"] = baud
    inference["confidence"] = "high" if clock_source == "internal" else "partial"
    inference["reason"] = None if clock_source == "internal" else "scr_clock_source_unknown"
    noun = "baud" if clock_source == "internal" else "baud generator"
    source_note = "; SCR clock source unknown" if clock_source == "unknown" else ""
    inference["comment"] = (
        f"{channel} {_mode_summary(smr)} {noun} {_format_bps(baud)} "
        f"(BRR N={brr}, CKS n={n}, clock={clock_hz} Hz{source_note})"
    )
    return inference


def _configuration_key(channel: str, inference: Mapping[str, object], clock_hz: int | None) -> tuple[object, ...]:
    return (
        channel,
        inference.get("smr"),
        inference.get("brr"),
        inference.get("clock_source"),
        clock_hz,
    )


def _baud_denominator(mode: str, n: int, brr: int) -> int:
    base = 8 if mode == "sync" else 64
    return base * (1 << (2 * n)) * (brr + 1)


def _mode_summary(smr: int) -> str:
    if smr & 0x80:
        return "sync"
    char_len = "7-bit" if smr & 0x40 else "8-bit"
    if smr & 0x20:
        parity = "odd parity" if smr & 0x10 else "even parity"
    else:
        parity = "no parity"
    stop = "2 stop" if smr & 0x08 else "1 stop"
    return f"async {char_len} {parity} {stop}"


def _format_bps(baud: float) -> str:
    rounded = round(baud)
    if abs(baud - rounded) < 0.001:
        return f"{rounded} bps"
    return f"{baud:.3f}".rstrip("0").rstrip(".") + " bps"


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