#!/usr/bin/python
#
# (C) Copyright 2011 Mo Morsi (mo@morsi.org)
#
# gsnap - snap gtk gui
#
# 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.

import os
import sys
import time
import threading

GLADE_FILE="/usr/share/snap/snap-redux.glade"
if not os.path.isfile(GLADE_FILE):
    GLADE_FILE=os.path.dirname(os.path.realpath(__file__)) + "/../resources/snap-redux.glade" 

LOGO_FILE='/usr/share/snap/snap.png'
if not os.path.isfile(LOGO_FILE):
    LOGO_FILE=os.path.dirname(os.path.realpath(__file__)) + "/../resources/snap.png"

import snap, snap.config, snap.callback
#from snap.exceptions import *
#from snap.configmanager import ConfigManager

from gi.repository import Gtk, Gdk, GObject

# A helper class to launch the backup / restore operation in a thread
class GSnapThread(threading.Thread):
  def __init__(self, snapbase):
      threading.Thread.__init__ ( self )
      self.snapbase = snapbase
      self.error = None

  def run(self):
      try:
          if snap.config.options.mode == snap.config.ConfigOptions.BACKUP:
              self.snapbase.backup()
          else:
              self.snapbase.restore()
      except (snap.exceptions.SnapError), e:
          self.error = e

# The main window
class ModeSelectionWindow:

    @classmethod
    def set_instance(cls, instance):
       '''store a singleton instance of this window for future reference'''
       cls.instance = instance

    @classmethod
    def get_instance(cls):
        '''get singleton instance of this class'''
        return cls.instance

    def __init__(self):
       ModeSelectionWindow.set_instance(self)

       self.builder = Gtk.Builder() 
       self.builder.add_objects_from_file(GLADE_FILE, ["mode_selection", "main_logo"])

       self.window = self.builder.get_object("mode_selection") 
                                        
       # map the gui signals
       self.builder.connect_signals(self)
       self.window.connect("destroy", Gtk.main_quit)

       # set the logo path
       self.builder.get_object("main_logo").set_from_file(LOGO_FILE)
    
    def show(self):
        self.window.show()

    def show_backup_window(self, widget):
        backup_window = BackupOperationWindow()
        self.window.hide()
        backup_window.show()

    def show_restore_window(self, widget):
        restore_window = RestoreOperationWindow()
        self.window.hide()
        restore_window.show()

    def show_help_dialog(self, widget):
        help_dialog = HelpDialog()
        help_dialog.show()

    def close_window(self, widget):
        Gtk.main_quit()

# The backup operation window
class BackupOperationWindow:
    def __init__(self):
       self.builder = Gtk.Builder() 
       self.builder.add_objects_from_file(GLADE_FILE, 
          [ "backup_options",      "backup_snapfile",        "backup_log_level", 
            "backup_repos_toggle", "backup_packages_toggle", "backup_services_toggle",
            "backup_files_toggle", "select_backup_files_button",
            "backup_encryption_toggle", "backup_encryption_key"])
       self.window = self.builder.get_object("backup_options") 
                                        
       # map the gui signals
       self.builder.connect_signals(self)
       self.window.connect("destroy", self.close_backup_window)

       self._setup_log_levels()

       # XXX bit of a hack, since GtkFileChooserButton will not currently allow
       #  use to operate in 'save' mode, eg where the user can create a new file,
       #  we create a temporary one and set it here for the time being
       # https://bugzilla.gnome.org/show_bug.cgi?id=662837
       self.snapbase = snap.SnapBase()
       snapfile_id = time.strftime('%m.%d.%Y-%H.%M.%S')
       snapfile = "/tmp/snapfile-" + snapfile_id + ".tgz"
       file(snapfile, "w")
       self.builder.get_object("backup_snapfile").set_filename(snapfile)

    def show(self):
       self.window.show()

    def _setup_log_levels(self):
        combobox = self.builder.get_object("backup_log_level")
        self.log_levels = ["quiet", "normal", "verbose", "debug"]
        for level in self.log_levels:
            combobox.append_text(level)

    def _get_config(self):
        # get_active_text() is not working for me
        snap.config.options.log_level = self.log_levels[self.builder.get_object("backup_log_level").get_active()]
        snap.config.options.snapfile  = self.builder.get_object("backup_snapfile").get_uri().replace("file://", "")

        snap.config.options.mode = snap.config.ConfigOptions.BACKUP

        password = self.builder.get_object("backup_encryption_key").get_text()
        if password != "Password":
            snap.config.options.encryption_password = password

        
        snap.config.options.target_backends['repos'] = self.builder.get_object("backup_repos_toggle").get_active()
        snap.config.options.target_backends['packages'] = self.builder.get_object("backup_packages_toggle").get_active()
        snap.config.options.target_backends['services'] = self.builder.get_object("backup_services_toggle").get_active()
        if self.builder.get_object("backup_files_toggle").get_active():
            snap.config.options.target_backends['files'] = True
            files = BackupFilesSelectionWindow.get_instance().get_files()
            if len(files) > 0:
                snap.config.options.target_includes['files'] = files
        else:
            snap.config.options.target_backends['files'] = False


    def start_backup(self, widget):
        OutputDialog.get_instance().show()
        try:
          conf = snap.config.Config()
          conf.read_config()
          self._get_config()
          conf.verify_integrity()
          snapbase = snap.SnapBase()

          # run the backup / report any errors
          th = GSnapThread(snapbase)
          th.start()
          if th.error:
              raise th.error
          OutputDialog.get_instance().quit_when_closed = True
        except (snap.exceptions.SnapError), e:
            print "ERROR ", e.message
            # FIXME this will cause a deadlock since we are in the thread
            #GSnapCallback.get_instance().append_to_buffer(e.message)

    def close_backup_window(self, widget):
        self.window.hide()
        ModeSelectionWindow.get_instance().show()

    def backup_encryption_toggled(self, widget):
        key = self.builder.get_object("backup_encryption_key")
        key.set_visible(widget.get_active())

    def backup_encryption_key_focus_in(self, widget, param):
        if widget.get_text() == "Password":
            widget.set_text("")
            widget.set_visibility(False)

    def backup_encryption_key_focus_out(self, widget, param):
        if widget.get_text() == "":
            widget.set_text("Password")
            widget.set_visibility(True)

    def backup_files_toggled(self, widget):
        button = self.builder.get_object("select_backup_files_button")
        button.set_visible(widget.get_active())

    def select_backup_files_clicked(self, widget):
        window = BackupFilesSelectionWindow()
        window.show()


