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

"""
Read from a file or from stdin.

File is under EUPL1.2
Author: kujiu
"""

import argparse

import os.path
import secrets
import speechd
import chardet
import queue
import threading
import sys


PUNCTUATION = {
    'none': speechd.PunctuationMode.NONE,
    'some': speechd.PunctuationMode.SOME,
    'all': speechd.PunctuationMode.ALL,
}


class SpdRead:
    def __init__(self, module, voice, lang, rate, punctuation, spelling):

        """
        Spd read software
        """
        self.module = module or 'default'
        self.rate = rate
        self.lang = lang
        self.voice = voice
        self.spelling = spelling
        self.punctuation = punctuation
        self._queue = queue.Queue()
        self._semaphore = threading.Semaphore(1)
        self._state = 'init'
        self.spdc = speechd.client.Client()

    def __del__(self):
        if getattr(self, 'spdc', None):
            self.spdc.close()

    def run(self, source):
        """
        Run application, blocking method
        """
        self._state = "running"
        try:
            self.set_speechd_parameters()
            if source:
                self.queue_file(source)
            else:
                self.queue_stdin()

            self.start_worker()
            self.join()
        except KeyboardInterrupt:
            self.stop()

    def stop(self):
        self._state = "stopping"
        self._semaphore.release()
        self.spdc.close()

    def start_worker(self):
        """
        Start play thread.
        """
        threading.Thread(target=self.say_loop, daemon=True).start()

    def join(self):
        """
        Join thread
        """
        return self._queue.join()

    def set_speechd_parameters(self):
        """
        Configure speechd
        """
        self.spdc.set_priority('important')
        if self.module != 'default' and self.module:
            self.spdc.set_output_module(self.module)

        voices = [
            voice[0]
            for voice
            in self.spdc.list_synthesis_voices()
            if self._voice_match(voice)]
        self.spdc.set_language(self.lang)
        if len(voices) == 1:
            self.spdc.set_synthesis_voice(voices[0])
        elif len(voices) >= 1:
            self.spdc.set_synthesis_voice(secrets.choice(voices))
        else:
            print("No voices installed for lang %s." % self.lang)
            self.stop()

        self.spdc.set_punctuation(PUNCTUATION[self.punctuation])
        self.spdc.set_rate(self.rate)
        self.spdc.set_spelling(self.spelling)

    def queue_file(self, source):
        """
        Queue speech from a file
        """
        with open(source, "rb") as fin:
            buffer = []
            for line in fin.readlines():
                encoding = chardet.detect(line)["encoding"]
                line = line[:-1].strip()
                line = line.decode(encoding, "replace").strip()
                if line:
                    buffer.append(line)
                elif len(buffer):
                    to_say = '\n'.join(buffer)
                    buffer = []
                    self.queue_speech(to_say)

                if self._state != 'running':
                    break

            if len(buffer) and self._state == 'running':
                self.queue_speech('\n'.join(buffer))

    def queue_stdin(self):
        """
        Queue speech from stdin
        """
        try:
            buffer = []
            for line in sys.stdin.buffer.readlines():
                if not len(line):
                    break

                encoding = chardet.detect(line)["encoding"]
                line = line[:-1].strip()
                line = line.decode(encoding, "replace").strip()
                if line:
                    buffer.append(line)
                elif len(buffer):
                    to_say = '\n'.join(buffer)
                    buffer = []
                    self.queue_speech(to_say)

                if self._state != 'running':
                    break

            if len(buffer) and self._state == 'running':
                self.queue_speech('\n'.join(buffer))

        except KeyboardInterrupt:
            self.stop()

    def next(self, *args, **kwargs):
        """
        Wait for speechd
        """
        pass

    def _voice_match(self, voice):
        """
        Check if a voice is in correct language
        """
        if self.voice:
            return voice[0].lower() == self.voice.lower()
        if voice[1].lower() != self.lang.lower()[0:2]:
            return False
        if len(self.lang) > 4 and voice[2].lower() != self.lang[3:5]:
            return False
        return True

    def queue_speech(self, text):
        """
        Queue text to speech
        """
        self._queue.put_nowait(text)

    def spd_callback(self, callback_type):
        """
        Process and of speechd
        """
        if callback_type == speechd.CallbackType.CANCEL:
            print("Cancelled speaking")
            self.stop()
        self._semaphore.release()

    def say_loop(self):
        """
        Say blocks
        """
        while self._semaphore.acquire() and self._state == 'running':
            try:
                text = self._queue.get_nowait()
                self.spdc.speak(
                    text, self.spd_callback,
                    event_types=(
                        speechd.CallbackType.CANCEL,
                        speechd.CallbackType.END))
                self._queue.task_done()
            except queue.Empty:
                print("End of speech queue")
                self.stop()


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Read with TTS.")
    parser.add_argument(
        '-l', '--lang', default='en',
        help='Language')
    parser.add_argument(
        '-V', '--voice', default='',
        help='Voice')
    parser.add_argument(
        '-m', '--module', default='default',
        help='Speech Dispatcher output module')
    parser.add_argument(
        '-r', '--rate', default=15,
        help='Speech Dispatcher rate', type=int)
    parser.add_argument(
        '-p', '--punctuation', default='none',
        help='Punctuation mode (none, all or some)')
    parser.add_argument(
        '-S', '--spelling', default=False, action='store_true',
        help='Spelling mode.')
    parser.add_argument(
        'source', default='', nargs='?',
        help='Source file (if not stdin)')

    options = parser.parse_args()

    source = os.path.expanduser(
        os.path.expandvars(options.source)
    )

    module = getattr(options, 'module', None)
    rate = getattr(options, 'rate', 15)
    if options.punctuation not in PUNCTUATION.keys():
        print('Punctuation must be one of all, some or none')
        exit(2)

    reader = SpdRead(
        module, options.voice,
        options.lang, rate,
        options.punctuation, options.spelling)

    reader.run(options.source)