#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.

import os
import termios
import threading
import tty
from queue import Empty
from queue import Queue

from fenrirscreenreader.core import debug
from fenrirscreenreader.core.speechDriver import speech_driver


class SpeakQueue(Queue):
    def clear(self):
        try:
            while True:
                self.get_nowait()
        except Empty:
            pass


class hardware_serial_driver(speech_driver):
    cancel_command = b""
    default_baud_rate = 9600

    def __init__(self):
        speech_driver.__init__(self)
        self.device = ""
        self.baud_rate = self.default_baud_rate
        self.serial_port = None
        self.text_queue = SpeakQueue()
        self.lock = threading.Lock()
        self.worker_thread = None
        self._stop_worker = False

    def initialize(self, environment):
        self.env = environment
        self._is_initialized = False
        settings_manager = self.env["runtime"]["SettingsManager"]
        self.device = self._clean_device_setting(
            settings_manager.get_setting("speech", "hardware_device")
        )
        self.baud_rate = settings_manager.get_setting_as_int(
            "speech", "hardware_baud_rate"
        )
        self._debug(
            "Hardware speech initialize: "
            f"requested_device={self.device}, baud_rate={self.baud_rate}",
            debug.DebugLevel.INFO,
            on_any_level=True,
        )
        self._open_serial_port()
        self._is_initialized = self.serial_port is not None
        if not self._is_initialized:
            raise RuntimeError("hardware speech device is not available")
        if self._is_initialized:
            self._stop_worker = False
            self.worker_thread = threading.Thread(
                target=self._worker, daemon=True
            )
            self.worker_thread.start()

    def _clean_device_setting(self, device):
        if not isinstance(device, str):
            return ""
        device = device.split("#", 1)[0].split(";", 1)[0].strip()
        return device

    def shutdown(self):
        if not self._is_initialized:
            return
        self._stop_worker = True
        self.clear_buffer()
        self.text_queue.put(None)
        if self.worker_thread:
            self.worker_thread.join(timeout=0.5)
        self._close_serial_port()
        self._is_initialized = False

    def speak(self, text, queueable=True, ignore_punctuation=False):
        if not self._is_initialized:
            return
        if not queueable:
            self.cancel()
        if not isinstance(text, str) or text == "":
            return
        self._debug(
            "Hardware speech queued text: "
            f"{len(text)} chars, queue_size={self.text_queue.qsize()}",
            debug.DebugLevel.INFO,
            on_any_level=True,
        )
        self.text_queue.put(text)

    def cancel(self):
        if not self._is_initialized:
            return
        self.clear_buffer()
        if self.cancel_command:
            self._write_bytes(self.cancel_command, "cancel")

    def clear_buffer(self):
        if not self._is_initialized:
            return
        self.text_queue.clear()

    def set_rate(self, rate):
        if not self._is_initialized:
            return
        if not isinstance(rate, float):
            return
        self._write_bytes(self._rate_command(rate), "rate")

    def set_pitch(self, pitch):
        if not self._is_initialized:
            return
        if not isinstance(pitch, float):
            return
        self._write_bytes(self._pitch_command(pitch), "pitch")

    def set_volume(self, volume):
        if not self._is_initialized:
            return
        if not isinstance(volume, float):
            return
        self._write_bytes(self._volume_command(volume), "volume")

    def _worker(self):
        while not self._stop_worker:
            text = self.text_queue.get()
            if text is None:
                return
            try:
                data = self._speak_bytes(text)
                self._debug(
                    "Hardware speech worker prepared speech bytes: "
                    f"{len(data)} bytes",
                    debug.DebugLevel.INFO,
                    on_any_level=True,
                )
                self._write_bytes(data, "speech")
            except Exception as error:
                self._debug(
                    f"Hardware speech worker failed: {error}",
                    debug.DebugLevel.ERROR,
                    on_any_level=True,
                )

    def _open_serial_port(self):
        if not self.device or self.device == "auto":
            self._debug(
                "Hardware speech requires an explicit serial device",
                debug.DebugLevel.ERROR,
                on_any_level=True,
            )
            return

        port = self._open_configured_serial_port(self.device)
        if port is not None:
            self._activate_serial_port(self.device, port)

    def _open_configured_serial_port(self, device):
        port = None
        try:
            port = os.open(device, os.O_RDWR | os.O_NOCTTY)
            tty.setraw(port)
            attrs = termios.tcgetattr(port)
            attrs[2] |= termios.CLOCAL | termios.CREAD
            baud_rate = self._termios_baud_rate(self.baud_rate)
            attrs[4] = baud_rate
            attrs[5] = baud_rate
            attrs[6][termios.VMIN] = 0
            attrs[6][termios.VTIME] = 0
            attrs[0] &= ~(termios.IXON | termios.IXOFF | termios.IXANY)
            termios.tcsetattr(port, termios.TCSANOW, attrs)
            return port
        except (OSError, termios.error) as error:
            self._close_port(port)
            self._debug(
                f"Hardware speech device open failed: {device}: {error}",
                debug.DebugLevel.ERROR,
                on_any_level=True,
            )
            return None

    def _activate_serial_port(self, device, port):
        self.serial_port = port
        self.device = device
        self._debug(
            "Hardware speech device opened: "
            f"{device}, baud_rate={self.baud_rate}",
            debug.DebugLevel.INFO,
            on_any_level=True,
        )

    def _close_serial_port(self):
        with self.lock:
            if self.serial_port is None:
                return
            self._close_port(self.serial_port)
            self.serial_port = None

    def _close_port(self, port):
        if port is None:
            return
        try:
            os.close(port)
        except OSError as error:
            self._debug(
                f"Hardware speech device close failed: {error}",
                debug.DebugLevel.WARNING,
            )

    def _write_bytes(self, data, description="data"):
        if not data:
            return
        with self.lock:
            if self.serial_port is None:
                return
            try:
                total_written = 0
                while total_written < len(data):
                    bytes_written = os.write(
                        self.serial_port, data[total_written:]
                    )
                    if bytes_written == 0:
                        raise OSError("serial write returned 0 bytes")
                    total_written += bytes_written
                preview = self._format_bytes_preview(data)
                self._debug(
                    "Hardware speech wrote "
                    f"{total_written} {description} bytes: {preview}",
                    debug.DebugLevel.INFO,
                    on_any_level=True,
                )
            except OSError as error:
                self._debug(
                    f"Hardware speech write failed: {error}",
                    debug.DebugLevel.ERROR,
                    on_any_level=True,
                )

    def _termios_baud_rate(self, baud_rate):
        baud_name = f"B{baud_rate}"
        if hasattr(termios, baud_name):
            return getattr(termios, baud_name)
        self._debug(
            f"Unsupported hardware speech baud rate {baud_rate}; using 9600",
            debug.DebugLevel.WARNING,
        )
        return termios.B9600

    def _clean_text(self, text):
        text = text.replace("\r", " ").replace("\n", " ")
        return "".join(
            char if 0x20 <= ord(char) <= 0x7E else " "
            for char in text
        )

    def _scale(self, value, minimum, maximum):
        value = max(0.0, min(1.0, value))
        return int(round(minimum + value * (maximum - minimum)))

    def _format_bytes_preview(self, data, limit=32):
        preview = data[:limit]
        hex_preview = " ".join(f"{byte:02x}" for byte in preview)
        ascii_preview = "".join(
            chr(byte) if 0x20 <= byte <= 0x7E else "."
            for byte in preview
        )
        suffix = "" if len(data) <= limit else " ..."
        return (
            f"hex=[{hex_preview}{suffix}] "
            f"ascii=[{ascii_preview}{suffix}]"
        )

    def _debug(self, message, level, on_any_level=False):
        try:
            self.env["runtime"]["DebugManager"].write_debug_out(
                message, level, on_any_level=on_any_level
            )
        except Exception:
            pass

    def _speak_bytes(self, text):
        raise NotImplementedError

    def _rate_command(self, rate):
        return b""

    def _pitch_command(self, pitch):
        return b""

    def _volume_command(self, volume):
        return b""