# The backup operation window
class RestoreOperationWindow:
    def __init__(self):
       self.builder = Gtk.Builder() 
       self.builder.add_objects_from_file(GLADE_FILE,
         ["restore_options", "restore_snapfile", "restore_log_level",
          "restore_encryption_toggle", "restore_encryption_key"])
       self.window = self.builder.get_object("restore_options") 

       # map the gui signals
       self.builder.connect_signals(self)
       self.window.connect("destroy", self.close_restore_window)

       self._setup_log_levels()

    def show(self):
       self.window.show()

    def _setup_log_levels(self):
        combobox = self.builder.get_object("restore_log_level")
        self.log_levels = ["quiet", "normal", "verbose", "debug"]
        for level in self.log_levels:
            combobox.append_text(level)

    def _get_config(self):
        snap.config.options.mode = snap.config.ConfigOptions.RESTORE

        if self.builder.get_object("restore_snapfile").get_uri() == None:
            return

        # get_active_text() is not working for me
        snap.config.options.log_level = self.log_levels[self.builder.get_object("restore_log_level").get_active()]
        snap.config.options.snapfile  = self.builder.get_object("restore_snapfile").get_uri().replace("file://", "")

        password = self.builder.get_object("restore_encryption_key").get_text()
        if password != "Password":
            snap.config.options.encryption_password = password

    def start_restore(self, widget):
        OutputDialog.get_instance().show()
        try:
            conf = snap.config.Config()
            conf.read_config()
            self._get_config()
            conf.verify_integrity()
            snapbase = snap.SnapBase()

            # run the restore / report any errors
            th = GSnapThread(snapbase)
            th.start()
            if th.error:
                raise th.error
            OutputDialog.get_instance().quit_when_closed = True
        except (snap.exceptions.SnapError), e:
            print "ERROR ", e.message
            # FIXME this will cause a deadlock since we are in the thread
            #GSnapCallback.get_instance().append_to_buffer(e.message)

    def restore_encryption_toggled(self, widget):
        key = self.builder.get_object("restore_encryption_key")
        key.set_visible(widget.get_active())

    def restore_encryption_key_focus_in(self, widget, param):
        if widget.get_text() == "Password":
            widget.set_text("")
            widget.set_visibility(False)

    def restore_encryption_key_focus_out(self, widget, param):
        if widget.get_text() == "":
            widget.set_text("Password")
            widget.set_visibility(True)

    def close_restore_window(self, widget):
        self.window.hide()
        ModeSelectionWindow.get_instance().show()

# The help dialog
class HelpDialog:
    def __init__(self):
       self.builder = Gtk.Builder() 
       self.builder.add_objects_from_file(GLADE_FILE, ["help_window"])
       self.window = self.builder.get_object("help_window") 
                                        
       # map the gui signals
       self.builder.connect_signals(self)

    def show(self):
       self.window.show()

    def close_help_dialog(self, widget):
        self.window.hide()
        ModeSelectionWindow.get_instance().show()

