from __future__ import annotations

import re
from collections.abc import Iterable, Mapping

from .formatting import h16
from .model import Instruction


LCD_STATUS_CONTROL = 0xF200
LCD_DATA = 0xF201
LCD_ADDRESSES = {
    LCD_STATUS_CONTROL: "lcd_status_control",
    LCD_DATA: "lcd_data",
}
ADDRESS_RE = re.compile(r"H'(?P<hex>[0-9A-Fa-f]{4})|0x(?P<c_hex>[0-9A-Fa-f]{4})")
REGISTER_RE = re.compile(r"\b(?P<reg>R[0-7])\b")


def analyze_lcd_driver(instructions: Mapping[int, Instruction] | Iterable[Instruction]) -> dict[str, object]:
    ordered = _instruction_sequence(instructions)
    by_address = {ins.address: ins for ins in ordered}
    accesses = [_access_for_instruction(ins) for ins in ordered]
    accesses = [access for access in accesses if access]
    instruction_metadata: dict[int, list[dict[str, object]]] = {}
    for access in accesses:
        instruction_metadata.setdefault(int(access["address"]), []).append(access)

    polling_loops = _find_polling_loops(ordered, by_address)
    for loop in polling_loops:
        for address, role in (
            (int(loop["read_address"]), "lcd_busy_status_read"),
            (int(loop["test_address"]), "lcd_busy_flag_test"),
            (int(loop["branch_address"]), "lcd_busy_wait_branch"),
        ):
            instruction_metadata.setdefault(address, []).append(
                {
                    "address": address,
                    "kind": role,
                    "summary": loop["summary"],
                    "loop_start": loop["read_address"],
                },
            )

    routines = _routine_candidates(ordered, accesses, polling_loops)
    return {
        "addresses": [
            {"address": address, "name": name, "role": _address_role(address)}
            for address, name in LCD_ADDRESSES.items()
        ],
        "accesses": accesses,
        "polling_loops": polling_loops,
        "routines": routines,
        "instructions": instruction_metadata,
    }


def lcd_comment_for_instruction(analysis: Mapping[str, object] | None, address: int) -> str:
    metadata = lcd_metadata_for_instruction(analysis, address)
    if not metadata:
        return ""
    summaries = []
    for item in metadata:
        if isinstance(item, Mapping) and item.get("summary"):
            summary = str(item["summary"])
            if summary not in summaries:
                summaries.append(summary)
    return "; ".join(summaries)


def lcd_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 list(metadata) if isinstance(metadata, list) else []


def _instruction_sequence(instructions: Mapping[int, Instruction] | Iterable[Instruction]) -> list[Instruction]:
    values = instructions.values() if isinstance(instructions, Mapping) else instructions
    return sorted(values, key=lambda ins: ins.address)


def _access_for_instruction(ins: Instruction) -> dict[str, object] | None:
    address = _lcd_address_for_instruction(ins)
    if address is None:
        return None
    direction = _direction_for_instruction(ins, address)
    role = _access_role(ins, address, direction)
    register = _last_register(ins.operands)
    return {
        "address": ins.address,
        "instruction": ins.text,
        "lcd_address": address,
        "lcd_name": LCD_ADDRESSES[address],
        "direction": direction,
        "role": role,
        "register": register,
        "summary": _access_summary(address, direction, role),
    }


def _lcd_address_for_instruction(ins: Instruction) -> int | None:
    for address in ins.references:
        if address in LCD_ADDRESSES:
            return address
    for match in ADDRESS_RE.finditer(ins.operands):
        value = int(match.group("hex") or match.group("c_hex"), 16)
        if value in LCD_ADDRESSES:
            return value
    return None


def _direction_for_instruction(ins: Instruction, lcd_address: int) -> str:
    mnemonic = ins.mnemonic.upper()
    operands = [part.strip() for part in ins.operands.split(",")]
    if mnemonic.startswith("MOVFPE"):
        return "read"
    if mnemonic.startswith("MOVTPE"):
        return "write"
    if len(operands) >= 2:
        source_has_lcd = _operand_has_address(operands[0], lcd_address)
        dest_has_lcd = _operand_has_address(operands[-1], lcd_address)
        if source_has_lcd and not dest_has_lcd:
            return "read"
        if dest_has_lcd and not source_has_lcd:
            return "write"
    return "unknown"


def _operand_has_address(operand: str, address: int) -> bool:
    return f"H'{address:04X}" in operand.upper() or f"0X{address:04X}" in operand.upper()


def _access_role(ins: Instruction, address: int, direction: str) -> str:
    if address == LCD_STATUS_CONTROL and direction == "read":
        return "lcd_status_read"
    if address == LCD_STATUS_CONTROL and direction == "write":
        return "lcd_command_or_address_write"
    if address == LCD_DATA and direction == "read":
        return "lcd_data_read"
    if address == LCD_DATA and direction == "write":
        return "lcd_data_write"
    return "lcd_access"


