# -*- coding: utf-8 -*-
"""
/***************************************************************************
 QgisSoundEffects
                                 A QGIS plugin
 Add sound effects to QGIS to make work less boring
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2024-07-07
        git sha              : $Format:%H$
        copyright            : (C) 2024 by Dror Bogin
        email                : dror.bogin@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import json
import os
from qgis.PyQt.QtCore import QCoreApplication, QUrl
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from PyQt5.QtTextToSpeech import QTextToSpeech, QVoice
from qgis.core import (QgsProcessingProvider, QgsProcessingAlgorithm, 
                        QgsProcessingParameterNumber,QgsProcessingParameterEnum,
                        QgsProcessingParameterString,QgsTask, QgsApplication, Qgis,
                        QgsMessageLog,QgsProcessingParameterFile)


from qgis.PyQt.QtGui import QIcon

MESSAGE_CATEGORY = 'QGIS Sound Effects'

class PlaySoundEffectAlgorithm(QgsProcessingAlgorithm):
    
    
    SOUND = 'SOUND'
    VOLUME = 'VOLUME'
    LOOPS_ENABLED = 'LOOPS_ENABLED'
    LOOPS = 'LOOPS'
    OUTPUT = 'OUTPUT'
    
    def name(self):
        return 'play_sound'
    
    def displayName(self):
        return 'Play Sound Effect'
    
    def group(self):
        return ''

    def groupId(self):
        return ''
    
    def shortHelpString(self):
        return 'Play a sound effect'
    
    def icon(self):
        return QIcon(':/plugins/qgs_sound_effects/qgs_effects_icon.png')
    
    def tr(self, string):
        return QCoreApplication.translate('Processing', string)
    
    def initAlgorithm(self, config):
        self.plugin_dir = os.path.dirname(__file__)
        with open(os.path.join(self.plugin_dir,'sounds.json')) as f:
            self.sounds_config = json.load(f)
            f.close()
        self.sound_names = [self.sounds_config[s]['label'] for s in self.sounds_config.keys()]

        sound_param = QgsProcessingParameterEnum(
                self.SOUND,
                self.tr('Sound'),
                options=self.sound_names,
                defaultValue=self.sound_names[0]
            )
        volume_param = QgsProcessingParameterNumber(
                self.VOLUME,
                self.tr('Volume'),
                type=QgsProcessingParameterNumber.Double,
                optional=True,
                defaultValue=1.0,
                maxValue=1.0,
                minValue=0.0
            )
        params = [sound_param, volume_param]
        for param in params:
            self.addParameter(param,
                createOutput = True)
            

    def prepareAlgorithm(self, parameters, context, feedback):
        self.play_sound = self.parameterAsEnum(parameters, self.SOUND, context)
        self.play_volume = self.parameterAsDouble(parameters, self.VOLUME, context)

        keys = self.sounds_config.keys()
        self.play_sound = self.sounds_config[list(keys)[self.play_sound]]
        self.file_path = os.path.join(self.plugin_dir, self.play_sound['filename'])
        self.player = QMediaPlayer()
        feedback.pushInfo('Playing sound effect: {} at volume {}'.format(self.play_sound['label'], self.play_volume))

        self.player = QMediaPlayer()
        self.player.setVolume(int(self.play_volume*100))
        self.player.setMedia(QMediaContent(QUrl.fromLocalFile(self.file_path)))
        self.player.audioAvailableChanged.connect(lambda: self.player.play())
        return super().prepareAlgorithm(parameters, context, feedback)


    def processAlgorithm(self, parameters, context, feedback):
        try:
            # will not be triggered in most cases as the sound should have not been loaded yet
            # but just in case
            if not self.player.isAudioAvailable():
                self.player.setMedia(QMediaContent(QUrl.fromLocalFile(self.file_path)))
                self.player.audioAvailableChanged.connect(lambda: self.player.play())
                self.player.setVolume(int(self.play_volume*100))
                self.player.play()
            else:
                self.player.play()
            feedback.pushInfo('Sound effect played')
            return {self.OUTPUT:{
            'SOUND': self.play_sound['label'], 
            'VOLUME': self.play_volume, 
            'OUTPUT': 'Played Sound Effect'}
            }

        except Exception as e:
            return {self.OUTPUT: 'Failed to play sound effect', 'ERROR': str(e)}
            
    
    def createInstance(self):
        return PlaySoundEffectAlgorithm()
    
class SaySomeTextAlgorithm(QgsProcessingAlgorithm):
    """This algorithm uses the PyQt5 QTextToSpeech class and your Operating System's native text-to-speech engine to say some text.
    If you have multiple text-to-speech engines installed, the first one will be used.
    If you have multiple voices installed, You can select the voice to use from the available voices.
    If you have no text-to-speech engine or no voices installed, the algorithm will fail."""
     
    TEXT = 'TEXT'
    VOICE = 'VOICE'
    VOLUME = 'VOLUME'
    OUTPUT = 'OUTPUT'
    
    def name(self):
        return 'say_text'
    
    def displayName(self):
        return 'Say Some Text'
    
    def group(self):
        return ''

    def groupId(self):
        return ''
    
    def shortHelpString(self):
        return 'Use text to speech to say input text'
    
    def icon(self):
        return QIcon(':/plugins/qgs_sound_effects/qgs_effects_icon.png')
    
    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def flags(self) -> QgsProcessingAlgorithm.Flags:
        return super().flags() #| QgsProcessingAlgorithm.FlagNoThreading
    
    def initAlgorithm(self, config):
        self.plugin_dir = os.path.dirname(__file__)
        self.engine = None
        self.engineNames = QTextToSpeech.availableEngines()

        if len(self.engineNames) > 0:
            self.engine = QTextToSpeech(self.engineNames[0])
        else:
            raise Exception('No text to speech engine available')
        
        self.engine.stateChanged.connect(self.onStateChanged)

        self.voices = self.engine.availableVoices()
        if len(self.voices) == 0:
            raise Exception('No voices available for text to speech')
        
        self.voice_names = [v.name() for v in self.voices]
        self.voice_genders = [v.genderName(v.gender()) for v in self.voices]
        self.voice_ages = [v.ageName(v.age()) for v in self.voices]
        self.voice_choices = ['{} - {} - {}'.format(self.voice_names[i], self.voice_genders[i], self.voice_ages[i]) for i in range(len(self.voices))]


        text_param = QgsProcessingParameterString(
                self.TEXT,
                self.tr('Text'),
                optional=False
            )
        voice_param = QgsProcessingParameterEnum(
                self.VOICE,
                self.tr('Voice'),
                options=self.voice_choices,
                defaultValue=self.voice_choices[0]
            )

        volume_param = QgsProcessingParameterNumber(
                self.VOLUME,
                self.tr('Volume'),
                type=QgsProcessingParameterNumber.Double,
                optional=True,
                defaultValue=1.0,
                maxValue=1.0,
                minValue=0.0
            )
        params = [text_param, voice_param, volume_param]
        for param in params:
            self.addParameter(param,
                createOutput = True)
            

    def onStateChanged(self, state):
        QgsMessageLog.logMessage('State changed to {}'.format(state), MESSAGE_CATEGORY, Qgis.Info)
        if state == QTextToSpeech.State.Ready:
            QgsMessageLog.logMessage('Say Task "{name}" was completed'.format(name=self.description()), Qgis.Info)
            self.engine.say(self.text_to_say)
            self.finished.emit()
        elif state == QTextToSpeech.State.BackendError:
            QgsMessageLog.logMessage('Say Task "{name}" failed'.format(name=self.description()), Qgis.Info)
            self.error.emit()
        elif state == QTextToSpeech.State.Speaking:
            pass
        elif state == QTextToSpeech.State.Paused:
            pass
        else:
            pass
            

    def prepareAlgorithm(self, parameters, context, feedback):
        self.engine.stop()
        self.selected_voice = self.parameterAsEnum(parameters, self.VOICE, context)
        self.text_to_say = self.parameterAsString(parameters, self.TEXT, context)
        self.play_volume = self.parameterAsDouble(parameters, self.VOLUME, context)

        voice = self.voices[self.selected_voice]
        if type(voice) is not QVoice:
            raise Exception('Selected voice is not valid')

        self.engine.setVoice(voice)
        if self.play_volume < 0 or self.play_volume > 1:
            raise Exception('Volume must be between 0 and 1')
        
        self.engine.setVolume(float(self.play_volume))

        return super().prepareAlgorithm(parameters, context, feedback)
    
    @staticmethod
    def speak(task, text_to_say, engine, voice, volume, feedback):
        task.setProgress(0)
        QgsMessageLog.logMessage('Started speaking task "{}"'.format(text_to_say),
                             MESSAGE_CATEGORY, Qgis.Info)
        engine.setVoice(voice)
        engine.setVolume(volume)
        engine.stop()
        engine.stateChanged.connect(engine.say(text_to_say))
        engine.resume()
        task.setProgress(100)
        feedback.pushInfo('Finished speaking task "{}"'.format(text_to_say))
        return True

    def task_finished(context, successful, results):
        if not successful:
            QgsMessageLog.logMessage('Speaking Task finished unsucessfully',
                                    MESSAGE_CATEGORY, Qgis.Warning)
        else:
            QgsMessageLog.logMessage('Speaking Task finished', MESSAGE_CATEGORY, Qgis.Info)
        

    def processAlgorithm(self, parameters, context, feedback):
        try:
            self.engine.say(self.text_to_say)
            self.task = QgsTask.fromFunction('Say Task', self.speak, on_finished=self.task_finished, text_to_say=self.text_to_say, engine=self.engine, voice=self.voices[self.selected_voice], volume=self.play_volume)

            QgsApplication.taskManager().addTask(self.task)
            feedback.pushInfo('Saying text: {} with voice: {} at volume {}'.format(self.text_to_say, self.voice_choices[self.selected_voice], self.play_volume))
    
            return {self.OUTPUT:{
            'TEXT': self.text_to_say,
            'VOICE': self.voice_choices[self.selected_voice], 
            'VOLUME': self.play_volume, 
            'OUTPUT': 'Played Sound Effect'}
            }

        except Exception as e:
            return {self.OUTPUT: 'Failed to say something', 'ERROR': str(e)}
        
    
    def postProcessAlgorithm(self, context, feedback):
        return super().postProcessAlgorithm(context, feedback)
            
    
    def createInstance(self):
        return SaySomeTextAlgorithm()
    

class PlayAudioFileAlgorithm(QgsProcessingAlgorithm):
    """This algorithm plays a local audio file.
        Supported formats may vary depending on the operating system.
    """

    FILE = 'FILE'
    VOLUME = 'VOLUME'
    OUTPUT = 'OUTPUT'

    def name(self):
        return 'play_audio_file'
    
    def displayName(self):
        return 'Play Audio File'
    
    def group(self):
        return ''
    
    def groupId(self):
        return ''
    
    def shortHelpString(self):
        return 'Play an audio file'
    
    def icon(self):
        return QIcon(':/plugins/qgs_sound_effects/qgs_effects_icon.png')
    
    def tr(self, string):
        return QCoreApplication.translate('Processing', string)
    
    def initAlgorithm(self, config):
        file_param = QgsProcessingParameterFile(
                self.FILE,
                self.tr('Audio File'),
                fileFilter='Audio Files (*.mp3 *.wav *.ogg *.flac *.m4a *.wma *.aac *.aiff *.au *.mid *.midi *.mpc *.oga *.opus *.ra *.ram *.spx *.xspf);; All Files (*.*)'
            )
        volume_param = QgsProcessingParameterNumber(
                self.VOLUME,
                self.tr('Volume'),
                type=QgsProcessingParameterNumber.Integer,
                optional=True,
                defaultValue=100,
                maxValue=100,
                minValue=0
            )
        params = [file_param, volume_param]
        for param in params:
            self.addParameter(param,
                createOutput = True)
            
    
    def prepareAlgorithm(self, parameters, context, feedback):
        self.file_path = self.parameterAsFile(parameters, self.FILE, context)
        self.play_volume = self.parameterAsInt(parameters, self.VOLUME, context)
        self.player = QMediaPlayer()
        
        feedback.pushInfo('Playing audio file: {} at volume {}'.format(self.file_path, self.play_volume))
        self.player.setVolume(int(self.play_volume))
        self.player.setMedia(QMediaContent(QUrl.fromLocalFile(self.file_path)))
        return super().prepareAlgorithm(parameters, context, feedback)
    
    def processAlgorithm(self, parameters, context, feedback):
        try:
            if not self.player.isAudioAvailable():
                self.player.setMedia(QMediaContent(QUrl.fromLocalFile(self.file_path)))
                self.player.audioAvailableChanged.connect(lambda: self.player.play())
                self.player.setVolume(int(self.play_volume))
                self.player.play()
            else:
                self.player.play()
            feedback.pushInfo('Audio file played')
            return {self.OUTPUT:{
            'FILE': self.file_path, 
            'VOLUME': self.play_volume, 
            'OUTPUT': 'Played Audio File'}
            }

        except Exception as e:
            return {self.OUTPUT: 'Failed to play audio file', 'ERROR': str(e)}
        
    def createInstance(self):
        return PlayAudioFileAlgorithm()
    

class QgisSoundEffectsProvider(QgsProcessingProvider):
    def __init__(self):
        QgsProcessingProvider.__init__(self)

    def id(self):
        return 'qgis_sound_effects'
    
    def name(self):
        return self.tr('QGIS Sound Effects')
    
    def icon(self):
        return QIcon(':/plugins/qgs_sound_effects/qgs_effects_icon.png')
    
    def unload(self):
        pass
    
    def loadAlgorithms(self):
        self.addAlgorithm(PlaySoundEffectAlgorithm())
        self.addAlgorithm(SaySomeTextAlgorithm())
        self.addAlgorithm(PlayAudioFileAlgorithm())

    