# Copyright (C) 2008 LottaNZB Development Team
# 
# 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; version 3.
# 
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

import gtk
import locale
import webbrowser
import warnings
import re
import shutil
import urllib

# Ignore GTK warnings
warnings.simplefilter("ignore", gtk.Warning)

import logging
log = logging.getLogger(__name__)

from gobject import idle_add
from os.path import join, isfile, dirname, basename
from shutil import move
from subprocess import Popen
from gtk import RESPONSE_OK, BUTTONS_OK_CANCEL

from kiwi.ui.dialogs import info, error
from kiwi.ui.delegates import Delegate
from kiwi.ui.objectlist import ObjectList, Column

from lottanzb import __version__, newzbin
from lottanzb.core import App
from lottanzb.config import GConfigSection
from lottanzb.prefsgui import PrefsWindow
from lottanzb.modes import standalone, remote_frontend
from lottanzb.util import gproperty, _
from lottanzb.log import LogWindow
from lottanzb.backend import Download

try: import gnome
except ImportError: gnome = None

try: import gnomevfs
except ImportError: gnomevfs = None

class Config(GConfigSection):
    start_minimized = gproperty(type=bool, default=False)
    window_width = gproperty(type=int, default=700)
    window_height = gproperty(type=int, default=350)

class MainWindow(Delegate):
    gladefile = "main_window"
    
    def __init__(self, config):
        self.config = config
        
        gtk.window_set_default_icon_name("lottanzb")
        
        try:
            gnome.program_init("lottanzb", __version__,
                properties = { gnome.PARAM_APP_DATADIR: "/usr/share" })
        except:
            pass
        
        Delegate.__init__(self)
        
        self.create_status_icon()
        self.enable_rgba_support()
        
        self.download_list = DownloadList()
        self.download_list.connect("selection-changed", self.update_toolbar_buttons)
        self.download_list.get_treeview().connect("button-press-event", self.show_context_menu)
        self.download_list_container.add(self.download_list)
        
        self.up.set_menu(self.up_menu)
        self.toplevel.resize(self.config.window_width, self.config.window_height)
        
        # Signal autoconnecting doesn't work for some reason in this case.
        self.toplevel.connect("configure_event", self.on_main_window__configure_event)
        
        if self.config.start_minimized:
            self.status_open.set_active(False)
        else:
            self.show()
        
        self.handle_mode_changed()
        
        thread_save_handler = lambda a, b: idle_add(self.handle_mode_changed)
        App().mode_manager.connect("notify::active-mode", thread_save_handler)
        App().backend.connect("updated", self.update)
    
    def enable_rgba_support(self):
        """Try to enable RGBA support
        
        The method gtk.gdk.Drawable#get_rgba_colormap is available in
        PyGTK 2.10 and above.
        """
        
        try:
            gtk_screen = self.status_icon.get_screen()
            colormap = gtk_screen.get_rgba_colormap()
            
            if not colormap:
                colormap = gtk_screen.get_rgb_colormap()
            
            gtk.widget_set_default_colormap(colormap)
        except:
            pass
    
    def create_status_icon(self):
        self.status_icon = gtk.StatusIcon()
        self.status_icon.set_from_icon_name("lottanzb")
        self.status_icon.set_tooltip("LottaNZB")
        
        self.status_icon.connect("popup-menu", self.show_status_menu, self.status_menu)
        self.status_icon.connect("activate", self.toggle)
    
    def show_add_dialog(self, *args):
        dialog = AddFileDialog()
    
    def quit(self, *args):
        App().quit()
    
    def remove_download(self, download):
        q = _("Are you sure you want to cancel this download?")
        
        if download and info(q, buttons=BUTTONS_OK_CANCEL) == RESPONSE_OK:
            download.dequeue()
    
    def handle_mode_changed(self, *args):
        mode = App().mode_manager.active_mode
        download_dir = True
        newzbin_support = True
        remote = isinstance(mode, remote_frontend.Mode)
        
        try:
            assert gnomevfs
            assert mode.hella_config
        except:
            download_dir = False
        
        try:
            assert mode.hella_config.newzbin_support()
        except:
            newzbin_support = remote
        
        # It's currently not possible to enqueue NZB files when connected
        # to a HellaNZB daemon running on another machine.
        self.menu_add.set_sensitive(not remote)
        self.toolbar_add.set_sensitive(not remote)
        self.status_add.set_sensitive(not remote)
        
        self.open_download_dir.set_property("visible", download_dir)
        self.add_newzbin.set_sensitive(newzbin_support)
        self.edit_preferences.set_sensitive(isinstance(mode, standalone.Mode))
        
        self.update_toolbar_buttons()
    
    def update(self, *args):
        backend = App().backend
        downloads = backend.downloads
        active_download = downloads.get_active_download()
        
        if active_download:
            self.pause.set_active(backend.paused)
            self.speed.set_text(_("%s KB/s") % (backend.speed))
            self.remaining.set_text(_("Remaining: %s MB") % (
                active_download.remaining) + " - %s" % (
                backend.formatETA(backend.eta)))
            
            if backend.downloads.get_queued():
                self.total_remaining.show()
                self.total_remaining_icon.show()
                self.total_remaining.set_text(_("Total remaining: %s MB") % (
                    backend.downloads.get_total_size()) + " - %s" % (
                    backend.formatETA(downloads.get_total_eta())))
            else:
                self.total_remaining.hide()
                self.total_remaining_icon.hide()
        else:
            self.speed.set_text(_("None"))
            self.remaining.set_markup(_("None"))
        
        self.download_list.add_list(downloads)
        self.update_toolbar_buttons()
    
    def update_toolbar_buttons(self, *args):
        download = self.download_list.get_selected()
        active = bool(download and download.movable())
        
        states = {}
        
        for button in ["up", "down", "remove"]:
            states[button] = active
        
        if download:
            if download.state == Download.State.DOWNLOADING:
                states["up"] = False
            if App().backend.downloads[-1] is download:
                states["down"] = False
        
        for action, state in states.iteritems():
            self.get_widget(action).set_sensitive(state)
            self.get_widget("context_%s" % action).set_sensitive(state)
    
    def toggle(self, *args):
        window = self.toplevel
        
        if window.get_property("visible"):
            window.hide()
        else:
            window.show()
        
        return True
    
    def show_status_menu(self, icon, button, activate_time, menu):
        self.status_menu.popup(None, None, gtk.status_icon_position_menu, button, activate_time, icon)
    
    def show_context_menu(self, button, event):
        if event.button == 3:
            self.context_menu.popup(None, None, None, event.button, event.time)
    
    def on_main_window__show(self, widget):
        self.status_open.set_active(True)
    
    def on_main_window__hide(self, widget):
        self.status_open.set_active(False)
    
    def on_main_window__configure_event(self, widget, *args):
        size = widget.get_size()
        
        self.config.window_width = size[0]
        self.config.window_height = size[1]
    
    def on_main_window__button_press_event(self, button, event):
        if event.button == 3:
            self.context_menu.popup(None, None, None, event.button, event.time)
    
    def on_edit_preferences__activate(self, widget):
        window = PrefsWindow()
    
    def on_select_mode__activate(self, widget):
        App().mode_manager.show_selection_window()
    
    def on_help_content__activate(self, widget):
        try:
            if App().uninstalled:
                for l in [locale.getlocale()[0][:2], "C"]:
                    help_file = App().help_dir(join(l, "lottanzb.xml"))
                    
                    if isfile(help_file):
                        Popen(["yelp", help_file])
                        return
            else:
                gnome.help_display("lottanzb")
        except:
            webbrowser.open_new_tab("http://www.lottanzb.org/help")
    
    def on_about__activate(self, widget):
        dialog = AboutDialog()
    
    def on_bug__activate(self, widget):
        webbrowser.open_new_tab("http://bugs.launchpad.net/lottanzb/+filebug")
    
    def on_add_newzbin__activate(self, widget):
        dialog = AddNewzbinDialog()
    
    def on_log__activate(self, widget):
        window = LogWindow()
    
    def on_remove__clicked(self, widget):
        self.remove_download(self.download_list.get_selected())
    
    def on_up__clicked(self, widget):
        self.download_list.get_selected().move_up()
    
    def on_down__clicked(self, widget):
        self.download_list.get_selected().move_down()
    
    def on_up_to_top__activate(self, widget):
        self.download_list.get_selected().move(0)
    
    def on_pause__toggled(self, widget):
        if widget.get_active():
            App().backend.pause()
        else:
            App().backend.resume()
    
    def on_clear__clicked(self, widget):
        App().backend.clear_finished()
    
    def on_open_download_dir__clicked(self, widget):
        mode = App().mode_manager.active_mode
        
        try:
            gnomevfs.url_show("file://" + mode.hella_config.DEST_DIR)
        except:
            pass
    
    def on_status_open__toggled(self, widget):
        if widget.get_active() != self.toplevel.get_property("visible"):
            self.toggle()
    
    on_close__activate = toggle
    on_main_window__delete_event = toggle
    
    on_menu_add__activate = show_add_dialog
    on_status_add__activate = show_add_dialog
    
    on_status_quit__activate = quit
    on_quit__activate = quit
    
    on_toolbar_add__clicked = on_menu_add__activate
    on_context_up__activate = on_up__clicked
    on_up_move__activate = on_up__clicked
    on_context_down__activate = on_down__clicked
    on_context_remove__activate = on_remove__clicked

