"""PySequencer example that displays all variables from one MSCB node.

To use this script, copy it to the
``<experiment_dir>/userfiles/sequencer/`` directory. When starting the
sequence, enter the Ethernet submaster and node address.
"""

import ctypes
import html
import os
import struct
import sys

import mscb

MSCBF_FLOAT = 1 << 0
MSCBF_SIGNED = 1 << 1
UNIT_STRING = 56


def check_status(status, operation):
    if status != mscb.MSCB_SUCCESS:
        raise RuntimeError("%s failed with MSCB status %d" % (operation, status))


def decode_value(info, data):
    """Decode raw MSCB bytes using the variable metadata."""
    width = int(info.width)
    raw = data[:width]

    if int(info.unit) == UNIT_STRING:
        return raw.split(b"\0", 1)[0].decode("latin-1", errors="replace")

    if int(info.flags) & MSCBF_FLOAT:
        if width == 4:
            return struct.unpack("<f", raw)[0]
        if width == 8:
            return struct.unpack("<d", raw)[0]
        raise RuntimeError("unsupported floating-point width %d" % width)

    return int.from_bytes(
        raw,
        byteorder="little",
        signed=bool(int(info.flags) & MSCBF_SIGNED),
    )


def format_value(value):
    if isinstance(value, float):
        return "%.3f" % value
    return str(value)


def define_params(seq):
    seq.register_param("device", "Ethernet submaster hostname or IP", "mscb000")
    seq.register_param("node", "MSCB node address", 6)
    seq.register_param("password", "Optional submaster password", "")


def sequence(seq):
    device = str(seq.get_param("device"))
    node = int(seq.get_param("node"))
    password = str(seq.get_param("password"))

    fd = mscb.mscb_init(device, len(device), password, 0)
    if fd < 0:
        raise RuntimeError("Cannot connect to %s: MSCB status %d" % (device, fd))

    try:
        check_status(mscb.mscb_ping(fd, node, 0, 1), "mscb_ping")

        node_info = mscb.MSCB_INFO()
        check_status(
            mscb.mscb_info(fd, node, ctypes.byref(node_info)),
            "mscb_info",
        )

        lines = ["Node %d variables:" % node]
        variable_count = 0
        for index in range(int(node_info.n_variables)):
            info = mscb.MSCB_INFO_VAR()
            status = mscb.mscb_info_variable(fd, node, index, ctypes.byref(info))
            if status != mscb.MSCB_SUCCESS:
                lines.append("%3d: <metadata error: MSCB status %d>" % (index, status))
                continue
            variable_count += 1

            name = bytes(info.name).split(b"\0", 1)[0].decode(
                "latin-1", errors="replace"
            )
            if not name:
                name = "(unnamed)"

            width = int(info.width)
            if width <= 0:
                lines.append(
                    "%3d: %-16s <invalid width %d>" % (index, name, width)
                )
                continue

            size = ctypes.c_int(width)
            data = ctypes.create_string_buffer(width)
            status = mscb.mscb_read(fd, node, index, data, ctypes.byref(size))
            if status != mscb.MSCB_SUCCESS:
                lines.append(
                    "%3d: %-16s <read error: MSCB status %d>"
                    % (index, name, status)
                )
                continue

            try:
                value = decode_value(info, data.raw[:size.value])
            except (RuntimeError, struct.error, ValueError) as exc:
                value = "<decode error: %s>" % exc
            lines.append("%3d: %-16s %s" % (index, name, format_value(value)))

        if variable_count == 0:
            lines.append("(no variables)")

        message = (
            '<pre style="font-family: monospace; text-align: left; margin: 0;">'
            + html.escape("\n".join(lines) + "\n")
            + "</pre>"
        )
        seq.sequencer_msg(message, wait=True)
    finally:
        mscb.mscb_exit(fd)
