# control an rTorrent client via XMLRPC,
# and collect rtorrent files from IRC for later download
#
# (c) 2007-2008 by Ralf Ertzinger <ralf@camperquake.de>
# licensed under GNU GPL v2

use strict;
use Irssi 20020324 qw (command_bind command_runsub signal_add_first signal_add_last);
use vars qw($VERSION %IRSSI);
use XML::Simple;
use Data::Dumper;
use File::Spec;
use List::Util qw(max);
use xmlrtorrent;

my @talkers;
my $talker;
my $conf;
my $conffile = File::Spec->catfile(Irssi::get_irssi_dir(), 'xmlrtorrent.xml');
my $scriptdir = File::Spec->catfile(Irssi::get_irssi_dir(), 'scripts');
my $plugindir = File::Spec->catfile($scriptdir, 'xmlrtorrent');
my %torrentlist = ();
my $torrentindex = 1;
my $rtorrent;

my @outputstack = (undef);

my $PARAMS = {
    'XMLURL' => 'http://localhost/RPC2',
    'USERNAME' => '',
    'PASSWORD' => '',
    '_QUEUE' => {},
};

# activate debug here
my $debug = 1;

# "message public", SERVER_REC, char *msg, char *nick, char *address, char *target
signal_add_last("message public" => sub {check_for_link(\@_,1,4,2,0);});
# "message own_public", SERVER_REC, char *msg, char *target
signal_add_last("message own_public" => sub {check_for_link(\@_,1,2,-1,0);});

# "message private", SERVER_REC, char *msg, char *nick, char *address
signal_add_last("message private" => sub {check_for_link(\@_,1,-1,2,0);});
# "message own_private", SERVER_REC, char *msg, char *target, char *orig_target
signal_add_last("message own_private" => sub {check_for_link(\@_,1,2,-1,0);});

# "message irc action", SERVER_REC, char *msg, char *nick, char *address, char *target
signal_add_last("message irc action" => sub {check_for_link(\@_,1,4,2,0);});
# "message irc own_action", SERVER_REC, char *msg, char *target
signal_add_last("message irc own_action" => sub {check_for_link(\@_,1,2,-1,0);});

# For tab completion
signal_add_first('complete word', \&sig_complete);

my $xmlrtorrent_commands = {
    'save' => sub {
        cmd_save();
    },

    'set' => sub {
        cmd_set(@_);
    },
    
    'show' => sub {
        cmd_show(@_);
    },

    'help' => sub {
        cmd_help(@_);
    },

    'queue' => sub {
        cmd_queue(@_);
    },

    'remote' => sub {
        cmd_remote(@_);
    },

    'talker' => sub {
	cmd_talker(@_);
    },

    'debug' => sub {
        $debug = 1;
        write_irssi('Enabled debugging');
    },

    'nodebug' => sub {
        $debug = 0;
        write_irssi('Disabled debugging');
    },
};

sub write_irssi {
    my @text = @_;
    my $output = $outputstack[0];

    $text[0] = '%%mxmlrtorrent: %%n' . $text[0];

    if (defined($output) and ref($output)) {
        $output->print(sprintf(shift(@text), @text), MSGLEVEL_CLIENTCRAP);
    } else {
        Irssi::print(sprintf(shift(@text), @text));
    }

}

sub push_output {
    unshift(@outputstack, shift);
}

sub pop_output {
    shift(@outputstack);
}

sub write_debug {
    if ($debug) {
        write_irssi(@_);
    }
}