class DownloadList(ObjectList):
    def __init__(self):
        self.default_columns = [
            Column("name", title=_("Name"), expand=True),
            Column("size", title=_("Size"), expand=True, format_func=self.format_size),
            DownloadProgressColumn()
        ]
        
        ObjectList.__init__(self, self.default_columns)
        
        drag_targets = [("DOWNLOAD_ROW", gtk.TARGET_SAME_WIDGET, 0)]
        drop_targets = drag_targets + [("text/plain", 0, 1)]
        treeview = self.get_treeview()
        
        treeview.connect("drag-data-get", self.drag_data_get)
        treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, drag_targets, gtk.gdk.ACTION_MOVE)
        
        treeview.connect("drag-data-received", self.drag_data_received)
        treeview.enable_model_drag_dest(drop_targets, gtk.gdk.ACTION_MOVE)
        
        # FIXME: Temporary. We need a working plugin system first.
        self.categories_config = App().config.plugins.categories
        self.categories_config.connect("notify::enabled", self.handle_categories_plugin)
        self.handle_categories_plugin()
        
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.show()
    
    def _load(self, instances, clear):
        # Do nothing if empty list or None provided
        if clear and not instances:
            self.unselect_all()
            self.clear()
            return
        
        model = self._model
        iters = self._iters
        old_instances = self[:]
        
        # Do not always just clear the list, check if we have the same
        # instances in the list we want to insert and merge in the new items.
        if clear:
            for index, instance in enumerate(instances):
                try:
                    old_index = self.index(instance)
                except ValueError:
                    self.insert(index, instance)
                else:
                    if old_index != index:
                        old_treeiter = model[old_index].iter
                        dest_treeiter = model[index].iter
                        
                        model.move_before(old_treeiter, dest_treeiter)
                    
                    self.update(instance)
            
            if old_instances:
                for instance in self[len(instances):]:
                    self.remove(instance)
        else:
            for instance in iter(instances):
                iters[id(instance)] = model.append((instance,))
        
        # As soon as we have data for that list, we can autosize it, and
        # we don't want to autosize again, or we may cancel user
        # modifications.
        if self._autosize:
            self._treeview.columns_autosize()
            self._autosize = False
    
    def drag_data_get(self, treeview, context, selection, info, timestamp):
        item = self.get_selected()
        
        if item.movable():
            selection.set("DOWNLOAD_ROW", 8, str(item.id))
    
    def drag_data_received(self, treeview, context, x, y, selection, info, timestamp):
        drop_info = treeview.get_dest_row_at_pos(x, y)
        
        if not selection.data:
            return
        
        if info == 0:
            self.handle_download_move(int(selection.data), drop_info)
        elif info == 1:
            self.handle_nzb_drop(selection.data)
    
    # FIXME: Temporary. We need a working plugin system first.
    def handle_categories_plugin(self, *args):
        new_columns = self.default_columns[:]
        
        if self.categories_config.enabled:
            column = Column("category", title=_("Category"), expand=True)
            new_columns.insert(1, column)
        
        self.set_columns(new_columns)
    
    def handle_download_move(self, download_id, drop_info):
        download = App().backend.downloads.get_by_id(download_id)
        dest_index = App().backend.downloads.get_incomplete().index(download)
        target_index = None
        
        if drop_info:
            path, position = drop_info
            target_index = path[0]
            
            if target_index - dest_index == 1 and position == gtk.TREE_VIEW_DROP_BEFORE:
                target_index += -1
            elif dest_index - target_index == 1 and position == gtk.TREE_VIEW_DROP_AFTER:
                target_index += 1
            else:
                if dest_index < target_index and position == gtk.TREE_VIEW_DROP_BEFORE:
                    target_index += -1
                elif dest_index > target_index and position == gtk.TREE_VIEW_DROP_AFTER:
                    target_index += 1
        
        App().backend.move(download, target_index)
    
    def handle_nzb_drop(self, uris):
        files = []
        
        for file in uris.replace("file://", "").split():
            if file.lower().endswith(".nzb"):
                files.append(urllib.unquote(file).strip())
        
        if len(files) == 1:
            dialog = AddFileDialog(files[0])
        elif len(files) >= 1:
            for file in files:
                App().backend.enqueue(file)
    
    @staticmethod
    def format_size(size):
        if size:
            return "%s MB" % size
        else:
            return ""

