#!/usr/bin/env python

"""
deluge-shell: Deluge shell.
"""

# deluge-shell: Deluge shell.
#
# Copyright (C) 2007, 2008 Sadrul Habib Chowdhury <sadrul@users.sourceforge.net>
#
# This application is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This application 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this application; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301
# USA

from deluge.ui.client import aclient as client
import deluge.common as common
import deluge.error
import readline
import logging
import gobject

import sys

class Enumerate(object):
    def __init__(self, names):
        for number, name in enumerate(names.split()):
            setattr(self, name, number)

NORMAL = chr(27) + '[00;00m'
NORMAL_B = chr(27) + '[01m'
RED = chr(27) + '[00;31m'
RED_B = chr(27) + '[01;31m'
GREEN = chr(27) + '[00;32m'
GREEN_B = chr(27) + '[01;32m'
YELLOW = chr(27) + '[00;33m'
YELLOW_B = chr(27) + '[01;33m'
BLUE = chr(27) + '[00;34m'
BLUE_B = chr(27) + '[01;34m'
MAGENTA = chr(27) + '[00;35m'
MAGENTA_B = chr(27) + '[01;35m'
CYAN = chr(27) + '[01;36m'
CYAN_B = chr(27) + '[01;36m'

COLORS = Enumerate('RED GREEN YELLOW BLUE MAGENTA CYAN DEFAULT')
ATTRS = Enumerate('BOLD NORMAL')

def ATTR(color, attr, str):
    ret = chr(27) + '['
    if attr == ATTRS.BOLD:
        ret = ret + "01"
    else:
        ret = ret + "02"

    colormaps = {
            COLORS.RED : "31",
            COLORS.GREEN : "32",
            COLORS.YELLOW : "33",
            COLORS.BLUE : "34",
            COLORS.MAGENTA : "35",
            COLORS.CYAN : "36",
            COLORS.DEFAULT : "39"
        }

    if color in colormaps:
        ret = ret + ";" + colormaps[color]
    ret = ret + "m"
    return ret + str + chr(27) + "[0m"

status_keys = ["state",
        "save_path",
        "tracker",
        "next_announce",
        "name",
        "total_size",
        "progress",
        "num_seeds",
        "total_seeds",
        "num_peers",
        "total_peers",
        "eta",
        "download_payload_rate",
        "upload_payload_rate",
        "ratio",
        "distributed_copies",
        "num_pieces",
        "piece_length",
        "total_done",
        "files",
        "file_priorities",
        "file_progress",
        "peers",
        "is_seed",
        ]

class Command:
    def __init__(self):
        pass

    def execute(self, cmd):
        pass

    def usage(self):
        print ""

    def help(self):
        pass

    def match_torrents(self, array):
        torrents = []
        def _got_session_state(tors):
            if not array or len(array) == 0:
                for tor in tors:
                    torrents.append(tor)
                return
            for match in array:
                for tor in tors:
                    if match == tor[0:len(match)]:
                        torrents.append(tor)
                        break
        client.get_session_state(_got_session_state)
        client.force_call()
        return torrents

class CommandAdd(Command):
    """Command to add a torrent."""
    def execute(self, cmd):
        if len(cmd) < 2:
            self.usage()
            return

        save_path = None
        readpath = False
        if cmd[1] == '-p':
            if len(cmd) < 4:
                self.usage()
                return
            del cmd[1]
            readpath = True
        else:
            def _got_config(configs):
                global save_path
                save_path = configs['download_location']
            client.get_config(_got_config)
            client.force_call()

        command = " ".join(cmd[1:])
        paths = command.split(';')
        if readpath:
            save_path = paths[0].strip()   # Perhaps verify that the path exists?
            client.set_config({'download_location': save_path})
            del paths[0]

        if not save_path:
            print "There's no save-path specified. You must specify a path to save the downloaded files.\n"
            return

        for iter in range(0, len(paths)):
            paths[iter] = paths[iter].strip()
            if len(paths[iter]) == 0:
                del paths[iter]

        try:
            client.add_torrent_file(paths)
        except Exception, msg:
            print "*** Error:", str(msg), "\n"

    def usage(self):
        print "Usage: add [-p <save-location>;] <torrent-file>; [<torrent-file>; ...]"
        print "       (Note that a ';' must follow a path)"
        print ""

    def help(self):
        print "Add a torrent"

class CommandConfig(Command):
    def execute(self, cmd):
        del cmd[0]
        def _on_get_config(config):
            keys = config.keys()
            keys.sort()
            for key in keys:
                if cmd and key not in cmd:    continue
                color = NORMAL
                value = config[key]
                if isinstance(value, bool):
                    color = YELLOW
                elif isinstance(value, int) or isinstance(value, float):
                    color = GREEN
                elif isinstance(value, str):
                    color = CYAN
                elif isinstance(value, list):
                    color = MAGENTA
                    
                print ("* " + BLUE_B + "%s:" + color + " %s" + NORMAL) % (key, value)
        client.get_config(_on_get_config)

    def usage(self):
        print "Usage: configs [key1 [key2 ...]]"
        print ""

    def help(self):
        print "Show configuration values"

