#!/usr/bin/python3
# gTranscribe is a software focussed on easy transcription of spoken words.
# Copyright (C) 2013-2016 Philip Rinn <rinni@inventati.org>
# Copyright (C) 2010 Frederik Elwert <frederik.elwert@web.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# 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 <https://www.gnu.org/licenses/>.

import sys
import os
import sqlite3
import re
import logging
import optparse
import locale
import gettext
from gettext import gettext as _
import dbus
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSpell', '3.0')
from gi.repository import GLib, GObject, Gtk, Gdk, GtkSpell
from dbus.mainloop.glib import DBusGMainLoop
import signal

# Add project root directory (enable symlink, and trunk execution).
PROJECT_ROOT_DIRECTORY = os.path.abspath(
    os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0]))))

python_path = []
if (os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'gtranscribe')) and
   PROJECT_ROOT_DIRECTORY not in sys.path):
    python_path.insert(0, PROJECT_ROOT_DIRECTORY)
    sys.path.insert(0, PROJECT_ROOT_DIRECTORY)
if python_path:
    os.putenv('PYTHONPATH', "%s:%s" % (os.getenv('PYTHONPATH', ''),
              ':'.join(python_path)))

from gtranscribe import AboutDialog
from gtranscribe.helpers import *
from gtranscribe.player import gTranscribePlayer
from gtranscribe.fileinfo import FileInfo

GObject.threads_init()
locale.setlocale(locale.LC_ALL, '')
gettext.textdomain('gTranscribe')
DBusGMainLoop(set_as_default=True)