class DownloadProgressColumn(Column):
    def __init__(self):
        Column.__init__(self, "", title=_("Progress"), width=200)
    
    def _guess_renderer_for_type(self, model):
        return gtk.CellRendererProgress(), "progress"
    
    def cell_data_func(self, tree_column, renderer, model, treeiter, *args):
        download = self._objectlist[treeiter]
        
        if download.state == Download.State.FINISHED:
            state = _("Finished")
        elif download.state == Download.State.PROCESSING:
            if download.recovery:
                state = _("Repairing and extracting")
            else:
                state = _("Processing")
        elif download.state == Download.State.DOWNLOADING:
            state = "%s %%" % (str(download.progress))
            
            if App().backend.paused:
                state = _("Paused at: %s%%") % (download.progress)
            elif download.recovery:
                state = _("Downloading PAR recovery files: %s") % (download.progress)
        else:
            if download.recovery:
                state = _("Queued: Need PAR recovery")
            else:
                state = _("Queued")
        
        renderer.set_property("value", download.progress)
        renderer.set_property("text", state)

class AboutDialog(Delegate):
    gladefile = "about_dialog"
    
    def __init__(self):
        Delegate.__init__(self)
        
        self.toplevel.set_version(__version__)
        self.toplevel.set_name("LottaNZB")
        self.toplevel.connect("response", self.on_response)
        self.show()
    
    def on_response(self, widget, response):
        self.hide()

