# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 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.
#

"""Internal Torrent class

Attributes:
    LT_TORRENT_STATE_MAP (dict): Maps the torrent state from libtorrent to Deluge state.

"""

from __future__ import division, unicode_literals

import logging
import os
import socket

from twisted.internet.defer import Deferred, DeferredList

import deluge.component as component
from deluge._libtorrent import lt
from deluge.common import decode_bytes
from deluge.configmanager import ConfigManager, get_config_dir
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
from deluge.decorators import deprecated
from deluge.event import (
    TorrentFolderRenamedEvent,
    TorrentStateChangedEvent,
    TorrentTrackerStatusEvent,
)

try:
    from urllib.parse import urlparse
except ImportError:
    # PY2 fallback
    from urlparse import urlparse  # pylint: disable=ungrouped-imports

try:
    from future_builtins import zip
except ImportError:
    # Ignore on Py3.
    pass

log = logging.getLogger(__name__)

LT_TORRENT_STATE_MAP = {
    'queued_for_checking': 'Checking',
    'checking_files': 'Checking',
    'downloading_metadata': 'Downloading',
    'downloading': 'Downloading',
    'finished': 'Seeding',
    'seeding': 'Seeding',
    'allocating': 'Allocating',
    'checking_resume_data': 'Checking',
}


def sanitize_filepath(filepath, folder=False):
    """Returns a sanitized filepath to pass to libtorrent rename_file().

    The filepath will have backslashes substituted along with whitespace
    padding and duplicate slashes stripped.

    Args:
        folder (bool): A trailing slash is appended to the returned filepath.
    """

    def clean_filename(filename):
        """Strips whitespace and discards dotted filenames"""
        filename = filename.strip()
        if filename.replace('.', '') == '':
            return ''
        return filename

    if '\\' in filepath or '/' in filepath:
        folderpath = filepath.replace('\\', '/').split('/')
        folderpath = [clean_filename(x) for x in folderpath]
        newfilepath = '/'.join([path for path in folderpath if path])
    else:
        newfilepath = clean_filename(filepath)

    if folder is True:
        newfilepath += '/'

    return newfilepath


def convert_lt_files(files):
    """Indexes and decodes files from libtorrent get_files().

    Args:
        files (list): The libtorrent torrent files.

    Returns:
        list of dict: The files.

        The format for the file dict::

            {
                "index": int,
                "path": str,
                "size": int,
                "offset": int
            }
    """
    filelist = []
    for index, _file in enumerate(files):
        try:
            file_path = _file.path.decode('utf8')
        except AttributeError:
            file_path = _file.path

        filelist.append(
            {
                'index': index,
                'path': file_path.replace('\\', '/'),
                'size': _file.size,
                'offset': _file.offset,
            }
        )

    return filelist


class TorrentOptions(dict):
    """TorrentOptions create a dict of the torrent options.

    Attributes:
        add_paused (bool): Add the torrrent in a paused state.
        auto_managed (bool): Set torrent to auto managed mode, i.e. will be started or queued automatically.
        download_location (str): The path for the torrent data to be stored while downloading.
        file_priorities (list of int): The priority for files in torrent, range is [0..7] however
            only [0, 1, 4, 7] are normally used and correspond to [Skip, Low, Normal, High]
        mapped_files (dict): A mapping of the renamed filenames in 'index:filename' pairs.
        max_connections (int): Sets maximum number of connections this torrent will open.
            This must be at least 2. The default is unlimited (-1).
        max_download_speed (float): Will limit the download bandwidth used by this torrent to the
            limit you set.The default is unlimited (-1) but will not exceed global limit.
        max_upload_slots (int): Sets the maximum number of peers that are
            unchoked at the same time on this torrent. This defaults to infinite (-1).
        max_upload_speed (float): Will limit the upload bandwidth used by this torrent to the limit
            you set. The default is unlimited (-1) but will not exceed global limit.
        move_completed (bool): Move the torrent when downloading has finished.
        move_completed_path (str): The path to move torrent to when downloading has finished.
        name (str): The display name of the torrent.
        owner (str): The user this torrent belongs to.
        pre_allocate_storage (bool): When adding the torrent should all files be pre-allocated.
        prioritize_first_last_pieces (bool): Prioritize the first and last pieces in the torrent.
        remove_at_ratio (bool): Remove the torrent when it has reached the stop_ratio.
        seed_mode (bool): Assume that all files are present for this torrent (Only used when adding a torent).
        sequential_download (bool): Download the pieces of the torrent in order.
        shared (bool): Enable the torrent to be seen by other Deluge users.
        stop_at_ratio (bool): Stop the torrent when it has reached stop_ratio.
        stop_ratio (float): The seeding ratio to stop (or remove) the torrent at.
        super_seeding (bool): Enable super seeding/initial seeding.
    """

    def __init__(self):
        super(TorrentOptions, self).__init__()
        config = ConfigManager('core.conf').config
        options_conf_map = {
            'add_paused': 'add_paused',
            'auto_managed': 'auto_managed',
            'download_location': 'download_location',
            'max_connections': 'max_connections_per_torrent',
            'max_download_speed': 'max_download_speed_per_torrent',
            'max_upload_slots': 'max_upload_slots_per_torrent',
            'max_upload_speed': 'max_upload_speed_per_torrent',
            'move_completed': 'move_completed',
            'move_completed_path': 'move_completed_path',
            'pre_allocate_storage': 'pre_allocate_storage',
            'prioritize_first_last_pieces': 'prioritize_first_last_pieces',
            'remove_at_ratio': 'remove_seed_at_ratio',
            'sequential_download': 'sequential_download',
            'shared': 'shared',
            'stop_at_ratio': 'stop_seed_at_ratio',
            'stop_ratio': 'stop_seed_ratio',
            'super_seeding': 'super_seeding',
        }
        for opt_k, conf_k in options_conf_map.items():
            self[opt_k] = config[conf_k]
        self['file_priorities'] = []
        self['mapped_files'] = {}
        self['name'] = ''
        self['owner'] = ''
        self['seed_mode'] = False


