"""Synchronous serial/TCP transport for standalone pyvisa-ar488 use.

When pyvisa-ar488 runs without mcgpib (e.g. in a Jupyter notebook or
standalone pymeasure script), this transport talks directly to the
AR488 adapter over a serial port or TCP socket.

The init sequence matches mcgpib's BridgeConnection.connect():
  ++verbose 0 / ++prompt 0 / ++auto 0 / ++mode 1
  ++eoi 1 / ++eos 0 / ++read_tmo_ms <timeout>
"""

import logging
import socket
import time
from typing import IO

import serial

from pyvisa_ar488.protocols import AR488Transport

logger = logging.getLogger(__name__)

# Inter-command delay (seconds) — prevents ESP32 brownout
_INTER_CMD_DELAY = 0.010


class DirectSerialTransport(AR488Transport):
    """Synchronous serial transport for AR488/Prologix adapters."""

    def __init__(
        self,
        port: str = "/dev/ttyUSB0",
        baudrate: int = 115200,
        read_timeout_ms: int = 3000,
    ):
        self._port = port
        self._baudrate = baudrate
        self._read_timeout_ms = read_timeout_ms
        self._serial: serial.Serial | None = None
        self._current_address: int | None = None

    def connect(self) -> None:
        self._serial = serial.Serial(
            port=self._port,
            baudrate=self._baudrate,
            timeout=self._read_timeout_ms / 1000.0,
            write_timeout=2.0,
        )
        # Drain any buffered output (macro 0 may have auto-executed)
        time.sleep(0.5)
        self._drain()
        self._init_sequence()
        logger.info("Connected to AR488 on %s @ %d baud", self._port, self._baudrate)

    def disconnect(self) -> None:
        if self._serial and self._serial.is_open:
            self._serial.close()
        self._serial = None
        self._current_address = None

    def is_connected(self) -> bool:
        return self._serial is not None and self._serial.is_open

    def send_command(self, command: str) -> None:
        self._write_line(command)

    def send_raw(self, command: str) -> None:
        self._write_line(command)
        time.sleep(_INTER_CMD_DELAY)

    def read_response(self, timeout_ms: int = 10000) -> str:
        self._write_line("++read eoi")
        return self._read_line(timeout_ms)

    def set_address(self, address: int) -> None:
        if self._current_address != address:
            self._write_line(f"++addr {address}")
            time.sleep(_INTER_CMD_DELAY)
            self._current_address = address

    def serial_poll(self, address: int) -> int:
        self._write_line(f"++spoll {address}")
        time.sleep(_INTER_CMD_DELAY)
        response = self._read_line(self._read_timeout_ms)
        return int(response.strip())

    def find_listeners(self) -> list[int]:
        self._write_line("++findlstn")
        response = self._read_line(max(5000, self._read_timeout_ms))
        addresses = []
        for token in response.split():
            token = token.strip()
            if token.isdigit():
                addr = int(token)
                if 0 <= addr <= 30:
                    addresses.append(addr)
        return addresses

    def interface_clear(self) -> None:
        self._write_line("++ifc")
        time.sleep(_INTER_CMD_DELAY)

    # --- Internal helpers ---

    def _init_sequence(self) -> None:
        """Run the AR488 init sequence — order matters."""
        self._write_line("++verbose 0")
        time.sleep(0.05)
        self._drain()

        self._write_line("++prompt 0")
        time.sleep(0.05)
        self._drain()

        for cmd in [
            "++auto 0",
            "++mode 1",
            "++eoi 1",
            "++eos 0",
            f"++read_tmo_ms {self._read_timeout_ms}",
        ]:
            self._write_line(cmd)
            time.sleep(_INTER_CMD_DELAY)

        # Verify communication
        self._write_line("++ver")
        version = self._read_line(2000)
        if not version:
            raise ConnectionError("AR488: no response to ++ver")
        logger.info("AR488 firmware: %s", version)

    def _write_line(self, line: str) -> None:
        if not self._serial:
            raise ConnectionError("Not connected")
        self._serial.write((line + "\r").encode("ascii"))

    def _read_line(self, timeout_ms: int = 3000) -> str:
        if not self._serial:
            raise ConnectionError("Not connected")
        old_timeout = self._serial.timeout
        self._serial.timeout = timeout_ms / 1000.0
        try:
            raw = self._serial.readline()
            if not raw:
                raise TimeoutError(f"AR488 read timeout after {timeout_ms}ms")
            return raw.decode("ascii", errors="replace").strip()
        finally:
            self._serial.timeout = old_timeout

    def _drain(self) -> None:
        """Discard any buffered data."""
        if not self._serial:
            return
        self._serial.timeout = 0.1
        while self._serial.readline():
            pass
        self._serial.timeout = self._read_timeout_ms / 1000.0