class AddFileDialog(Delegate):
    gladefile = "add_file_dialog"
    
    def __init__(self, file=None):
        Delegate.__init__(self)
        
        filter = gtk.FileFilter()
        filter.add_pattern("*.nzb")
        filter.add_pattern("*.NZB")
        
        self.toplevel.set_filter(filter)
        
        if App().config.plugins.categories.enabled:
            self.create_category_selection()
        
        if file:
            try:
                self.toplevel.select_filename(file)
            except:
                logging.error(_("Could not set the filename."))
        
        self.show()
    
    def on_add_file_dialog__response(self, widget, response):
        if response == gtk.RESPONSE_OK:
            self.enqueue_selection()
        else:
            self.toplevel.destroy()
    
    def enqueue_selection(self):
        files = self.toplevel.get_filenames()
        
        for file in files:
            if len(files) == 1:
                name = self.name.get_text()
            else:
                name = self.pretty_nzb_name(file)
            
            if isfile(file):
                if App().config.plugins.categories.enabled:
                    category = self.category.get_active_text()
                    separator = App().config.plugins.categories.separator
                    dest_file = App().temp_dir(category + separator + name + ".nzb")
                else:
                    dest_file = App().temp_dir(name + ".nzb")
                
                move(file, dest_file)
                App().backend.enqueue(dest_file)
            else:
                self.toplevel.set_current_folder(self.toplevel.get_filename())
        
        self.toplevel.destroy()
    
    def on_add_file_dialog__selection_changed(self, widget):
        files = self.toplevel.get_filenames()
        
        if len(files) > 1:
            self.name.set_text(_("Multiple files selected"))
            self.name.set_sensitive(False)
        else:
            if len(files) == 1 and isfile(files[0]):
                self.name.set_text(self.pretty_nzb_name(files[0]))
            else:
                self.name.set_text("")
            
            self.name.set_sensitive(True)
    
    def check_focus(self):
        pass
    
    def create_category_selection(self):
        label = gtk.Label(_("Category:"))
        label.show()
        
        self.category = gtk.combo_box_new_text()
        map(self.category.append_text, App().config.plugins.categories.categories_list)
        self.category.set_active(len(App().config.plugins.categories.categories_list) - 1)
        self.category.show()
        
        self.table.resize(2, 2)
        self.table.attach(label, 0, 1, 1, 2, gtk.SHRINK, gtk.SHRINK)
        self.table.attach(self.category, 1, 2, 1, 2)
    
    @staticmethod
    def pretty_nzb_name(name):
        name = re.compile(r"\.nzb", re.I).sub("", name)
        name = re.compile(r"[\._]").sub(" ", name)
        
        return name.split("/")[-1].strip()