class gTranscribeWindow:

    def __init__(self):
        builder = Gtk.Builder()
        builder.set_translation_domain('gTranscribe')
        builder.add_from_file(get_data_file('ui', 'gTranscribe.glade'))
        builder.connect_signals(self)
        self.window = builder.get_object("gtranscribe_window")

        self.text_view = builder.get_object("text_view")
        spellchecker = GtkSpell.Checker()
        spellchecker.set_language(locale.getdefaultlocale()[0])
        spellchecker.attach(self.text_view)

        self.spinbutton_jump = builder.get_object("spinbutton_jump")
        self.spinbutton_jump.set_range(0, 5000)
        self.spinbutton_jump.set_increments(10, 100)

        self.rewind_button = builder.get_object('button_seek_back')
        self.rewind_button.set_sensitive(False)
        self.forward_button = builder.get_object('button_seek_forward')
        self.forward_button.set_sensitive(False)

        self.rewind_menu = builder.get_object('rewind')
        self.rewind_menu.set_sensitive(False)
        self.forward_menu = builder.get_object('forward')
        self.forward_menu.set_sensitive(False)

        self.speedscale = builder.get_object('scale_speed')

        self.play_action = builder.get_object("action_play")
        self.play_action.set_sensitive(False)
        self.slider = builder.get_object('scale_position')
        self.slider.set_sensitive(False)
        self.icon_play = builder.get_object("icon_play")

        self.dur_label = builder.get_object('label_duration')
        self.pos_label = builder.get_object('label_position')
        self.time_str = '00:00.0'
        self.dur_label.set_text(self.time_str)
        self.pos_label.set_text(self.time_str)

        self.oldstate = None
        self.seeking = False
        self.exact_seeking = True
        self._update_id = None

        self.filename = None
        self.about_dialog = None

        self.position = 0

        # TODO: Make these configurable
        self.JUMP_BACK_INTERVAL = datetime.time(second=1)
        self.JUMP_BACK = True
        self.SEEK_INTERVAL = self.JUMP_BACK_INTERVAL
        self.TIMESTAMP_FORMAT = "%s"  # allows formats like "[%s]", "<%s>", ...

        try:
            session_bus = dbus.SessionBus()
            settings_daemon_proxy = session_bus.get_object(
                'org.gnome.SettingsDaemon',
                '/org/gnome/SettingsDaemon/MediaKeys')
            self.settings_daemon = dbus.Interface(
                settings_daemon_proxy, 'org.gnome.SettingsDaemon.MediaKeys')
            self.settings_daemon.GrabMediaPlayerKeys('gTranscribe', 0)
        except dbus.DBusException:
            self.settings_daemon = None
        else:
            self.settings_daemon.connect_to_signal('MediaPlayerKeyPressed',
                                                   self.on_media_key)
            self.window.connect('focus-in-event', self.on_focus)

        # make sure our cache directory exists
        self.cache_dir = os.path.join(GLib.get_user_cache_dir(), "gTranscribe")
        if not os.path.exists(self.cache_dir):
            os.makedirs(self.cache_dir)

        # set up meta data database
        con = sqlite3.connect(os.path.join(self.cache_dir, "metadata.db"))
        cur = con.cursor()
        cur.execute('CREATE TABLE IF NOT EXISTS metadata(md5 TEXT PRIMARY KEY,\
                    position INTEGER, speed REAL)')
        con.commit()
        cur.close()
        con.close()

        # Code for other initialization actions should be added here.
        self.player = gTranscribePlayer()
        self.player.connect('ready', self.on_file_ready)
        self.player.connect('ended', self.on_file_ended)

        if filepath:
            # Open a file passed as command-line parameter.
            # Use idle_add so window is settled.
            GObject.idle_add(self.open_file, filepath)

    def _get_update_ui(self):
        return self._update_id is not None

    def _set_update_ui(self, update):
        if update:
            self._update_id = GObject.timeout_add(50, self.play_loop)
        else:
            # run play_loop one more time to make sure UI is up to date.
            self.play_loop()
            try:
                GObject.source_remove(self._update_id)
            except TypeError:
                # source no longer available, do nothing
                pass
            self._update_id = None

    update_ui = property(_get_update_ui, _set_update_ui)

    # Called when the user clicks the 'About' menu. We use
    # gtk_show_about_dialog() which is a convenience function
    # to show a GtkAboutDialog. This dialog will NOT be modal
    # but will be on top of the main application window.
    def about(self, menuitem, data=None):
        if self.about_dialog:
            return
        about_dialog = AboutDialog.AboutDialog(self)
        about_dialog.show()
        self.about_dialog = True

    def open(self, action):
        open_dlg = Gtk.FileChooserDialog(_('Open Audio File'), self.window,
                                         Gtk.FileChooserAction.OPEN,
                                         (Gtk.STOCK_CANCEL,
                                          Gtk.ResponseType.CANCEL,
                                          Gtk.STOCK_OK, Gtk.ResponseType.OK))
        filter = Gtk.FileFilter()
        filter.set_name(_('All Audio Files'))
        filter.add_mime_type('audio/*')
        open_dlg.add_filter(filter)
        open_dlg.set_filter(filter)
        result = open_dlg.run()
        if result == Gtk.ResponseType.OK:
            self.open_file(open_dlg.get_filename())
        open_dlg.destroy()

    def open_recent(self, item):
        uri = item.get_current_uri()
        path = uri_to_filepath(uri)
        self.open_file(path)

    def open_file(self, filepath):
        self.play_action.set_active(False)
        self.slider.set_value(0)
        self.md5 = md5_of_file(filepath)
        # insert md5 into database so we can just update afterwards
        con = sqlite3.connect(os.path.join(self.cache_dir, "metadata.db"))
        cur = con.cursor()
        cur.execute('INSERT OR IGNORE INTO metadata (md5) VALUES (?)',
                    (self.md5,))
        con.commit()
        cur.close()
        con.close()
        self.player.open(filepath)

    def on_file_ready(self, signal, filepath):
        logging.debug('received signal "ready"')
        GObject.idle_add(self.update_file, filepath)

    def update_file(self, filepath):
        self.position = 0
        fileinfo = FileInfo(filepath, self.md5)
        if fileinfo.position:
            logging.debug('Resuming at position %s' %
                          ns_to_time(fileinfo.position))
            self.player.position = fileinfo.position
            self.position = fileinfo.position
        if fileinfo.speed:
            logging.debug('Resuming with speed %s' % fileinfo.speed)
            self.speedscale.set_value(fileinfo.speed)
        duration = ns_to_time(self.player.duration)
        if duration.hour:
            self.time_str = '%H:%M:%S.%f'
        else:
            self.time_str = '%M:%S.%f'
        # set duration
        dur_str = trim(duration.strftime(self.time_str))
        self.dur_label.set_text(dur_str)
        # set position
        self.set_position_label(time_to_ns(duration))
        # set window title
        filename = os.path.basename(filepath)
        self.window.set_title(u'gTranscribe \u2013 %s' % filename)
        self.play_action.set_sensitive(True)
        self.slider.set_sensitive(True)
        self.rewind_button.set_sensitive(True)
        self.forward_button.set_sensitive(True)
        self.rewind_menu.set_sensitive(True)
        self.forward_menu.set_sensitive(True)

    def on_file_ended(self, signal):
        logging.debug('received signal "ended"')
        self.play_action.set_active(False)
        # Automatically save text if a filename is given to avoid data loss
        if self.filename is not None:
            self.on_save_menu_item_activate(self.window)
        self.player.reset()

    def on_media_key(self, application, key):
        logging.debug('Received media key %s for %s' % (key, application))
        if application == 'gTranscribe':
            if key == 'Play':
                self.play_action.activate()
            elif key in ('Stop', 'Pause'):
                self.play_action.set_active(False)
            elif key == 'Next':
                self.forward(None)
            elif key == 'Previous':
                self.rewind(None)

    def on_focus(self, widget, event):
        if self.settings_daemon:
            self.settings_daemon.GrabMediaPlayerKeys('gTranscribe', 0)

    def play(self, action):
        logging.debug('play action triggered')
        if action.get_active():
            self.icon_play.set_from_stock(Gtk.STOCK_MEDIA_PAUSE, 2)
            # It's not resuming at the correct position if we don't set the
            # position explicitly
            self.player.position = self.position
            self.player.play()
            self.window.update_ui = True
            self._set_update_ui(True)
        else:
            self.icon_play.set_from_stock(Gtk.STOCK_MEDIA_PLAY, 2)
            self.window.update_ui = False
            self._set_update_ui(False)
            self.player.pause()
            if self.JUMP_BACK:
                interval = time_to_ns(self.JUMP_BACK_INTERVAL)
                newpos = self.position - interval
                if newpos < 0:
                    newpos = 0
                self.player.position = newpos
                GObject.idle_add(self.play_loop, True)
            if hasattr(self, 'md5'):
                fileinfo = FileInfo(self.player.filename, self.md5)
                fileinfo.position = self.position

    def play_loop(self, once=False, update_scale=True):
        try:
            self.position = self.player.position
        except:
            logging.warn("query failed, can't get current position")
            return False
        try:
            duration = self.player.duration
        except:
            logging.warn("query failed, can't get file duration")
            return False
        self.set_position_label(duration, update_scale)
        if once:
            return False
        else:
            return True

    def set_position_label(self, duration, update_scale=True):
        if duration > 0:
            frac = float(self.position) / float(duration)
            if update_scale:
                scalepos = frac * self.slider.get_adjustment().get_upper()
                self.slider.set_value(scalepos)
            pos_str = trim(ns_to_time(self.position).strftime(self.time_str))
            self.pos_label.set_text(pos_str)

    def dec_speed(self, action):
        self.speedscale.set_value(self.speedscale.get_value() - 0.1)

    def inc_speed(self, action):
        self.speedscale.set_value(self.speedscale.get_value() + 0.1)

    def forward(self, action=None, user_data=None):
        new_position = self.position + time_to_ns(self.SEEK_INTERVAL)
        if new_position > self.player.duration:
            new_position = self.player.duration
        self.player.position = new_position
        GObject.idle_add(self.play_loop, True)

    def rewind(self, action=None, user_data=None):
        new_position = self.position - time_to_ns(self.SEEK_INTERVAL)
        if new_position < 0:
            new_position = 0
        self.player.position = new_position
        GObject.idle_add(self.play_loop, True)

    def copy_position(self, action):
        pos_str = trim(ns_to_time(self.position).strftime(self.time_str))
        clipboard = Gtk.clipboard_get()
        clipboard.set_text(self.TIMESTAMP_FORMAT % pos_str)

    def on_scale_speed_format_value(self, widget, value, data=None):
        return u'%s\xd7' % locale.format('%.1f', value)

    def on_scale_speed_value_changed(self, range):
        value = range.get_value()
        if (value != self.player.rate) and hasattr(self, 'md5'):
            self.player.rate = value
            fileinfo = FileInfo(self.player.filename, self.md5)
            fileinfo.speed = value

    def on_scale_position_value_changed(self, range):
        if not self.seeking:
            # Slider changed programmatically, do nothing
            return
        value = range.get_value()
        max_value = self.slider.get_adjustment().get_upper()
        new_position = self.player.duration * (value / max_value)
        self.player.position = new_position
        # Update only position label
        GObject.idle_add(self.play_loop, True, False)

    def on_scale_position_button_press_event(self, widget, event):
        logging.debug('Begin seeking')
        self.oldstate = self.player.state
        self.player.pause()
        self.seeking = True

    def on_scale_position_button_release_event(self, widget, event):
        logging.debug('End seeking')
        self.seeking = False
        self.player.state = self.oldstate
        if not self.player.playing:
            self.window.update_ui = False
            self._set_update_ui(False)
        else:
            self.window.update_ui = True
            self._set_update_ui(True)

    def on_volumebutton_value_changed(self, scalebutton, value):
        self.player.volume = value

    def quit(self, widget, data=None):
        """Signal handler for closing the gTranscribeWindow."""
        self.on_destroy(widget, data=None)

    def on_destroy(self, widget, data=None):
        """Called when the gTranscribeWindow is closed."""
        # Clean up code for saving application state should be added here.
        if self.player.filename:
            fileinfo = FileInfo(self.player.filename, self.md5)
            fileinfo.position = self.position
            fileinfo.speed = self.player.rate
        if self.settings_daemon:
            self.settings_daemon.ReleaseMediaPlayerKeys('gTranscribe')
        Gtk.main_quit()

    def on_jump_value_changed(self, range):
        self.JUMP_BACK_INTERVAL = ns_to_time(range.get_value_as_int() *
                                             1000000)
        self.SEEK_INTERVAL = self.JUMP_BACK_INTERVAL

    # Called when the user clicks the 'Open' menu. We need to prompt for save
    # if the file has been modified, allow the user to choose a file to open,
    # and then load it into the buffer for the GtkTextView.
    # The previous contents are overwritten.
    def on_open_menu_item_activate(self, menuitem, data=None):
        self.filename = get_open_filename(self)
        if self.filename:
            try:
                # get the file contents
                fin = open(self.filename, "r")
                text = fin.read()
                fin.close()
                # get possibly the last position
                pos = text[-10:-1]
                logging.debug('Try to get the last position from %s' % pos)
                pos_tag = re.compile('\[\d\d:\d\d.\d\]')
                if pos_tag.match(pos):
                    pos = pos[-8:-1]
                    pos_tag = re.compile('\d\d:\d\d.\d')
                    if pos_tag.match(pos):
                        self.player.position = time_to_ns(
                            datetime.datetime.strptime(pos, "%M:%S.%f"))
                        GObject.idle_add(self.play_loop, True)
                        logging.debug('Set position on load')
                # disable the text view while loading the buffer with the text
                self.text_view.set_sensitive(False)
                buff = self.text_view.get_buffer()
                buff.set_text(text)
                buff.set_modified(False)
                self.text_view.set_sensitive(True)
            except:
                # error loading file, show message to user
                self.filename = None
                error_message(self, "Could not open file: %s" % filename)

    # Called when the user clicks the 'Save' menu. We need to allow the user to
    # choose a file to save if it's an untitled document
    def on_save_menu_item_activate(self, menuitem, data=None):
        if self.filename is None:
            self.filename = get_save_filename(self)
        try:
            # disable text view while getting contents of buffer
            buff = self.text_view.get_buffer()
            self.text_view.set_sensitive(False)
            text = buff.get_text(buff.get_start_iter(), buff.get_end_iter(),
                                 True)
            self.text_view.set_sensitive(True)
            buff.set_modified(False)
            fout = open(self.filename, "w")
            fout.write(text)
            fout.close()
            self.text_view.grab_focus()
        except:
            # error writing file, show message to user
            error_message(self, "Could not save file: %s" % self.filename)

    def on_text_insert(self, widget, event):
        keyname = Gdk.keyval_name(event.keyval)
        if keyname == 'Return':
            pos_str = ' [' + trim(
                ns_to_time(self.position).strftime(self.time_str)) + ']'
            text_buffer = self.text_view.get_buffer()
            text_buffer.insert_at_cursor(pos_str)
        # handling for yellow footswitch "science" from cleware
        if keyname == 'F9':
            self.play_action.activate()
        elif keyname == 'F4':
            self.play_action.set_active(False)

    # Run main application window
    def main(self):
        self.spinbutton_jump.set_value(time_to_ns(self.JUMP_BACK_INTERVAL) /
                                       1000000)
        self.window.connect('key_press_event', self.on_text_insert)
        self.window.show()
        Gtk.main()


