#!/usr/bin/env python3
"""
HC900 Register Map Builder

Converts HC Designer CSV exports (SummaryFunctionBlockReport, SignalTags, Variables)
into a unified JSON register map for the Modbus TCP gateway.

Usage:
    python3 build_register_map.py \
        --loop-csv ../docs/SummaryFucntionBlockReport.csv \
        --signal-csv ../docs/SignalTags.csv \
        --variable-csv ../docs/Variables.csv \
        -o ../docs/register-map.json
"""

import csv
import json
import argparse
from pathlib import Path

# ─── PID loop parameter offset map (based on manual Table 6-3) ───

LOOP_PARAM_OFFSETS = [
    # (name, offset_hex, type, access, description)
    # PV / SP / OP
    ("PV",               0x00, "float32", "R",    "Process Variable"),
    ("RSP",              0x02, "float32", "R",    "Remote Set Point (SP2)"),
    ("SP",               0x04, "float32", "RW",   "Working Set Point"),
    ("OP",               0x06, "float32", "RW",   "Output"),
    # Gains / tuning
    ("GAIN1",            0x0C, "float32", "RW",   "Gain #1 / Prop Band #1"),
    ("DIR",              0x0E, "float32", "R",    "Direction (0=Direct, 1=Reverse)"),
    ("RESET1",           0x10, "float32", "RW",   "Reset #1"),
    ("RATE1",            0x12, "float32", "RW",   "Rate #1"),
    # Ranges
    ("PV_LO",            0x16, "float32", "R",    "PV Low Range"),
    ("PV_HI",            0x18, "float32", "R",    "PV High Range"),
    # Alarms
    ("ALM1_SP1",         0x1A, "float32", "RW",   "Alarm #1 SP #1"),
    ("ALM1_SP2",         0x1C, "float32", "RW",   "Alarm #1 SP #2"),
    # Local SPs
    ("LSP1",             0x2A, "float32", "RW",   "Local SP #1"),
    ("LSP2",             0x2C, "float32", "RW",   "Local SP #2"),
    # Limits
    ("SP_LO",            0x34, "float32", "RW",   "SP Low Limit"),
    ("SP_HI",            0x36, "float32", "RW",   "SP High Limit"),
    ("OP_LO",            0x3A, "float32", "RW",   "Output Low Limit"),
    ("OP_HI",            0x3C, "float32", "RW",   "Output High Limit"),
    ("DEV",              0x4A, "float32", "R",    "Deviation (SP-PV)"),
    # Bit-packed status / control
    ("FUZZY_EN",         0xB7, "uint16",  "RW",   "Fuzzy Enable"),
    ("ATUNE_REQ",        0xB8, "uint16",  "RW",   "Autotune Request"),
    ("MODE",             0xBA, "uint16",  "RW",   "Auto/Manual (0=Man, 1=Auto)"),
    ("SP_STATE",         0xBB, "uint16",  "RW",   "Set Point State (0=SP1, 1=SP2)"),
    ("RSP_STATE",        0xBC, "uint16",  "RW",   "Remote/Local SP (0=LSP, 1=RSP)"),
    ("LOOP_STATUS",      0xBE, "uint16",  "R",    "Loop Status (bit packed)"),
]

# ─── helpers ───

def hex_addr(s: str) -> int:
    return int(s.strip(), 16)

def strip_header(rows, header_marker: str):
    """Skip rows until we find the actual data header."""
    for i, row in enumerate(rows):
        if row and row[0] == header_marker:
            return rows[i + 1:]
    return rows

# ─── parsers ───

def parse_loop_csv(path: Path) -> list[dict]:
    """Parse SummaryFunctionBlockReport.csv → list of loop dicts."""
    loops = []
    with open(path, "r", encoding="utf-8-sig") as f:
        reader = csv.reader(f)
        rows = list(reader)

    for row in rows:
        if len(row) < 6:
            continue
        hex_addr_raw = row[0].strip()
        if not hex_addr_raw.startswith("0x"):
            continue
        tag = row[2].strip()
        desc = row[3].strip()
        typ = row[4].strip()
        num = row[5].strip()
        if typ != "PID":
            continue
        addr = hex_addr(hex_addr_raw)
        loops.append({
            "tag": tag,
            "description": desc,
            "start_addr": addr,
            "loop_number": int(num.replace("#", "")),
        })
    return loops


