# This file is copyright 2011, by Bill Cox, and released under the LGPL license,
# version 2, or (at your option) any later version.
# Modified in 2020 by Isaac Porat to comply with python 3 and NVDA version 2019.3.1
# Modified in 2021 by Isaac Porat to comply with NVDA version 21.1

from collections import OrderedDict
import speech
from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking
from autoSettingsUtils.driverSetting import BooleanDriverSetting
import languageHandler
import logHandler

from . import _sh_genscripts
from ._speechhub import *

def log(level, text):
    """This function is passed to the speech-hub driver so it can interface to
       our logger."""
    if level == INFO_MESSAGE:
        logHandler.log.info(text)
    elif level == WARNING_MESSAGE:
        logHandler.log.debugWarning(text)
    elif level == IO_MESSAGE:
        logHandler.log.io(text)
    elif level == DEBUG_MESSAGE:
        logHandler.log.debug(text)
    else:
        logHandler.log.error("Invalid logging level:" + text)

log(DEBUG_MESSAGE, "Loading speechhub MODULE_NAME")

class SynthDriver(SynthDriver):
    supportedSettings = (
        SynthDriver.VoiceSetting(),
        SynthDriver.VariantSetting(),
        SynthDriver.RateSetting(),
        SynthDriver.PitchSetting(),
        SynthDriver.VolumeSetting(),
        BooleanDriverSetting("rateBoost",_("Rate boos&t")),
    )
    supportedCommands = {
     speech.commands.IndexCommand,
     speech.commands.CharacterModeCommand,
     speech.commands.LangChangeCommand,
     speech.commands.PitchCommand,
    }
    # Notifications introduced in NVDA 1019.3 do not map directly to the SpeechHub API
    # Nevertheless SH synthDrivers work well
    # The only minor issue is that with NVDA's say all command
    # When cancel is applied the current line is one line above the last spoken (partially before been cancelled)
    # So to hear the last line use the down arrow
    # To achieve the required behaviour the synthIndexReached notification
    # is applied at the end of the speak function for empty strings and
    # in the speakEndNotification function which is fires by SH only when a message is spoken fully.
    # The synthDoneSpeaking is applied in the speakEndNotification
    # Again this function is only fired by SH when speech is completed fully (not when cancelled)
    supportedNotifications = {
        synthIndexReached,
        synthDoneSpeaking
    }

    name="sh_MODULE_NAME"
    description="Speech Hub SHOWN_NAME"
    _rateBoost = False

    def _get_rateBoost(self):
        return self._rateBoost

    def _set_rateBoost(self, enable):
        if enable == self._rateBoost:
            return
        self._rateBoost = enable
        self.sh.setRateBoost(enable)

    @classmethod
    def check(cls):
        if cls.name == "sh_espeak":
            # This gets called when the synth dialog comes up, once per existing
            # engine.  We only want to generate scripts once, so we do it on the
            # espeak engine check.
            log(INFO_MESSAGE, "generating scripts")
            _sh_genscripts.genEngineScripts()
        log(WARNING_MESSAGE, "Checked that speechhub MODULE_NAME is up")
        sh = getSpeechHub(log)
        modules = sh.listOutputModules()
        sh.quit()
        return "MODULE_NAME" in modules

    def __init__(self):
        log(INFO_MESSAGE, "Initializing speechhub MODULE_NAME")
        self.sh = getSpeechHub(log, self.speakEndNotification)
        self.sh.setLanguage(languageHandler.getLanguage())
        self.sh.setPunctuation("none")
        self.myRate = 50
        self.myPitch = 50
        self.myVolume = 100
        self.myVoice = None
        self.myVariant = "max"
        self.myVoices = self.findAvailableVoices() # Compute myVoices

    def terminate(self):
        log(WARNING_MESSAGE, "NVDA is terminating speech-hub")
        self.sh.quit()

    def speakWrapper(self, text, language, spellMode, pitchMultiplier):
        pitch = int(self.myPitch * pitchMultiplier)
        log(WARNING_MESSAGE, "Calling SH with speach parameters with text: -" + text + "-")
        self.sh.setPitch((pitch - 50)*2)
        self.sh.setLanguage(language)
        if spellMode and len(text) == 1:
            self.sh.sendChar(text)
        else:
            self.sh.speak(text)

    def speak(self, speechSequence):
        language = self.sh.language
        spellMode = False
        pitchMultiplier = 1
        text = ""
        numMessages = 0
        self.lastIndexSent = None
        log(WARNING_MESSAGE, "Speak function start:")
        for item in speechSequence:
            #log(INFO_MESSAGE, "item type: " + str(type(item)))
            if isinstance(item, speech.commands.PitchCommand):
                if (item.multiplier > 1):
                    pitchMultiplier = item.multiplier
            elif isinstance(item, str):
                text = text + item
                numMessages = numMessages + 1
            elif isinstance(item, speech.commands.IndexCommand):
                self.lastIndexSent = item.index
                self.sh.addIndex(item.index)
            elif isinstance(item, speech.commands.CharacterModeCommand):
                if text != "":
                    self.speakWrapper(text, language, spellMode, pitchMultiplier)
                    log(WARNING_MESSAGE, "speaking " + text)
                    text = ""
                    numMessages = 0
                spellMode = item.state
            elif isinstance(item, speech.commands.LangChangeCommand):
                if text != "":
                    self.speakWrapper(text, language, spellMode, pitchMultiplier)
                    log(WARNING_MESSAGE, "speaking " + text)
                    text = ""
                    numMessages = 0
                if item.lang:
                    language = item.lang
                else:
                    language = languageHandler.getLanguage()
                language = item.lang
            elif isinstance(item,speech.SpeechCommand):
                log(WARNING_MESSAGE, "Unsupported speech command: %s" % item)
            else:
                log(WARNING_MESSAGE, "Unknown speech: %s"%item)

        if text != "":
            log(WARNING_MESSAGE, "speaking " + text)
            self.speakWrapper(text, language, spellMode, pitchMultiplier)
        else: # fire the synthIndexReached on empty string to push the next speak call with the next message if exists 
            self._utteranceIndexNotification(self.lastIndexSent)

    # Get all the available languages in the current language in all the
    # engines.
    def findAvailableVoices(self):
        currentLanguage = languageHandler.getLanguage()
        # Cycle through all the engines, and get all the voices matching the language.
        voices = OrderedDict()
        m = "MODULE_NAME"
        log(DEBUG_MESSAGE, "Listing voices for " + m)
        firstVoice = None
        if self.sh.setOutputModule(m):
            voiceList = self.sh.listVoices()
            if voiceList != None:
                for v in voiceList:
                    log(DEBUG_MESSAGE, "Found voice " + v)
                    # The format returned by speech-hub is "name language dialect", and the name
                    # may contain spaces.
                    l = v.rsplit(None, 2)
                    name = l[0]
                    language = l[1]
                    dialect = l[2]
                    log(DEBUG_MESSAGE, "adding id = " + name + " language = " + language + " dialect = " + dialect)
                    if firstVoice == None:
                        firstVoice = name
                    niceName = name.replace("_", " ")
                    voices[name] = VoiceInfo(name, niceName, language)
                    if language == currentLanguage and self.myVoice == None:
                        self.myVoice = name
        if self.myVoice == None:
            self.myVoice = firstVoice
        return voices

    # Get all the available languages in the current language in all the
    # engines.
    def _getAvailableVoices(self):
        voices = self.myVoices
        if self.myVoice != None and not self.myVoice in self.myVoices:
            # We have to add the current voice or NVDA will crash.
            currentLanguage = languageHandler.getLanguage()
            voices = self.myVoices.copy()
            voices[self.myVoice] = VoiceInfo(self.myVoice, self.myVoice, currentLanguage)
        return voices

    def _get_rate(self):
        return self.myRate

    def _get_pitch(self):
        return self.myPitch

    def _get_volume(self):
        return self.myVolume

    def _get_voice(self):
        return self.myVoice
 
    def _get_lastIndex(self):
        return self.sh.getLastIndex()

    def _set_rate(self, rate):
        self.myRate = rate
        self.sh.setRate((rate - 50)*2)

    def _set_pitch(self, value):
        self.myPitch = value

    def _set_volume(self, value):
        self.myVolume = value
        self.sh.setVolume((value - 50)*2)

    def _set_voice(self, value):
        if value == None:
            return
        if not value in self.myVoices and len(self.myVoices) > 0:
            value = self.myVoices.keys()[0]
        if value in self.myVoices:
            self.myVoice = value
            self.sh.setVoice(value)

    def cancel(self):
        self.sh.cancel()

    def pause(self, switch):
        self.sh.pause(switch)

    def _get_variant(self):
        return self.myVariant

    def _set_variant(self, variant):
        self.sh.setVariant(variant)
        self.myVariant = variant

    def _getAvailableVariants(self):
        variantList = self.sh.listVariants()
        variants = OrderedDict()
        for name in variantList:
            variants[name] = VoiceInfo(name, name)
        return variants

    def _utteranceIndexNotification(self, index):
        # Note that the call below very rarely during 'speak all' raises Windows OS exception
        # However this exception seems to be caught in manager.py in handleIndex function, so cannot be caught here
        # By manually printing the index below, it seems that the index seems to be perfectly logical, so not sure if the error relates to this code or not 
        synthIndexReached.notify(synth=self, index=index)
        log(WARNING_MESSAGE, "Utterance index Notification: " + str(index))

    # This function is called by SH when a message has been spoken in full; if cancelled, this function is not called  
    def speakEndNotification(self, index):
        self._utteranceIndexNotification(self.lastIndexSent)
        synthDoneSpeaking.notify(synth=self)
        log(WARNING_MESSAGE, "Speak end Notification: " + str(index))