class CommandConfigSet(Command):
    def execute(self, cmd):
        try:
            key = cmd[1]
        except IndexError:
            self.usage()
            return

        # Returns (correct_type, type_value)
        def convert_type(target, source):
            if isinstance(source, list):
                return False, None

            if isinstance(source, bool):
                # Because every non-empty string becomes 'True'
                if target in ["True", "true", "1"]:
                    return True, True
                elif target in ["False", "false", "0"]:
                    return True, False
                return False, None

            try:
                val = type(source)(target)
                return True, val
            except:
                pass
            return False, None

        def update_config_value(value, config_val):
            if config_val == None:
                print RED + "* Invalid configuration name '%s'" % key + NORMAL
                return
            success, value = convert_type(value, config_val)
            if not success:
                print RED + "* Configuration value provided has incorrect type." + NORMAL
            else:
                client.set_config({key: value})
                client.force_call()
                print GREEN + "* Configuration value successfully updated." + NORMAL
            return False

        def _got_config_value(config_val):
            global c_val
            c_val = config_val

        value = " ".join(cmd[2:])
        client.get_config_value(_got_config_value, key)
        client.force_call()
        update_config_value(value, c_val)

    def help(self):
        print "Change a configuration setting"

    def usage(self):
        print "Usage: config-set key value"

class CommandExit(Command):
    def execute(self, cmd):
        print "Thanks"
        sys.exit(0)

    def help(self):
        print "Exit from the client."

class CommandHelp(Command):
    def execute(self, cmd):
        if len(cmd) < 2:
            print NORMAL_B + "Available commands:" + NORMAL
            for cmd in sorted(commands.keys()):
                print "\t*", "%s:" % (BLUE_B + cmd + NORMAL),
                command = commands[cmd]
                command.help()
        else:
            for c in cmd[1:]:
                if c not in commands:
                    print RED + "Unknown command:", c + NORMAL
                else:
                    print "*", "%s:" % (BLUE_B + c + NORMAL),
                    command = commands[c]
                    command.help()
                    print GREEN,
                    command.usage()
                    print NORMAL,

    def usage(self):
        print "Usage: help [cmd1 [cmd2 ...]]"
        print ""

    def help(self):
        print "Show help"

class CommandInfo(Command):
    def execute(self, cmd):
        brief = (len(cmd) < 2)
        torrents = self.match_torrents(cmd[1:])
        for tor in torrents:
            self.show_info(tor, brief)

    def usage(self):
        print "Usage: info [<torrent-id> [<torrent-id> ...]]"
        print "       You can give the first few characters of a torrent-id to identify the torrent."
        print ""

    def help(self):
        print "Show information about the torrents"

    def show_info(self, torrent, brief):
        def _got_torrent_status(state):
            print ATTR(COLORS.BLUE, ATTRS.BOLD, "*** ID:"), torrent
            print ATTR(COLORS.BLUE, ATTRS.BOLD, "*** Name:"), state['name']
            print ATTR(COLORS.BLUE, ATTRS.BOLD, "*** Path:"), state['save_path']

            if not state['is_seed']:
                print ATTR(COLORS.GREEN, 0, "*** Completed:"), common.fsize(state['total_done']) + "/" + common.fsize(state['total_size'])
            print ATTR(COLORS.GREEN, 0, "*** Status:"), state['state']

            state['state_i'] = common.TORRENT_STATE.index(state['state'])
            if state['state_i'] == 2: # Downloading
                print ATTR(COLORS.GREEN, 0, "*** Download Speed:"), common.fspeed(state['download_payload_rate'])
            if state['state_i'] in [2, 3]: # Downloading, or Seeding
                print ATTR(COLORS.GREEN, 0, "*** Upload Speed:"), common.fspeed(state['upload_payload_rate'])
            if state['state_i'] == 2: # Downloading
                print ATTR(COLORS.GREEN, 0, "*** ETA:"), "%s" % common.ftime(state['eta'])

            if brief == False:
                print ATTR(COLORS.DEFAULT, ATTRS.BOLD, "*** Seeders:"), "%s (%s)" % (state['num_seeds'], state['total_seeds'])
                print ATTR(COLORS.DEFAULT, ATTRS.BOLD, "*** Peers:"), "%s (%s)" % (state['num_peers'], state['total_peers'])
                print ATTR(COLORS.DEFAULT, ATTRS.BOLD, "*** Share Ratio:"), "%.1f" % state['ratio']
                print ATTR(COLORS.DEFAULT, ATTRS.BOLD, "*** Availability:"), "%.1f" % state['distributed_copies']
                print ATTR(COLORS.CYAN, ATTRS.BOLD, "*** Files:")
                for i, file in enumerate(state['files']):
                    status = ""
                    if not state['is_seed']:
                        if state['file_priorities'][i] == 0:
                            status = " - Do not download"
                        else:
                            status = " - %1.f%% completed" % (state['file_progress'][i] * 100)
                    print "\t* %s (%s)%s" % (file['path'], common.fsize(file['size']), status)

                print ATTR(COLORS.MAGENTA, ATTRS.BOLD, "*** Peers:")
                if len(state['peers']) == 0:
                    print "\t* None"
                for peer in state['peers']:
                    print ("\t*" + BLUE_B + " %-21s" + GREEN_B + " %-25s " + CYAN + "Up: %-12s" + MAGENTA_B + " Down: %-12s" + NORMAL) % \
                        (peer['ip'], peer['client'] + ["", " (seed)"][not not peer['seed']],
                            common.fspeed(peer['up_speed']), common.fspeed(peer['down_speed']))
            print ""
        client.get_torrent_status(_got_torrent_status, torrent, status_keys)

