"""Threading-based serial reader/writer."""
import threading
import queue
import time
from typing import Callable, Optional

import serial

from .protocol import FrameParser, build_ping


class SerialWorker:
    """Manages serial port in a background thread."""

    def __init__(self, port: str, baudrate: int = 460800):
        self.port = port
        self.baudrate = baudrate
        self.ser: Optional[serial.Serial] = None
        self._rx_thread: Optional[threading.Thread] = None
        self._running = False
        self._parser = FrameParser()
        self._tx_queue: queue.Queue = queue.Queue()
        self._frame_callback: Optional[Callable] = None
        self._status_callback: Optional[Callable] = None
        self.connected = False
        self.pkt_count = 0
        self.drop_count = 0
        self._last_seq = -1

    def set_frame_callback(self, cb: Callable):
        self._frame_callback = cb

    def set_status_callback(self, cb: Callable):
        self._status_callback = cb

    def start(self):
        self._running = True
        self._rx_thread = threading.Thread(target=self._run, daemon=True)
        self._rx_thread.start()

    def stop(self):
        self._running = False
        if self._rx_thread:
            self._rx_thread.join(timeout=2)
        if self.ser and self.ser.is_open:
            self.ser.close()
        self.connected = False

    def send(self, data: bytes):
        self._tx_queue.put(data)

    def _notify_status(self, connected: bool):
        self.connected = connected
        if self._status_callback:
            self._status_callback(connected)

    def _run(self):
        while self._running:
            # Connect
            if not self.ser or not self.ser.is_open:
                try:
                    self.ser = serial.Serial(
                        self.port, self.baudrate, timeout=0.1
                    )
                    self._parser = FrameParser()
                    self._notify_status(True)
                except serial.SerialException:
                    self._notify_status(False)
                    time.sleep(1)
                    continue

            # TX
            try:
                while not self._tx_queue.empty():
                    data = self._tx_queue.get_nowait()
                    self.ser.write(data)
            except (serial.SerialException, OSError):
                self._handle_disconnect()
                continue

            # RX
            try:
                data = self.ser.read(256)
                if data:
                    for cmd, payload in self._parser.feed(data):
                        self.pkt_count += 1
                        if self._frame_callback:
                            self._frame_callback(cmd, payload)
            except (serial.SerialException, OSError):
                self._handle_disconnect()

    def _handle_disconnect(self):
        try:
            if self.ser:
                self.ser.close()
        except Exception:
            pass
        self.ser = None
        self._notify_status(False)
        time.sleep(1)