# The output dialog
class OutputDialog:
    @classmethod
    def set_instance(cls, instance):
       '''store a singleton instance of this dialog for future reference'''
       cls.instance = instance

    @classmethod
    def get_instance(cls):
        '''get singleton instance of this class'''
        return cls.instance

    def __init__(self):
       OutputDialog.set_instance(self)

       self.builder = Gtk.Builder() 
       self.builder.add_objects_from_file(GLADE_FILE, 
         ["output_window", "output_text", "output_closed_button"])
       self.window = self.builder.get_object("output_window") 

       # map the gui signals
       self.builder.connect_signals(self)
       self.window.connect("destroy", self.close_output_dialog)

       # set the output buffer
       snap.callback.snapcallback.set_output_buffer(self.builder.get_object("output_text").get_buffer())

       self.quit_when_closed = False

    def show(self):
       self.window.show()

    def close_output_dialog(self, widget):
        if self.quit_when_closed:
            Gtk.main_quit()
        else:
            self.window.hide()

# Window to show/add/remove files which have been selected to backup 
class BackupFilesSelectionWindow:
    @classmethod
    def set_instance(cls, instance):
       '''store a singleton instance of this window for future reference'''
       cls.instance = instance

    @classmethod
    def get_instance(cls):
        '''get singleton instance of this class'''
        return cls.instance

    def __init__(self):
        self.set_instance(self)

        self.builder = Gtk.Builder()
        self.builder.add_objects_from_file(GLADE_FILE,
          ["backup_files_selection",
           "backup_files_treeview", "backup_files_treeview_selection",
           "backup_files_selection_remove", "backup_files_selection_add",
           "backup_files_selection_close"])
        self.window = self.builder.get_object("backup_files_selection")

        # map the gui signals
        self.builder.connect_signals(self)
        self.window.connect("destroy", self.close_backup_files_selection)

        # add a column to the treeview
        self.liststore = Gtk.ListStore(str)
        self.treeview = self.builder.get_object("backup_files_treeview")
        col = Gtk.TreeViewColumn('File')
        renderer = Gtk.CellRendererText()
        col.pack_start(renderer, True)
        col.add_attribute(renderer, "text", 0)
        self.treeview.append_column(col)
        self.treeview.set_model(self.liststore)

    def show(self):
       self.window.show()

    # helper to add a list of filenames to window
    def add_file_paths(self, file_paths=[]):
        for path in file_paths:
            self.liststore.append([path])

    # helper to get list of filenames in window
    def get_files(self):
        files = []
        for tfile in self.liststore:
            files.append(tfile[0])
        return files

    def close_backup_files_selection(self, widget):
        self.window.hide()

    def backup_files_selection_changed(self, widget):
        self.builder.get_object("backup_files_selection_remove"). \
             set_sensitive(self.treeview.get_selection().count_selected_rows() > 0)

    def add_backup_files(self, widget):
        dialog = SelectDirectoryDialog()
        dialog.show()

    def remove_backup_files(self, widget):
        selected, itr = self.treeview.get_selection().get_selected()
        if itr:
            self.liststore.remove(itr)

# File chooser dialog to select directories to backup
class SelectDirectoryDialog:
    def __init__(self):
        self.builder = Gtk.Builder()
        self.builder.add_objects_from_file(GLADE_FILE,
          ["select_directory_dialog",
           "cancel_add_selected_directory", "add_selected_directory"])
        self.window = self.builder.get_object("select_directory_dialog")

        # map the gui signals
        self.builder.connect_signals(self)
        self.window.connect("destroy", self.close_select_directory_dialog)

    def show(self):
       self.window.show()

    def add_selected_dir(self, widget):
        filenames = self.window.get_filenames()
        BackupFilesSelectionWindow.get_instance().add_file_paths(filenames)
        self.window.hide()

    def close_select_directory_dialog(self, widget):
        self.window.hide()

# Redirect callback text to main window
class GSnapCallback(snap.callback.Callback):
    @classmethod
    def set_instance(cls, instance):
       '''store a singleton instance of this window for future reference'''
       cls.instance = instance

    @classmethod
    def get_instance(cls):
        '''get singleton instance of this class'''
        return cls.instance

    def __init__(self):
        GSnapCallback.set_instance(self)

    def set_output_buffer(self, output_buffer):
        self.outputbuffer = output_buffer

    def append_to_buffer(self, newtext):
        # always show the output dialog on new text
        Gdk.threads_enter()
        OutputDialog.get_instance().show()

        nl = ''
        if self.outputbuffer.get_char_count() != 0:
            nl = '\n'
        self.outputbuffer.insert(self.outputbuffer.get_end_iter(), nl + newtext)
        Gdk.threads_leave()

    def message(self, msg):
        self.append_to_buffer(msg)

    def warn(self, warning):
        self._append_to_buffer("WARNING: " + warning)

    def error(self, error):
        self._append_to_buffer("ERROR: " + error)


if __name__ == "__main__":
    # initialize callback
    snap.callback.snapcallback = GSnapCallback()

    # initialize windows
    BackupFilesSelectionWindow()
    OutputDialog()
    ModeSelectionWindow().show()

    # start gtk loop
    Gdk.threads_init()
    GObject.threads_init()
    Gdk.threads_enter()
    Gtk.main()
    Gdk.threads_leave()
