# -*- coding: utf-8 -*-
# core.py
#
# Copyright (C) 2006 Alon Zakai ('Kripken') <kripkensteiner@gmail.com>
#
# 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; either version 2, or (at your option)
# any later version.
# 
# 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 Street, Fifth Floor
#     Boston, MA  02110-1301, USA.
#
#  In addition, as a special exception, the copyright holders give
#  permission to link the code of portions of this program with the OpenSSL
#  library.
#  You must obey the GNU General Public License in all respects for all of
#  the code used other than OpenSSL. If you modify file(s) with this
#  exception, you may extend this exception to your version of the file(s),
#  but you are not obligated to do so. If you do not wish to do so, delete
#  this exception statement from your version. If you delete this exception
#  statement from all source files in the program, then also delete it here.

# Deluge Library, previously known as python-libtorrent:
#
#   Deluge is a Python library for torrenting, that includes
#   Deluge, which is Python code, and Deluge_core, which is also a Python
#   module, but written in C++, and includes the libtorrent torrent library. Only
#   Deluge should be visible, and only it should be imported, in the client.
#   Deluge_core contains mainly libtorrent-interfacing code, and a few other things
#   that make most sense to write at that level. Deluge contains all other
#   torrent-system management: queueing, configuration management, persistent
#   list of torrents, etc.
#

# Documentation:
#    Torrents have 3 structures:     
#        1. torrent_info - persistent data, like name, upload speed cap, etc.
#        2. core_torrent_state - transient state data from the core. This may take
#                time to calculate, so we do if efficiently
#        3. supp_torrent_state - supplementary torrent data, from Deluge

import cPickle as pickle
import os
import re
import shutil
import platform
import common
import time

import deluge_core
import pref

# Constants

TORRENTS_SUBDIR = "torrentfiles"

STATE_FILENAME = "persistent.state"
PREFS_FILENAME = "prefs.state"
DHT_FILENAME = "dht.state"

PREF_FUNCTIONS = {
    "listen_on" : deluge_core.set_listen_on,
    "max_half_open" : deluge_core.set_max_half_open,
    "connection_speed" : deluge_core.connection_speed,
    "max_connections_global" : deluge_core.set_max_connections_global,
    "max_active_torrents" : None, # no need for a function, applied constantly
    "max_upload_slots_global" : deluge_core.set_max_upload_slots_global,
    "auto_seed_ratio" : None, # no need for a function, applied constantly
    "auto_seed_time" : None, # no need for a function, applied constantly
    "max_download_speed_bps" : deluge_core.set_download_rate_limit,
    "max_upload_speed_bps" : deluge_core.set_upload_rate_limit,
    "enable_dht" : None, # not a normal pref in that is is applied only on start
    "use_upnp" : deluge_core.use_upnp,
    "use_lsd" : deluge_core.use_lsd,
    "use_natpmp" : deluge_core.use_natpmp,
    "use_utpex" : deluge_core.use_utpex,
}

STATE_MESSAGES = (_("Queued"),
                  _("Checking"),
                  _("Connecting"),
                  _("Downloading Metadata"),
                  _("Downloading"),
                  _("Finished"),
                  _("Seeding"),
                  _("Allocating"))

# Priorities
PRIORITY_DONT_DOWNLOAD = 0
PRIORITY_NORMAL = 1
PRIORITY_HIGH = 2
PRIORITY_HIGHEST = 5

PRIORITY_DICT = {PRIORITY_DONT_DOWNLOAD: _("Don't download"),
                 PRIORITY_NORMAL: _("Normal"),
                 PRIORITY_HIGH: _("High"),
                 PRIORITY_HIGHEST: _("Highest")}

# Exceptions

class DelugeError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class InvalidEncodingError(DelugeError):
    pass

class FilesystemError(DelugeError):
    pass

# Note: this may be raised both from deluge-core.cpp and deluge.py, for
# different reasons, both related to duplicate torrents
class DuplicateTorrentError(DelugeError):
    pass

class InvalidTorrentError(DelugeError):
    pass

class InvalidUniqueIDError(DelugeError):
    pass

class InsufficientFreeSpaceError(DelugeError):
    def __init__(self, free_space, needed_space):
        self.free_space = free_space
        self.needed_space = needed_space
    def __str__(self):
        return "%d %d" % self.free_space, self.needed_space + _("bytes needed")

# A cached data item

class cached_data:
    CACHED_DATA_EXPIRATION = 1
    
    def __init__(self, get_method):
        self._get_method = get_method
        self._cache = {}
        self._expire_info = {}

    def get(self, key, cached=True):
        now = time.time()
        exp = self._expire_info.get(key)
        if not exp or now > exp + self.CACHED_DATA_EXPIRATION or not cached:
            self._cache[key] = self._get_method(key)
            self._expire_info[key] = now

        return self._cache[key]
        
# Persistent information for a single torrent

class torrent_info:
    def __init__(self, filename, save_dir, compact, user_paused=False):
        self.filename = filename
        self.save_dir = save_dir
        self.compact = compact
        self.user_paused = user_paused
        self.uploaded_memory = 0
        self.upload_rate_limit = 0
        self.download_rate_limit = 0
        self.webseed_urls = []
        self.desired_ratio = 1.0
        self.trackers = ""
        self.trackers_changed = 0
        self.seed_time = 0.0

        self.delete_me = False # set this to true, to delete it on next sync
        self.del_data = False # set this to true, to delete data on next sync
        self.del_torrent = False # set this to true, to delete torrent on next sync

# The persistent state of the torrent system. Everything in this will be pickled

