#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#

import logging
import os
from base64 import b64decode, b64encode
from xml.sax.saxutils import escape as xml_escape
from xml.sax.saxutils import unescape as xml_unescape

from gi.repository import Gtk
from gi.repository.GObject import TYPE_INT64, TYPE_UINT64

import deluge.component as component
from deluge.bencode import bdecode
from deluge.common import (
    create_magnet_uri,
    decode_bytes,
    fsize,
    get_magnet_info,
    is_infohash,
    is_magnet,
    is_url,
    resource_filename,
)
from deluge.configmanager import ConfigManager
from deluge.httpdownloader import download_file
from deluge.ui.client import client
from deluge.ui.common import TorrentInfo

from .common import (
    get_clipboard_text,
    listview_replace_treestore,
    reparent_iter,
    windowing,
)
from .dialogs import ErrorDialog
from .edittrackersdialog import trackers_tiers_from_text
from .path_chooser import PathChooser
from .torrentview_data_funcs import cell_data_size

log = logging.getLogger(__name__)


class AddTorrentDialog(component.Component):
    def __init__(self):
        component.Component.__init__(self, 'AddTorrentDialog')
        self.builder = Gtk.Builder()
        # The base dialog
        self.builder.add_from_file(
            resource_filename(
                __package__, os.path.join('glade', 'add_torrent_dialog.ui')
            )
        )
        # The infohash dialog
        self.builder.add_from_file(
            resource_filename(
                __package__, os.path.join('glade', 'add_torrent_dialog.infohash.ui')
            )
        )
        # The url dialog
        self.builder.add_from_file(
            resource_filename(
                __package__, os.path.join('glade', 'add_torrent_dialog.url.ui')
            )
        )

        self.dialog = self.builder.get_object('dialog_add_torrent')

        self.dialog.connect('delete-event', self._on_delete_event)

        self.builder.connect_signals(self)

        # download?, path, filesize, sequence number, inconsistent?
        self.files_treestore = Gtk.TreeStore(
            bool, str, TYPE_UINT64, TYPE_INT64, bool, str
        )
        self.files_treestore.set_sort_column_id(1, Gtk.SortType.ASCENDING)

        # Holds the files info
        self.files = {}
        self.infos = {}
        self.core_config = {}
        self.options = {}

        self.previous_selected_torrent = None

        self.listview_torrents = self.builder.get_object('listview_torrents')
        self.listview_files = self.builder.get_object('listview_files')

        self.prefetching_magnets = []

        render = Gtk.CellRendererText()
        render.connect('edited', self._on_torrent_name_edit)
        render.set_property('editable', True)
        column = Gtk.TreeViewColumn(_('Torrent'), render, text=1)
        self.listview_torrents.append_column(column)

        render = Gtk.CellRendererToggle()
        render.connect('toggled', self._on_file_toggled)
        column = Gtk.TreeViewColumn(None, render, active=0, inconsistent=4)
        self.listview_files.append_column(column)

        column = Gtk.TreeViewColumn(_('Filename'))
        render = Gtk.CellRendererPixbuf()
        column.pack_start(render, False)
        column.add_attribute(render, 'icon-name', 5)
        render = Gtk.CellRendererText()
        render.set_property('editable', True)
        render.connect('edited', self._on_filename_edited)
        column.pack_start(render, True)
        column.add_attribute(render, 'text', 1)
        column.set_expand(True)
        self.listview_files.append_column(column)

        render = Gtk.CellRendererText()
        column = Gtk.TreeViewColumn(_('Size'))
        column.pack_start(render, True)
        column.set_cell_data_func(render, cell_data_size, 2)
        self.listview_files.append_column(column)

        self.torrent_liststore = Gtk.ListStore(str, str, str)
        self.listview_torrents.set_model(self.torrent_liststore)
        self.listview_torrents.set_tooltip_column(2)
        self.listview_files.set_model(self.files_treestore)

        self.listview_files.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
        self.listview_torrents.get_selection().connect(
            'changed', self._on_torrent_changed
        )
        self.torrent_liststore.connect('row-inserted', self.update_dialog_title_count)
        self.torrent_liststore.connect('row-deleted', self.update_dialog_title_count)

        self.setup_move_completed_path_chooser()
        self.setup_download_location_path_chooser()

        # Get default config values from the core
        self.core_keys = [
            'pre_allocate_storage',
            'max_connections_per_torrent',
            'max_upload_slots_per_torrent',
            'max_upload_speed_per_torrent',
            'max_download_speed_per_torrent',
            'prioritize_first_last_pieces',
            'sequential_download',
            'add_paused',
            'download_location',
            'download_location_paths_list',
            'move_completed',
            'move_completed_path',
            'move_completed_paths_list',
            'super_seeding',
        ]
        # self.core_keys += self.move_completed_path_chooser.get_config_keys()
        self.builder.get_object('notebook1').connect(
            'switch-page', self._on_switch_page
        )

    def start(self):
        self.update_core_config()

    def show(self, focus=False):
        self.update_core_config(True, focus)

    def _show(self, focus=False):
        main_window = component.get('MainWindow')
        if main_window.is_on_active_workspace():
            self.dialog.set_transient_for(main_window.window)
        else:
            self.dialog.set_transient_for(None)
            self.dialog.set_position(Gtk.WindowPosition.CENTER)

        if focus:
            timestamp = main_window.get_timestamp()
            if windowing('X11'):
                # Use present with X11 set_user_time since
                # present_with_time is inconsistent.
                self.dialog.present()
                self.dialog.get_window().set_user_time(timestamp)
            else:
                self.dialog.present_with_time(timestamp)
        else:
            self.dialog.present()

    def hide(self):
        self.dialog.hide()
        self.files = {}
        self.infos = {}
        self.options = {}
        self.previous_selected_torrent = None
        self.torrent_liststore.clear()
        self.files_treestore.clear()
        self.prefetching_magnets = []
        self.dialog.set_transient_for(component.get('MainWindow').window)

    def _on_config_values(self, config, show=False, focus=False):
        self.core_config = config
        if self.core_config:
            self.set_default_options()
        if show:
            self._show(focus)

    def update_core_config(self, show=False, focus=False):
        # Send requests to the core for these config values
        d = client.core.get_config_values(self.core_keys)
        d.addCallback(self._on_config_values, show, focus)

    def _add_torrent_liststore(self, info_hash, name, filename, files, filedata):
        """Add a torrent to torrent_liststore."""
        if info_hash in self.files:
            return False

        torrent_row = [info_hash, name, xml_escape(filename)]
        row_iter = self.torrent_liststore.append(torrent_row)
        self.files[info_hash] = files
        self.infos[info_hash] = filedata
        self.listview_torrents.get_selection().select_iter(row_iter)

        self.set_default_options()
        self.save_torrent_options(row_iter)

        return row_iter

    def update_dialog_title_count(self, *args):
        """Update the AddTorrent dialog title with current torrent count."""
        self.dialog.set_title(_('Add Torrents (%d)') % len(self.torrent_liststore))

    def show_already_added_dialog(self, count):
        """Show a message about trying to add duplicate torrents."""
        log.debug('Tried to add %d duplicate torrents!', count)
        ErrorDialog(
            _('Duplicate torrent(s)'),
            _(
                'You cannot add the same torrent twice.'
                ' %d torrents were already added.' % count
            ),
            self.dialog,
        ).run()

    def add_from_files(self, filenames):
        already_added = 0

        for filename in filenames:
            # Get the torrent data from the torrent file
            try:
                info = TorrentInfo(filename)
            except Exception as ex:
                log.debug('Unable to open torrent file: %s', ex)
                ErrorDialog(_('Invalid File'), ex, self.dialog).run()
                continue

            if not self._add_torrent_liststore(
                info.info_hash, info.name, filename, info.files, info.filedata
            ):
                already_added += 1

        if already_added:
            self.show_already_added_dialog(already_added)

    def _on_uri_metadata(self, result, uri, trackers):
        """Process prefetched metadata to allow file priority selection."""
        info_hash, metadata = result
        log.debug('magnet metadata for %s (%s)', uri, info_hash)
        if info_hash not in self.prefetching_magnets:
            return

        if metadata:
            metadata = bdecode(b64decode(metadata))
            info = TorrentInfo.from_metadata(metadata, [[t] for t in trackers])
            self.files[info_hash] = info.files
            self.infos[info_hash] = info.filedata
        else:
            log.info('Unable to fetch metadata for magnet: %s', uri)
        self.prefetching_magnets.remove(info_hash)
        self._on_torrent_changed(self.listview_torrents.get_selection())

    def _on_uri_metadata_fail(self, result, info_hash):
        self.prefetching_magnets.remove(info_hash)
        self._on_torrent_changed(self.listview_torrents.get_selection())

    def prefetch_waiting_message(self, torrent_id, files):
        """Show magnet files fetching or failed message above files list."""
        if torrent_id in self.prefetching_magnets:
            self.builder.get_object('prefetch_label').set_text(
                _('Please wait for files...')
            )
            self.builder.get_object('prefetch_spinner').show()
            self.builder.get_object('prefetch_hbox').show()
        elif not files:
            self.builder.get_object('prefetch_label').set_text(
                _('Unable to download files for this magnet')
            )
            self.builder.get_object('prefetch_spinner').hide()
            self.builder.get_object('prefetch_hbox').show()
        else:
            self.builder.get_object('prefetch_hbox').hide()

    def add_from_magnets(self, uris):
        """Add a list of magnet URIs to torrent_liststore."""
        already_added = 0

        for uri in uris:
            magnet = get_magnet_info(uri)
            if not magnet:
                log.error('Invalid magnet: %s', uri)
                continue

            torrent_id = magnet['info_hash']
            files = magnet['files_tree']
            if not self._add_torrent_liststore(
                torrent_id, magnet['name'], uri, files, None
            ):
                already_added += 1
                continue

            if files:
                continue

            self.prefetching_magnets.append(torrent_id)
            self.prefetch_waiting_message(torrent_id, None)
            d = client.core.prefetch_magnet_metadata(uri)
            d.addCallback(self._on_uri_metadata, uri, magnet['trackers'])
            d.addErrback(self._on_uri_metadata_fail, torrent_id)

        if already_added:
            self.show_already_added_dialog(already_added)

    def _on_torrent_changed(self, treeselection):
        (model, row) = treeselection.get_selected()
        if row is None or not model.iter_is_valid(row):
            self.files_treestore.clear()
            self.previous_selected_torrent = None
            return

        if model[row][0] not in self.files:
            self.files_treestore.clear()
            self.previous_selected_torrent = None
            return

        # Save the previous torrents options
        self.save_torrent_options()

        torrent_id = model.get_value(row, 0)
        # Update files list
        files_list = self.files[torrent_id]
        self.prepare_file_store(files_list)

        if self.core_config == {}:
            self.update_core_config()

        # Update the options frame
        self.update_torrent_options(torrent_id)
        # Update magnet prefetch message
        self.prefetch_waiting_message(torrent_id, files_list)

        self.previous_selected_torrent = row

    def _on_torrent_name_edit(self, w, row, new_name):
        # TODO: Update torrent name
        pass

    def _on_switch_page(self, widget, page, page_num):
        # Save the torrent options when switching notebook pages
        self.save_torrent_options()

    def prepare_file_store(self, files):
        with listview_replace_treestore(self.listview_files):
            split_files = {}
            for idx, _file in enumerate(files):
                self.prepare_file(
                    _file, _file['path'], idx, _file.get('download', True), split_files
                )
            self.add_files(None, split_files)
        root = Gtk.TreePath.new_first()
        self.listview_files.expand_row(root, False)

    def prepare_file(self, _file, file_name, file_num, download, files_storage):
        first_slash_index = file_name.find(os.path.sep)
        if first_slash_index == -1:
            files_storage[file_name] = (file_num, _file, download)
        else:
            file_name_chunk = file_name[: first_slash_index + 1]
            if file_name_chunk not in files_storage:
                files_storage[file_name_chunk] = {}
            self.prepare_file(
                _file,
                file_name[first_slash_index + 1 :],
                file_num,
                download,
                files_storage[file_name_chunk],
            )

    def add_files(self, parent_iter, split_files):
        ret = 0
        for key, value in split_files.items():
            if key.endswith(os.path.sep):
                chunk_iter = self.files_treestore.append(
                    parent_iter, [True, key, 0, -1, False, 'folder-symbolic']
                )
                chunk_size = self.add_files(chunk_iter, value)
                self.files_treestore.set(chunk_iter, 2, chunk_size)
                ret += chunk_size
            else:
                self.files_treestore.append(
                    parent_iter,
                    [
                        value[2],
                        key,
                        value[1]['size'],
                        value[0],
                        False,
                        'text-x-generic-symbolic',
                    ],
                )
                ret += value[1]['size']
        if parent_iter and self.files_treestore.iter_has_child(parent_iter):
            # Iterate through the children and see what we should label the
            # folder, download true, download false or inconsistent.
            itr = self.files_treestore.iter_children(parent_iter)
            download = []
            download_value = False
            inconsistent = False
            while itr:
                download.append(self.files_treestore.get_value(itr, 0))
                itr = self.files_treestore.iter_next(itr)

            if sum(download) == len(download):
                download_value = True
            elif sum(download) == 0:
                download_value = False
            else:
                inconsistent = True

            self.files_treestore.set_value(parent_iter, 0, download_value)
            self.files_treestore.set_value(parent_iter, 4, inconsistent)
        return ret

    def load_path_choosers_data(self):
        self.move_completed_path_chooser.set_text(
            self.core_config['move_completed_path'], cursor_end=False, default_text=True
        )
        self.download_location_path_chooser.set_text(
            self.core_config['download_location'], cursor_end=False, default_text=True
        )
        self.builder.get_object('chk_move_completed').set_active(
            self.core_config['move_completed']
        )
        self.move_completed_path_chooser.set_sensitive(
            self.core_config['move_completed']
        )

    def setup_move_completed_path_chooser(self):
        self.move_completed_hbox = self.builder.get_object(
            'hbox_move_completed_chooser'
        )
        self.move_completed_path_chooser = PathChooser(
            'move_completed_paths_list', parent=self.dialog
        )
        self.move_completed_hbox.add(self.move_completed_path_chooser)
        self.move_completed_hbox.show_all()

    def setup_download_location_path_chooser(self):
        self.download_location_hbox = self.builder.get_object(
            'hbox_download_location_chooser'
        )
        self.download_location_path_chooser = PathChooser(
            'download_location_paths_list', parent=self.dialog
        )
        self.download_location_hbox.add(self.download_location_path_chooser)
        self.download_location_hbox.show_all()

    def update_torrent_options(self, torrent_id):
        if torrent_id not in self.options:
            self.set_default_options()
            return

        options = self.options[torrent_id]

        self.download_location_path_chooser.set_text(
            options['download_location'], cursor_end=True
        )
        self.move_completed_path_chooser.set_text(
            options['move_completed_path'], cursor_end=True
        )

        self.builder.get_object('spin_maxdown').set_value(options['max_download_speed'])
        self.builder.get_object('spin_maxup').set_value(options['max_upload_speed'])
        self.builder.get_object('spin_maxconnections').set_value(
            options['max_connections']
        )
        self.builder.get_object('spin_maxupslots').set_value(
            options['max_upload_slots']
        )
        self.builder.get_object('chk_paused').set_active(options['add_paused'])
        self.builder.get_object('chk_pre_alloc').set_active(
            options['pre_allocate_storage']
        )
        self.builder.get_object('chk_prioritize').set_active(
            options['prioritize_first_last_pieces']
        )
        self.builder.get_object('chk_sequential_download').set_active(
            options['sequential_download']
        )
        self.builder.get_object('chk_move_completed').set_active(
            options['move_completed']
        )
        self.builder.get_object('chk_super_seeding').set_active(
            options['super_seeding']
        )

    def save_torrent_options(self, row=None):
        # Keeps the torrent options dictionary up-to-date with what the user has
        # selected.
        if row is None:
            if self.previous_selected_torrent and self.torrent_liststore.iter_is_valid(
                self.previous_selected_torrent
            ):
                row = self.previous_selected_torrent
            else:
                return

        torrent_id = self.torrent_liststore.get_value(row, 0)

        if torrent_id in self.options:
            options = self.options[torrent_id]
        else:
            options = {}

        options['download_location'] = decode_bytes(
            self.download_location_path_chooser.get_text()
        )
        options['move_completed_path'] = decode_bytes(
            self.move_completed_path_chooser.get_text()
        )
        options['pre_allocate_storage'] = self.builder.get_object(
            'chk_pre_alloc'
        ).get_active()
        options['move_completed'] = self.builder.get_object(
            'chk_move_completed'
        ).get_active()
        options['max_download_speed'] = self.builder.get_object(
            'spin_maxdown'
        ).get_value()
        options['max_upload_speed'] = self.builder.get_object('spin_maxup').get_value()
        options['max_connections'] = self.builder.get_object(
            'spin_maxconnections'
        ).get_value_as_int()
        options['max_upload_slots'] = self.builder.get_object(
            'spin_maxupslots'
        ).get_value_as_int()
        options['add_paused'] = self.builder.get_object('chk_paused').get_active()
        options['prioritize_first_last_pieces'] = self.builder.get_object(
            'chk_prioritize'
        ).get_active()
        options['sequential_download'] = (
            self.builder.get_object('chk_sequential_download').get_active() or False
        )
        options['move_completed'] = self.builder.get_object(
            'chk_move_completed'
        ).get_active()
        options['seed_mode'] = self.builder.get_object('chk_seed_mode').get_active()
        options['super_seeding'] = self.builder.get_object(
            'chk_super_seeding'
        ).get_active()

        self.options[torrent_id] = options

        # Save the file priorities
        files_priorities = self.build_priorities(
            self.files_treestore.get_iter_first(), {}
        )

        if len(files_priorities) > 0:
            for i, file_dict in enumerate(self.files[torrent_id]):
                file_dict['download'] = files_priorities[i]

    def build_priorities(self, _iter, priorities):
        while _iter is not None:
            if self.files_treestore.iter_has_child(_iter):
                self.build_priorities(
                    self.files_treestore.iter_children(_iter), priorities
                )
            elif not self.files_treestore.get_value(_iter, 1).endswith(os.path.sep):
                priorities[
                    self.files_treestore.get_value(_iter, 3)
                ] = self.files_treestore.get_value(_iter, 0)
            _iter = self.files_treestore.iter_next(_iter)
        return priorities

    def set_default_options(self):
        if not self.core_config:
            # update_core_config will call this method again.
            self.update_core_config()
            return

        self.load_path_choosers_data()

        self.builder.get_object('chk_pre_alloc').set_active(
            self.core_config['pre_allocate_storage']
        )
        self.builder.get_object('spin_maxdown').set_value(
            self.core_config['max_download_speed_per_torrent']
        )
        self.builder.get_object('spin_maxup').set_value(
            self.core_config['max_upload_speed_per_torrent']
        )
        self.builder.get_object('spin_maxconnections').set_value(
            self.core_config['max_connections_per_torrent']
        )
        self.builder.get_object('spin_maxupslots').set_value(
            self.core_config['max_upload_slots_per_torrent']
        )
        self.builder.get_object('chk_paused').set_active(self.core_config['add_paused'])
        self.builder.get_object('chk_prioritize').set_active(
            self.core_config['prioritize_first_last_pieces']
        )
        self.builder.get_object('chk_sequential_download').set_active(
            self.core_config['sequential_download']
        )
        self.builder.get_object('chk_move_completed').set_active(
            self.core_config['move_completed']
        )
        self.builder.get_object('chk_seed_mode').set_active(False)
        self.builder.get_object('chk_super_seeding').set_active(
            self.core_config['super_seeding']
        )

    def get_file_priorities(self, torrent_id):
        # A list of priorities
        files_list = []

        for file_dict in self.files[torrent_id]:
            if not file_dict['download']:
                files_list.append(0)
            else:
                # Default lt file priority is 4
                files_list.append(4)

        return files_list

    def _on_file_toggled(self, render, path):
        (model, paths) = self.listview_files.get_selection().get_selected_rows()
        if len(paths) > 1:
            for path in paths:
                row = model.get_iter(path)
                self.toggle_iter(row)
        else:
            row = model.get_iter(path)
            self.toggle_iter(row)
        self.update_treeview_toggles(self.files_treestore.get_iter_first())

    def toggle_iter(self, _iter, toggle_to=None):
        if toggle_to is None:
            toggle_to = not self.files_treestore.get_value(_iter, 0)
        self.files_treestore.set_value(_iter, 0, toggle_to)
        if self.files_treestore.iter_has_child(_iter):
            child = self.files_treestore.iter_children(_iter)
            while child is not None:
                self.toggle_iter(child, toggle_to)
                child = self.files_treestore.iter_next(child)

    def update_treeview_toggles(self, _iter):
        toggle_inconsistent = -1
        this_level_toggle = None
        while _iter is not None:
            if self.files_treestore.iter_has_child(_iter):
                toggle = self.update_treeview_toggles(
                    self.files_treestore.iter_children(_iter)
                )
                if toggle == toggle_inconsistent:
                    self.files_treestore.set_value(_iter, 4, True)
                else:
                    self.files_treestore.set_value(_iter, 0, toggle)
                    # set inconsistent to false
                    self.files_treestore.set_value(_iter, 4, False)
            else:
                toggle = self.files_treestore.get_value(_iter, 0)
            if this_level_toggle is None:
                this_level_toggle = toggle
            elif this_level_toggle != toggle:
                this_level_toggle = toggle_inconsistent
            _iter = self.files_treestore.iter_next(_iter)
        return this_level_toggle

    def on_button_file_clicked(self, widget):
        log.debug('on_button_file_clicked')
        # Setup the filechooserdialog
        chooser = Gtk.FileChooserDialog(
            _('Choose a .torrent file'),
            None,
            Gtk.FileChooserAction.OPEN,
            buttons=(
                _('_Cancel'),
                Gtk.ResponseType.CANCEL,
                _('_Open'),
                Gtk.ResponseType.OK,
            ),
        )

        chooser.set_transient_for(self.dialog)
        chooser.set_select_multiple(True)
        chooser.set_property('skip-taskbar-hint', True)
        chooser.set_local_only(False)

        # Add .torrent and * file filters
        file_filter = Gtk.FileFilter()
        file_filter.set_name(_('Torrent files'))
        file_filter.add_pattern('*.' + 'torrent')
        chooser.add_filter(file_filter)
        file_filter = Gtk.FileFilter()
        file_filter.set_name(_('All files'))
        file_filter.add_pattern('*')
        chooser.add_filter(file_filter)

        # Load the 'default_load_path' from the config
        self.config = ConfigManager('gtk3ui.conf')
        if (
            'default_load_path' in self.config
            and self.config['default_load_path'] is not None
        ):
            chooser.set_current_folder(self.config['default_load_path'])

        # Run the dialog
        response = chooser.run()

        if response == Gtk.ResponseType.OK:
            result = [decode_bytes(f) for f in chooser.get_filenames()]
            self.config['default_load_path'] = decode_bytes(
                chooser.get_current_folder()
            )
        else:
            chooser.destroy()
            return

        chooser.destroy()
        self.add_from_files(result)

    def on_button_url_clicked(self, widget):
        log.debug('on_button_url_clicked')
        dialog = self.builder.get_object('url_dialog')
        entry = self.builder.get_object('entry_url')

        dialog.set_default_response(Gtk.ResponseType.OK)
        dialog.set_transient_for(self.dialog)
        entry.grab_focus()

        text = get_clipboard_text()
        if text and is_url(text) or is_magnet(text):
            entry.set_text(text)

        dialog.show_all()
        response = dialog.run()

        if response == Gtk.ResponseType.OK:
            url = decode_bytes(entry.get_text())
        else:
            url = None

        entry.set_text('')
        dialog.hide()

        # This is where we need to fetch the .torrent file from the URL and
        # add it to the list.
        log.debug('url: %s', url)
        if url:
            if is_url(url):
                self.add_from_url(url)
            elif is_magnet(url):
                self.add_from_magnets([url])
            else:
                ErrorDialog(
                    _('Invalid URL'),
                    '{} {}'.format(url, _('is not a valid URL.')),
                    self.dialog,
                ).run()

    def add_from_url(self, url):
        dialog = Gtk.Dialog(
            _('Downloading...'),
            flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
            parent=self.dialog,
        )
        dialog.set_transient_for(self.dialog)

        pb = Gtk.ProgressBar()
        dialog.vbox.pack_start(pb, True, True, 0)
        dialog.show_all()

        # Create a tmp file path
        import tempfile

        tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')

        def on_part(data, current_length, total_length):
            if total_length:
                percent = current_length / total_length
                pb.set_fraction(percent)
                pb.set_text(
                    '%.2f%% (%s / %s)'
                    % (percent * 100, fsize(current_length), fsize(total_length))
                )
            else:
                pb.pulse()
                pb.set_text('%s' % fsize(current_length))

        def on_download_success(result):
            self.add_from_files([result])
            dialog.destroy()

        def on_download_fail(result):
            log.debug('Download failed: %s', result)
            dialog.destroy()
            ErrorDialog(
                _('Download Failed'),
                '{} {}'.format(_('Failed to download:'), url),
                details=result.getErrorMessage(),
                parent=self.dialog,
            ).run()
            return result

        d = download_file(url, tmp_file, on_part)
        os.close(tmp_fd)
        d.addCallbacks(on_download_success, on_download_fail)

    def on_button_hash_clicked(self, widget):
        log.debug('on_button_hash_clicked')
        dialog = self.builder.get_object('dialog_infohash')
        entry = self.builder.get_object('entry_hash')
        textview = self.builder.get_object('text_trackers')

        dialog.set_default_response(Gtk.ResponseType.OK)
        dialog.set_transient_for(self.dialog)
        entry.grab_focus()

        text = get_clipboard_text()
        if is_infohash(text):
            entry.set_text(text)

        dialog.show_all()
        response = dialog.run()
        infohash = decode_bytes(entry.get_text()).strip()
        if response == Gtk.ResponseType.OK and is_infohash(infohash):
            # Create a list of trackers from the textview buffer
            tview_buf = textview.get_buffer()
            trackers_text = decode_bytes(
                tview_buf.get_text(*tview_buf.get_bounds(), include_hidden_chars=False)
            )
            log.debug('Create torrent tracker lines: %s', trackers_text)
            trackers = list(trackers_tiers_from_text(trackers_text).keys())

            # Convert the information to a magnet URI, this is just easier to
            # handle this way.
            log.debug('trackers: %s', trackers)
            magnet = create_magnet_uri(infohash, infohash, trackers)
            log.debug('magnet URI: %s', magnet)
            self.add_from_magnets([magnet])

        entry.set_text('')
        textview.get_buffer().set_text('')
        dialog.hide()

    def on_button_remove_clicked(self, widget):
        log.debug('on_button_remove_clicked')
        (model, row) = self.listview_torrents.get_selection().get_selected()
        if row is None:
            return

        torrent_id = model.get_value(row, 0)

        model.remove(row)
        del self.files[torrent_id]
        del self.infos[torrent_id]

    def on_button_trackers_clicked(self, widget):
        log.debug('on_button_trackers_clicked')

    def on_button_cancel_clicked(self, widget):
        log.debug('on_button_cancel_clicked')
        self.hide()

    def on_button_add_clicked(self, widget):
        log.debug('on_button_add_clicked')
        self.add_torrents()
        self.hide()

    def add_torrents(self):
        (model, row) = self.listview_torrents.get_selection().get_selected()
        if row is not None:
            self.save_torrent_options(row)

        torrents_to_add = []

        row = self.torrent_liststore.get_iter_first()
        while row is not None:
            torrent_id = self.torrent_liststore.get_value(row, 0)
            filename = xml_unescape(
                decode_bytes(self.torrent_liststore.get_value(row, 2))
            )
            try:
                options = self.options[torrent_id]
            except KeyError:
                options = None

            file_priorities = self.get_file_priorities(torrent_id)
            if options is not None:
                options['file_priorities'] = file_priorities

            if self.infos[torrent_id]:
                torrents_to_add.append(
                    (
                        os.path.split(filename)[-1],
                        b64encode(self.infos[torrent_id]),
                        options,
                    )
                )
            elif is_magnet(filename):
                client.core.add_torrent_magnet(filename, options).addErrback(log.debug)

            row = self.torrent_liststore.iter_next(row)

        def on_torrents_added(errors):
            if errors:
                log.info(
                    'Failed to add %d out of %d torrents.',
                    len(errors),
                    len(torrents_to_add),
                )
                for e in errors:
                    log.info('Torrent add failed: %s', e)
            else:
                log.info('Successfully added %d torrents.', len(torrents_to_add))

        if torrents_to_add:
            client.core.add_torrent_files(torrents_to_add).addCallback(
                on_torrents_added
            )

    def on_button_apply_clicked(self, widget):
        log.debug('on_button_apply_clicked')
        (model, row) = self.listview_torrents.get_selection().get_selected()
        if row is None:
            return

        self.save_torrent_options(row)

        # The options, except file renames, we want all the torrents to have
        options = self.options[model.get_value(row, 0)].copy()
        options.pop('mapped_files', None)

        # Set all the torrent options
        row = model.get_iter_first()
        while row is not None:
            torrent_id = model.get_value(row, 0)
            self.options[torrent_id].update(options)
            row = model.iter_next(row)

    def on_button_revert_clicked(self, widget):
        log.debug('on_button_revert_clicked')
        (model, row) = self.listview_torrents.get_selection().get_selected()
        if row is None:
            return

        del self.options[model.get_value(row, 0)]
        self.set_default_options()

    def on_chk_move_completed_toggled(self, widget):
        value = widget.get_active()
        self.move_completed_path_chooser.set_sensitive(value)

    def _on_delete_event(self, widget, event):
        self.hide()
        return True

    def get_file_path(self, row, path=''):
        if not row:
            return path

        path = self.files_treestore[row][1] + path
        return self.get_file_path(self.files_treestore.iter_parent(row), path)

    def _on_filename_edited(self, renderer, path, new_text):
        index = self.files_treestore[path][3]

        new_text = new_text.strip(os.path.sep).strip()

        # Return if the text hasn't changed
        if new_text == self.files_treestore[path][1]:
            return

        # Get the tree iter
        itr = self.files_treestore.get_iter(path)

        # Get the torrent_id
        (model, row) = self.listview_torrents.get_selection().get_selected()
        torrent_id = model[row][0]

        if 'mapped_files' not in self.options[torrent_id]:
            self.options[torrent_id]['mapped_files'] = {}

        if index > -1:
            # We're renaming a file! Yay! That's easy!
            if not new_text:
                return
            parent = self.files_treestore.iter_parent(itr)
            file_path = os.path.join(self.get_file_path(parent), new_text)
            # Don't rename if filename exists
            if parent:
                for row in self.files_treestore[parent].iterchildren():
                    if new_text == row[1]:
                        return
            if os.path.sep in new_text:
                # There are folders in this path, so we need to create them
                # and then move the file iter to top
                split_text = new_text.split(os.path.sep)
                for s in split_text[:-1]:
                    parent = self.files_treestore.append(
                        parent, [True, s, 0, -1, False, 'folder-symbolic']
                    )

                self.files_treestore[itr][1] = split_text[-1]
                reparent_iter(self.files_treestore, itr, parent)
            else:
                # Update the row's text
                self.files_treestore[itr][1] = new_text

            # Update the mapped_files dict in the options with the index and new
            # file path.
            # We'll send this to the core when adding the torrent so it knows
            # what to rename before adding.
            self.options[torrent_id]['mapped_files'][index] = file_path
            self.files[torrent_id][index]['path'] = file_path
        else:
            # Folder!
            def walk_tree(row):
                if not row:
                    return

                # Get the file path base once, since it will be the same for
                # all siblings
                file_path_base = self.get_file_path(
                    self.files_treestore.iter_parent(row)
                )

                # Iterate through all the siblings at this level
                while row:
                    # We recurse if there are children
                    if self.files_treestore.iter_has_child(row):
                        walk_tree(self.files_treestore.iter_children(row))

                    index = self.files_treestore[row][3]

                    if index > -1:
                        # Get the new full path for this file
                        file_path = file_path_base + self.files_treestore[row][1]

                        # Update the file path in the mapped_files dict
                        self.options[torrent_id]['mapped_files'][index] = file_path
                        self.files[torrent_id][index]['path'] = file_path

                    # Get the next siblings iter
                    row = self.files_treestore.iter_next(row)

            # Update the treestore row first so that when walking the tree
            # we can construct the new proper paths

            # We need to check if this folder has been split
            if os.path.sep in new_text:
                # It's been split, so we need to add new folders and then re-parent
                # itr.
                parent = self.files_treestore.iter_parent(itr)
                split_text = new_text.split(os.path.sep)
                for s in split_text[:-1]:
                    # We don't iterate over the last item because we'll just use
                    # the existing itr and change the text
                    parent = self.files_treestore.append(
                        parent, [True, s + os.path.sep, 0, -1, False, 'folder-symbolic']
                    )

                self.files_treestore[itr][1] = split_text[-1] + os.path.sep

                # Now re-parent itr to parent
                reparent_iter(self.files_treestore, itr, parent)
                itr = parent

                # We need to re-expand the view because it might contracted
                # if we change the root iter
                root = Gtk.TreePath.new_first()
                self.listview_files.expand_row(root, False)
            else:
                # This was a simple folder rename without any splits, so just
                # change the path for itr
                self.files_treestore[itr][1] = new_text + os.path.sep

            # Walk through the tree from 'itr' and add all the new file paths
            # to the 'mapped_files' option
            walk_tree(itr)
