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

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

import shlex
import subprocess
from queue import Empty
from queue import Queue
from subprocess import Popen
from threading import Lock
from threading import Thread

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 driver(speech_driver):
    def __init__(self):
        speech_driver.__init__(self)
        self.proc = None
        self.speechThread = Thread(target=self.worker)
        self.lock = Lock()
        self.textQueue = SpeakQueue()

    def initialize(self, environment):
        self.env = environment
        self.minVolume = self.env["runtime"][
            "SettingsManager"
        ].get_setting_as_int("speech", "fenrir_min_volume")
        self.maxVolume = self.env["runtime"][
            "SettingsManager"
        ].get_setting_as_int("speech", "fenrir_max_volume")
        self.minPitch = self.env["runtime"][
            "SettingsManager"
        ].get_setting_as_int("speech", "fenrir_min_pitch")
        self.maxPitch = self.env["runtime"][
            "SettingsManager"
        ].get_setting_as_int("speech", "fenrir_max_pitch")
        self.minRate = self.env["runtime"][
            "SettingsManager"
        ].get_setting_as_int("speech", "fenrir_min_rate")
        self.maxRate = self.env["runtime"][
            "SettingsManager"
        ].get_setting_as_int("speech", "fenrir_max_rate")

        self.speechCommand = self.env["runtime"][
            "SettingsManager"
        ].get_setting("speech", "generic_speech_command")
        if self.speechCommand == "":
            self.speechCommand = 'espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice -- "fenrirText"'
        if False:  # for debugging overwrite here
            # self.speechCommand = 'spd-say --wait -r 100 -i 100  "fenrirText"'
            self.speechCommand = 'flite -t "fenrirText"'

        self._is_initialized = True
        if self._is_initialized:
            self.speechThread.start()

    def shutdown(self):
        if not self._is_initialized:
            return
        self.cancel()
        self.textQueue.put(-1)

    def speak(self, text, queueable=True, ignore_punctuation=False):
        if not self._is_initialized:
            return
        if not queueable:
            self.cancel()
        utterance = {
            "text": text,
            "volume": self.volume,
            "rate": self.rate,
            "pitch": self.pitch,
            "module": self.module,
            "language": self.language,
            "voice": self.voice,
        }
        self.textQueue.put(utterance.copy())

    def cancel(self):
        if not self._is_initialized:
            return
        self.clear_buffer()
        self.lock.acquire(True)
        try:
            if self.proc:
                try:
                    self.proc.terminate()
                    # Wait for process to finish to prevent zombies
                    try:
                        self.proc.wait(timeout=1.0)
                    except subprocess.TimeoutExpired:
                        # If terminate didn't work, force kill
                        self.proc.kill()
                        self.proc.wait(timeout=1.0)
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "speech_driver:Cancel:self.proc.terminate():" + str(e),
                        debug.DebugLevel.WARNING,
                    )
                    try:
                        self.proc.kill()
                        # Wait after kill to prevent zombies
                        self.proc.wait(timeout=1.0)
                    except Exception as e:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "speech_driver:Cancel:self.proc.kill():" + str(e),
                            debug.DebugLevel.WARNING,
                        )
                self.proc = None
        finally:
            # Ensure lock is always released, even if process termination fails
            self.lock.release()


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

    def set_voice(self, voice):
        if not self._is_initialized:
            return
        self.voice = str(voice)

    def set_pitch(self, pitch):
        if not self._is_initialized:
            return
        self.pitch = str(
            self.minPitch + pitch * (self.maxPitch - self.minPitch)
        )

    def set_rate(self, rate):
        if not self._is_initialized:
            return
        self.rate = str(self.minRate + rate * (self.maxRate - self.minRate))

    def set_module(self, module):
        if not self._is_initialized:
            return
        self.module = str(module)

    def set_language(self, language):
        if not self._is_initialized:
            return
        self.language = str(language)

    def set_volume(self, volume):
        if not self._is_initialized:
            return
        self.volume = str(
            self.minVolume + volume * (self.maxVolume - self.minVolume)
        )

    def worker(self):
        while True:
            utterance = self.textQueue.get()

            if isinstance(utterance, int):
                if utterance == -1:
                    return
                else:
                    continue
            elif not isinstance(utterance, dict):
                continue
            # no text means nothing to speak
            if "text" not in utterance:
                continue
            if not isinstance(utterance["text"], str):
                continue
            if utterance["text"] == "":
                continue
            # check for valid data fields
            if "volume" not in utterance:
                utterance["volume"] = ""
            if not isinstance(utterance["volume"], str):
                utterance["volume"] = ""
            if "module" not in utterance:
                utterance["module"] = ""
            if not isinstance(utterance["module"], str):
                utterance["module"] = ""
            if "language" not in utterance:
                utterance["language"] = ""
            if not isinstance(utterance["language"], str):
                utterance["language"] = ""
            if "voice" not in utterance:
                utterance["voice"] = ""
            if not isinstance(utterance["voice"], str):
                utterance["voice"] = ""
            if "pitch" not in utterance:
                utterance["pitch"] = ""
            if not isinstance(utterance["pitch"], str):
                utterance["pitch"] = ""
            if "rate" not in utterance:
                utterance["rate"] = ""
            if not isinstance(utterance["rate"], str):
                utterance["rate"] = ""

            popen_speech_command = shlex.split(self.speechCommand)
            for idx, word in enumerate(popen_speech_command):
                word = word.replace("fenrirVolume", str(utterance["volume"]))
                word = word.replace("fenrirModule", str(utterance["module"]))
                word = word.replace(
                    "fenrirLanguage", str(utterance["language"])
                )
                word = word.replace("fenrirVoice", str(utterance["voice"]))
                word = word.replace("fenrirPitch", str(utterance["pitch"]))
                word = word.replace("fenrirRate", str(utterance["rate"]))
                # Properly quote text to prevent command injection
                word = word.replace(
                    "fenrirText", shlex.quote(str(utterance["text"]))
                )
                popen_speech_command[idx] = word

            try:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "speech_driver:worker:" + " ".join(popen_speech_command),
                    debug.DebugLevel.INFO,
                )
                self.lock.acquire(True)
                self.proc = Popen(
                    popen_speech_command,
                    stdin=None,
                    stdout=None,
                    stderr=None,
                    shell=False,
                )
                self.lock.release()
                self.proc.wait()
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "speech_driver:worker:" + str(e), debug.DebugLevel.ERROR
                )

            self.lock.acquire(True)
            self.proc = None
            self.lock.release()