class persistent_state:
    def __init__(self):
        # Torrents is a dict with instance of torrent_info -> unique_ID 
        self.torrents = {}

        # Prepare queue (queue is pickled, just like everything else)
        # queue[x] is the unique_ID of the x-th queue position. Simple.
        self.queue = []

# The manager for the torrent system

class Manager:
    # blank_slate mode ignores the two pickle files and DHT state file, i.e. you start
    # completely fresh. When quitting, the old files will be overwritten
    def __init__(self, client_ID, version, user_agent, base_dir, blank_slate=False):
        self.base_dir = base_dir
        # Keeps track of DHT running state
        self.dht_running = False

        # Load the preferences
        self.config = pref.Preferences(os.path.join(self.base_dir, PREFS_FILENAME))
        
        TORRENTS_SUBDIR = self.config.get("default_torrent_path")

        # Ensure directories exist
        if not os.path.exists(TORRENTS_SUBDIR):
            os.mkdir(TORRENTS_SUBDIR)

        # Pre-initialize the core's data structures
        deluge_core.pre_init(DelugeError,
                          InvalidEncodingError,
                          SystemError,
                          FilesystemError,
                          DuplicateTorrentError,
                          InvalidTorrentError)

        # Start up the core
        deluge_core.init(client_ID,
                             int(version[0]),
                             int(version[1]),
                             int(version[2]),
                             int(version[3]),
                             user_agent)

        self.constants = deluge_core.constants()

        # Unique IDs are NOT in the state, since they are temporary for each session
        self.unique_IDs = {} # unique_ID -> a torrent_info object, i.e. persistent data

        # Cached torrent core_states. We do not poll the core in a costly 
        # manner.
        self.cached_core_torrent_states = \
            cached_data(deluge_core.get_torrent_state)

        # supplementary torrent states
        self.supp_torrent_states = {} # unique_ID->dict of data

        # Cached torrent core_states. We do not poll the core in a costly 
        # manner.
        self.cached_core_torrent_peer_infos = \
            cached_data(deluge_core.get_peer_info)
        
        # Cached torrent core_states. We do not poll the core in a costly 
        # manner.
        self.cached_core_torrent_file_infos = \
            cached_data(deluge_core.get_file_info)

        # Apply preferences. Note that this is before any torrents are added
        self.apply_prefs()
        
        # Event callbacks for use with plugins
        self.event_callbacks = {}

        # unique_ids removed by core
        self.removed_unique_ids = {}

        # unique_ids with files just removed by user
        self.update_files_removed = {}

        PREF_FUNCTIONS["enable_dht"] = self.set_DHT 

        # Unpickle the state, or create a new one
        self.state = persistent_state()
        if not blank_slate:
            try:
                pkl_file = open(os.path.join(self.base_dir, STATE_FILENAME), 
                                'rb')
                state = pickle.load(pkl_file)
                pkl_file.close()
                
                if isinstance(state.torrents, list):
                    # One time convert of old torrents list to dict
                    state.torrents = dict((x, None) for x in state.torrents)
                
                fr_sorted = []
                for torrent in state.torrents:
                    if os.path.exists(torrent.filename + ".fastresume"):
                        fr_sorted.insert(0, torrent)
                    else:
                        fr_sorted.append(torrent)
                        
                # Add torrents to core and unique_IDs
                for torrent in fr_sorted:
                    if not os.path.exists(torrent.filename):
                        print "Missing file: %s" % torrent.filename
                        continue
                    if torrent not in self.unique_IDs.values():
                        try:
                            unique_ID = deluge_core.add_torrent(torrent.filename,
                                                                torrent.save_dir,
                                                                torrent.compact)
                        except Exception, e:
                            print "Unable to add torrent: ", e

                        self.unique_IDs[unique_ID] = torrent
                        self.state.torrents[torrent] = unique_ID
                        
                        # Apply per torrent prefs after torrent added to core
                        self.apply_prefs_per_torrent(unique_ID)
                        #remove fastresume for non-seed
                        try:
                            torrent_state = self.get_core_torrent_state(unique_ID)
                        except:
                            pass
                        else:
                            if not torrent_state['is_finished']:
                                try:
                                    os.remove(self.unique_IDs[unique_ID].filename + ".fastresume")
                                except:
                                    pass

                # Make sure the Queue is in the correct order and remove any
                # entries that were not added to the session due to error.
                self.state.queue = state.queue[:]
                for uid in state.queue:
                    if uid not in self.state.torrents:
                        self.state.queue.remove(uid)
                    
                # Sync with the core: tell core about torrents, and get 
                # unique_IDs
                self.sync()

                # Apply the queue at this time, after all is loaded and ready
                self.apply_queue()
            except:
                pass

    def quit(self):
        # Pickle the state
        self.pickle_state()

        # Stop DHT, if needed
        self.set_DHT(False)

        #close upnp
        deluge_core.use_upnp(0)

        if self.config.get("use_compact_storage"):
            for unique_ID in self.unique_IDs.keys():
                self.pause(unique_ID)

        # Save fastresume data
        print "Saving fastresume data..."
        self.save_fastresume_data()

        # Shutdown torrent core
        print "Quitting the core..."
        deluge_core.quit()

        #kill dbus on windows
        if common.windows_check():
            if platform.win32_ver()[1] > '6':
                os.popen4('taskkill.exe /IM dbus-daemon-deluge.exe /F')
            else:
                os.popen4('tskill.exe dbus-daemon-deluge')

    def pickle_state(self):
        print "save uploaded memory"
        state = persistent_state()
        state.torrents = dict(self.state.torrents.iteritems())
        state.queue = self.state.queue[:]
        
        for torrent in state.torrents:
            uid = state.torrents[torrent]
            try:
                torrent.uploaded_memory = self.unique_IDs[uid].uploaded_memory +\
                    self.get_core_torrent_state(uid, False)['total_upload']
            except AttributeError:
                torrent.uploaded_memory = 0
        # Pickle the state so if we experience a crash, the latest state is 
        # available
        print "Pickling state..."
        if not os.path.exists(self.base_dir):
            os.makedirs(self.base_dir)
        output = open(os.path.join(self.base_dir, STATE_FILENAME), 'wb')
        pickle.dump(state, output)
        output.close()

    # Preference management functions
    def get_config(self):
        # This returns the preference object
        return self.config
        
    def get_pref(self, key):
        # Get the value from the preferences object
        return self.config.get(key)
    
    # Get file piece range
    def get_file_piece_range(self, unique_id):
        try:
            return deluge_core.get_file_piece_range(unique_id)
        except Exception, e:
            print "Unable to get file piece range:", e
    
    # Check if piece is finished
    def has_piece(self, unique_id, piece_index):
        try:
            return deluge_core.has_piece(unique_id, piece_index)
        except Exception, e:
            print "Unable to get has piece:", e
    
    # Dump torrent info without adding
    def dump_torrent_file_info(self, torrent):
        try:
            ret = deluge_core.dump_file_info(torrent)
        except SystemError:
            print "invalid file"
        else:
            return ret

    # Dump trackers from torrent file
    def dump_trackers(self, torrent):
        try:
            return deluge_core.dump_trackers(torrent)
        except Exception, e:
            print "Unable to dump trackers:", e
    
    # Torrent addition and removal functions

    def add_torrent(self, filename, save_dir, compact, user_paused=False):
        self.add_torrent_ns(filename, save_dir, compact, user_paused)
        return self.sync() # Syncing will create a new torrent in the core, and return it's ID
    
    # When duplicate torrent error, use to find duplicate when merging tracker lists
    def test_duplicate(self, torrent, unique_id):
        try:
            return deluge_core.test_duplicate(torrent, unique_id)
        except Exception, e:
            print "Unable to test duplicate:", e

    def remove_torrent(self, unique_ID, data_also, torrent_also):
        temp = self.unique_IDs[unique_ID]
        try:
            temp_fileinfo = deluge_core.get_file_info(unique_ID)
        except Exception, e:
            print "Unable to get file info:", e
        self.remove_torrent_ns(unique_ID, data_also)
        self.sync()

        # Remove .torrent file if asked to do so
        if torrent_also:
            os.remove(temp.filename)

    # A function to try and reload a torrent from a previous session. This is
    # used in the event that Deluge crashes and a blank state is loaded.
    def add_old_torrent(self, filename, save_dir, compact):
        if not filename in os.listdir(self.config.get("default_torrent_path")):
            #raise InvalidTorrentError(_("File was not found") + ": " + filename)
            print "File not found: ", filename
            return

        full_new_name = os.path.join(self.config.get("default_torrent_path"), filename)

        # Create torrent object
        new_torrent = torrent_info(full_new_name, save_dir, compact)
        self.state.torrents[new_torrent] = None
        
        return self.sync()

    # A separate function, because people may want to call it from time to time
    def save_fastresume_data(self, uid=None):
        if uid == None:
            for unique_ID in self.unique_IDs:
                try:
                    state = deluge_core.get_torrent_state(unique_ID)
                    if not state['is_finished'] and state['state'] != 0 and state['state'] != 1:
                        os.remove(self.unique_IDs[unique_ID].filename + ".fastresume")
                        deluge_core.save_fastresume(unique_ID, self.unique_IDs[unique_ID].filename)
                except OSError:
                    print "no old fastresume to delete"
                    try:
                        deluge_core.save_fastresume(unique_ID, self.unique_IDs[unique_ID].filename)
                    except:
                        pass
                except Exception, e:
                    print "Unable to save fastresume: ", e
        else:
            # Do not save fastresume if torrent is Queued for checking or being checked
            try:
                state = deluge_core.get_torrent_state(uid)
                if state['state'] == 0 or state['state'] == 1:
                    return
                os.remove(self.unique_IDs[uid].filename + ".fastresume")
                deluge_core.save_fastresume(uid, self.unique_IDs[uid].filename)
            except OSError:
                print "no old fastresume to delete"
                try:
                    deluge_core.save_fastresume(uid, self.unique_IDs[uid].filename)
                except:
                    pass
            except Exception, e:
                print "Unable to save fastresume: ", e
        return True

    # State retrieval functions

    def get_state(self):
        ret = deluge_core.get_session_info()

        # Get additional data from our level
        ret['is_listening'] = deluge_core.is_listening()
        ret['port'] = deluge_core.listening_port()
        if self.dht_running == True:
            ret['DHT_nodes'] = deluge_core.get_DHT_info()

        return ret

    # This is the EXTERNAL function, for the GUI. It returns the core_state + supp_state
    def get_torrent_state(self, unique_ID):
        # Check to see if unique_ID exists:
        if unique_ID not in self.unique_IDs:
            print "Asked for a torrent that doesn't exist"
            return
        try:
            ret = self.get_core_torrent_state(unique_ID).copy()

            # Add the deluge-level things to the deluge_core data
            ret.update(self.get_supp_torrent_state(unique_ID))

            # Get queue position
            torrent = self.unique_IDs[unique_ID]
            ret['queue_pos'] = self.state.queue.index(torrent) + 1
            return ret
        except:
            return None

    def get_torrent_peer_info(self, unique_ID):
        # Perhaps at some time we may add info here
        return self.get_core_torrent_peer_info(unique_ID)
    
    def get_torrent_file_info(self, unique_ID):
        return self.get_core_torrent_file_info(unique_ID)

    def get_piece_info(self, unique_ID, piece_index):
        try:
            return deluge_core.get_piece_info(unique_ID, piece_index)
        except Exception, e:
            print "Unable to get piece info:", e

    def get_all_piece_info(self, unique_ID):
        try:
            return deluge_core.get_all_piece_info(unique_ID)
        except Exception, e:
            print "Unable to get all piece info:", e
    
    def get_torrent_unique_id(self, torrent):
        return self.state.torrents[torrent]

    # Queueing functions

    def queue_top(self, unique_ID):
        torrent = self.unique_IDs[unique_ID]
        self.state.queue.insert(0,
            self.state.queue.pop(self.get_queue_index(torrent)))
        self.apply_queue()
        self.pickle_state()

    def queue_up(self, unique_ID):
        torrent = self.unique_IDs[unique_ID]
        curr_index = self.get_queue_index(torrent)
        if curr_index > 0:
            temp = self.state.queue[curr_index - 1]
            self.state.queue[curr_index - 1] = torrent
            self.state.queue[curr_index] = temp
            self.apply_queue()
            self.pickle_state()

    def queue_down(self, unique_ID):
        torrent = self.unique_IDs[unique_ID]
        curr_index = self.get_queue_index(torrent)
        if curr_index < (len(self.state.queue) - 1):
            temp = self.state.queue[curr_index + 1]
            self.state.queue[curr_index + 1] = torrent
            self.state.queue[curr_index] = temp
            self.apply_queue()
            self.pickle_state()

    def queue_bottom(self, unique_ID, enforce_queue=True):
        torrent = self.unique_IDs[unique_ID]
        curr_index = self.get_queue_index(torrent)
        if curr_index < (len(self.state.queue) - 1):
            self.state.queue.remove(torrent)
            self.state.queue.append(torrent)
            if enforce_queue:
                self.apply_queue()
            self.pickle_state()

    def clear_completed(self):
        for unique_ID in self.unique_IDs:
            torrent_state = self.get_core_torrent_state(unique_ID)
            if torrent_state['progress'] == 1.0:
                self.remove_torrent_ns(unique_ID, False, True)
                self.removed_unique_ids[unique_ID] = 1

        self.sync()
        self.apply_queue()

    # Enforce the queue: pause/unpause as needed, based on queue and user_pausing
    # This should be called after changes to relevant parameters (user_pausing, or
    # altering max_active_torrents), or just from time to time
    # ___ALL queuing code should be in this function, and ONLY here___
    def apply_queue(self):
        # Counter for currently active torrent in the queue. Paused in core
        # but having self.is_user_paused(unique_ID) == False is 
        # also considered active.
        active_torrent_cnt = 0

        # Pause and resume torrents
        for torrent in self.state.queue:
            unique_ID = self.state.torrents[torrent]
            # Get not cached torrent state so we don't pause/resume torrents
            # more than 1 time - if cached torrent_state['is_paused'] can be
            # still paused after we already paused it.
            torrent_state = self.get_core_torrent_state(unique_ID, False)
            
            if not torrent_state['is_paused'] or \
               (torrent_state['is_paused'] and not \
                self.is_user_paused(unique_ID)):
                active_torrent_cnt += 1 
            
            if (active_torrent_cnt <= self.get_pref('max_active_torrents') or \
                self.get_pref('max_active_torrents') == -1) and \
               torrent_state['is_paused'] and not \
               self.is_user_paused(unique_ID):
                # This torrent is a seed so skip all the free space checking
                if torrent_state['is_finished']:
                    self.resume(unique_ID)
                    continue
                    
                # Before we resume, we should check if the torrent is using Full Allocation 
                # and if there is enough space on to finish this file.
                if not self.unique_IDs[unique_ID].compact:
                    avail = self.calc_free_space(directory = self.unique_IDs\
                        [unique_ID].save_dir)
                    total_needed = torrent_state["total_wanted"] - torrent_state["total_wanted_done"]
                    if total_needed < avail:
                        # We have enough free space, so lets resume this torrent
                        self.resume(unique_ID)
                    else:
                        self.pause(unique_ID)
                        self.unique_IDs[unique_ID].user_paused = True
                        nice_need = common.fsize(total_needed)
                        nice_free = common.fsize(avail)
                        import gtk
                        import dialogs
                        gtk.gdk.threads_enter()
                        dialogs.show_popup_warning(None, _("There is not enough \
free disk space to complete your download.") + "\n" + "This torrents will be \
paused" + "\n" + _("Space Needed:") + " " + nice_need + "\n" + _("Available \
Space:") + " " + nice_free)
                        gtk.gdk.threads_leave()
                        print "Not enough free space to resume this torrent!"
                else: #We're using compact allocation so lets just resume
                    self.resume(unique_ID)
            elif not torrent_state['is_paused'] and \
                 ((active_torrent_cnt > self.get_pref('max_active_torrents') and \
                   self.get_pref('max_active_torrents') != -1) or \
                  self.is_user_paused(unique_ID)):
                self.pause(unique_ID)

        # Handle autoseeding - downqueue as needed
        if not self.get_pref('clear_max_ratio_torrents') \
            and self.get_pref('auto_seed_ratio') > 0 \
            and self.get_pref('auto_end_seeding'):
            for unique_ID in self.unique_IDs:
                torrent_state = self.get_core_torrent_state(unique_ID)
                if (torrent_state["total_wanted"] - torrent_state["total_wanted_done"] == 0) and not torrent_state['is_paused']:
                    ratio = self.calc_ratio(unique_ID, torrent_state)
                    if ratio >= self.get_pref('auto_seed_ratio'):
                        self.queue_bottom(unique_ID, enforce_queue=False) # don't recurse!
                        self.set_user_pause(unique_ID, True, enforce_queue=False)

        if not self.get_pref('clear_max_rtime_torrents') \
            and self.get_pref('auto_seed_time') > 0 \
            and self.get_pref('auto_time_end_seeding'):
            for unique_ID in self.unique_IDs:
                torrent_state = self.get_core_torrent_state(unique_ID)
                if (torrent_state["total_wanted"] - torrent_state["total_wanted_done"] == 0) and not torrent_state['is_paused']:
                    seed_time = self.unique_IDs[unique_ID].seed_time 
                    now = time.time()
                    if (now - seed_time) >= (self.get_pref('auto_seed_time') * 3600):
                        self.queue_bottom(unique_ID, enforce_queue=False) # don't recurse!
                        self.set_user_pause(unique_ID, True, enforce_queue=False)
        
        if self.get_pref('clear_max_ratio_torrents') and self.get_pref('auto_end_seeding'):
            for unique_ID in self.unique_IDs.keys():
                torrent_state = self.get_core_torrent_state(unique_ID)
                if (torrent_state["total_wanted"] - torrent_state["total_wanted_done"] == 0):
                    ratio = self.calc_ratio(unique_ID, torrent_state)
                    if ratio >= self.get_pref('auto_seed_ratio'):
                        self.removed_unique_ids[unique_ID] = 1
                        self.remove_torrent(unique_ID, False, True)
        if self.get_pref('clear_max_time_torrents') and self.get_pref('auto_time_end_seeding'):
            for unique_ID in self.unique_IDs.keys():
                torrent_state = self.get_core_torrent_state(unique_ID)
                if (torrent_state["total_wanted"] - torrent_state["total_wanted_done"] == 0):
                    seed_time = self.unique_IDs[unique_ID].seed_time 
                    now = time.time()
                    if (now - seed_time) >= (self.get_pref('auto_seed_time') * 3600):
                        self.removed_unique_ids[unique_ID] = 1
                        self.remove_torrent(unique_ID, False, True)
    # Event handling

    def connect_event(self, event_type, callback):
        if event_type not in self.event_callbacks:
            self.event_callbacks[event_type] = []
        self.event_callbacks[event_type].append(callback)

    def disconnect_event(self, event_type, callback):
        if event_type in self.event_callbacks and \
           callback in self.event_callbacks[event_type]:
            self.event_callbacks[event_type].remove(callback)

    def handle_events(self):
        # Handle them for the backend's purposes, but still send them up in case the client
        # wants to do something - show messages, for example
        def pop_event():
            try:
                event = deluge_core.pop_event()
            except:
                return None
            else:
                return event
                
        ret = []
        while True:
            event = pop_event()
            if event is None:
                break

