#!/usr/bin/env python3
"""Example: use the C-compatible functions from the Python MSCB module.

Sequence:
1) connect
2) address node
3) read an integer variable
4) write the variable
5) read it again
6) close
"""

from __future__ import annotations

import argparse
import ctypes
import sys
from pathlib import Path
from types import ModuleType


class MscbError(RuntimeError):
    """Raised when an MSCB operation does not return MSCB_SUCCESS."""


def _load_mscb() -> ModuleType:
    """Import python/mscb.py when this example is run from any directory."""
    python_dir = Path(__file__).resolve().parents[1] / "python"
    python_dir_str = str(python_dir)
    if python_dir_str not in sys.path:
        sys.path.insert(0, python_dir_str)
    import mscb

    return mscb


def _check(mscb: ModuleType, status: int, operation: str) -> None:
    if status != mscb.MSCB_SUCCESS:
        raise MscbError(f"{operation} failed with MSCB status {status}")


def _variable_info(mscb: ModuleType, fd: int, node: int, index: int):
    info = mscb.MSCB_INFO_VAR()
    status = mscb.mscb_info_variable(fd, node, index, ctypes.byref(info))
    _check(mscb, status, f"mscb_info_variable(node={node}, index={index})")
    return info


def _read_integer(mscb: ModuleType, fd: int, node: int, index: int) -> int:
    info = _variable_info(mscb, fd, node, index)
    width = int(info.width)
    if width < 1 or width > 8:
        raise MscbError(f"variable {index} has unsupported integer width {width}")
    if int(info.flags) & 1:  # MSCBF_FLOAT
        raise MscbError(f"variable {index} is floating-point, not an integer")
    if int(info.unit) in (55, 56):  # UNIT_ASCII, UNIT_STRING
        raise MscbError(f"variable {index} is textual, not an integer")

    size = ctypes.c_int(width)
    data = ctypes.create_string_buffer(width)
    status = mscb.mscb_read(fd, node, index, data, ctypes.byref(size))
    _check(mscb, status, f"mscb_read(node={node}, index={index})")

    signed = bool(int(info.flags) & (1 << 1))  # MSCBF_SIGNED
    return int.from_bytes(data.raw[: size.value], "little", signed=signed)


def _write_integer(mscb: ModuleType, fd: int, node: int, index: int, value: int) -> None:
    info = _variable_info(mscb, fd, node, index)
    width = int(info.width)
    if width < 1 or width > 8:
        raise MscbError(f"variable {index} has unsupported integer width {width}")
    if int(info.flags) & 1 or int(info.unit) in (55, 56):
        raise MscbError(f"variable {index} is not an integer")

    signed = bool(int(info.flags) & (1 << 1))
    try:
        payload = int(value).to_bytes(width, "little", signed=signed)
    except OverflowError as exc:
        raise MscbError(f"value {value} does not fit in variable {index}") from exc

    data = ctypes.create_string_buffer(payload, len(payload))
    status = mscb.mscb_write(fd, node, index, data, len(payload))
    _check(mscb, status, f"mscb_write(node={node}, index={index})")


def _parse_int(value: str) -> int:
    return int(value, 0)


def main(argv: list[str]) -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--device", default="mscb440", help="Ethernet submaster hostname or IP[:port]")
    parser.add_argument("-p", "--password", default="", help="Optional submaster password")
    parser.add_argument("-n", "--node", type=_parse_int, default=6, help="MSCB node address")
    parser.add_argument("-i", "--index", type=_parse_int, default=160, help="Integer variable index")
    parser.add_argument("-v", "--value", type=_parse_int, default=0, help="Integer value to write")
    args = parser.parse_args(argv)

    mscb = _load_mscb()
    fd = mscb.mscb_init(args.device, len(args.device), args.password, 0)
    if fd < 0:
        print(f"Cannot connect to {args.device}: mscb_init returned {fd}", file=sys.stderr)
        return 1

    try:
        _check(
            mscb,
            mscb.mscb_addr(fd, mscb.MCMD_ADDR_NODE16, args.node, 0, 10),
            f"mscb_addr({args.node})",
        )

        before = _read_integer(mscb, fd, args.node, args.index)
        print(f"before: node={args.node} index={args.index} value={before}")

        _write_integer(mscb, fd, args.node, args.index, args.value)

        after = _read_integer(mscb, fd, args.node, args.index)
        print(f"after : node={args.node} index={args.index} value={after}")
    except MscbError as exc:
        print(f"MSCB error: {exc}", file=sys.stderr)
        return 1
    finally:
        mscb.mscb_exit(fd)

    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
