from __future__ import annotations

import argparse
from pathlib import Path

from .analysis import build_call_graph, collect_labels, linear_sweep, trace
from .board_profile import analyze_board_profile
from .cycles import annotate_cycles
from .data_analysis import analyze_unreached_data
from .dataflow import analyze_dataflow
from .decoder import H8536Decoder
from .formatting import parse_int
from .indirect import analyze_indirect_flow
from .lcd_driver import analyze_lcd_driver
from .lcd_text import analyze_lcd_text
from .peripheral_access import analyze_peripheral_access
from .render import format_callgraph_dot, format_listing, write_json
from .rom import Rom
from .sci import analyze_sci
from .sci_protocol import analyze_sci_protocol
from .serial_reconstruction import analyze_serial_reconstruction
from .symbols import discover_symbols
from .timing import summarize_timing
from .vectors import read_dtc_vectors_max, read_dtc_vectors_min, read_vectors_max, read_vectors_min


def default_end(data: bytes) -> int:
    last = 0
    for idx, value in enumerate(data):
        if value != 0xFF:
            last = idx
    return min(len(data), ((last + 0x100) // 0x100) * 0x100)


def main() -> int:
    parser = argparse.ArgumentParser(description="Disassemble/decompile a Hitachi H8/536 ROM image.")
    parser.add_argument("rom", nargs="?", default="ROM/M27C512@DIP28_1.BIN", type=Path, help="ROM binary to decode")
    parser.add_argument("--out", type=Path, default=Path("build/rom_decompiled.asm"), help="assembly listing output")
    parser.add_argument("--json", type=Path, default=None, help="optional structured JSON output")
    parser.add_argument("--mode", choices=("min", "max"), default="min", help="CPU/vector mode; this ROM appears to be min")
    parser.add_argument("--start", type=parse_int, default=0x0000, help="decode lower address limit")
    parser.add_argument("--end", type=parse_int, default=None, help="decode upper address limit, exclusive")
    parser.add_argument("--entry", type=parse_int, action="append", default=[], help="extra entry point to trace")
    parser.add_argument("--br", type=parse_int, default=None, help="optional BR value for @aa:8 short absolute operands")
    parser.add_argument("--clock-hz", type=parse_int, default=None, help="oscillator clock in Hz for SCI baud inference")
    parser.add_argument(
        "--board-profile",
        choices=("sony_rcp_tx7", "none"),
        default="sony_rcp_tx7",
        help="emit board-specific annotations for known physical traces",
    )
    parser.add_argument("--linear", action="store_true", help="linear-sweep the selected range instead of tracing from vectors")
    parser.add_argument("--cycles", action="store_true", help="append Appendix A cycle estimates to assembly comments")
    parser.add_argument("--timing", action="store_true", help="include straight-line block and loop cycle summaries")
    parser.add_argument("--callgraph-dot", type=Path, default=None, help="optional Graphviz DOT call graph output")
    args = parser.parse_args()
    if args.clock_hz is not None and args.clock_hz <= 0:
        parser.error("--clock-hz must be positive")

    data = args.rom.read_bytes()
    rom = Rom(data)
    end = args.end if args.end is not None else default_end(data)

    vectors = read_vectors_min(rom) if args.mode == "min" else read_vectors_max(rom)
    dtc_vectors = read_dtc_vectors_min(rom) if args.mode == "min" else read_dtc_vectors_max(rom)
    starts = [target for _name, target in vectors.values()] + args.entry

    labels: dict[int, str] = {}
    decoder = H8536Decoder(rom, br=args.br, labels=labels)
    decoder.br = args.br
    if args.linear:
        instructions = linear_sweep(decoder, args.start, end)
    else:
        instructions = trace(decoder, starts, args.start, end)

    labels.update(collect_labels(instructions.values(), vectors))
    decoder.labels = labels

    # Re-decode now that labels are known, so operands can use stable symbols.
    decoder.br = args.br
    if args.linear:
        instructions = linear_sweep(decoder, args.start, end)
    else:
        instructions = trace(decoder, starts, args.start, end)
    labels.update(collect_labels(instructions.values(), vectors))
    annotate_cycles(instructions, args.mode)
    data_candidates = analyze_unreached_data(rom, instructions, args.start, end)
    call_graph = build_call_graph(instructions, vectors, labels)
    dataflow = analyze_dataflow(instructions, labels, call_graph)
    symbols = discover_symbols(instructions, data_candidates=data_candidates)
    timing_summary = summarize_timing(instructions, labels, call_graph) if args.timing else None
    sci_analysis = analyze_sci(instructions, clock_hz=args.clock_hz)
    sci_protocol = analyze_sci_protocol(instructions)
    serial_reconstruction = analyze_serial_reconstruction(instructions)
    board_profile = (
        None
        if args.board_profile == "none"
        else analyze_board_profile(instructions, board=args.board_profile)
    )
    peripheral_access = analyze_peripheral_access(instructions)
    indirect_flow = analyze_indirect_flow(rom, instructions, labels)
    lcd_text = analyze_lcd_text(rom, instructions, start=args.start, end=end)
    lcd_driver = analyze_lcd_driver(instructions)

    args.out.parent.mkdir(parents=True, exist_ok=True)
    args.out.write_text(
        format_listing(
            args.rom,
            rom,
            instructions,
            vectors,
            labels,
            args.mode,
            traced=not args.linear,
            dtc_vectors=dtc_vectors,
            data_candidates=data_candidates,
            timing_summary=timing_summary,
            show_cycles=args.cycles,
            sci_analysis=sci_analysis,
            sci_protocol=sci_protocol,
            serial_reconstruction=serial_reconstruction,
            board_profile=board_profile,
            peripheral_access=peripheral_access,
            indirect_flow=indirect_flow,
            dataflow=dataflow,
            symbols=symbols,
            lcd_text=lcd_text,
            lcd_driver=lcd_driver,
        ),
        encoding="utf-8",
    )
    if args.json:
        args.json.parent.mkdir(parents=True, exist_ok=True)
        write_json(
            args.json,
            instructions,
            vectors,
            labels,
            dtc_vectors=dtc_vectors,
            data_candidates=data_candidates,
            call_graph=call_graph,
            timing_summary=timing_summary,
            sci_analysis=sci_analysis,
            sci_protocol=sci_protocol,
            serial_reconstruction=serial_reconstruction,
            board_profile=board_profile,
            peripheral_access=peripheral_access,
            indirect_flow=indirect_flow,
            dataflow=dataflow,
            symbols=symbols,
            lcd_text=lcd_text,
            lcd_driver=lcd_driver,
        )
    if args.callgraph_dot:
        args.callgraph_dot.parent.mkdir(parents=True, exist_ok=True)
        args.callgraph_dot.write_text(format_callgraph_dot(call_graph), encoding="utf-8")

    invalid = sum(1 for ins in instructions.values() if not ins.valid)
    print(f"wrote {args.out} ({len(instructions)} items, {invalid} invalid/data bytes)")
    if args.json:
        print(f"wrote {args.json}")
    if args.callgraph_dot:
        print(f"wrote {args.callgraph_dot}")
    return 0