#            print "EVENT: ", event

            ret.append(event)

            if 'unique_ID' in event and \
               event['unique_ID'] not in self.unique_IDs:
                continue

            if event['event_type'] in self.event_callbacks:
                for callback in self.event_callbacks[event['event_type']]:
                    callback(event)
            
            if event['event_type'] is self.constants['EVENT_TORRENT_PAUSED'] or \
                event['event_type'] is self.constants['EVENT_FINISHED_CHECKING']:
                self.save_fastresume_data(event['unique_ID'])

            if event['event_type'] is self.constants['EVENT_FINISHED']:
                if event['message'] == "torrent has finished downloading":

                    # save fast resume once torrent finishes so as to not recheck
                    # seed if client crashes
                    self.save_fastresume_data(event['unique_ID'])
                    self.unique_IDs[event['unique_ID']].seed_time = time.time()
                    # Queue seeding torrent to bottom if needed
                    if self.get_pref('queue_seeds_to_bottom'):
                        self.queue_bottom(event['unique_ID'])
                    

            elif event['event_type'] is self.constants['EVENT_FILE_ERROR']:
                import gtk
                import dialogs
                gtk.gdk.threads_enter()
                dialogs.show_popup_warning(None, event['message'])
                gtk.gdk.threads_leave()

            elif event['event_type'] is self.constants['EVENT_TRACKER_ANNOUNCE']:
                self.set_supp_torrent_state_val(event['unique_ID'], 
                                                "tracker_status",
                                                _("Announce sent"))
            elif event['event_type'] is self.constants['EVENT_TRACKER_REPLY']:
                self.set_supp_torrent_state_val(event['unique_ID'], 
                                                "tracker_status",
                                                _("Announce OK"))
                if event['num_complete'] == -1 or event['num_incomplete'] == -1:
                    self.scrape_tracker(event['unique_ID'])
                    
            elif event['event_type'] is self.constants['EVENT_TRACKER_ALERT']:
                match = re.search('tracker:\s*".*"\s*(.*)', event["message"])
                message = match and match.groups()[0] or ""
                try:
                    tracker_status = "%s: %s (%s=%s, %s=%s)" % \
                        (_("Alert"), message, 
                         _("HTTP code"), event["status_code"], 
                         _("times in a row"), event["times_in_row"])
                        
                    self.set_supp_torrent_state_val(event['unique_ID'], 
                                                "tracker_status",
                                                tracker_status)
                except UnicodeDecodeError:
                    print "There was a problem displaying a tracker alert. Most\
likely the tracker did not responsd in utf-8."

            elif event['event_type'] is self.constants['EVENT_TRACKER_WARNING']:
                # Probably will need proper formatting later, not tested yet
                try:
                    tracker_status = '%s: %s' % (_("Warning"), event["message"])
                
                    self.set_supp_torrent_state_val(event['unique_ID'], 
                                                "tracker_status",
                                                tracker_status)
                except UnicodeDecodeError:
                    print "There was a problem displaying a tracker alert. Most\
likely the tracker did not responsd in utf-8."

        return ret

    # Priorities functions
    def clear_update_files_removed(self):
        self.update_files_removed = {}

    def prioritize_files(self, unique_ID, priorities, update_files_removed=False):
        assert(len(priorities) == \
                   self.get_core_torrent_state(unique_ID)['num_files'])
        self.unique_IDs[unique_ID].priorities = priorities[:]
        try:
            deluge_core.prioritize_files(unique_ID, priorities)
        except Exception, e:
            print "Unable to prioritize files:", e
            
        if update_files_removed:
            self.update_files_removed[unique_ID] = 1
        
        if self.get_pref('prioritize_first_last_pieces'):
            self.prioritize_first_last_pieces(unique_ID)

    def get_priorities(self, unique_ID):
        try:
            return self.unique_IDs[unique_ID].priorities[:]
        except AttributeError:
            # return normal priority for all files by default
            
            num_files = self.get_core_torrent_state(unique_ID)['num_files']
            return [PRIORITY_NORMAL] * num_files

    def prioritize_first_last_pieces(self, unique_ID):
        """Try to prioritize first and last pieces of each file in torrent
        
        Currently it tries to prioritize 1% in the beginning and in the end
        of each file in the torrent
        
        """
        try:
            deluge_core.prioritize_first_last_pieces(unique_ID, 
            self.unique_IDs[unique_ID].priorities)
        except Exception, e:
            print "Unable to prioritize first and last pieces:", e

    # Advanced statistics - these may be SLOW. The client should call these only
    # when needed, and perhaps only once in a long while (they are mostly just
    # approximations anyhow

    def calc_swarm_speed(self, unique_ID):
        pieces_per_sec = deluge_stats.calc_swarm_speed(self.get_core_torrent_peer_info(unique_ID))
        piece_length = self.get_core_torrent_state(unique_ID)

        return pieces_per_sec * piece_length

    # Miscellaneous minor functions

    def set_user_pause(self, unique_ID, new_value, enforce_queue=True):
        if self.config.get("max_active_torrents") == 0:
            self.config.set("max_active_torrents", \
                self.config.get("max_active_torrents_tmp"))
            self.apply_prefs()
        try:
            self.unique_IDs[unique_ID].user_paused = new_value
            if enforce_queue:
                self.apply_queue()
            self.pickle_state()
        except:
            print "pause failed\n"

    def set_ratio(self, unique_ID, num):
        try:
            deluge_core.set_ratio(unique_ID, float(num))
        except Exception, e:
            print "Unable to set ratio: ", e

    def is_user_paused(self, unique_ID):
        return self.unique_IDs[unique_ID].user_paused

    def get_num_torrents(self):
        return deluge_core.get_num_torrents()

    def get_queue(self):
        return self.state.queue

    def update_tracker(self, unique_ID):
        try:
            deluge_core.reannounce(unique_ID)
        except Exception, e:
            print "Unable to reannounce: ", e

    def scrape_tracker(self, unique_ID):
        try:
            deluge_core.scrape_tracker(unique_ID)
        except Exception, e:
            print "Unable to scrape tracker ", e

    def pause(self, unique_ID):
        try:
            state = self.get_torrent_state(unique_ID)
            if not state["is_paused"]:
                deluge_core.pause(unique_ID)
        except:
            print "pause failed\n"

    def resume(self, unique_ID):
        state = self.get_torrent_state(unique_ID)
        if state["is_paused"]:
            try:
                deluge_core.resume(unique_ID)
                # We have to re-apply per torrent settings after resume. This has to
                # be done until ticket #118 in libtorrent is fixed.
                self.apply_prefs_per_torrent(unique_ID)
            except:
                print "pause failed\n"

    def pause_all(self):
        if self.config.get('max_active_torrents') != 0:
            self.config.set('max_active_torrents_tmp', \
                self.config.get('max_active_torrents'))
            self.config.set('max_active_torrents', 0)
            self.apply_prefs()

    def resume_all(self):
        if self.config.get('max_active_torrents') == 0:
            self.config.set('max_active_torrents', \
                self.config.get('max_active_torrents_tmp'))
            self.apply_prefs()

    def move_storage(self, unique_ID, directory):
        try:
            deluge_core.move_storage(unique_ID, directory)
        except Exception, e:
            print "Unable to move storage: ", e

    ####################
    # Internal functions
    ####################

    def get_core_torrent_state(self, unique_ID, cached=True):
        return self.cached_core_torrent_states.get(unique_ID, cached)

    def get_supp_torrent_state(self, unique_ID):
        return self.supp_torrent_states.get(unique_ID, {})

    def set_supp_torrent_state_val(self, unique_ID, key, val):
        if unique_ID not in self.supp_torrent_states:
            self.supp_torrent_states[unique_ID] = {}

        self.supp_torrent_states[unique_ID][key] = val

    def get_core_torrent_peer_info(self, unique_ID):
        return self.cached_core_torrent_peer_infos.get(unique_ID)
    
    def get_core_torrent_file_info(self, unique_ID):
        return self.cached_core_torrent_file_infos.get(unique_ID)

    # Functions for checking if enough space is available

    def calc_free_space(self, directory):
        if not common.windows_check(): 
            import statvfs
            try:
                dir_stats = os.statvfs(directory)
            except:
                return 0
            block_size = dir_stats[statvfs.F_BSIZE]
            avail_blocks = dir_stats[statvfs.F_BAVAIL]
            return long(block_size * avail_blocks)
        else:
            import string
            import win32file
            sectorsPerCluster, bytesPerSector, numFreeClusters, \
                totalNumClusters_Not_Used = win32file.GetDiskFreeSpace(\
                    directory[0] + ":\\")
            sectorsPerCluster = long(sectorsPerCluster)
            bytesPerSector = long(bytesPerSector)
            numFreeClusters = long(numFreeClusters)
            return long(numFreeClusters * sectorsPerCluster * bytesPerSector)

    # Non-syncing functions. Used when we loop over such events, and sync manually at the end

    def add_torrent_ns(self, filename, save_dir, compact, user_paused):
        # Cache torrent file
        (temp, filename_short) = os.path.split(filename)

        # if filename_short in os.listdir(self.base_dir + "/" + TORRENTS_SUBDIR):
        #     raise DuplicateTorrentError("Duplicate Torrent, it appears: " + filename_short)

        full_new_name = os.path.join(self.config.get("default_torrent_path"), filename_short)

        try:
            shutil.copy(filename, full_new_name)
        except Exception, e:
            if str(e).find('are the same file'):
                pass
            else:
                raise

        # Create torrent object
        new_torrent = torrent_info(full_new_name, save_dir, compact, user_paused)
        self.state.torrents[new_torrent] = None

    def remove_torrent_ns(self, unique_ID, data_also, torrent_also=False):
        self.unique_IDs[unique_ID].delete_me = True
        self.unique_IDs[unique_ID].del_data = data_also
        self.unique_IDs[unique_ID].del_torrent = torrent_also

    # Sync the state.torrents and unique_IDs lists with the core
    # ___ALL syncing code with the core is here, and ONLY here___
    # Also all self-syncing is done here (various lists)

    ##
    ## I had to make some changes here to get things to work properly
    ## Some of these changes may be hack-ish, so look at them and make
    ## sure nothing is wrong.
    ##
    def sync(self, called_on_start=False):
        ret = None # We return new added unique ID(s), or None
        no_space = False

        to_remove = []
        # Add torrents to core and unique_IDs
        for torrent in self.state.torrents:
            if not os.path.exists(torrent.filename):
                print "Missing file: %s" % torrent.filename
                to_remove.append(torrent)
                continue
            if torrent not in self.unique_IDs.values():
                try:
                    unique_ID = deluge_core.add_torrent(torrent.filename,
                                                        torrent.save_dir,
                                                        torrent.compact)
                except DelugeError, e:
                    del self.state.torrents[torrent]
                    raise e
                    continue
                except Exception, e:
                    print "Unable to add torrent: ", e
                    to_remove.append(torrent)
                    continue
                    
                ret = unique_ID
                self.unique_IDs[unique_ID] = torrent
                self.state.torrents[torrent] = unique_ID
                
                # Apply per torrent prefs after torrent added to core
                self.apply_prefs_per_torrent(unique_ID)
                #remove fastresume for non-seed
                try:
                    torrent_state = self.get_core_torrent_state(unique_ID)
                except:
                    pass
                else:
                    if not torrent_state['is_finished']:
                        try:
                            os.remove(self.unique_IDs[unique_ID].filename + ".fastresume")
                        except:
                            pass
        
        # Remove torrents from self.state because they were not added to the
        # libtorrent session.
        for torrent in to_remove:
            del self.state.torrents[torrent]

        # Remove torrents from core, unique_IDs and queue
        to_delete = []
        for unique_ID in self.unique_IDs.keys():
