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

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

from fenrirscreenreader.core import debug
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.core.settingsData import settings_data
import subprocess
import time


class SpeechHelperMixin:
    """Helper methods for querying speech-dispatcher modules and voices.

    Provides caching and query functionality for speech-dispatcher module
    and voice enumeration, reusing proven logic from voice_browser.py.
    """

    def __init__(self):
        self._modules_cache = None
        self._voices_cache = {}  # {module_name: [voice_list]}
        self._cache_timestamp = 0
        self._cache_timeout = 300  # 5 minutes

    def get_speechd_modules(self):
        """Get available speech-dispatcher modules (cached).

        Returns:
            list: Available module names (e.g., ['espeak-ng', 'festival'])
        """
        now = time.time()

        # Return cached if valid
        if (self._modules_cache and
                (now - self._cache_timestamp) < self._cache_timeout):
            return self._modules_cache

        # Query spd-say
        try:
            result = subprocess.run(
                ["spd-say", "-O"],
                capture_output=True,
                text=True,
                timeout=8
            )
            if result.returncode == 0:
                lines = result.stdout.strip().split("\n")
                self._modules_cache = [
                    line.strip() for line in lines[1:]
                    if line.strip() and line.strip().lower() != "dummy"
                ]
                self._cache_timestamp = now
                return self._modules_cache
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                (f"QuickMenuManager get_speechd_modules: "
                 f"Error querying modules: {e}"),
                debug.DebugLevel.ERROR
            )

        return []

    def get_module_voices(self, module):
        """Get voices for a specific module (cached per-module).

        Args:
            module (str): Module name (e.g., 'espeak-ng')

        Returns:
            list: Available voice names for this module
        """
        # Return cached if available
        if module in self._voices_cache:
            return self._voices_cache[module]

        # Query spd-say
        try:
            result = subprocess.run(
                ["spd-say", "-o", module, "-L"],
                capture_output=True,
                text=True,
                timeout=8
            )
            if result.returncode == 0:
                lines = result.stdout.strip().split("\n")
                voices = []
                for line in lines[1:]:
                    if not line.strip():
                        continue
                    if module.lower() == "espeak-ng":
                        voice = self._process_espeak_voice(line)
                        if voice:
                            voices.append(voice)
                    elif module.lower() == "voxin":
                        # For Voxin, store voice name with language
                        voice_data = self._process_voxin_voice(line)
                        if voice_data:
                            voices.append(voice_data)
                    else:
                        # For other modules, extract first field (voice name)
                        parts = line.strip().split()
                        if parts:
                            voices.append(parts[0])

                self._voices_cache[module] = voices
                return voices
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                (f"QuickMenuManager get_module_voices: "
                 f"Error querying voices for {module}: {e}"),
                debug.DebugLevel.ERROR
            )

        return []

    def _process_espeak_voice(self, voice_line):
        """Process espeak-ng voice format into usable voice name.

        Args:
            voice_line (str): Raw line from spd-say -L output

        Returns:
            str: Processed voice name (e.g., 'en-us' or 'en-us+f3')
        """
        parts = [p for p in voice_line.split() if p]
        if len(parts) < 2:
            return None
        lang_code = parts[-2].lower()
        variant = parts[-1].lower()
        return (f"{lang_code}+{variant}"
                if variant and variant != "none" else lang_code)

    def _process_voxin_voice(self, voice_line):
        """Process Voxin voice format with language information.

        Args:
            voice_line (str): Raw line from spd-say -o voxin -L output
                             Format: NAME LANGUAGE VARIANT

        Returns:
            str: Voice name with language encoded (e.g., 'daniel-embedded-high|en-GB')
        """
        parts = [p for p in voice_line.split() if p]
        if len(parts) < 2:
            return None
        voice_name = parts[0]
        language = parts[1]
        # Encode language with voice for later extraction
        return f"{voice_name}|{language}"

    def _select_default_voice(self, voices):
        """Select a sensible default voice from list, preferring user's
        language.

        Args:
            voices (list): List of available voice names

        Returns:
            str: Selected default voice (matches user language if possible)
        """
        if not voices:
            return ""

        # Get current voice to preserve language preference
        current_voice = self.env["runtime"]["SettingsManager"].get_setting(
            "speech", "voice"
        )

        # Get configured language from settings
        configured_lang = self.env["runtime"]["SettingsManager"].get_setting(
            "speech", "language"
        )

        # Extract language code from current voice if available
        current_lang = None
        if current_voice:
            # Extract language code (e.g., 'en-gb' from 'en-gb+male')
            current_lang = current_voice.split('+')[0].lower()

        # Build preference list: current language, configured language, English
        preferences = []
        if current_lang:
            preferences.append(current_lang)
        if configured_lang:
            preferences.append(configured_lang.lower())
        preferences.extend(['en-gb', 'en-us', 'en'])

        # Remove duplicates while preserving order
        seen = set()
        preferences = [x for x in preferences
                       if not (x in seen or seen.add(x))]

        # Try exact matches for preferred languages
        for pref in preferences:
            for voice in voices:
                # Extract language if voice is in "name|lang" format
                voice_to_check = voice
                if "|" in voice:
                    _, voice_lang = voice.split("|", 1)
                    voice_to_check = voice_lang
                if voice_to_check.lower() == pref:
                    return voice

        # Try voices starting with preferred language codes
        for pref in preferences:
            for voice in voices:
                # Extract language if voice is in "name|lang" format
                voice_to_check = voice
                if "|" in voice:
                    _, voice_lang = voice.split("|", 1)
                    voice_to_check = voice_lang
                if voice_to_check.lower().startswith(pref):
                    return voice

        # Fall back to first available voice
        return voices[0]

    def invalidate_speech_cache(self):
        """Clear cached module and voice data."""
        self._modules_cache = None
        self._voices_cache = {}
        self._cache_timestamp = 0