def get_data_file(*path_segments):
    # Where to look for data (ui and image files). By default,
    # this is ../data, relative your trunk layout
    data_directory = os.path.abspath(
        os.path.join(os.path.dirname(__file__), '../data/'))

    # If this path does not exist fall back to system wide directory
    if not os.path.exists(data_directory):
        data_directory = os.path.abspath(
            os.path.join(os.path.dirname(__file__), '../share/gTranscribe/'))

    return os.path.join(data_directory, *path_segments)


if __name__ == "__main__":
    # Support for command line options
    parser = optparse.OptionParser(version="%prog %ver")
    parser.add_option(
        "-v", "--verbose", action="store_true", dest="verbose",
        help=_("Show debug messages"))
    (options, args) = parser.parse_args()

    # Set the logging level to show debug messages
    if options.verbose:
        logging.basicConfig(level=logging.DEBUG)
        logging.debug('logging enabled')

    filepath = None
    if args and os.path.isfile(args[0]):
        filepath = args[0]

    # Catch Ctrl+C and quit the program
    signal.signal(signal.SIGINT, lambda a, b: gtranscriber.quit(gtranscriber))
    GLib.timeout_add(500, lambda: True)

    # Run the application
    gtranscriber = gTranscribeWindow()
    gtranscriber.main()
