#!/usr/bin/python3
# gTranscribe is a software focused on easy transcription of spoken words.
# Copyright (C) 2013-2020 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/>.

#pylint: disable=wrong-import-position
import sys
import os
import re
import logging
import argparse
import locale
import gettext
from gettext import gettext as _
import signal
import inspect
import dbus
from dbus.mainloop.glib import DBusGMainLoop
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gtk, Gdk, GdkPixbuf
try:
    gi.require_version('Gspell', '1')
    from gi.repository import Gspell
    SPELL_FALLBACK = False
except ValueError:
    gi.require_version('GtkSpell', '3.0')
    from gi.repository import GtkSpell
    SPELL_FALLBACK = True

# Add project root directory to sys.path.
PROJECT_ROOT_DIRECTORY = os.path.dirname(os.path.dirname(
    os.path.realpath(inspect.getfile(inspect.currentframe()))))

if PROJECT_ROOT_DIRECTORY not in sys.path:
    sys.path.insert(0, PROJECT_ROOT_DIRECTORY)

from gtranscribe.helpers import *
from gtranscribe.player import gTranscribePlayer
from gtranscribe.metadata import MetaData

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


class gTranscribeWindow:

    def __init__(self, audiofile):
        self.time_str = '00:00.0'

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

        self.filename = None
        self.md5 = None

        self.position = 0

        # TODO: Make these configurable
        self.jump_back_interval = datetime.time(second=1)
        self.seek_interval = datetime.time(second=1)

        self._init_ui()

        # Initialize database for meta data
        MetaData.init_db(self)

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

        # 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 audiofile:
            # Open a file passed as command-line parameter.
            # Use idle_add so window is settled.
            GLib.idle_add(self.open_file, audiofile)

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

        icon_theme = Gtk.IconTheme.get_default()
        if icon_theme.has_icon('gtranscribe'):
            icon = icon_theme.load_icon('gtranscribe', 128, 0)
        else:
            icon = GdkPixbuf.Pixbuf.new_from_file_at_scale(
                get_data_file(PROJECT_ROOT_DIRECTORY, 'icons', 'scalable',
                              'apps', 'gtranscribe.svg'), 128, 128, 0)
        self.window.set_default_icon(icon)

        self.text_view = builder.get_object("text_view")
        if SPELL_FALLBACK:
            schecker = GtkSpell.Checker()
            schecker.set_language(locale.getdefaultlocale()[0])
            schecker.attach(self.text_view)
        else:
            schecker = Gspell.TextView.get_from_gtk_text_view(self.text_view)
            Gspell.TextView.basic_setup(schecker)
        self.text_buffer = self.text_view.get_buffer()

        spinbutton_jump = builder.get_object("spinbutton_jump")
        spinbutton_jump.set_range(0, 5000)
        spinbutton_jump.set_increments(10, 100)
        spinbutton_jump.set_value(time_to_ns(self.jump_back_interval) /
                                  1000000)

        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.jump_menu = builder.get_object('jump')
        self.jump_menu.set_sensitive(False)

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

        self.play_action = builder.get_object("button_play")
        self.play_action.set_sensitive(False)
        self.play_menu = builder.get_object('play')
        self.play_menu.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.dur_label.set_text(self.time_str)
        self.pos_label.set_text(self.time_str)

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

    def _set_update_ui(self, update):
        if update:
            self._update_id = GLib.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:
                GLib.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)

    # pylint: disable=unused-argument
    def about(self, action):
        about_dialog = Gtk.AboutDialog()
        about_dialog.set_transient_for(self.window)
        about_dialog.set_modal(True)
        about_dialog.set_name("gTranscribe")
        about_dialog.set_version("0.9")
        about_dialog.set_copyright("Copyright \u00A9 2013-2020 Philip Rinn\n"
                                   "Copyright \u00A9 2010 Frederik Elwert")
        about_dialog.set_comments(_("gTranscribe is a software focused "
                                    "on easy transcription of spoken words."))
        about_dialog.set_website("https://github.com/innir/gtranscribe")
        about_dialog.set_authors(["Philip Rinn <rinni@inventati.org>"])
        about_dialog.set_license_type(Gtk.License.GPL_3_0)
        about_dialog.run()
        about_dialog.destroy()

    # pylint: disable=unused-argument
    def open(self, action):
        audiofile = get_open_filename(self, _('Open Audio File'),
                                      _('All Audio Files'), 'audio/*')
        if audiofile is not None:
            self.open_file(audiofile)

    def open_file(self, audiofile):
        self.play_action.set_active(False)
        self.slider.set_value(0)
        self.md5 = md5_of_file(audiofile)
        # insert md5 into database so we can just update afterwards
        MetaData.store_md5(self)
        self.player.open(audiofile)

    # pylint: disable=unused-argument
    def on_file_ready(self, sig, audiofile):
        logger.debug('received signal "ready"')
        GLib.idle_add(self.update_file, audiofile)

    def update_file(self, audiofile):
        self.position = 0
        fileinfo = MetaData(audiofile, self.md5)
        if fileinfo.position:
            logger.debug('Resuming at position %s',
                         ns_to_time(fileinfo.position))
            self.player.position = fileinfo.position
            self.position = fileinfo.position
        if fileinfo.speed:
            logger.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(audiofile)
        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.play_menu.set_sensitive(True)
        self.rewind_menu.set_sensitive(True)
        self.forward_menu.set_sensitive(True)
        self.jump_menu.set_sensitive(True)
        self.speedscale.set_sensitive(True)

    # pylint: disable=unused-argument
    def on_file_ended(self, sig):
        logger.debug('received signal "ended"')
        # Automatically save text if a filename is given to avoid data loss
        if self.filename is not None:
            self.save_text(self.window)
        self.player.reset()
        self.play_action.set_active(False)

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

    # pylint: disable=unused-argument
    def on_focus(self, widget, event):
        if self.settings_daemon:
            self.settings_daemon.GrabMediaPlayerKeys('gTranscribe', 0)

    def play(self, action):
        if action.get_active() or self.play_menu.get_label() == _("Play"):
            logger.debug('play action triggered')
            self.icon_play.set_from_icon_name(Gtk.STOCK_MEDIA_PAUSE, 2)
            self.play_menu.set_label(_("Pause"))
            # 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:
            logger.debug('pause action triggered')
            self.icon_play.set_from_icon_name(Gtk.STOCK_MEDIA_PLAY, 2)
            self.play_menu.set_label(_("Play"))
            self.window.update_ui = False
            self._set_update_ui(False)
            self.player.pause()
            self.player.move_position(-time_to_ns(self.jump_back_interval))
            GLib.idle_add(self.play_loop, True)
            if self.md5 is not None:
                fileinfo = MetaData(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:
            logger.warning("query failed, can't get current position")
            return False
        try:
            duration = self.player.duration
        except:
            logger.warning("query failed, can't get file duration")
            return False
        self.set_position_label(duration, update_scale)
        if once:
            return False
        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):
        self.player.move_position(time_to_ns(self.seek_interval))
        GLib.idle_add(self.play_loop, True)

    def rewind(self, action=None, user_data=None):
        self.player.move_position(-time_to_ns(self.seek_interval))
        GLib.idle_add(self.play_loop, True)

    # pylint: disable=unused-argument
    def jump(self, action):
        # Only do this if an audio file is already loaded
        if self.md5 is not None:
            # Get the current cursor position
            position = self.text_buffer.get_iter_at_mark(
                self.text_buffer.get_insert())
            # Get the cursor position relative  to the beginning of this line
            line_offset = position.get_line_offset()
            # Get beginning of the line
            line_start = position.get_offset() - line_offset
            # Get the text at the end of the last line
            pos = self.text_buffer.get_text(
                self.text_buffer.get_iter_at_offset(line_start - 10),
                self.text_buffer.get_iter_at_offset(line_start - 1), True)
            logger.debug('Try to get the position from %s', pos)
            #pylint: disable=anomalous-backslash-in-string
            pos_tag = re.compile('\[\d\d:\d\d.\d\]')
            if pos_tag.match(pos):
                pos = pos[-8:-1]
                #pylint: disable=anomalous-backslash-in-string
                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"))
                    GLib.idle_add(self.play_loop, True)
                    self.text_buffer.place_cursor(
                        self.text_buffer.get_iter_at_offset(line_start))
                    logger.debug('Set position')

    # pylint: disable=unused-argument,no-self-use
    def on_scale_speed_format_value(self, widget, value, data=None):
        return u'%s\xd7' % locale.format_string('%.1f', value)

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

    def on_scale_position_value_changed(self, rel_position):
        if not self.seeking:
            # Slider changed without user action, do nothing
            return
        value = rel_position.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
        GLib.idle_add(self.play_loop, True, False)

    # pylint: disable=unused-argument
    def on_scale_position_button_press_event(self, widget, event):
        logger.debug('Begin seeking')
        self.oldstate = self.player.state
        self.player.pause()
        self.seeking = True

    # pylint: disable=unused-argument
    def on_scale_position_button_release_event(self, widget, event):
        logger.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)

    # pylint: disable=unused-argument
    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)

    # pylint: disable=unused-argument
    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 is not None:
            fileinfo = MetaData(self.player.filename, self.md5)
            fileinfo.position = self.position
            fileinfo.speed = self.player.rate
        if self.settings_daemon is not None:
            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)

    # pylint: disable=unused-argument
    def open_text(self, action):
        """
        Called when the user clicks the 'Open Text' menu.
        The previous contents of the GtkTextView is overwritten.
        """
        self.filename = None
        self.filename = get_open_filename(self, _("Open Text File"),
                                          _('Plain Text Files'), 'text/plain')
        if self.filename is not None:
            # get the file contents
            fin = open(self.filename, "r")
            text = fin.read()
            fin.close()
            # Only do this if an audio file is already loaded
            if hasattr(self, 'md5'):
                # Try to get the last position
                pos = text[-15:]
                logger.debug('Try to get the last position from %s', pos)
                #pylint: disable=anomalous-backslash-in-string
                pos_tag = re.compile('.*[\[#]((\d\d:)?\d\d:\d\d[-.]\d)[\]#]')
                if pos_tag.match(pos):
                    pos = pos_tag.match(pos).group(1).replace("-", ".")
                    if len(pos) == 10:
                        time_str = "%H:%M:%S.%f"
                    else:
                        time_str = "%M:%S.%f"
                    self.player.position = time_to_ns(
                        datetime.datetime.strptime(pos, time_str))
                    GLib.idle_add(self.play_loop, True)
                    logger.debug('Set position on load to %s', pos)
            # disable the text view while loading the buffer with the text
            self.text_view.set_sensitive(False)
            self.text_buffer.set_text(text)
            self.text_buffer.set_modified(False)
            self.text_view.set_sensitive(True)
            self.text_view.grab_focus()
            GLib.idle_add(self.text_view.scroll_mark_onscreen,
                          self.text_buffer.get_insert())

    # pylint: disable=unused-argument
    def save_text(self, action):
        """
        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.
        """
        if self.filename is None:
            self.filename = get_save_filename(self)
        try:
            # disable text view while getting contents of buffer
            self.text_view.set_sensitive(False)
            text = self.text_buffer.get_text(self.text_buffer.get_start_iter(),
                                             self.text_buffer.get_end_iter(),
                                             True)
            self.text_view.set_sensitive(True)
            self.text_buffer.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)

    # pylint: disable=unused-argument
    def on_text_insert(self, widget, event):
        keyname = Gdk.keyval_name(event.keyval)
        if (keyname in ('Return', 'F8')) and self.position > 0:
            pos_str = ' #' + trim(
                ns_to_time(self.position).strftime(self.time_str.replace(".", "-"))) + '#'
            self.text_buffer.insert_at_cursor(pos_str)
        # handling for yellow foot switch "science" from cleware
        if keyname == 'F9':
            if self.slider.is_sensitive():
                self.play_action.activate()
        elif keyname == 'F4':
            self.play_action.set_active(False)

    def main(self):
        """Run main application window."""
        self.window.connect('key_press_event', self.on_text_insert)
        self.window.show()
        Gtk.main()


if __name__ == "__main__":
    # Support for command line options
    parser = argparse.ArgumentParser()
    parser.add_argument("-v", "--verbose", action="store_true",
                        help=_("Show debug messages"))
    parser.add_argument("file", nargs='?', help=_("Audio file to load"))
    args = parser.parse_args()

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

    audio_file = None
    if args.file and os.path.isfile(args.file):
        audio_file = args.file

    # 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(audio_file)
    gtranscriber.main()