class AddNewzbinDialog(Delegate):
    gladefile = "add_newzbin_dialog"
    
    def __init__(self):
        Delegate.__init__(self)
        
        self.show()
    
    def on_id__content_changed(self, widget):
        try:
            int(widget.get_text())
        except ValueError:
            self.add.set_sensitive(False)
        else:
            self.add.set_sensitive(True)
    
    def on_add__clicked(self, button):
        report_id = self.id.get_text()
        
        if isinstance(App().mode_manager.active_mode, standalone.Mode):
            hella_config = App().mode_manager.active_mode.hella_config
            
            if hella_config.newzbin_support():
                downloader = newzbin.Downloader( \
                    hella_config.newzbin_username, \
                    hella_config.newzbin_password, \
                    report_id)
                
                downloader.connect("completed", self.on_downloader_completed)
                downloader.start()
        else:
            App().backend.newzbinEnqueue(report_id)
        
        self.hide()
    
    def on_downloader_completed(self, downloader):
        if downloader.exception:
            error(downloader.exception)
        else:
            categories_config = App().config.plugins.categories
            
            if categories_config.enabled:
                prefix = downloader.category + categories_config.separator
                src = downloader.filename
                dest = join(dirname(src), prefix + basename(src))
                
                shutil.move(downloader.filename, dest)
                
                downloader.filename = dest
            
            App().backend.enqueue(downloader.filename)
    
    def on_cancel__clicked(self, button):
        self.hide()