# This is shamelessly stolen from pythons urlgrabber
sub format_number {
    my $number = shift;
    my $SI = shift || 0;
    my @symbols = ('', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y');
    my $step = $SI?1000:1024;
    my $thresh = 999;
    my $depth = 0;
    my $max_depth = $#symbols;
    my $format;

    while (($number > $thresh) and ($depth < $max_depth)) {
        $depth += 1;
        $number /= $step;
    }

    if ($number =~ /^[+-]?\d+$/) {
        # Integer.
        $format = '%i%s';
    } elsif ($number < 9.95) {
        $format = '%.1f%s';
    } else {
        $format = '%.0f%s';
    }
    return sprintf($format, $number, $symbols[$depth]);
}



sub check_for_link {
    my ($signal,$parammessage,$paramchannel,$paramnick,$paramserver) = @_;
    my $server = $signal->[$paramserver];
    my $target = $signal->[$paramchannel];
    my $message = ($parammessage == -1) ? '' : $signal->[$parammessage];
    my $nick = ($paramnick == -1)?defined($server)?$server->{'nick'}:'':$signal->[$paramnick];
    my $g;
    my $m;
    my $p;

    my $witem;
    if (defined $server) {
        $witem = $server->window_item_find($target);
    } else {
        $witem = Irssi::window_item_find($target);
    }

    # Look if we should ignore this line
    if ($message =~ m,(?:\s|^)/nosave(?:\s|$),) {
        return;
    }

    push_output($witem);

    # Look if there is a torrent link in there
    $message =~ m,(http://\S*\.(?:torrent|penis)),;
    $m = $1;
    while (defined($m)) {
        write_debug('Torrent-URL: %s', $m);
        $torrentlist{$torrentindex++} = {'CHANNEL' => $target, 'NICK' => $nick, 'URL' => $m};

        # Remove the matched part from the message and try again (there may be
        # more!)
        $message =~ s/$m//;

        $message =~ m|(http://.*\.torrent)|;
        $m = $1;
    }

    pop_output();
}

# Handle the queue of unhandled torrents
sub cmd_queue {
    my ($subcmd, $id, @params) = @_;

    if ('remove' eq $subcmd) {
        if (defined($id)) {
            delete($torrentlist{$id});
        }
    } elsif ('clear' eq $subcmd) {
        %torrentlist = ();
    } elsif ('confirm' eq $subcmd) {
        my $u;
        return unless(defined($id) and exists($torrentlist{$id}));

        $u = $torrentlist{$id}->{'URL'};

        write_debug('Sending %s to rtorrent', $u);
        unless(defined($rtorrent->load_start($talker, $u))) {
            write_irssi('%%RError sending URL %s: %s', $u, $rtorrent->errstr());
        } else {
            write_irssi('%s enqueued', $u);
            delete($torrentlist{$id});
        }
    } elsif ('add' eq $subcmd) {
        unless(defined($id)) {
            return;
        }
        $torrentlist{$torrentindex++} = {'CHANNEL' => '', 'NICK' => '', 'URL' => $id};
    } elsif (('list' eq $subcmd) or !defined($subcmd))  {
        my $l;
        write_irssi('List of queued torrents');
        if (0 == scalar(keys(%torrentlist))) {
            write_irssi('  (no torrents in local queue)');
        } else {
            foreach (sort(keys(%torrentlist))) {
                write_irssi('  %3d: %s@%s: %s', $_,
                        $torrentlist{$_}->{'NICK'},
                        $torrentlist{$_}->{'CHANNEL'},
                        $torrentlist{$_}->{'URL'});
            }
        }
    } else {
        write_irssi('Unknown subcommand: %s', $subcmd);
    }
}

# Handle the remote rtorrent queue
sub cmd_remote {
    my ($subcmd, $id, @params) = @_;
    my $rqueue;

    if (('list' eq $subcmd) or !defined($subcmd)) {
        unless(defined($rqueue = $rtorrent->download_list($talker))) {
            write_irssi('Error getting list of downloads: %s', $rtorrent->errstr());
            return;
        }

        write_irssi('List of remote torrents');
        if (0 == scalar(@{$rqueue})) {
            write_irssi('  (no torrents in remote queue)');
        } else {
            foreach (@{$rqueue}) {
                write_irssi('  %s%s: %sB/%sB done (%d%%), %sB/s up, %sB/s down',
                        $_->[5]->value()?'*':' ',
                        $_->[0]->value(),
                        format_number($_->[2]->value()),
                        format_number($_->[1]->value()),
                        ($_->[2]->value()*100)/$_->[1]->value(),
                        format_number($_->[3]->value()),
                        format_number($_->[4]->value()));
            }
        }
    }
}


sub cmd_save {
    
    my %mappedqueue;

    # XML::Simple has some problems with numbers as nodenames,
    # so we have to modify our queue a bit.
    %mappedqueue = map {("_$_" => $torrentlist{$_})} keys(%torrentlist);

    eval {
        open(CONF, '>'.$conffile) or die 'Could not open config file';
        $conf->{'xmlrtorrent'}->{'_QUEUE'} = \%mappedqueue;
        print CONF XML::Simple::XMLout($conf, KeepRoot => 1, KeyAttr => {'config' => 'module', 'option' => 'key'});
        close(CONF);
    };
    if ($@) {
        write_irssi('Could not save config to %s: %s', ($conffile, $@));
    } else {
        write_irssi('configuration saved to %s', $conffile);
    }
}

sub cmd_set {
    my $target = shift;
    my $key = shift;
    my $val = shift;
    my $p;

    foreach $p (@talkers) {
        if ($p->{'NAME'} eq $target) {
            $p->setval($key, $val);
            return;
        }
    }
    write_irssi(undef, 'No such module');
}

sub cmd_show {
    my $target = shift;
    my $p;
    my $e;

    if (defined($target)) {
        foreach $p (@talkers) {
            if ($p->{'NAME'} eq $target) {
                write_irssi($p->getconfstr());
                return;
            }
        }
        write_irssi('No such module');
    } else {
        write_irssi('Loaded talkers:');
        foreach $p (@talkers) {
            write_irssi(' %s', $p->{'NAME'});
        };
    }
}

sub cmd_help {
    my $target = shift;
    my $p;

    if (defined($target)) {
        foreach $p (@talkers) {
            if ($p->{'NAME'} eq $target) {
                write_irssi($p->gethelpstr());
                return;
            }
        }
        write_irssi('No such module');
    } else {
	write_irssi(<<'EOT');
Supported commands:
 save: save the current configuration
 help [modulename]: display this help or module specific help
 show [modulename]: show loaded modules or the current parameters of a module
 talker [modulename]: display or set the talker to use
 debug: enable debugging messages
 nodebug: disable debugging messages
EOT
;
    }
}

sub cmd_talker {
    my $target = shift;
    my $p;

    if (defined($target)) {
        foreach $p (@talkers) {
            if (($p->{'NAME'} eq $target) && ($p->{'TYPE'} eq 'talker')) {
                $talker = $p;
                $conf->{'videosite'}->{'talker'} = $target;
                return;
            }
        }
        write_irssi('No such talker');
    } else {
        write_irssi('Current talker: %s', $conf->{'videosite'}->{'talker'});
    }
}



# save on unload
sub sig_command_script_unload {
    my $script = shift;
    if ($script =~ /(.*\/)?xmlrtorrent(\.pl)?$/) {
        cmd_save();
    }
}

sub ploader {

    my $dir = shift;
    my $pattern = shift;
    my $type = shift;
    my @list;
    my $p;
    my $g;
    my @g = ();

    opendir(D, $dir) || return ();
    @list = grep {/$pattern/ && -f File::Spec->catfile($dir, $_) } readdir(D);
    closedir(D);

    foreach $p (@list) {
        write_debug('Trying to load %s:', $p);
        $p =~ s/\.pm$//;
        eval qq{ require xmlrtorrent::$p; };
        if ($@) {
            write_irssi('Failed to load plugin: %s', "$@");
            next;
        }

        $g = eval qq{ xmlrtorrent::$p->new(); };
        if ($@) {
            write_irssi('Failed to instanciate: %s', "$@");
            delete($INC{$p});
            next;
        }

        write_debug('found %s %s', $g->{'TYPE'}, $g->{'NAME'});
        if ($type eq $g->{'TYPE'}) {
            push(@g, $g);
            $g->setio(sub {Irssi::print(shift)});
        } else {
            write_irssi('%s has wrong type (got %s, expected %s)', $p, $g->{'TYPE'}, $type);
            delete($INC{$p});
        }
    }

    write_debug('Loaded %d plugins', $#g+1);
    
    return @g;
}

sub _load_modules($) {

    my $path = shift;

    foreach (keys(%INC)) {
        if ($INC{$_} =~ m|^$path|) {
            write_debug('Removing %s from $INC', $_);
            delete($INC{$_});
        }
    }
    @talkers = ploader($path, '.*Talker\.pm$', 'talker');
}

sub init_xmlrtorrent {

    my $bindings = shift;
    my $p;

    unless(-r $conffile && defined($conf = XML::Simple::XMLin($conffile, ForceArray => ['config', 'option'], KeepRoot => 1, KeyAttr => {'config' => 'module', 'option' => 'key'}))) {
        # No config, start with an empty one
        write_debug('No config found, using defaults');
        $conf = { 'xmlrtorrent' => { }};
    }
    foreach (keys(%{$PARAMS})) {
        unless (exists($conf->{'xmlrtorrent'}->{$_})) {
            $conf->{'xmlrtorrent'}->{$_} = $PARAMS->{$_};
        }
    }

    _load_modules($plugindir);

    unless (defined(@talkers)) {
        write_irssi('No talkers found, can not proceed.');
        return;
    }

    $talker = $talkers[0];
    foreach $p (@talkers) {
        if ($conf->{'xmlrtorrent'}->{'talker'} eq $p->{'NAME'}) {
            $talker = $p;
        }
    }
    write_debug(undef, 'Selected %s as talker', $talker->{'NAME'});
    $conf->{'videosite'}->{'talker'} = $talker->{'NAME'};


    # Restore the queue
    %torrentlist = %{$conf->{'xmlrtorrent'}->{'_QUEUE'}};
    %torrentlist = map { my $a = substr($_, 1); ("$a" => $torrentlist{$_}) } keys(%torrentlist);
    $torrentindex = max(keys(%torrentlist)) + 1;

    unless(defined($rtorrent = xmlrtorrent->new())) {
        write_irssi('Could not initialize XMLRPC instance');
        return;
    }

    if ($bindings) {

        Irssi::signal_add_first('command script load', 'sig_command_script_unload');
        Irssi::signal_add_first('command script unload', 'sig_command_script_unload');
        Irssi::signal_add('setup saved', 'cmd_save');


        Irssi::command_bind('torrent' => \&cmdhandler);
    }

    write_irssi('xmlrtorrent initialized');
}

sub sig_complete {
    my ($complist, $window, $word, $linestart, $want_space) = @_;
    my @matches;

    if ($linestart !~ m|^/torrent\b|) {
        return;
    }

    ${$want_space} = 0;

    Irssi::signal_stop();
}

sub cmdhandler {
    my ($data, $server, $witem) = @_;
    my ($cmd, @params) = split(/\s+/, $data);

    push_output($witem);

    if (exists($xmlrtorrent_commands->{$cmd})) {
        $xmlrtorrent_commands->{$cmd}->(@params);
    } else {
        write_irssi('Unknown command: %s', $cmd);
    }

    pop_output();
}

unshift(@INC, $scriptdir);
init_xmlrtorrent(1);