def _access_summary(address: int, direction: str, role: str) -> str:
    if role == "lcd_status_read":
        return f"LCD status read from E-clock {h16(address)}"
    if role == "lcd_command_or_address_write":
        return f"LCD command/address write to E-clock {h16(address)}"
    if role == "lcd_data_read":
        return f"LCD data read from E-clock {h16(address)}"
    if role == "lcd_data_write":
        return f"LCD data write to E-clock {h16(address)}"
    return f"LCD {direction} at E-clock {h16(address)}"


def _find_polling_loops(
    ordered: list[Instruction],
    by_address: Mapping[int, Instruction],
) -> list[dict[str, object]]:
    loops: list[dict[str, object]] = []
    for index, ins in enumerate(ordered):
        access = _access_for_instruction(ins)
        if not access or access["role"] != "lcd_status_read":
            continue
        register = access.get("register")
        if not isinstance(register, str):
            continue
        test = _next_register_bit_test(ordered, index, register)
        if test is None:
            continue
        branch = _next_back_branch_to(ordered, by_address, ordered.index(test), ins.address, test.address)
        if branch is None:
            continue
        loops.append(
            {
                "read_address": ins.address,
                "test_address": test.address,
                "branch_address": branch.address,
                "register": register,
                "bit": 7,
                "summary": f"LCD busy-flag poll: read {h16(LCD_STATUS_CONTROL)}, test bit 7, branch until clear",
            },
        )
    return loops


def _next_register_bit_test(ordered: list[Instruction], index: int, register: str) -> Instruction | None:
    for candidate in ordered[index + 1 : index + 5]:
        if not candidate.mnemonic.upper().startswith("BTST"):
            continue
        if "#7" not in candidate.operands:
            continue
        if re.search(rf"\b{re.escape(register)}\b", candidate.operands):
            return candidate
    return None


def _next_back_branch_to(
    ordered: list[Instruction],
    by_address: Mapping[int, Instruction],
    index: int,
    read_address: int,
    test_address: int,
) -> Instruction | None:
    _ = by_address
    for candidate in ordered[index + 1 : index + 5]:
        if candidate.kind != "branch":
            continue
        targets = [int(target) for target in candidate.targets]
        if read_address in targets or test_address in targets:
            return candidate
    return None


def _routine_candidates(
    ordered: list[Instruction],
    accesses: list[dict[str, object]],
    polling_loops: list[dict[str, object]],
) -> list[dict[str, object]]:
    if not accesses:
        return []
    address_to_access = {int(access["address"]): access for access in accesses}
    loop_addresses = {int(loop["read_address"]) for loop in polling_loops}
    routines: dict[tuple[int, int], dict[str, object]] = {}
    for address in sorted(address_to_access):
        start, end = _routine_span(ordered, address)
        key = (start, end)
        routine = routines.setdefault(
            key,
            {
                "start": start,
                "end": end,
                "accesses": [],
                "roles": [],
                "role_hint": "lcd_access",
            },
        )
        routine["accesses"].append(address_to_access[address])
    for routine in routines.values():
        roles = sorted({str(access["role"]) for access in routine["accesses"]})
        routine["roles"] = roles
        routine["role_hint"] = _routine_role_hint(routine, loop_addresses)
    return sorted(routines.values(), key=lambda item: int(item["start"]))


def _routine_span(ordered: list[Instruction], address: int) -> tuple[int, int]:
    addresses = [ins.address for ins in ordered]
    index = addresses.index(address)
    start = ordered[index].address
    cursor = index - 1
    while cursor >= 0:
        previous = ordered[cursor]
        if previous.kind in {"return", "rte"}:
            break
        if previous.address + max(previous.size, 1) != start:
            break
        start = previous.address
        cursor -= 1

    end = ordered[index].address
    cursor = index
    while cursor < len(ordered):
        current = ordered[cursor]
        end = current.address
        if current.kind in {"return", "rte"}:
            break
        next_index = cursor + 1
        if next_index >= len(ordered):
            break
        next_address = ordered[next_index].address
        if current.address + max(current.size, 1) != next_address:
            break
        cursor = next_index
    return start, end


def _routine_role_hint(routine: Mapping[str, object], loop_addresses: set[int]) -> str:
    access_addresses = {int(access["address"]) for access in routine.get("accesses", []) if isinstance(access, Mapping)}
    roles = {str(role) for role in routine.get("roles", [])}
    if access_addresses & loop_addresses and "lcd_data_write" in roles and "lcd_command_or_address_write" in roles:
        return "lcd_wait_and_transfer"
    if access_addresses & loop_addresses:
        return "lcd_wait_ready"
    if "lcd_data_write" in roles:
        return "lcd_write_data"
    if "lcd_command_or_address_write" in roles:
        return "lcd_write_command_or_address"
    if "lcd_data_read" in roles:
        return "lcd_read_data"
    return "lcd_access"


def _last_register(operands: str) -> str | None:
    matches = list(REGISTER_RE.finditer(operands))
    return matches[-1].group("reg") if matches else None


def _address_role(address: int) -> str:
    if address == LCD_STATUS_CONTROL:
        return "status/control register inferred from busy polling and command writes"
    if address == LCD_DATA:
        return "data register inferred from paired data reads/writes"
    return "unknown"