"""class AddMultipleFilesDialog(Window):
    def __init__(self, files):
        Window.__init__(self, "AddMultipleFilesDialog", {
            "on_AddMultipleFilesDialog_response"                : self.handleResponse,
            "on_AddMultipleFilesDialogTreeview_cursor_changed"  : self.handleSelectionChange,
            "on_AddMultipleFilesDialogName_changed"             : self.handleNameChange
        })
        
        self.fileListStore = gtk.ListStore(str, str, int, str)
        self.tree_view = self.get_widget("AddMultipleFilesDialogTreeview")
        self.table = self.get_widget("AddMultipleFilesDialogTable")
        
        self.fields = {
            "name"      : self.get_widget("AddMultipleFilesDialogName"),
            "category"  : self.getCategoryBox()
        }
        
        self.tree_view.set_model(self.fileListStore)
        self.tree_view.set_reorderable(False)
        self.tree_view.show()
        
        column = gtk.TreeViewColumn(_("Name"), gtk.CellRendererText(), text=0)
        column.set_resizable(True)
        column.set_expand(True)
        
        self.tree_view.append_column(column)
        
        if App().config.plugins.categories.enabled:
            column = gtk.TreeViewColumn(_("Category"), gtk.CellRendererText(), text=1)
            column.set_resizable(True)
            column.set_expand(True)
            
            self.tree_view.append_column(column)
        
        self.catChange = 0
        
        self.files = files
        self.createFileListStore()
        
        self.show()
        
    def handleNameChange(self, *args):
        if self.fields["name"].get_text():
            name = self.fields["name"].get_text()
            
            if name != self.getSelectedFile()[0]:
                model, selection_iter = self.tree_view.get_selection().get_selected()
                model.set_value(selection_iter, 0, name)
    
    def handleApplyToAll(self, *args):
        for file in self.fileListStore:
            category = self.fields["category"].get_active_text()
            
            if category != file[1]:
                file[1] = category
        
    def handleCategoryChange(self, *args):
        if self.catChange:
            category = self.fields["category"].get_active_text()
            
            if category != self.getSelectedFile()[1]:
                model, selection_iter = self.tree_view.get_selection().get_selected()
                model.set_value(selection_iter, 1, category)
    
    def createFileListStore(self):
        files = self.files
        self.fileList = []
        id = 0
        
        for file in files:
            if file[-4:].lower() == ".nzb":
                name = file[:-4].split("/")[-1].replace(".", " ").replace("_", " ")
            
            self.fileList.append((id, name, file, ""))
            id += 1
            
        for file in self.fileList:  
            self.fileListStore.append([file[1], file[3], file[0], file[2]])
    
    def handleResponse(self, widget, response):
        if response:
            self.handleNameChange()
            self.enqueueSelection()
        else:
            self.destroy()
    
    def handleSelectionChange(self, *args):
        self.fields["name"].set_text(self.getSelectedFile()[0])
        
        for no in range(0,9):
            self.catChange = 0
            self.fields["category"].set_active(no)
            
            if self.fields["category"].get_active_text() == self.getSelectedFile()[1]:
                if App().config.plugins.categories.enabled:
                    if not self.fields["category"].get_active_text():
                        self.fields["category"].set_active(8)
                        self.handleApplyToAll()
                        break
                else:
                    break
        
        self.catChange = 1
        
    def getSelectedFile(self):
        model, selection_iter = self.tree_view.get_selection().get_selected()
        
        if selection_iter:
            return (model.get_value(selection_iter, 0), model.get_value(selection_iter, 1), model.get_value(selection_iter, 2), model.get_value(selection_iter, 3))
    
    def enqueueSelection(self):
        files = self.fileListStore
        
        if files:
            for file in files:
                category = file[1]
                name = file[0]
                fullname = file[3]
                
                if category:
                    move(fullname, tempDir() + category + Download.CATEGORY_SEPARATOR + name + ".nzb")
                    returnvalue = tempDir() + category + Download.CATEGORY_SEPARATOR + name + ".nzb"
                else:
                    move(fullname, tempDir() + name + ".nzb")
                    returnvalue = tempDir() + name + ".nzb"
                
                App().backend.enqueue(returnvalue)
            
            self.destroy()
    
    def getCategoryBox(self):
        categoryBox = gtk.combo_box_new_text()
        categoryLabel = gtk.Label(_("Category:"))
        categoryApply = gtk.Button("Apply to all")
        categoryApply.connect("clicked", self.handleApplyToAll)
        
        categoryBox.append_text("")
        
        for category in Download.CATEGORIES:
            categoryBox.append_text(category)
        
        if App().config.plugins.categories.enabled:
            categoryBox.set_active(2)
            categoryBox.show()
            categoryLabel.show()
            categoryApply.show()
        else: 
            categoryBox.set_active(0)
            categoryBox.hide()
            categoryLabel.hide()
            categoryApply.hide()
            
        categoryBox.connect("changed", self.handleCategoryChange) 
        categoryBox.set_sensitive(App().config.plugins.categories.enabled)
        
        self.table.attach(categoryLabel, 0, 1, 2, 3, gtk.FILL, gtk.SHRINK)
        self.table.attach(categoryBox, 1, 2, 2, 3, gtk.FILL, gtk.SHRINK)
        self.table.attach(categoryApply, 2, 3, 2, 3, gtk.FILL, gtk.SHRINK)
        
        return categoryBox"""