class DirectTcpTransport(AR488Transport):
    """Synchronous TCP transport for WiFi-connected AR488 adapters."""

    def __init__(
        self,
        host: str = "192.168.1.100",
        port: int = 23,
        read_timeout_ms: int = 3000,
    ):
        self._host = host
        self._port = port
        self._read_timeout_ms = read_timeout_ms
        self._sock: socket.socket | None = None
        self._file: IO[bytes] | None = None
        self._current_address: int | None = None

    def connect(self) -> None:
        self._sock = socket.create_connection(
            (self._host, self._port), timeout=10.0
        )
        self._sock.settimeout(self._read_timeout_ms / 1000.0)
        self._file = self._sock.makefile("rb")
        time.sleep(0.5)
        self._drain()
        self._init_sequence()
        logger.info("Connected to AR488 at %s:%d", self._host, self._port)

    def disconnect(self) -> None:
        if self._file:
            self._file.close()
            self._file = None
        if self._sock:
            self._sock.close()
            self._sock = None
        self._current_address = None

    def is_connected(self) -> bool:
        return self._sock is not None

    def send_command(self, command: str) -> None:
        self._write_line(command)

    def send_raw(self, command: str) -> None:
        self._write_line(command)
        time.sleep(_INTER_CMD_DELAY)

    def read_response(self, timeout_ms: int = 10000) -> str:
        self._write_line("++read eoi")
        return self._read_line(timeout_ms)

    def set_address(self, address: int) -> None:
        if self._current_address != address:
            self._write_line(f"++addr {address}")
            time.sleep(_INTER_CMD_DELAY)
            self._current_address = address

    def serial_poll(self, address: int) -> int:
        self._write_line(f"++spoll {address}")
        time.sleep(_INTER_CMD_DELAY)
        response = self._read_line(self._read_timeout_ms)
        return int(response.strip())

    def find_listeners(self) -> list[int]:
        self._write_line("++findlstn")
        response = self._read_line(max(5000, self._read_timeout_ms))
        addresses = []
        for token in response.split():
            token = token.strip()
            if token.isdigit():
                addr = int(token)
                if 0 <= addr <= 30:
                    addresses.append(addr)
        return addresses

    def interface_clear(self) -> None:
        self._write_line("++ifc")
        time.sleep(_INTER_CMD_DELAY)

    def _init_sequence(self) -> None:
        self._write_line("++verbose 0")
        time.sleep(0.05)
        self._drain()

        self._write_line("++prompt 0")
        time.sleep(0.05)
        self._drain()

        for cmd in [
            "++auto 0",
            "++mode 1",
            "++eoi 1",
            "++eos 0",
            f"++read_tmo_ms {self._read_timeout_ms}",
        ]:
            self._write_line(cmd)
            time.sleep(_INTER_CMD_DELAY)

        self._write_line("++ver")
        version = self._read_line(2000)
        if not version:
            raise ConnectionError("AR488: no response to ++ver (TCP)")
        logger.info("AR488 firmware (TCP): %s", version)

    def _write_line(self, line: str) -> None:
        if not self._sock:
            raise ConnectionError("Not connected")
        self._sock.sendall((line + "\r").encode("ascii"))

    def _read_line(self, timeout_ms: int = 3000) -> str:
        if not self._file or not self._sock:
            raise ConnectionError("Not connected")
        old_timeout = self._sock.gettimeout()
        self._sock.settimeout(timeout_ms / 1000.0)
        try:
            raw = self._file.readline()
            if not raw:
                raise TimeoutError(f"AR488 TCP read timeout after {timeout_ms}ms")
            return raw.decode("ascii", errors="replace").strip()
        except TimeoutError:
            raise
        finally:
            self._sock.settimeout(old_timeout)

    def _drain(self) -> None:
        if not self._sock or not self._file:
            return
        self._sock.settimeout(0.1)
        try:
            while self._file.readline():
                pass
        except TimeoutError:
            pass
        self._sock.settimeout(self._read_timeout_ms / 1000.0)