def parse_signal_or_variable_csv(path: Path, access: str) -> list[dict]:
    """Parse SignalTags.csv or Variables.csv → list of tag dicts."""
    tags = []
    with open(path, "r", encoding="utf-8-sig") as f:
        reader = csv.reader(f)
        rows = list(reader)

    for row in rows:
        if len(row) < 8:
            continue
        hex_addr_raw = row[0].strip()
        if not hex_addr_raw.startswith("0x"):
            continue
        tag = row[2].strip()
        desc = row[3].strip()
        typ = row[6].strip() if len(row) > 6 else "float 32"
        eu = row[7].strip() if len(row) > 7 else ""
        # normalise type
        if "float" in typ.lower():
            data_type = "float32"
            register_count = 2
        elif "signed 16" in typ.lower() or "unsigned 16" in typ.lower() or "integer" in typ.lower():
            data_type = "uint16"
            register_count = 1
        else:
            data_type = "float32"
            register_count = 2

        addr = hex_addr(hex_addr_raw)
        tags.append({
            "tag": tag,
            "description": desc,
            "addr": addr,
            "count": register_count,
            "type": data_type,
            "access": access,
            "eu": eu,
        })
    return tags


# ─── main ───

def build(loop_path, signal_path, variable_path, output_path):
    loops = parse_loop_csv(loop_path)
    signals = parse_signal_or_variable_csv(signal_path, "R")
    variables = parse_signal_or_variable_csv(variable_path, "RW")

    registers = []

    # 1. Expand PID loops into individual parameters
    for loop in loops:
        base = loop["start_addr"]
        for param in LOOP_PARAM_OFFSETS:
            name, off, dtype, access, desc_template = param
            reg_count = 2 if dtype == "float32" else 1
            addr = base + off
            tag_name = f"{loop['tag']}.{name}"
            description = f"{loop['description']} / {desc_template}" if loop['description'] else desc_template
            registers.append({
                "tag": tag_name,
                "addr": addr,
                "count": reg_count,
                "type": dtype,
                "access": access,
                "description": description,
            })

    # 2. Signal Tags (read-only)
    for sig in signals:
        registers.append({
            "tag": sig["tag"],
            "addr": sig["addr"],
            "count": sig["count"],
            "type": sig["type"],
            "access": sig["access"],
            "description": sig["description"],
            "eu": sig["eu"],
        })

    # 3. Variables (R/W)
    for var in variables:
        registers.append({
            "tag": var["tag"],
            "addr": var["addr"],
            "count": var["count"],
            "type": var["type"],
            "access": var["access"],
            "description": var["description"],
            "eu": var["eu"],
        })

    # sort by address
    registers.sort(key=lambda r: r["addr"])

    output = {
        "controller": "HC900-C70 Rev 4.4x",
        "report_generated": "2026-06-01",
        "float_format": "FP_B",
        "notes": "FP_B = IEEE 754 Big Endian (byte order 4,3,2,1). "
                 "Address is the first register (0-based). "
                 "float32 uses 2 consecutive registers.",
        "register_count": len(registers),
        "registers": registers,
    }

    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(output, f, indent=2, ensure_ascii=False)

    print(f"✓ Register map written to {output_path}")
    print(f"  Loops:     {len(loops)}")
    print(f"  Signals:   {len(signals)}")
    print(f"  Variables: {len(variables)}")
    print(f"  Total registers in map: {len(registers)}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Build HC900 register map JSON from CSV exports")
    parser.add_argument("--loop-csv", required=True, help="SummaryFunctionBlockReport.csv")
    parser.add_argument("--signal-csv", required=True, help="SignalTags.csv")
    parser.add_argument("--variable-csv", required=True, help="Variables.csv")
    parser.add_argument("-o", "--output", default="register-map.json", help="Output JSON path")
    args = parser.parse_args()
    build(args.loop_csv, args.signal_csv, args.variable_csv, args.output)