class CommandPause(Command):
    def execute(self, cmd):
        if len(cmd) < 2:
            self.usage()
            return
        try:
            torrents = self.match_torrents(cmd[1:])
            client.pause_torrent(torrents)
        except Exception, msg:
            print "Error:", str(msg), "\n"

    def usage(self):
        print "Usage: pause <torrent-id> [<torrent-id> ...]"
        print ""

    def help(self):
        print "Pause a torrent"

class CommandResume(Command):
    def execute(self, cmd):
        if len(cmd) < 2:
            self.usage()
            return
        try:
            torrents = self.match_torrents(cmd[1:])
            client.resume_torrent(torrents)
        except Exception, msg:
            print "Error:", str(msg), "\n"

    def usage(self):
        print "Usage: resume <torrent-id> [<torrent-id> ...]"
        print ""

    def help(self):
        print "Resume a torrent"

class CommandRemove(Command):
    def execute(self, cmd):
        if len(cmd) < 2:
            self.usage()
            return
        try:
            torrents = self.match_torrents(cmd[1:])
            client.remove_torrent(torrents, False, False)
        except Exception, msg:
            print "*** Error:", str(msg), "\n"

    def usage(self):
        print "Usage: rm <torrent-id>"
        print ""

    def help(self):
        print "Remove a torrent"

class CommandHalt(Command):
    def execute(self, cmd):
        client.shutdown()

    def help(self):
        print "Shutdown the deluge server."

class CommandConnect(Command):
    def execute(self, cmd):
        host = 'localhost'
        port = 58846
        if len(cmd) > 1:
            host = cmd[1]
        if len(cmd) > 2:
            port = int(cmd[2])

        if host[:7] != "http://":
            host = "http://" + host

        client.set_core_uri("%s:%d" % (host, port))

    def usage(self):
        print "Usage: connect [<host> [<port>]]"
        print "       'localhost' is the default server. 58846 is the default port."
        print ""

    def help(self):
        print "Connect to a new deluge server."

commands = {
    'add' : CommandAdd(),
    'configs' : CommandConfig(),
    'config-set' : CommandConfigSet(),
    'exit' : CommandExit(),
    'help' : CommandHelp(),
    'info' : CommandInfo(),
    'pause' : CommandPause(),
    'quit' : CommandExit(),
    'resume' : CommandResume(),
    'rm' : CommandRemove(),
    'del' : CommandRemove(),
    'halt' : CommandHalt(),
    'connect' : CommandConnect(),
}

logging.disable(logging.ERROR)
client.set_core_uri("http://localhost:58846")

class NullUI:
    def __init__(self, args):
        print "Welcome to deluge-shell. Type 'help' to see a list of available commands."

        readline.read_init_file()

        while True:
            try:
                inp = raw_input("> ").strip()
            except:
                inp = 'quit'

            if len(inp) == 0:    continue
            inp = inp.split(" ")

            print ""
            cmd = inp[0]
            found = False
            if cmd not in commands:
                print RED + "Invalid command!" + NORMAL
                commands['help'].execute([])
            else:
                command = commands[cmd]
                try:
                    command.execute(inp)
                    client.force_call()
                except deluge.error.NoCoreError, e:
                    print "*** Operation failed. You are not connected to a deluge daemon."
                    print "    Perhaps you want to 'connect' first."
                    print ""
                #except deluge.error.InvalidTorrent, e:
                #    print "*** Operation failed. You tried to perform an operation on a non-existent torrent."
                #    print "    Use the 'info' command for the list of torrents."
                #    print ""
            print

        print "Thanks."