#            print torrent
            if self.unique_IDs[unique_ID].delete_me:
                deluge_core.remove_torrent(unique_ID, \
                    self.unique_IDs[unique_ID].del_data)
                to_delete.append(unique_ID)

        for unique_ID in to_delete:
            del self.state.torrents[self.unique_IDs[unique_ID]]
            self.state.queue.remove(self.unique_IDs[unique_ID])
            try:
                if self.unique_IDs[unique_ID].del_torrent:
                    os.remove(self.unique_IDs[unique_ID].filename)
            except:
                pass
            # Remove .fastresume
            try:
                # Must be after removal of the torrent, because that saves a new .fastresume
                os.remove(self.unique_IDs[unique_ID].filename + ".fastresume")
            except OSError:
                pass # Perhaps there never was one to begin with
            del self.unique_IDs[unique_ID]
            
        # Add torrents to queue - at the end, of course
        for torrent in self.unique_IDs.values():
            if torrent not in self.state.queue:
                if self.get_pref('queue_above_completed') and \
                   len(self.state.queue) > 0 and not called_on_start:
                    for index, torrent_tmp in enumerate(self.state.queue):
                        unique_ID = self.state.torrents[torrent_tmp]
                        torrent_state = self.get_core_torrent_state(unique_ID)
                        if torrent_state['progress'] == 1.0:
                            break
                    if torrent_state['progress'] == 1.0:
                        self.state.queue.insert(index, torrent)
                    else:
                        self.state.queue.append(torrent)
                else:
                    self.state.queue.append(torrent)

        # run through queue, remove those that no longer exists
        to_delete = []
        for torrent in self.state.queue:
            if torrent not in self.state.torrents:
                to_delete.append(torrent)
                
        for torrent in to_delete:
            self.state.queue.remove(torrent)

        assert(len(self.unique_IDs) == len(self.state.torrents))

        assert(len(self.unique_IDs) == len(self.state.queue))
        assert(len(self.unique_IDs) == deluge_core.get_num_torrents())
        
        #if no_space:
            #self.apply_queue()

        self.pickle_state()

        return ret

    def get_queue_index(self, torrent):
        return self.state.queue.index(torrent)

    def apply_prefs(self):
        print "Applying preferences"

        for pref in PREF_FUNCTIONS:
            if PREF_FUNCTIONS[pref] is not None:
                if (PREF_FUNCTIONS[pref] == PREF_FUNCTIONS["listen_on"]):
                    if not self.get_pref("random_port"):
                        PREF_FUNCTIONS[pref](self.get_pref(pref))
                    else:
                        if deluge_core.listening_port() != 0:
                            if self.get_pref("listen_on")[0] <= deluge_core.listening_port() <= self.get_pref("listen_on")[1]:
                                import random
                                randrange = lambda: random.randrange(49152, 65535)
                                ports = [randrange(), randrange()]
                                ports.sort()
                                deluge_core.set_listen_on(ports)
                            
                        else:
                            import random
                            randrange = lambda: random.randrange(49152, 65535)
                            ports = [randrange(), randrange()]
                            ports.sort()
                            deluge_core.set_listen_on(ports)
                else:
                    PREF_FUNCTIONS[pref](self.get_pref(pref))
                                                
        # We need to reapply priorities to files and per torrent options after
        # preferences were changed.
        for unique_ID in self.unique_IDs:
            self.prioritize_files(unique_ID, self.get_priorities(unique_ID))
            self.apply_prefs_per_torrent(unique_ID)

    def apply_prefs_per_torrent(self, unique_ID):
        self.set_max_connections_per_torrent(unique_ID, 
            self.get_pref("max_connections_per_torrent"))
        self.set_max_upload_slots_per_torrent(unique_ID, 
            self.get_pref("max_upload_slots_per_torrent"))

    def set_DHT(self, start=False):
        if start and not self.dht_running:
            print "Starting DHT..."
            deluge_core.start_DHT(os.path.join(self.base_dir, DHT_FILENAME))
            self.dht_running = True
        elif not start and self.dht_running:
            print "Stopping DHT..."
            deluge_core.stop_DHT(os.path.join(self.base_dir, DHT_FILENAME))
            self.dht_running = False

    # Calculations

    def calc_ratio(self, unique_ID, torrent_state):
        try:
            up = float((self.unique_IDs[unique_ID].uploaded_memory + self.get_core_torrent_state(unique_ID, False)['total_upload']) / 1024)
        except AttributeError:
            up = 0
            
        down = float(torrent_state["total_done"] / 1024)
        try:
            ret = up/down
        except:
            ret = 0.0
            
        return ret    

    def create_torrent(self, filename, source_directory, trackers, comments=None,
                    piece_size=32, author="Deluge", private=False, webseeds=None):
        return deluge_core.create_torrent(filename, source_directory, trackers, comments, piece_size, author, private, webseeds)

    # Creates/resets the IP filter list
    def reset_ip_filter(self):
        return deluge_core.reset_IP_filter()

    # Adds an IP range (as two dotted quad strings) to the filter 
    def add_range_to_ip_filter(self, start, end):
        return deluge_core.add_range_to_IP_filter(start, end)

    def set_ip_filter(self):
        return deluge_core.set_IP_filter()

    def proxy_settings(self, server, login, paswd, portnum, proxytype, proxy):
        return deluge_core.proxy_settings(server, login, paswd, portnum, proxytype, proxy)

    def pe_settings(self, out_enc_policy, in_enc_policy, allowed_enc_level, prefer_rc4):
        return deluge_core.pe_settings(out_enc_policy, in_enc_policy, allowed_enc_level, prefer_rc4)

    def get_trackers(self, unique_ID):
        if self.unique_IDs[unique_ID].trackers:
            return self.unique_IDs[unique_ID].trackers
        else:
            return deluge_core.get_trackers(unique_ID)

    def replace_trackers(self, unique_ID, trackerslist):
        try:
            self.unique_IDs[unique_ID].trackers = trackerslist
            self.unique_IDs[unique_ID].trackers_changed = 1
            return deluge_core.replace_trackers(unique_ID, trackerslist)
        except:
            pass

    def set_priv(self, unique_ID, on_off):
        return deluge_core.set_priv(unique_ID, on_off)

    def set_max_connections_per_torrent(self, unique_ID, max_connections):
        return deluge_core.set_max_connections_per_torrent(unique_ID, 
                   max_connections)
    
    def set_max_upload_slots_per_torrent(self, unique_ID, max_upload_slots):
        return deluge_core.set_max_upload_slots_per_torrent(unique_ID, 
                   max_upload_slots)

    def set_per_upload_rate_limit(self, unique_ID, speed):
        return deluge_core.set_per_upload_rate_limit(unique_ID, speed)

    def set_per_download_rate_limit(self, unique_ID, speed):
        return deluge_core.set_per_download_rate_limit(unique_ID, speed)

    def get_per_upload_rate_limit(self, unique_ID):
        return deluge_core.get_per_upload_rate_limit(unique_ID)

    def get_per_download_rate_limit(self, unique_ID):
        return deluge_core.get_per_download_rate_limit(unique_ID)

    def add_url_seed(self, unique_ID, address):
        return deluge_core.add_url_seed(unique_ID, address)

    def get_torrent_path(self, unique_ID):
        torrent_path = self.unique_IDs[unique_ID].save_dir
        torrent_state = self.get_torrent_state(unique_ID)
        if torrent_state["num_files"] > 1:
            file = self.get_torrent_file_info(unique_ID)[0]
            torrent_path = os.path.join(torrent_path,
                                        file["path"].split("/", 1)[0])
        return torrent_path

    def rename_file(self, unique_ID, filename, filesize):
        return deluge_core.remap_files(unique_ID, filename, filesize)
