# -*- coding: utf-8 -*-
#
# Graphical interface for the eSpeak speech synthesizer
#
# Copyright © 2009-2012 Siegfried-Angel Gevatter Pujals <siegfried@gevatter.com>
# Copyright © 2009 Joe Burmeister <joe.a.burmeister@googlemail.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 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import os
import gtk
import gobject
import gtksourceview2
import gettext
import locale
import gtkspell
from optparse import OptionParser
from espeak import espeak

from resources import *
import about_dialog
from language import LanguageManager
from voicebox import VoiceBox

gtk.gdk.threads_init()

class Application:

    _espeak_language = None
    _language_manager = None
    _voicebox = None
    _spell = None
    
    @property
    def _espeak_gender(self):
        gender = settings.get_string(settings_gender)
        return espeak.Gender.Male if gender == 'male' else (
            espeak.Gender.Female if gender == 'female' else None)
    
    def __init__(self):
        self._builder = gtk.Builder()
        self._builder.set_translation_domain('espeak-gui')
        self._builder.add_from_file(interface_file)
        self._builder.connect_signals(self)
        
        self._recentmanager = gtk.RecentManager()
        
        for gender in ('male', 'female', 'any'):
            menuitem = self._builder.get_object('voice_gender_%s' % gender)
            menuitem.connect('toggled', self._e_voice_gender_toggled, gender)
            if gender == (settings.get_string(settings_gender) or 'any'):
                menuitem.set_active(True)
        
        selected_punctuation = settings.get_string(settings_punctuation) or 'any'
        for punctuation in ('all', 'custom', 'any'):
            menuitem = self._builder.get_object('punctuation_%s' % punctuation)
            menuitem.connect('toggled', self._e_punctuation_toggled, punctuation)
            if punctuation == selected_punctuation:
                menuitem.set_active(True)
        espeak.set_parameter(espeak.Parameter.Punctuation,
            getattr(espeak.Punctuation, selected_punctuation.capitalize()))
        
        self._main = self._builder.get_object('main_window')
        self._main.set_icon_from_file(icon_file)
        
        self._scrollbox = self._builder.get_object('scrolledwindow')
        self._playbutton = self._builder.get_object('button_read')
        self._pausebutton = self._builder.get_object('button_pause')
        self._speedspin = self._builder.get_object('speedspin')
        self._about_dialog = about_dialog.create_about_dialog(self._main)
        
        self._buffer = gtksourceview2.Buffer()
        self._buffer.connect('changed', self._e_text_changed)
        self._inputbox = gtksourceview2.View(self._buffer)
        self._inputbox.set_pixels_above_lines(2)
        self._inputbox.set_pixels_below_lines(2)
        self._inputbox.set_left_margin(4)
        self._inputbox.set_right_margin(4)
        self._inputbox.set_wrap_mode(gtk.WRAP_WORD);
        self._scrollbox.add(self._inputbox)
        #self._inputbox.connect('populate-popup', self._e_on_context_menu)
                
        self._language_manager = LanguageManager(self._get_voice_list())
        self._voicebox = VoiceBox(
            self._builder.get_object('voicebox'), self._language_manager)
        self._voicebox.set_voice(
            settings.get_string(settings_selected_voice) or \
            self._language_manager.get_default_language())
        
        self._playing = False
        self._paused = False
        self._speedspin.set_value(settings.get_int(settings_speed) or 160)
        
        espeak.set_SynthCallback(self._e_eventCB)
        
        self._speaking_tag = self._buffer.create_tag('speaking')
        self._speaking_tag.set_property('background', 'black')
        self._speaking_tag.set_property('foreground', 'white')
        
        self._speaking_text_offset = 0
        self._spoken_chars_offset = 0
        
        if arguments:
            self._load_files(*arguments)
        
        self._main.show_all()
        self._pausebutton.set_visible(False)
        gtk.main()
    
    def _autodetect(self):
        text = self._buffer.get_text(self._buffer.get_start_iter(),
            self._buffer.get_end_iter())
        return self._language_manager.autodetect(text)
    
    def synth(self, text):
        parameters = {}
        if self._espeak_gender:
            parameters['gender'] = self._espeak_gender
        if self._espeak_language not in (None, 'autodetect'):
            parameters['language'] = self._espeak_language
        else:
            guess = self._autodetect()
            self._voicebox.set_guess(guess)
            parameters['language'] = guess.get_language()
        print parameters
        espeak.set_voice(**parameters)
        return espeak.synth(text)
    
    def _get_voice_list(self):
        """ Returns the output of espeak.list_voices() after sorting it. """
        return sorted(espeak.list_voices(), cmp=locale.strcoll,
            key=lambda voice: voice.name)
    
    def _e_voicebox_changed(self, widget):
        (voice_name, voice_id) = self._voicebox.get_voice()
        self._active_voice = voice_name
        self._espeak_language = voice_id
        settings.set_string(settings_selected_voice, voice_id)
        if not self._spell:
            self._spell = gtkspell.Spell(self._inputbox)
        try:
            self._spell.set_language(voice_id.split('/', 1)[-1])
        except RuntimeError:
           # unsupported language
            self._spell.detach()
            self._spell = None
    
    def _set_voice(self, voice):
        self._voicebox.set_voice(voice)
    
    def _cleanup_speech_end(self):
        self._playing = False
        self._paused = False
        self._buffer.remove_tag(self._speaking_tag,
            self._buffer.get_start_iter(), self._buffer.get_end_iter())
        self._inputbox.show_all()
        self._inputbox.set_editable(True)
        self._playbutton.set_label(_('Play'))
        self._pausebutton.set_label(_('Pause'))
        self._pausebutton.set_visible(False)
        self._playbutton.show()
    
    def _e_eventCB(self, event, pos, length):
        
        gtk.gdk.threads_enter()
        
        if event == espeak.core.event_WORD:
            pos += (self._speaking_text_offset - 1)
            self._buffer.remove_tag(self._speaking_tag, self._buffer.get_start_iter(),
                self._buffer.get_end_iter())
            
            self._spoken_chars_offset = pos
            s = self._buffer.get_iter_at_offset(pos)
            e = self._buffer.get_iter_at_offset(length+pos)
            pos = self._inputbox.get_iter_location(s)
            
            adj = self._scrollbox.get_vadjustment()
            halfPage = adj.page_size / 2
            if (pos.y + pos.height) > (adj.value + halfPage):
                adj.set_value(min(adj.value+halfPage/2, adj.upper))
            elif pos.y < adj.value:
                adj.set_value(pos.y)
            
            self._buffer.apply_tag(self._speaking_tag, s, e)
            self._inputbox.show_all()
        
        if event == espeak.event_MSG_TERMINATED:
            self._cleanup_speech_end()
        
        gtk.gdk.threads_leave()
        
        return True
    
    def _play(self):
        if self._playing:
            if not self._paused:
                return
        else:
            self._playing = True
            self._spoken_chars_offset = 0
        
        self._inputbox.set_editable(False)
        text = ''
        
        if self._paused:
            self._speaking_text_offset = self._spoken_chars_offset
            text = self._buffer.get_text(
                self._buffer.get_iter_at_offset(self._spoken_chars_offset),
                self._buffer.get_end_iter())
        
        elif self._buffer.get_has_selection():
            selection = self._buffer.get_selection_bounds()
            
            s = selection[0]
            e = selection[1]
            
            self._speaking_text_offset = s.get_offset()
            text = self._buffer.get_text(s, e)
            self._buffer.select_range(self._buffer.get_start_iter(),
                self._buffer.get_start_iter())
        
        else:
            self._speaking_text_offset = 0
            text = self._buffer.get_text(self._buffer.get_start_iter(),
                self._buffer.get_end_iter())
        
        self._playbutton.set_label(_('Stop'))
        self._pausebutton.set_visible(True)
        
        if not self.synth(text):
            self._cleanup_speech_end()
    
    def _e_text_changed(self, *discard):
        text = self._buffer.get_text(self._buffer.get_start_iter(),
            self._buffer.get_end_iter())
        self._playbutton.set_sensitive(text != "")
    
    def _e_button_read_cd(self, *discard):
        if not self._playing:
            self._play()
        else:
            espeak.cancel()
            self._cleanup_speech_end()
        
        self._playbutton.show()
    
    def _e_button_pause_cd(self, *discard):
        if self._paused:
            self._play()
            self._pausebutton.set_label(_('Pause'))
            self._paused = False
        else:
            espeak.cancel()
            self._pausebutton.set_label(_('Play'))
            self._paused = True
    
    def _e_play(self, *discard):
        self._play()
        self._paused = False
    
    def _e_speed_changed(self, widget):
        speed = int(widget.get_value())
        espeak.set_parameter(espeak.Parameter.Rate, speed)
        settings.set_int(settings_speed, speed)
    
    def _e_new(self, *discard):
        self._buffer.delete(self._buffer.get_start_iter(),
            self._buffer.get_end_iter())
    
    def _e_new_window(self, *discard):
        new_instance()
    
    @staticmethod
    def _ensure_uri(filename):
        if filename.startswith('/'):
            return 'file://%s' % filename
        return filename

    @staticmethod
    def _ensure_filename(filename):
        if filename.startswith('file://'):
            return filename[7:]
        return filename
    
    def _load_files(self, *filenames):
        text = ''
        multiple_files = len(filenames) > 1
        for filename in filenames:
            filename = self._ensure_filename(filename)
            if multiple_files:
                if text:
                    text += '\n' * (3 - len(text[-2:]) + len(text[-2:].strip()))
                text += _('File:') + ' %s\n\n' % os.path.basename(filename)
            try:
                f = open(filename)
            except IOError, error:
                text += _('This file couldn\'t be opened: %(strerror)s.') % \
                    { 'strerror': error.strerror }
            else:
                content = f.read()
                if content:
                    text += content
                elif multiple_files:
                    text += _('This file is empty.')
                del content
                f.close()
                self._recentmanager.add_item(
                    self._ensure_uri(os.path.abspath(filename)))
        self._buffer.set_text(text)
    
    @staticmethod
    def _set_file_filters(dialog):
        filter = gtk.FileFilter()
        filter.set_name(_('All files'))
        filter.add_pattern('*')
        dialog.add_filter(filter)
        
        filter = gtk.FileFilter()
        filter.set_name(_('Plain text'))
        filter.add_mime_type('text/plain')
        filter.add_pattern('*.txt')
        dialog.add_filter(filter)
    
    def _e_open(self, *discard):
        dialog = gtk.FileChooserDialog(_('Open...'), self._main,
            gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL,
            gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))
        dialog.set_select_multiple(True)
        dialog.set_default_response(gtk.RESPONSE_OK)
        self._set_file_filters(dialog)
        
        response = dialog.run()
        if response == gtk.RESPONSE_OK:
            self._load_files(*dialog.get_filenames())
        
        dialog.destroy()
    
    def _e_save(self, *discard):
        # TRANSLATORS: This is the title of the file selection dialog
        dialog = gtk.FileChooserDialog(_('Save as...'), self._main,
            gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL,
            gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK))
        dialog.set_default_response(gtk.RESPONSE_OK)
        self._set_file_filters(dialog)
        
        response = dialog.run()
        if response == gtk.RESPONSE_OK:
            f = open(dialog.get_filename(), 'w')
            if f != None:
                f.write(self._buffer.get_text(self._buffer.get_start_iter(),
                    self._buffer.get_end_iter()) + '\n')
                f.close()
                self._recentmanager.add_item(
                    self._ensure_uri(dialog.get_filename()))
            else:
                msg = gtk.MessageDialog(self._main, gtk.DIALOG_MODAL,
                    gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
                    _('Could not open file "%s" for saving.') % dialog.get_filename())
                msg.run()
                msg.destroy()
        
        dialog.destroy()
    
    def _e_undo(self, *discard):
        if self._buffer.can_undo():
            self._buffer.undo()
    
    def _e_redo(self, *discard):
        if self._buffer.can_redo():
            self._buffer.redo()
    
    def _e_cut(self, *discard):
        if self._inputbox.get_editable():
            self._buffer.cut_clipboard(gtk.Clipboard(), True)
        else:
            self._e_copy()

    def _e_copy(self, *discard):
        self._buffer.copy_clipboard(gtk.Clipboard())

    def _e_paste(self, *discard):
        if self._inputbox.get_editable():
            self._buffer.paste_clipboard(gtk.Clipboard(), None, True)
    
    def _e_delete(self, *discard):
        if self._inputbox.get_editable():
            self._buffer.delete_selection(True,True)

    def _e_voice_gender_toggled(self, menuitem, gender):
        if menuitem.get_active():
            settings.set_string(settings_gender, gender)

    def _e_punctuation_toggled(self, menuitem, punctuation):
        if menuitem.get_active():
            settings.set_string(settings_punctuation, punctuation)
        espeak.set_parameter(espeak.Parameter.Punctuation,
             getattr(espeak.Punctuation, punctuation.capitalize()))
    
    #def _e_on_context_menu(self, entry, menu):
    #
    #    # Add a entry to the contextual menu
    #    menulabel = gtk.MenuItem(_("Language"))
    #    menu.append(menulabel)
    #    
    #    # Populate it with all available languages
    #    languages = gtk.Menu()
    #    menulabel.set_submenu(languages)
    #    text = self._buffer.get_text(self._buffer.get_start_iter(),
    #        self._buffer.get_end_iter())
    #    priority = language.identify(text)
    #    automatic = gtk.RadioMenuItem(label=_("Detect automatically"))
    #    languages.append(automatic)
    #    position = 1
    #    languages.append(gtk.SeparatorMenuItem())
    #    for voice in self._get_voice_list():
    #        item = gtk.RadioMenuItem(group=automatic, label=voice.name.capitalize())
    #        if voice.name.lower() in priority:
    #            languages.insert(item, position)
    #            position += 1
    #        else:
    #            languages.append(item)
    #    # Make all new entries visible
    #    menu.show_all()
    
    def _e_quit(self, *discard):
        espeak.set_SynthCallback(None)
        gtk.main_quit()
    
    def _e_about(self, *discard):
        self._about_dialog.run()

def main():
        
    app = Application()