class QuickMenuManager(SpeechHelperMixin):
    def __init__(self):
        SpeechHelperMixin.__init__(self)
        self.position = 0
        self.quickMenu = []
        self.settings = settings_data

    def initialize(self, environment):
        self.env = environment

        # Load base menu from config
        menu_string = self.env["runtime"]["SettingsManager"].get_setting(
            "menu", "quick_menu"
        )

        # Dynamically add speech-dispatcher specific items
        if self._is_speechd_driver_active():
            menu_string = self._add_speechd_menu_items(menu_string)

        self.load_menu(menu_string)

    def shutdown(self):
        pass

    def _is_speechd_driver_active(self):
        """Check if speechdDriver is currently active.

        Returns:
            bool: True if speech driver is speechdDriver
        """
        try:
            driver = self.env["runtime"]["SettingsManager"].get_setting(
                "speech", "driver"
            )
            return driver == "speechdDriver"
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                (f"QuickMenuManager _is_speechd_driver_active: "
                 f"Error checking driver: {e}"),
                debug.DebugLevel.ERROR
            )
            return False

    def _add_speechd_menu_items(self, menu_string):
        """Add speech-dispatcher module and voice to quick menu.

        Args:
            menu_string (str): Existing menu string from config

        Returns:
            str: Updated menu string with module and voice added
        """
        if not menu_string:
            return "speech#module;speech#voice"

        # Check if already present (user manually added)
        if "speech#module" in menu_string or "speech#voice" in menu_string:
            return menu_string

        # Add module and voice after existing items
        return f"{menu_string};speech#module;speech#voice"

    def load_menu(self, menuString):
        self.position = 0
        self.quickMenu = []
        if menuString == "":
            return
        entrys = menuString.split(";")
        for e in entrys:
            entry = e.split("#")
            if len(entry) != 2:
                continue
            try:
                t = self.settings[entry[0]][entry[1]]
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "QuickMenuManager load_menu: Setting not found "
                    + str(entry)
                    + ": "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
                print(entry[0], entry[1], "not found")
                continue
            self.quickMenu.append({"section": entry[0], "setting": entry[1]})

    def next_entry(self):
        if len(self.quickMenu) == 0:
            return False
        self.position += 1
        if self.position >= len(self.quickMenu):
            self.position = 0
        return True

    def prev_entry(self):
        if len(self.quickMenu) == 0:
            return False
        self.position -= 1
        if self.position < 0:
            self.position = len(self.quickMenu) - 1
        return True

    def next_value(self):
        if len(self.quickMenu) == 0:
            return False
        section = self.quickMenu[self.position]["section"]
        setting = self.quickMenu[self.position]["setting"]
        value_string = ""
        try:
            value_string = self.env["runtime"]["SettingsManager"].get_setting(
                section, setting
            )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "QuickMenuManager next_value: Error getting setting value: "
                + str(e),
                debug.DebugLevel.ERROR,
            )
            return False

        try:
            if isinstance(self.settings[section][setting], str):
                # Check for special string cycling cases
                if section == "speech" and setting == "module":
                    return self.cycle_speech_module("next")
                elif section == "speech" and setting == "voice":
                    return self.cycle_speech_voice("next")
                else:
                    # Generic strings not supported for cycling
                    value = str(value_string)
                    return False
            elif isinstance(self.settings[section][setting], bool):
                if value_string not in ["True", "False"]:
                    return False
                value = not value
                self.env["runtime"]["SettingsManager"].set_setting(
                    section, setting, str(value)
                )
            elif isinstance(self.settings[section][setting], int):
                value = int(value_string)
                value += 1
                self.env["runtime"]["SettingsManager"].set_setting(
                    section, setting, str(value)
                )
            elif isinstance(self.settings[section][setting], float):
                value = float(value_string)
                value += 0.05
                if value > 1.0:
                    value = 1.0
                self.env["runtime"]["SettingsManager"].set_setting(
                    section, setting, str(value)[:4]
                )
        except Exception as e:
            return False
        return True

    def prev_value(self):
        if len(self.quickMenu) == 0:
            return False
        section = self.quickMenu[self.position]["section"]
        setting = self.quickMenu[self.position]["setting"]
        value_string = ""
        try:
            value_string = self.env["runtime"]["SettingsManager"].get_setting(
                section, setting
            )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "QuickMenuManager prev_value: Error getting setting value: "
                + str(e),
                debug.DebugLevel.ERROR,
            )
            return False
        try:
            if isinstance(self.settings[section][setting], str):
                # Check for special string cycling cases
                if section == "speech" and setting == "module":
                    return self.cycle_speech_module("prev")
                elif section == "speech" and setting == "voice":
                    return self.cycle_speech_voice("prev")
                else:
                    # Generic strings not supported for cycling
                    value = str(value_string)
                    return False
            elif isinstance(self.settings[section][setting], bool):
                if value_string not in ["True", "False"]:
                    return False
                value = not value
                self.env["runtime"]["SettingsManager"].set_setting(
                    section, setting, str(value)
                )
            elif isinstance(self.settings[section][setting], int):
                value = int(value_string)
                value -= 1
                if value < 0:
                    value = 0
                self.env["runtime"]["SettingsManager"].set_setting(
                    section, setting, str(value)
                )
            elif isinstance(self.settings[section][setting], float):
                value = float(value_string)
                value -= 0.05
                if value < 0.0:
                    value = 0.0
                self.env["runtime"]["SettingsManager"].set_setting(
                    section, setting, str(value)[:4]
                )
        except Exception as e:
            return False
        return True

    def cycle_speech_module(self, direction):
        """Cycle to next/previous speech-dispatcher module.

        Args:
            direction (str): 'next' or 'prev'

        Returns:
            bool: True if successful, False otherwise
        """
        try:
            # Get available modules
            modules = self.get_speechd_modules()
            if not modules:
                self.env["runtime"]["OutputManager"].present_text(
                    "No modules available", interrupt=True
                )
                return False

            # Get current module
            current_module = self.env["runtime"]["SettingsManager"].get_setting(
                "speech", "module"
            )

            # Find current index
            try:
                current_index = (modules.index(current_module)
                                 if current_module else 0)
            except ValueError:
                current_index = 0

            # Cycle to next/previous
            if direction == "next":
                new_index = (current_index + 1) % len(modules)
            else:  # prev
                new_index = (current_index - 1) % len(modules)

            new_module = modules[new_index]

            # Update setting (runtime only)
            self.env["runtime"]["SettingsManager"].set_setting(
                "speech", "module", new_module
            )

            # Select sensible default voice for new module
            voices = self.get_module_voices(new_module)
            if voices:
                default_voice = self._select_default_voice(voices)

                # Parse voice name and language for modules like Voxin
                voice_name = default_voice
                voice_lang = None
                if "|" in default_voice:
                    voice_name, voice_lang = default_voice.split("|", 1)

                self.env["runtime"]["SettingsManager"].set_setting(
                    "speech", "voice", voice_name
                )

                # Apply voice to speech driver immediately
                if "SpeechDriver" in self.env["runtime"]:
                    try:
                        self.env["runtime"]["SpeechDriver"].set_module(
                            new_module
                        )
                        # Set language first if available
                        if voice_lang:
                            self.env["runtime"]["SpeechDriver"].set_language(
                                voice_lang
                            )
                        # Then set voice
                        self.env["runtime"]["SpeechDriver"].set_voice(
                            voice_name
                        )
                    except Exception as e:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            (f"QuickMenuManager cycle_speech_module: "
                             f"Error applying voice: {e}"),
                            debug.DebugLevel.ERROR
                        )

            # Announce new module
            self.env["runtime"]["OutputManager"].present_text(
                new_module, interrupt=True
            )

            return True

        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                f"QuickMenuManager cycle_speech_module: Error: {e}",
                debug.DebugLevel.ERROR
            )
            return False

    def cycle_speech_voice(self, direction):
        """Cycle to next/previous voice for current module.

        Args:
            direction (str): 'next' or 'prev'

        Returns:
            bool: True if successful, False otherwise
        """
        try:
            # Get current module
            current_module = self.env["runtime"]["SettingsManager"].get_setting(
                "speech", "module"
            )

            if not current_module:
                self.env["runtime"]["OutputManager"].present_text(
                    "No module selected", interrupt=True
                )
                return False

            # Get available voices for this module
            voices = self.get_module_voices(current_module)
            if not voices:
                self.env["runtime"]["OutputManager"].present_text(
                    f"No voices for module {current_module}", interrupt=True
                )
                return False

            # Get current voice
            current_voice = self.env["runtime"]["SettingsManager"].get_setting(
                "speech", "voice"
            )

            # Find current index (handle Voxin voice|language format)
            current_index = 0
            if current_voice:
                try:
                    # Try exact match first
                    current_index = voices.index(current_voice)
                except ValueError:
                    # For Voxin, compare just the voice name part
                    for i, voice in enumerate(voices):
                        voice_name = voice.split("|")[0] if "|" in voice else voice
                        if voice_name == current_voice:
                            current_index = i
                            break

            # Cycle to next/previous
            if direction == "next":
                new_index = (current_index + 1) % len(voices)
            else:  # prev
                new_index = (current_index - 1) % len(voices)

            new_voice = voices[new_index]

            # Parse voice name and language for modules like Voxin
            voice_name = new_voice
            voice_lang = None
            if "|" in new_voice:
                # Format: "voicename|language" (e.g., "daniel-embedded-high|en-GB")
                voice_name, voice_lang = new_voice.split("|", 1)

            # Update setting (runtime only) - store the voice name only
            self.env["runtime"]["SettingsManager"].set_setting(
                "speech", "voice", voice_name
            )

            # Apply voice to speech driver immediately
            if "SpeechDriver" in self.env["runtime"]:
                try:
                    # Set language first if available
                    if voice_lang:
                        self.env["runtime"]["SpeechDriver"].set_language(
                            voice_lang
                        )
                    # Then set voice
                    self.env["runtime"]["SpeechDriver"].set_voice(voice_name)
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        (f"QuickMenuManager cycle_speech_voice: "
                         f"Error applying voice: {e}"),
                        debug.DebugLevel.ERROR
                    )

            # Announce new voice (voice name only, not language)
            self.env["runtime"]["OutputManager"].present_text(
                voice_name, interrupt=True
            )

            return True

        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                f"QuickMenuManager cycle_speech_voice: Error: {e}",
                debug.DebugLevel.ERROR
            )
            return False

    def get_current_entry(self):
        if len(self.quickMenu) == 0:
            return ""
        try:
            return (
                _(self.quickMenu[self.position]["section"])
                + " "
                + _(self.quickMenu[self.position]["setting"])
            )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "QuickMenuManager get_current_entry: Error formatting entry: "
                + str(e),
                debug.DebugLevel.ERROR,
            )
            return _("setting invalid")

    def get_current_value(self):
        if len(self.quickMenu) == 0:
            return ""
        try:
            return self.env["runtime"]["SettingsManager"].get_setting(
                self.quickMenu[self.position]["section"],
                self.quickMenu[self.position]["setting"],
            )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "QuickMenuManager get_current_value: Error getting setting value: "
                + str(e),
                debug.DebugLevel.ERROR,
            )
            return _("setting value invalid")