class TorrentError(object):
    def __init__(self, error_message, was_paused=False, restart_to_resume=False):
        self.error_message = error_message
        self.was_paused = was_paused
        self.restart_to_resume = restart_to_resume


class Torrent(object):
    """Torrent holds information about torrents added to the libtorrent session.

    Args:
        handle: The libtorrent torrent handle.
        options (dict): The torrent options.
        state (TorrentState): The torrent state.
        filename (str): The filename of the torrent file.
        magnet (str): The magnet uri.

    Attributes:
        torrent_id (str): The torrent_id for this torrent
        handle: Holds the libtorrent torrent handle
        magnet (str): The magnet uri used to add this torrent (if available).
        status: Holds status info so that we don"t need to keep getting it from libtorrent.
        torrent_info: store the torrent info.
        has_metadata (bool): True if the metadata for the torrent is available, False otherwise.
        status_funcs (dict): The function mappings to get torrent status
        prev_status (dict): Previous status dicts returned for this torrent. We use this to return
            dicts that only contain changes from the previous.
            {session_id: status_dict, ...}
        waiting_on_folder_rename (list of dict): A list of Deferreds for file indexes we're waiting for file_rename
            alerts on. This is so we can send one folder_renamed signal instead of multiple file_renamed signals.
            [{index: Deferred, ...}, ...]
        options (dict): The torrent options.
        filename (str): The filename of the torrent file in case it is required.
        is_finished (bool): Keep track if torrent is finished to prevent some weird things on state load.
        statusmsg (str): Status message holds error/extra info about the torrent.
        state (str): The torrent's state
        trackers (list of dict): The torrent's trackers
        tracker_status (str): Status message of currently connected tracker
        tracker_host (str): Hostname of the currently connected tracker
        forcing_recheck (bool): Keep track if we're forcing a recheck of the torrent
        forcing_recheck_paused (bool): Keep track if we're forcing a recheck of the torrent so that
            we can re-pause it after its done if necessary
        forced_error (TorrentError): Keep track if we have forced this torrent to be in Error state.
    """

    def __init__(self, handle, options, state=None, filename=None, magnet=None):
        self.torrent_id = str(handle.info_hash())
        if log.isEnabledFor(logging.DEBUG):
            log.debug('Creating torrent object %s', self.torrent_id)

        # Get the core config
        self.config = ConfigManager('core.conf')
        self.rpcserver = component.get('RPCServer')

        self.handle = handle

        self.magnet = magnet
        self.status = self.handle.status()

        self.torrent_info = self.handle.get_torrent_info()
        self.has_metadata = self.status.has_metadata

        self.options = TorrentOptions()
        self.options.update(options)

        # Load values from state if we have it
        if state:
            self.set_trackers(state.trackers)
            self.is_finished = state.is_finished
            self.filename = state.filename
        else:
            self.set_trackers()
            self.is_finished = False
            self.filename = filename

        self.forced_error = None
        self.statusmsg = None
        self.state = None
        self.moving_storage_dest_path = None
        self.tracker_status = ''
        self.tracker_host = None
        self.forcing_recheck = False
        self.forcing_recheck_paused = False
        self.status_funcs = None
        self.prev_status = {}
        self.waiting_on_folder_rename = []

        self.update_status(self.handle.status())
        self._create_status_funcs()
        self.set_options(self.options)
        self.update_state()

        if log.isEnabledFor(logging.DEBUG):
            log.debug('Torrent object created.')

    def on_metadata_received(self):
        """Process the metadata received alert for this torrent"""
        self.has_metadata = True
        self.torrent_info = self.handle.get_torrent_info()
        if self.options['prioritize_first_last_pieces']:
            self.set_prioritize_first_last_pieces(True)
        self.write_torrentfile()

    # --- Options methods ---
    def set_options(self, options):
        """Set the torrent options.

        Args:
            options (dict): Torrent options, see TorrentOptions class for valid keys.
        """

        # Skip set_prioritize_first_last if set_file_priorities is in options as it also calls the method.
        if 'file_priorities' in options and 'prioritize_first_last_pieces' in options:
            self.options['prioritize_first_last_pieces'] = options.pop(
                'prioritize_first_last_pieces'
            )

        for key, value in options.items():
            if key in self.options:
                options_set_func = getattr(self, 'set_' + key, None)
                if options_set_func:
                    options_set_func(value)
                else:
                    # Update config options that do not have funcs
                    self.options[key] = value

    def get_options(self):
        """Get the torrent options.

        Returns:
            dict: the torrent options.
        """
        return self.options

    def set_max_connections(self, max_connections):
        """Sets maximum number of connections this torrent will open.

        Args:
            max_connections (int): Maximum number of connections

        Note:
            The minimum value for handle.max_connections is 2 (or -1 for unlimited connections).
            This is enforced by libtorrent and values 0 or 1 raise an assert with lt debug builds.
        """

        if max_connections == 0:
            max_connections = -1
        elif max_connections == 1:
            max_connections = 2

        self.options['max_connections'] = max_connections
        self.handle.set_max_connections(max_connections)

    def set_max_upload_slots(self, max_slots):
        """Sets maximum number of upload slots for this torrent.

        Args:
            max_slots (int): Maximum upload slots
        """
        self.options['max_upload_slots'] = max_slots
        self.handle.set_max_uploads(max_slots)

    def set_max_upload_speed(self, m_up_speed):
        """Sets maximum upload speed for this torrent.

        Args:
            m_up_speed (float): Maximum upload speed in KiB/s.
        """
        self.options['max_upload_speed'] = m_up_speed
        if m_up_speed < 0:
            value = -1
        else:
            value = int(m_up_speed * 1024)
        self.handle.set_upload_limit(value)

    def set_max_download_speed(self, m_down_speed):
        """Sets maximum download speed for this torrent.

        Args:
            m_up_speed (float): Maximum download speed in KiB/s.
        """
        self.options['max_download_speed'] = m_down_speed
        if m_down_speed < 0:
            value = -1
        else:
            value = int(m_down_speed * 1024)
        self.handle.set_download_limit(value)

    @deprecated
    def set_prioritize_first_last(self, prioritize):
        """Deprecated: Use set_prioritize_first_last_pieces."""
        self.set_prioritize_first_last_pieces(prioritize)

    def set_prioritize_first_last_pieces(self, prioritize):
        """Prioritize the first and last pieces in the torrent.

        Args:
            prioritize (bool): Prioritize the first and last pieces.

        """
        if not self.has_metadata:
            return

        self.options['prioritize_first_last_pieces'] = prioritize
        if not prioritize:
            # If we are turning off this option, call set_file_priorities to
            # reset all the piece priorities
            self.set_file_priorities(self.options['file_priorities'])
            return

        # A list of priorities for each piece in the torrent
        priorities = self.handle.piece_priorities()

        def get_file_piece(idx, byte_offset):
            return self.torrent_info.map_file(idx, byte_offset, 0).piece

        for idx in range(self.torrent_info.num_files()):
            file_size = self.torrent_info.files().file_size(idx)
            two_percent_bytes = int(0.02 * file_size)
            # Get the pieces for the byte offsets
            first_start = get_file_piece(idx, 0)
            first_end = get_file_piece(idx, two_percent_bytes) + 1
            last_start = get_file_piece(idx, file_size - two_percent_bytes)
            last_end = get_file_piece(idx, max(file_size - 1, 0)) + 1

            # Set the pieces in first and last ranges to priority 7
            # if they are not marked as do not download
            priorities[first_start:first_end] = [
                p and 7 for p in priorities[first_start:first_end]
            ]
            priorities[last_start:last_end] = [
                p and 7 for p in priorities[last_start:last_end]
            ]

        # Setting the priorites for all the pieces of this torrent
        self.handle.prioritize_pieces(priorities)

    def set_sequential_download(self, set_sequencial):
        """Sets whether to download the pieces of the torrent in order.

        Args:
            set_sequencial (bool): Enable sequencial downloading.
        """
        self.options['sequential_download'] = set_sequencial
        self.handle.set_sequential_download(set_sequencial)

    def set_auto_managed(self, auto_managed):
        """Set auto managed mode, i.e. will be started or queued automatically.

        Args:
            auto_managed (bool): Enable auto managed.
        """
        self.options['auto_managed'] = auto_managed
        if not (self.status.paused and not self.status.auto_managed):
            self.handle.auto_managed(auto_managed)
            self.update_state()

    def set_super_seeding(self, super_seeding):
        """Set super seeding/initial seeding.

        Args:
            super_seeding (bool): Enable super seeding.
        """
        self.options['super_seeding'] = super_seeding
        self.handle.super_seeding(super_seeding)

    def set_stop_ratio(self, stop_ratio):
        """The seeding ratio to stop (or remove) the torrent at.

        Args:
            stop_ratio (float): The seeding ratio.
        """
        self.options['stop_ratio'] = stop_ratio

    def set_stop_at_ratio(self, stop_at_ratio):
        """Stop the torrent when it has reached stop_ratio.

        Args:
            stop_at_ratio (bool): Stop the torrent.
        """
        self.options['stop_at_ratio'] = stop_at_ratio

    def set_remove_at_ratio(self, remove_at_ratio):
        """Remove the torrent when it has reached the stop_ratio.

        Args:
            remove_at_ratio (bool): Remove the torrent.
        """
        self.options['remove_at_ratio'] = remove_at_ratio

    def set_move_completed(self, move_completed):
        """Set whether to move the torrent when downloading has finished.

        Args:
            move_completed (bool): Move the torrent.

        """
        self.options['move_completed'] = move_completed

    def set_move_completed_path(self, move_completed_path):
        """Set the path to move torrent to when downloading has finished.

        Args:
            move_completed_path (str): The move path.
        """
        self.options['move_completed_path'] = move_completed_path

    def set_file_priorities(self, file_priorities):
        """Sets the file priotities.

        Args:
            file_priorities (list of int): List of file priorities.
        """
        if not self.has_metadata:
            return

        if log.isEnabledFor(logging.DEBUG):
            log.debug(
                'Setting %s file priorities to: %s', self.torrent_id, file_priorities
            )

        if file_priorities and len(file_priorities) == len(self.get_files()):
            self.handle.prioritize_files(file_priorities)
        else:
            log.debug('Unable to set new file priorities.')
            file_priorities = self.handle.file_priorities()

        if 0 in self.options['file_priorities']:
            # Previously marked a file 'skip' so check for any 0's now >0.
            for index, priority in enumerate(self.options['file_priorities']):
                if priority == 0 and file_priorities[index] > 0:
                    # Changed priority from skip to download so update state.
                    self.is_finished = False
                    self.update_state()
                    break

        # Store the priorities.
        self.options['file_priorities'] = file_priorities

        # Set the first/last priorities if needed.
        if self.options['prioritize_first_last_pieces']:
            self.set_prioritize_first_last_pieces(True)

    @deprecated
    def set_save_path(self, download_location):
        """Deprecated: Use set_download_location."""
        self.set_download_location(download_location)

    def set_download_location(self, download_location):
        """The location for downloading torrent data."""
        self.options['download_location'] = download_location

    def set_owner(self, account):
        """Sets the owner of this torrent.

        Args:
            account (str): The new owner account name.

        Notes:
            Only a user with admin level auth can change this value.

        """

        if self.rpcserver.get_session_auth_level() == AUTH_LEVEL_ADMIN:
            self.options['owner'] = account

    # End Options methods #

    def set_trackers(self, trackers=None):
        """Sets the trackers for this torrent.

        Args:
            trackers (list of dicts): A list of trackers.
        """
        if trackers is None:
            self.trackers = [tracker for tracker in self.handle.trackers()]
            self.tracker_host = None
            return

        if log.isEnabledFor(logging.DEBUG):
            log.debug('Setting trackers for %s: %s', self.torrent_id, trackers)

        tracker_list = []

        for tracker in trackers:
            new_entry = lt.announce_entry(str(tracker['url']))
            new_entry.tier = tracker['tier']
            tracker_list.append(new_entry)
        self.handle.replace_trackers(tracker_list)

        # Print out the trackers
        if log.isEnabledFor(logging.DEBUG):
            log.debug('Trackers set for %s:', self.torrent_id)
            for tracker in self.handle.trackers():
                log.debug(' [tier %s]: %s', tracker['tier'], tracker['url'])
        # Set the tracker list in the torrent object
        self.trackers = trackers
        if len(trackers) > 0:
            # Force a re-announce if there is at least 1 tracker
            self.force_reannounce()
        self.tracker_host = None

    def set_tracker_status(self, status):
        """Sets the tracker status.

        Args:
            status (str): The tracker status.

        Emits:
            TorrentTrackerStatusEvent upon tracker status change.

        """

        self.tracker_host = None

        if self.tracker_status != status:
            self.tracker_status = status
            component.get('EventManager').emit(
                TorrentTrackerStatusEvent(self.torrent_id, self.tracker_status)
            )

    def merge_trackers(self, torrent_info):
        """Merges new trackers in torrent_info into torrent"""
        log.info(
            'Adding any new trackers to torrent (%s) already in session...',
            self.torrent_id,
        )
        if not torrent_info:
            return
        # Don't merge trackers if either torrent has private flag set.
        if torrent_info.priv() or self.get_status(['private'])['private']:
            log.info('Adding trackers aborted: Torrent has private flag set.')
        else:
            for tracker in torrent_info.trackers():
                self.handle.add_tracker({'url': tracker.url, 'tier': tracker.tier})
            # Update torrent.trackers from libtorrent handle.
            self.set_trackers()

    def update_state(self):
        """Updates the state, based on libtorrent's torrent state"""
        status = self.handle.status()
        session_paused = component.get('Core').session.is_paused()
        old_state = self.state
        self.set_status_message()
        status_error = status.errc.message() if status.errc.value() else ''

        if self.forced_error:
            self.state = 'Error'
            self.set_status_message(self.forced_error.error_message)
        elif status_error:
            self.state = 'Error'
            # auto-manage status will be reverted upon resuming.
            self.handle.auto_managed(False)
            self.set_status_message(decode_bytes(status_error))
        elif status.moving_storage:
            self.state = 'Moving'
        elif not session_paused and status.paused and status.auto_managed:
            self.state = 'Queued'
        elif session_paused or status.paused:
            self.state = 'Paused'
        else:
            self.state = LT_TORRENT_STATE_MAP.get(str(status.state), str(status.state))

        if self.state != old_state:
            component.get('EventManager').emit(
                TorrentStateChangedEvent(self.torrent_id, self.state)
            )

        if log.isEnabledFor(logging.DEBUG):
            log.debug(
                'State from lt was: %s | Session is paused: %s\nTorrent state set from "%s" to "%s" (%s)',
                'error' if status_error else status.state,
                session_paused,
                old_state,
                self.state,
                self.torrent_id,
            )
            if self.forced_error:
                log.debug(
                    'Torrent Error state message: %s', self.forced_error.error_message
                )

    def set_status_message(self, message=None):
        """Sets the torrent status message.

        Calling method without a message will reset the message to 'OK'.

        Args:
            message (str, optional): The status message.

        """
        if not message:
            message = 'OK'
        self.statusmsg = message

    def force_error_state(self, message, restart_to_resume=True):
        """Forces the torrent into an error state.

        For setting an error state not covered by libtorrent.

        Args:
            message (str): The error status message.
            restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
                session can resume.
        """
        status = self.handle.status()
        self.handle.auto_managed(False)
        self.forced_error = TorrentError(message, status.paused, restart_to_resume)
        if not status.paused:
            self.handle.pause()
        self.update_state()

    def clear_forced_error_state(self, update_state=True):
        if not self.forced_error:
            return

        if self.forced_error.restart_to_resume:
            log.error('Restart deluge to clear this torrent error')

        if not self.forced_error.was_paused and self.options['auto_managed']:
            self.handle.auto_managed(True)
        self.forced_error = None
        self.set_status_message('OK')
        if update_state:
            self.update_state()

    def get_eta(self):
        """Get the ETA for this torrent.

        Returns:
            int: The ETA in seconds.

        """
        status = self.status
        eta = 0
        if (
            self.is_finished
            and self.options['stop_at_ratio']
            and status.upload_payload_rate
        ):
            # We're a seed, so calculate the time to the 'stop_share_ratio'
            eta = (
                int(status.all_time_download * self.options['stop_ratio'])
                - status.all_time_upload
            ) // status.upload_payload_rate
        elif status.download_payload_rate:
            left = status.total_wanted - status.total_wanted_done
            if left > 0:
                eta = left // status.download_payload_rate

        # Limit to 1 year, avoid excessive values and prevent GTK int overflow.
        return eta if eta < 31557600 else -1

    def get_ratio(self):
        """Get the ratio of upload/download for this torrent.

        Returns:
            float: The ratio or -1.0 (for infinity).

        """
        if self.status.total_done > 0:
            return self.status.all_time_upload / self.status.total_done
        else:
            return -1.0

    def get_files(self):
        """Get the files this torrent contains.

        Returns:
            list of dict: The files.

        """
        if not self.has_metadata:
            return []

        files = self.torrent_info.files()
        return convert_lt_files(files)

    def get_orig_files(self):
        """Get the original filenames of files in this torrent.

        Returns:
            list of dict: The files with original filenames.

        """
        if not self.has_metadata:
            return []

        files = self.torrent_info.orig_files()
        return convert_lt_files(files)

    def get_peers(self):
        """Get the peers for this torrent.

        A list of peers and various information about them.

        Returns:
            list of dict: The peers.

            The format for the peer dict::

                {
                    "client": str,
                    "country": str,
                    "down_speed": int,
                    "ip": str,
                    "progress": float,
                    "seed": bool,
                    "up_speed": int
                }
        """
        ret = []
        peers = self.handle.get_peer_info()

        for peer in peers:
            # We do not want to report peers that are half-connected
            if peer.flags & peer.connecting or peer.flags & peer.handshake:
                continue

            client = decode_bytes(peer.client)

            try:
                country = component.get('Core').geoip_instance.country_code_by_addr(
                    peer.ip[0]
                )
            except AttributeError:
                country = ''
            else:
                try:
                    country = ''.join(
                        [char if char.isalpha() else ' ' for char in country]
                    )
                except TypeError:
                    country = ''

            ret.append(
                {
                    'client': client,
                    'country': country,
                    'down_speed': peer.payload_down_speed,
                    'ip': '%s:%s' % (peer.ip[0], peer.ip[1]),
                    'progress': peer.progress,
                    'seed': peer.flags & peer.seed,
                    'up_speed': peer.payload_up_speed,
                }
            )

        return ret

    def get_queue_position(self):
        """Get the torrents queue position

        Returns:
            int: queue position
        """
        return self.handle.queue_position()

    def get_file_priorities(self):
        """Return the file priorities"""
        if not self.handle.has_metadata():
            return []

        if not self.options['file_priorities']:
            # Ensure file_priorities option is populated.
            self.set_file_priorities([])

        return self.options['file_priorities']

    def get_file_progress(self):
        """Calculates the file progress as a percentage.

        Returns:
            list of floats: The file progress (0.0 -> 1.0), empty list if n/a.
        """
        if not self.has_metadata:
            return []
        return [
            progress / _file.size if _file.size else 0.0
            for progress, _file in zip(
                self.handle.file_progress(), self.torrent_info.files()
            )
        ]

    def get_tracker_host(self):
        """Get the hostname of the currently connected tracker.

        If no tracker is connected, it uses the 1st tracker.

        Returns:
            str: The tracker host
        """
        if self.tracker_host:
            return self.tracker_host

        tracker = self.status.current_tracker
        if not tracker and self.trackers:
            tracker = self.trackers[0]['url']

        if tracker:
            url = urlparse(tracker.replace('udp://', 'http://'))
            if hasattr(url, 'hostname'):
                host = url.hostname or 'DHT'
                # Check if hostname is an IP address and just return it if that's the case
                try:
                    socket.inet_aton(host)
                except socket.error:
                    pass
                else:
                    # This is an IP address because an exception wasn't raised
                    return url.hostname

                parts = host.split('.')
                if len(parts) > 2:
                    if parts[-2] in ('co', 'com', 'net', 'org') or parts[-1] == 'uk':
                        host = '.'.join(parts[-3:])
                    else:
                        host = '.'.join(parts[-2:])
                self.tracker_host = host
                return host
        return ''

    def get_magnet_uri(self):
        """Returns a magnet uri for this torrent"""
        return lt.make_magnet_uri(self.handle)

    def get_name(self):
        """The name of the torrent (distinct from the filenames).

        Note:
            Can be manually set in options through `name` key. If the key is
            reset to empty string "" it will return the original torrent name.

        Returns:
            str: the name of the torrent.

        """
        if self.options['name']:
            return self.options['name']

        if self.has_metadata:
            # Use the top-level folder as torrent name.
            filename = decode_bytes(self.torrent_info.file_at(0).path)
            name = filename.replace('\\', '/', 1).split('/', 1)[0]
        else:
            name = decode_bytes(self.handle.name())

        if not name:
            name = self.torrent_id

        return name

    def get_progress(self):
        """The progress of this torrent's current task.

        Returns:
            float: The progress percentage (0 to 100).

        """

        def get_size(files, path):
            """Returns total size of 'files' currently located in 'path'"""
            files = [os.path.join(path, f) for f in files]
            return sum(os.stat(f).st_size for f in files if os.path.exists(f))

        if self.state == 'Error':
            progress = 100.0
        elif self.state == 'Moving':
            # Check if torrent has downloaded any data yet.
            if self.status.total_done:
                torrent_files = [f['path'] for f in self.get_files()]
                dest_path_size = get_size(torrent_files, self.moving_storage_dest_path)
                progress = dest_path_size / self.status.total_done * 100
            else:
                progress = 100.0
        else:
            progress = self.status.progress * 100

        return progress

    def get_time_since_transfer(self):
        """The time since either upload/download from peers"""
        time_since = (self.status.time_since_download, self.status.time_since_upload)
        try:
            return min(x for x in time_since if x != -1)
        except ValueError:
            return -1

    def get_status(self, keys, diff=False, update=False, all_keys=False):
        """Returns the status of the torrent based on the keys provided

        Args:
            keys (list of str): the keys to get the status on
            diff (bool): Will return a diff of the changes since the last
                call to get_status based on the session_id
            update (bool): If True the status will be updated from libtorrent
                if False, the cached values will be returned

        Returns:
            dict: a dictionary of the status keys and their values
        """
        if update:
            self.update_status(self.handle.status())

        if all_keys:
            keys = list(self.status_funcs)

        status_dict = {}

        for key in keys:
            status_dict[key] = self.status_funcs[key]()

        if diff:
            session_id = self.rpcserver.get_session_id()
            if session_id in self.prev_status:
                # We have a previous status dict, so lets make a diff
                status_diff = {}
                for key, value in status_dict.items():
                    if key in self.prev_status[session_id]:
                        if value != self.prev_status[session_id][key]:
                            status_diff[key] = value
                    else:
                        status_diff[key] = value

                self.prev_status[session_id] = status_dict
                return status_diff

            self.prev_status[session_id] = status_dict
            return status_dict

        return status_dict

    def update_status(self, status):
        """Updates the cached status.

        Args:
            status (libtorrent.torrent_status): a libtorrent torrent status
        """
        self.status = status

    def _create_status_funcs(self):
        """Creates the functions for getting torrent status"""
        self.status_funcs = {
            'active_time': lambda: self.status.active_time,
            'seeding_time': lambda: self.status.seeding_time,
            'finished_time': lambda: self.status.finished_time,
            'all_time_download': lambda: self.status.all_time_download,
            'storage_mode': lambda: self.status.storage_mode.name.split('_')[
                2
            ],  # sparse or allocate
            'distributed_copies': lambda: max(0.0, self.status.distributed_copies),
            'download_payload_rate': lambda: self.status.download_payload_rate,
            'file_priorities': self.get_file_priorities,
            'hash': lambda: self.torrent_id,
            'auto_managed': lambda: self.options['auto_managed'],
            'is_auto_managed': lambda: self.options['auto_managed'],
            'is_finished': lambda: self.is_finished,
            'max_connections': lambda: self.options['max_connections'],
            'max_download_speed': lambda: self.options['max_download_speed'],
            'max_upload_slots': lambda: self.options['max_upload_slots'],
            'max_upload_speed': lambda: self.options['max_upload_speed'],
            'message': lambda: self.statusmsg,
            'move_on_completed_path': lambda: self.options[
                'move_completed_path'
            ],  # Deprecated: move_completed_path
            'move_on_completed': lambda: self.options[
                'move_completed'
            ],  # Deprecated: Use move_completed
            'move_completed_path': lambda: self.options['move_completed_path'],
            'move_completed': lambda: self.options['move_completed'],
            'next_announce': lambda: self.status.next_announce.seconds,
            'num_peers': lambda: self.status.num_peers - self.status.num_seeds,
            'num_seeds': lambda: self.status.num_seeds,
            'owner': lambda: self.options['owner'],
            'paused': lambda: self.status.paused,
            'prioritize_first_last': lambda: self.options[
                'prioritize_first_last_pieces'
            ],
            # Deprecated: Use prioritize_first_last_pieces
            'prioritize_first_last_pieces': lambda: self.options[
                'prioritize_first_last_pieces'
            ],
            'sequential_download': lambda: self.options['sequential_download'],
            'progress': self.get_progress,
            'shared': lambda: self.options['shared'],
            'remove_at_ratio': lambda: self.options['remove_at_ratio'],
            'save_path': lambda: self.options[
                'download_location'
            ],  # Deprecated: Use download_location
            'download_location': lambda: self.options['download_location'],
            'seeds_peers_ratio': lambda: -1.0
            if self.status.num_incomplete == 0
            else (  # Use -1.0 to signify infinity
                self.status.num_complete / self.status.num_incomplete
            ),
            'seed_rank': lambda: self.status.seed_rank,
            'state': lambda: self.state,
            'stop_at_ratio': lambda: self.options['stop_at_ratio'],
            'stop_ratio': lambda: self.options['stop_ratio'],
            'time_added': lambda: self.status.added_time,
            'total_done': lambda: self.status.total_done,
            'total_payload_download': lambda: self.status.total_payload_download,
            'total_payload_upload': lambda: self.status.total_payload_upload,
            'total_peers': lambda: self.status.num_incomplete,
            'total_seeds': lambda: self.status.num_complete,
            'total_uploaded': lambda: self.status.all_time_upload,
            'total_wanted': lambda: self.status.total_wanted,
            'total_remaining': lambda: self.status.total_wanted
            - self.status.total_wanted_done,
            'tracker': lambda: self.status.current_tracker,
            'tracker_host': self.get_tracker_host,
            'trackers': lambda: self.trackers,
            'tracker_status': lambda: self.tracker_status,
            'upload_payload_rate': lambda: self.status.upload_payload_rate,
            'comment': lambda: decode_bytes(self.torrent_info.comment())
            if self.has_metadata
            else '',
            'creator': lambda: decode_bytes(self.torrent_info.creator())
            if self.has_metadata
            else '',
            'num_files': lambda: self.torrent_info.num_files()
            if self.has_metadata
            else 0,
            'num_pieces': lambda: self.torrent_info.num_pieces()
            if self.has_metadata
            else 0,
            'piece_length': lambda: self.torrent_info.piece_length()
            if self.has_metadata
            else 0,
            'private': lambda: self.torrent_info.priv() if self.has_metadata else False,
            'total_size': lambda: self.torrent_info.total_size()
            if self.has_metadata
            else 0,
            'eta': self.get_eta,
            'file_progress': self.get_file_progress,
            'files': self.get_files,
            'orig_files': self.get_orig_files,
            'is_seed': lambda: self.status.is_seeding,
            'peers': self.get_peers,
            'queue': lambda: self.status.queue_position,
            'ratio': self.get_ratio,
            'completed_time': lambda: self.status.completed_time,
            'last_seen_complete': lambda: self.status.last_seen_complete,
            'name': self.get_name,
            'pieces': self._get_pieces_info,
            'seed_mode': lambda: self.status.seed_mode,
            'super_seeding': lambda: self.status.super_seeding,
            'time_since_download': lambda: self.status.time_since_download,
            'time_since_upload': lambda: self.status.time_since_upload,
            'time_since_transfer': self.get_time_since_transfer,
        }

    def pause(self):
        """Pause this torrent.

        Returns:
            bool: True is successful, otherwise False.

        """
        # Turn off auto-management so the torrent will not be unpaused by lt queueing
        self.handle.auto_managed(False)
        if self.state == 'Error':
            log.debug('Unable to pause torrent while in Error state')
        elif self.status.paused:
            # This torrent was probably paused due to being auto managed by lt
            # Since we turned auto_managed off, we should update the state which should
            # show it as 'Paused'.  We need to emit a torrent_paused signal because
            # the torrent_paused alert from libtorrent will not be generated.
            self.update_state()
            component.get('EventManager').emit(
                TorrentStateChangedEvent(self.torrent_id, 'Paused')
            )
        else:
            try:
                self.handle.pause()
            except RuntimeError as ex:
                log.debug('Unable to pause torrent: %s', ex)

    def resume(self):
        """Resumes this torrent."""
        if self.status.paused and self.status.auto_managed:
            log.debug('Resume not possible for auto-managed torrent!')
        elif self.forced_error and self.forced_error.was_paused:
            log.debug(
                'Resume skipped for forced_error torrent as it was originally paused.'
            )
        elif (
            self.status.is_finished
            and self.options['stop_at_ratio']
            and self.get_ratio() >= self.options['stop_ratio']
        ):
            log.debug('Resume skipped for torrent as it has reached "stop_seed_ratio".')
        else:
            # Check if torrent was originally being auto-managed.
            if self.options['auto_managed']:
                self.handle.auto_managed(True)
            try:
                self.handle.resume()
            except RuntimeError as ex:
                log.debug('Unable to resume torrent: %s', ex)

        # Clear torrent error state.
        if self.forced_error and not self.forced_error.restart_to_resume:
            self.clear_forced_error_state()
        elif self.state == 'Error' and not self.forced_error:
            self.handle.clear_error()

    def connect_peer(self, peer_ip, peer_port):
        """Manually add a peer to the torrent

        Args:
            peer_ip (str) : Peer IP Address
            peer_port (int): Peer Port

        Returns:
            bool: True is successful, otherwise False
        """
        try:
            self.handle.connect_peer((peer_ip, peer_port), 0)
        except RuntimeError as ex:
            log.debug('Unable to connect to peer: %s', ex)
            return False
        return True

    def move_storage(self, dest):
        """Move a torrent's storage location

        Args:
            dest (str): The destination folder for the torrent data

        Returns:
            bool: True if successful, otherwise False

        """
        dest = decode_bytes(dest)

        if not os.path.exists(dest):
            try:
                os.makedirs(dest)
            except OSError as ex:
                log.error(
                    'Could not move storage for torrent %s since %s does '
                    'not exist and could not create the directory: %s',
                    self.torrent_id,
                    dest,
                    ex,
                )
                return False

        try:
            # lt needs utf8 byte-string. Otherwise if wstrings enabled, unicode string.
            # Keyword argument flags=2 (dont_replace) dont overwrite target files but delete source.
            try:
                self.handle.move_storage(dest.encode('utf8'), flags=2)
            except TypeError:
                self.handle.move_storage(dest, flags=2)
        except RuntimeError as ex:
            log.error('Error calling libtorrent move_storage: %s', ex)
            return False
        self.moving_storage_dest_path = dest
        self.update_state()
        return True

    def save_resume_data(self, flush_disk_cache=False):
        """Signals libtorrent to build resume data for this torrent.

        Args:
            flush_disk_cache (bool): Avoids potential issue with file timestamps
                and is only needed when stopping the session.

        Returns:
            None: The response with resume data is returned in a libtorrent save_resume_data_alert.

        """
        if log.isEnabledFor(logging.DEBUG):
            log.debug('Requesting save_resume_data for torrent: %s', self.torrent_id)
        flags = lt.save_resume_flags_t.flush_disk_cache if flush_disk_cache else 0
        # Don't generate fastresume data if torrent is in a Deluge Error state.
        if self.forced_error:
            component.get('TorrentManager').waiting_on_resume_data[
                self.torrent_id
            ].errback(UserWarning('Skipped creating resume_data while in Error state'))
        else:
            self.handle.save_resume_data(flags)

    def write_torrentfile(self, filedump=None):
        """Writes the torrent file to the state dir and optional 'copy of' dir.

        Args:
            filedump (str, optional): bencoded filedump of a torrent file.

        """

        def write_file(filepath, filedump):
            """Write out the torrent file"""
            log.debug('Writing torrent file to: %s', filepath)
            try:
                with open(filepath, 'wb') as save_file:
                    save_file.write(filedump)
            except IOError as ex:
                log.error('Unable to save torrent file to: %s', ex)

        filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')

        if filedump is None:
            lt_ct = lt.create_torrent(self.torrent_info)
            filedump = lt.bencode(lt_ct.generate())

        write_file(filepath, filedump)

        # If the user has requested a copy of the torrent be saved elsewhere we need to do that.
        if self.config['copy_torrent_file']:
            if not self.filename:
                self.filename = self.get_name() + '.torrent'
            filepath = os.path.join(self.config['torrentfiles_location'], self.filename)
            write_file(filepath, filedump)

    def delete_torrentfile(self, delete_copies=False):
        """Deletes the .torrent file in the state directory in config"""
        torrent_files = [
            os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
        ]
        if delete_copies:
            torrent_files.append(
                os.path.join(self.config['torrentfiles_location'], self.filename)
            )

        for torrent_file in torrent_files:
            log.debug('Deleting torrent file: %s', torrent_file)
            try:
                os.remove(torrent_file)
            except OSError as ex:
                log.warning('Unable to delete the torrent file: %s', ex)

    def force_reannounce(self):
        """Force a tracker reannounce"""
        try:
            self.handle.force_reannounce()
        except RuntimeError as ex:
            log.debug('Unable to force reannounce: %s', ex)
            return False
        return True

    def scrape_tracker(self):
        """Scrape the tracker

         A scrape request queries the tracker for statistics such as total
         number of incomplete peers, complete peers, number of downloads etc.
        """
        try:
            self.handle.scrape_tracker()
        except RuntimeError as ex:
            log.debug('Unable to scrape tracker: %s', ex)
            return False
        return True

    def force_recheck(self):
        """Forces a recheck of the torrent's pieces"""
        if self.forced_error:
            self.forcing_recheck_paused = self.forced_error.was_paused
            self.clear_forced_error_state(update_state=False)
        else:
            self.forcing_recheck_paused = self.status.paused

        try:
            self.handle.force_recheck()
            self.handle.resume()
            self.forcing_recheck = True
        except RuntimeError as ex:
            log.debug('Unable to force recheck: %s', ex)
            self.forcing_recheck = False
        return self.forcing_recheck

    def rename_files(self, filenames):
        """Renames files in the torrent.

        Args:
            filenames (list): A list of (index, filename) pairs.
        """
        for index, filename in filenames:
            # Make sure filename is a sanitized unicode string.
            filename = sanitize_filepath(decode_bytes(filename))
            # lt needs utf8 byte-string. Otherwise if wstrings enabled, unicode string.
            try:
                self.handle.rename_file(index, filename.encode('utf8'))
            except (UnicodeDecodeError, TypeError):
                self.handle.rename_file(index, filename)

    def rename_folder(self, folder, new_folder):
        """Renames a folder within a torrent.

        This basically does a file rename on all of the folders children.

        Args:
            folder (str): The orignal folder name
            new_folder (str): The new folder name

        Returns:
            twisted.internet.defer.Deferred: A deferred which fires when the rename is complete
        """
        log.debug('Attempting to rename folder: %s to %s', folder, new_folder)

        # Empty string means remove the dir and move its content to the parent
        if len(new_folder) > 0:
            new_folder = sanitize_filepath(new_folder, folder=True)

        def on_file_rename_complete(dummy_result, wait_dict, index):
            """File rename complete"""
            wait_dict.pop(index, None)

        wait_on_folder = {}
        self.waiting_on_folder_rename.append(wait_on_folder)
        for _file in self.get_files():
            if _file['path'].startswith(folder):
                # Keep track of filerenames we're waiting on
                wait_on_folder[_file['index']] = Deferred().addBoth(
                    on_file_rename_complete, wait_on_folder, _file['index']
                )
                new_path = _file['path'].replace(folder, new_folder, 1)
                try:
                    self.handle.rename_file(_file['index'], new_path.encode('utf8'))
                except (UnicodeDecodeError, TypeError):
                    self.handle.rename_file(_file['index'], new_path)

        def on_folder_rename_complete(dummy_result, torrent, folder, new_folder):
            """Folder rename complete"""
            component.get('EventManager').emit(
                TorrentFolderRenamedEvent(torrent.torrent_id, folder, new_folder)
            )
            # Empty folders are removed after libtorrent folder renames
            self.remove_empty_folders(folder)
            torrent.waiting_on_folder_rename = [
                _dir for _dir in torrent.waiting_on_folder_rename if _dir
            ]
            component.get('TorrentManager').save_resume_data((self.torrent_id,))

        d = DeferredList(list(wait_on_folder.values()))
        d.addBoth(on_folder_rename_complete, self, folder, new_folder)
        return d

    def remove_empty_folders(self, folder):
        """Recursively removes folders but only if they are empty.

        This cleans up after libtorrent folder renames.

        Args:
            folder (str): The folder to recursively check
        """
        # Removes leading slashes that can cause join to ignore download_location
        download_location = self.options['download_location']
        folder_full_path = os.path.normpath(
            os.path.join(download_location, folder.lstrip('\\/'))
        )

        try:
            if not os.listdir(folder_full_path):
                os.removedirs(folder_full_path)
                log.debug('Removed Empty Folder %s', folder_full_path)
            else:
                for root, dirs, dummy_files in os.walk(folder_full_path, topdown=False):
                    for name in dirs:
                        try:
                            os.removedirs(os.path.join(root, name))
                            log.debug(
                                'Removed Empty Folder %s', os.path.join(root, name)
                            )
                        except OSError as ex:
                            log.debug(ex)

        except OSError as ex:
            log.debug('Cannot Remove Folder: %s', ex)

    def cleanup_prev_status(self):
        """Checks the validity of the keys in the prev_status dict.

        If the key is no longer valid, the dict will be deleted.
        """
        # Dict will be modified so iterate over generated list
        for key in list(self.prev_status):
            if not self.rpcserver.is_session_valid(key):
                del self.prev_status[key]

    def _get_pieces_info(self):
        """Get the pieces for this torrent."""
        if not self.has_metadata or self.status.is_seeding:
            pieces = None
        else:
            pieces = []
            for piece, avail_piece in zip(
                self.status.pieces, self.handle.piece_availability()
            ):
                if piece:
                    pieces.append(3)  # Completed.
                elif avail_piece:
                    pieces.append(
                        1
                    )  # Available, just not downloaded nor being downloaded.
                else:
                    pieces.append(
                        0
                    )  # Missing, no known peer with piece, or not asked for yet.

            for peer_info in self.handle.get_peer_info():
                if peer_info.downloading_piece_index >= 0:
                    pieces[
                        peer_info.downloading_piece_index
                    ] = 2  # Being downloaded from peer.

        return pieces
