#! /usr/bin/perl -w

# XDCC-Client v0.8 (c) by Simon Fuhrmann <NightSlayer@gmx.de>.
#
# To load this pluging, you need X-Chat 2.0.8 or newer.
# Version 2.4.4 has a bug with perl scripts.
# Just type "/load <path_to_script>" to use it.
# To unload this script, type "/unloadall" or "/unload".
#
# Note that you need to execute commands in the right context to
# avoid getting unusual results. This is for example, if you connect
# multiple servers and request a package on the wrong server.
#
# WINDOWS USERS: Change the value for $default_file below.
#
# 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; version 2 dated June, 1991.
#
# 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.

use strict;
package XDCCC;

# This is the basic script configuration
my $command = "xc";             # X-Chat command
my $version = "0.8";            # XDCCC version
my $timeout = 60;               # Store packs this minutes long
my $check_interval = 5;         # Timeout check interval in minutes
my $default_file = "/tmp/xdcc-cache"; # Default filename save/load file

# Globals
my @packs;                      # Storage for all cached packs
my @search;                     # Search to pack association
my $last_check = time();        # Last timeout check
my @botlist;			# List for XDCC bots

Xchat::register("XDCC-Client", $version, "XDCC-Client makes XDCC much easier",
    \&XDCCC::unload_callback);

Xchat::print("Loading XDCC-Client $version by Simon Fuhrmann...");

# Installing server and command hooks
Xchat::hook_server("PRIVMSG", \&XDCCC::server_callback);
Xchat::hook_server("NOTICE", \&XDCCC::server_callback);
Xchat::hook_command($command, \&XDCCC::command_callback);

Xchat::print("XDCC-Client loaded. Try /$command to get an overview.");

# --------------------------------------------------------------------

sub unload_callback
{
  XDCCC::save_cache_to_file();
  Xchat::print("Unloading XDCC-Client. Bye.");
}

# --------------------------------------------------------------------

sub server_callback
{
  my $text = $_[1][3];                # Get server message

  # Remove mIRC color, bold and underline control chars.
  $text =~ s/(\cc\d{0,2}(\,\d{0,2})?|[\cb\co\cv\c_])//g;

  # Regex:            PACK       B  TIMES   B  SIZE     B  REST
  if (not $text =~ /^:[#.]\d{1,3}\s+\d{1,5}x\s+\[\s*.+\]\s+.*$/)
    {
      # The line seems not to contain any xdcc information
      return Xchat::EAT_NONE;
    }

  # This is the right line! Fetch some imformation.
  (my $nick = $_[0][0]) =~ s/^:(.*?)!.*/$1/;
  my $channel = $_[0][2];
  (my $packnum = $text) =~ s/^:[#.](\d{1,3})\s+.*/$1/;
  (my $times_get = $text) =~ s/^:[#.]\d{1,3}\s+(\d{1,5})x\s+.*/$1/;
  (my $size = $text) =~ s/^:[#.]\d{1,3}\s+\d{1,5}x\s+\[\s*(.+)\]\s+.*/$1/;
  (my $desc = $text) =~ s/^:[#.]\d{1,3}\s+\d{1,5}x\s+\[\s*.+\]\s+(.*)$/$1/;

  # If you're getting a listing in your query or via notice, fix channel
  my $ch_type = substr($channel, 0, 1);
  if (not $ch_type eq "#" and not $ch_type eq "+" and not $ch_type eq "&")
    {
      $channel = "[none]";
    }
  # If the channel name is too long (longer than 11 characters), remove some
  elsif (length($channel) > 11)
    {
      $channel = substr($channel, 0, 10) . "~";
    }

  # Xchat::print("$command: Match: N: $nick C: $channel " .
  #     "#: $packnum T: $times_get S: $size");
  # Xchat::print("Describtion: $desc");

  XDCCC::append_row_to_cache
      ([$packnum, $nick, $channel, $times_get, $size, $desc]);

  return Xchat::EAT_NONE;
}

# --------------------------------------------------------------------

sub command_callback
{
  # Will handle the empty call.
  if (not defined $_[0][1])
    {
      XDCCC::print_overview_text();
    }
  elsif ($_[0][1] eq "help")
    {
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
        return Xchat::EAT_ALL;
      }
      if (not defined $_[0][2]) {
        XDCCC::print_overview_text();
        return Xchat::EAT_ALL;
      }
      XDCCC::print_help_text($_[0][2]);
    }
  # Handles "get"
  elsif ($_[0][1] eq "get")
    {
      if (not defined $_[0][2]) {
        Xchat::print("$command: Missing parameter.");
        return Xchat::EAT_ALL;
      }
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
        return Xchat::EAT_ALL;
      }
      # Is second parameter numeric?
      if ($_[0][2] =~ /^\d+$/) {
        XDCCC::request_package($_[0][2]);
      } else {
        XDCCC::request_package_by_string($_[0][2]);
      }
      #Xchat::print("$command: Malformed command. Try /$command.");
      #return Xchat::EAT_ALL;
    }
  # Handles "list"
  elsif ($_[0][1] eq "list")
    {
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
        return Xchat::EAT_ALL;
      }
      XDCCC::check_package_timeout();
      if (defined $_[0][2]) {
        XDCCC::search_for_packs($_[1][2]);
      } else {
        XDCCC::search_all_packs();
      }
      XDCCC::update_atime_for_search();
      XDCCC::print_search_table(0);
    }
  elsif ($_[0][1] eq "ls")
    {
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
        return Xchat::EAT_ALL;
      }
      XDCCC::check_package_timeout();
      if (defined $_[0][2]) {
        XDCCC::search_for_packs($_[1][2]);
      } else {
        XDCCC::search_all_packs();
      }
      XDCCC::update_atime_for_search();
      XDCCC::print_search_table(1);
    }
  # Handles "show"
  elsif ($_[0][1] eq "show")
    {
      if (defined $_[0][2]) {
        Xchat::print("$command: To much parameters.");
        return Xchat::EAT_ALL;
      }
      if (@search == 0) {
        Xchat::print("$command: Sorry, last listing is empty.");
        return Xchat::EAT_ALL;
      }
      XDCCC::update_atime_for_search();
      XDCCC::print_search_table();
    }
  # Handles "grep", grep allows many parameters
  elsif ($_[0][1] eq "grep")
    {
      if (not defined $_[0][2]) {
        Xchat::print("$command: To few parameters.");
        return Xchat::EAT_ALL;
      }
      XDCCC::check_package_timeout();
      XDCCC::grep_for_packs($_[1][2]);
      XDCCC::update_atime_for_search();
      XDCCC::print_search_table();
    }
  elsif ($_[0][1] eq "new")
    {
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
        return Xchat::EAT_ALL;
      }
      if (defined $_[0][2] and not $_[0][2] eq "keep") {
        Xchat::print("$command: Malformed command. Try /$command.");
        return Xchat::EAT_ALL;
      }
      XDCCC::check_package_timeout();
      XDCCC::search_new_packs();
      if (not defined $_[0][2]) {
        XDCCC::update_atime_for_search();
      }
      XDCCC::print_search_table();
    }
  # Handles "clear"
  elsif ($_[0][1] eq "clear")
    {
      if (defined $_[0][2]) {
        Xchat::print("$command: To much parameters.");
        return Xchat::EAT_ALL;
      }
      if (@packs == 0) {
        Xchat::print("$command: Cache is already empty.");
        return Xchat::EAT_ALL;
      }
      @packs = ();
      @search = ();
      Xchat::print("$command: Cache has been cleared.");
    }
  elsif ($_[0][1] eq "sort")
    {
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
	return Xchat::EAT_ALL;
      }
      if (not defined $_[0][2]) {
        $_[0][2] = "name";
      }
      my $order = substr($_[0][2], 0, 1);
      my $field;
      if (not $order eq "+" and not $order eq "-") {
        $order = "+";
        $field = $_[0][2];
      } else {
        $field = substr($_[0][2], 1, length($_[0][2]) - 1);
      }
      # Start sorting sub.
      XDCCC::sort_package_list($field, $order);
    }
  elsif ($_[0][1] eq "botlist")
    {
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
	return Xchat::EAT_ALL;
      }
      XDCCC::create_botlist($_[0][2]);
      XDCCC::print_botlist();
    }
  elsif ($_[0][1] eq "getlist")
    {
      if (not defined $_[0][2]) {
        Xchat::print("$command: To few parameters.");
	return Xchat::EAT_ALL;
      }
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
	return Xchat::EAT_ALL;
      }
      XDCCC::request_package_lists($_[0][2]);
    }
  elsif ($_[0][1] eq "save")
    {
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
	return Xchat::EAT_ALL;
      }
      XDCCC::save_cache_to_file($_[0][2]);
    }
  elsif ($_[0][1] eq "load")
    {
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
	return Xchat::EAT_ALL;
      }
      XDCCC::load_file_to_cache($_[0][2]);
    }
  elsif ($_[0][1] eq "dump")
    {
      if (defined $_[0][3]) {
        Xchat::print("$command: To much parameters.");
	return Xchat::EAT_ALL;
      }
      XDCCC::dump_packages($_[0][2]);
    }
  # Handles everything else
  else
    {
      Xchat::print("$command: Unknown command: $_[0][1]. Try /$command.");
    }

  return Xchat::EAT_ALL;
}

# --------------------------------------------------------------------
# arg0: index of @search array

sub request_package
{
  # Was packnumber in the last search?
  if (not defined $search[$_[0]]) {
    Xchat::print("$command: Invalid pack number. Display a new list.");
    return;
  }
  # Get index of packages array
  my $i = $search[$_[0]];
  Xchat::command("ctcp $packs[$i][1] xdcc send $packs[$i][0]");
}

# --------------------------------------------------------------------
# arg0: string to search for in package names

sub request_package_by_string
{
  if (not defined $_[0]) {
    Xchat::print("$command: Critical program error. No argument passed.");
    return;
  }

  # Backup the old search list if search fails.
  my @search_backup = @search;

  XDCCC::search_for_packs($_[0]);
  if (@search == 0) {
    @search = @search_backup;
    Xchat::print("$command: Sorry, no packs found. Old list restored.");
  } elsif (@search == 1) {
    XDCCC::request_package(0);
  } else {
    XDCCC::print_search_table(1);
  }
}

# --------------------------------------------------------------------
# arg0: filename

sub save_cache_to_file
{
  if (not defined $_[0]) {
    $_[0] = $default_file;
  }
  if (not defined open(CACHE_FILE, "> $_[0]")) {
    Xchat::print("$command: Cannot open $_[0]: $!");
    Xchat::print("You may want to change the default filename.");
    return;
  }

  for (my $i = 0; $i < @packs; $i++) {
    my @row = @{$packs[$i]};
    print CACHE_FILE ($row[0] . " &# " . $row[1] . " &# " . $row[2] .
        " &# " . $row[3] . " &# " . $row[4] . " &# " . $row[5] ."\n");
  }

  close(CACHE_FILE);
  Xchat::print("$command: Cache has beed saved to \"$_[0]\".");
}

# --------------------------------------------------------------------
# arg0: filename

sub load_file_to_cache
{
  if (not defined $_[0]) {
    $_[0] = "$default_file";
  }
  if (not defined open(CACHE_FILE, "< $_[0]")) {
    Xchat::print("$command: Cannot open $_[0]: $!");
    return;
  }

  while (my $line = <CACHE_FILE>)
    {
      chomp($line);
      my @row = split(/ &# /, $line);
      XDCCC::append_row_to_cache(\@row);
    }
  close(CACHE_FILE);
  Xchat::print("$command: Cache has beed extended from \"$_[0]\".");
}

# --------------------------------------------------------------------
# arg0: Digit from 0 - 5
# arg1: Order: "+" or "-"

sub sort_package_list
{
  if ($_[0] eq "name") {
    if ($_[1] eq "-") {
      @packs = sort {uc($b->[5]) cmp uc($a->[5])} @packs;
    } else {
      @packs = sort {uc($a->[5]) cmp uc($b->[5])} @packs;
    }
  } elsif ($_[0] eq "nick") {
    if ($_[1] eq "-") {
      @packs = sort {uc($b->[1]) cmp uc($a->[1])} @packs;
    } else {
      @packs = sort {uc($a->[1]) cmp uc($b->[1])} @packs;
    }
  } elsif ($_[0] eq "chan") {
    if ($_[1] eq "-") {
      @packs = sort {uc($b->[2]) cmp uc($a->[2])} @packs;
    } else {
      @packs = sort {uc($a->[2]) cmp uc($b->[2])} @packs;
    }
  } elsif ($_[0] eq "pack") {
    if ($_[1] eq "-") {
      @packs = sort {$b->[0] <=> $a->[0]} @packs;
    } else {
      @packs = sort {$a->[0] <=> $b->[0]} @packs;
    }
  } elsif ($_[0] eq "sends") {
    if ($_[1] eq "-") {
      @packs = sort {$b->[3] <=> $a->[3]} @packs;
    } else {
      @packs = sort {$a->[3] <=> $b->[3]} @packs;
    }
  } elsif ($_[0] eq "size") {
    if ($_[1] eq "-") {
      @packs = sort {$b->[4] <=> $a->[4]} @packs;
    } else {
      @packs = sort {$a->[4] <=> $b->[4]} @packs;
    }
  } else {
    Xchat::print("$command: Invalid field. Try one of: name, nick, " .
        "size, chan, pack, sends.");
    return;
  }
  # Rest search result because @packs has changed.
  @search = ();
  Xchat::print("$command: Cache has been sorted. Display a new list.");
}

# --------------------------------------------------------------------

sub update_atime_for_search
{
  for (my $i = 0; $i < @search; $i++) {
    $packs[$search[$i]][6] = time();
  }
}

# --------------------------------------------------------------------
# arg0: cache fields: $packnum, $nick, $channel, $times_get, $size, $desc

sub append_row_to_cache
{
  if (@{$_[0]} != 6) {
    Xchat::print("$command: Critical program error. Row has @{$_[0]} " .
        "instead of 6 entries");
    return;
  }
  # Scan the cache for a duplicate entry. This is SLOW!
  for (my $i = 0; $i < @packs; $i++)
    {
      if ($packs[$i][0] == $_[0][0] and $packs[$i][1] eq $_[0][1])
        {
          # Found a match. Overwrite entry. Use same atime and new mtime.
          push(@{$_[0]}, ($packs[$i][6], time()));
          $packs[$i] = $_[0];
          return;
        }
    }
  # Pack was not fond in cache. Insert a new line. No atime, new mtime.
  push(@{$_[0]}, (0, time()));
  push(@packs, $_[0]);
}

# --------------------------------------------------------------------

sub check_package_timeout
{
  if (time() - $last_check < $check_interval * 60) {
    return;
  }

  my $removed = 0;
  for (my $i = 0; $i < @packs; $i++) {
    my @row = @{$packs[$i]};
    if (time() - $row[7] > $timeout * 60) {
      # Package is old. Remove from list.
      splice(@packs, $i, 1);
      $removed += 1;
      $i -= 1;
    }
  }

  $last_check = time();
  if ($removed > 0) {
    Xchat::print("$command: $removed packs removed due to timeout.");
  }
}

# --------------------------------------------------------------------
# No arguments

sub search_all_packs
{
  @search = ();
  for (my $i = 0; $i < @packs; $i++)
    {
      push(@search, $i);
    }
}

# --------------------------------------------------------------------
# arg0: Search string

sub search_for_packs
{
  if (not defined $_[0]) {
    Xchat::print("$command: Critical program error. No argument passed.");
    return;
  }
  # $_[0] = quotemeta($_[0]);
  $_[0] =~ s/\\/\\\\/g;
  @search = ();
  for (my $i = 0; $i < @packs; $i++) {
    if ($packs[$i][5] =~ /$_[0]/i) {
      push(@search, $i);
    }
  }
}

# --------------------------------------------------------------------
# No arguments.

sub search_new_packs
{
  @search = ();
  for (my $i = 0; $i < @packs; $i++) {
    # if ($packs[$i][6] < $packs[$i][7]) { # Selects new and updated packs
    if ($packs[$i][6] == 0) {              # Selects only new packs
      push(@search, $i);
    }
  }
}

# --------------------------------------------------------------------
# arg0: Search string

sub grep_for_packs
{
  if (not defined $_[0]) {
    Xchat::print("$command: Critical program error. No argument passed.");
    return;
  }
  # $_[0] = quotemeta($_[0]);
  $_[0] =~ s/\\/\\\\/g;
  @search = ();
  for (my $i = 0; $i < @packs; $i++) {
    my $line = "#$packs[$i][0] $packs[$i][1] $packs[$i][2] ".
        "$packs[$i][3]x $packs[$i][4] $packs[$i][5]";
    if ($line =~ /$_[0]/i) {
      push(@search, $i);
    }
  }
}

# --------------------------------------------------------------------
# If first argument is true, create a slim list.
# Returns "0" if packs were listed, "1" if not.

sub print_search_table
{
  my $slim = (defined($_[0]) and $_[0] == 1) ? 1 : 0;

  if (@packs == 0)
    {
      Xchat::print("$command: Sorry, no packs in cache.");
      return 1;
    }
  if (@search == 0)
    {
      Xchat::print("$command: Sorry, no packs found.");
      return 1;
    }

  if ($slim) {
    Xchat::print("----  ------------------------------");
    Xchat::print("Get   Description");
    Xchat::print("----  ------------------------------");
  } else {
    Xchat::print("----  ----------- ----------- ------ ------ ----  " .
        "--------------------");
    Xchat::print("Pack  Nick        Channel      Sends   Size  Get  " .
        "Description");
    Xchat::print("----  ----------- ----------- ------ ------ ----  " .
        "--------------------");
  }

  for (my $i = 0; $i < @search; $i++)
    {
      my @row = @{$packs[$search[$i]]};
      if ($slim) {
        Xchat::printf("%4s  %s", $i, $row[5]);
      } else {
        Xchat::printf("%4s  %-11s %-11s %6s %6s %4s  %s",
            "#" . $row[0], $row[1], $row[2], $row[3] . "x",
            $row[4], $i, $row[5]);
      }
    }
  
  return 0;
}

# --------------------------------------------------------------------

sub dump_packages
{
  my $filename = $_[0];
  my $handle;
  my @lines;

  # Prepare CSV in @lines
  push(@lines, "Nick;Pack;Channel;TimesGet;Size;Description;AccTime;ModTime\n");
  for (my $i = 0; $i < @packs; $i++) {
    my @row = @{$packs[$i]};
    # Extra processing for fields that need to be quoted
    foreach ($row[1], $row[2], $row[5]) {
      $_ =~ s/"/""/g; # Convert quote to a double quote
      $_ =~ s/^(.*)$/"$1"/; # Quote field
    }
    push(@lines, "$row[1];$row[0];$row[2];$row[3];$row[4];" .
      "$row[5];$row[6];$row[7]\n");
  }

  if (not defined $filename) {
    # Print @lines to screen
    Xchat::print("$command: ---- Start of CSV XDCC package dump ----");
    foreach (@lines) {
      Xchat::print($_);
    }
    Xchat::print("$command: ---- End of package dump ----");
  } else {
    # Print @lines to file
    if (not defined open(DUMPFILE, "> $_[0]")) {
      Xchat::print("$command: Cannot open $_[0]: $!");
      return;
    }
    foreach (@lines) {
      print DUMPFILE $_;
    }
    close(DUMPFILE);
    Xchat::print("$command: Package dump has been written to \"$filename\"");
  }
}

# --------------------------------------------------------------------

sub create_botlist
{
  @botlist = ();
  # Storeage for the nicklist and the modified nicklist
  my @nl;
  # Receive the user list from Xchat
  my @user_list = Xchat::get_list("users");
  # Extract nicks (without ops) in @nl, the nick list.
  foreach (@user_list) {
    # Exclude channel ops
    if (not $_->{"prefix"} eq "@") {
      push(@nl, $_->{"nick"});
    }
  }
  @nl = sort @nl;
  # Check if prefix was defined and use it to identify xdcc severs.
  if (defined $_[0]) {
    foreach (@nl) {
      if (uc($_[0]) eq uc(substr($_, 0, length($_[0])))) {
        push(@botlist, $_);
      }
    }
    return;
  }
  # Create a new modified nick list without numbers at the end.
  my @ml = @nl;
  foreach (@ml) { $_ =~ s/[0-9\\]*$//; }
  # This is a rough algorithm to identify xdcc servers.
  my $min_nicks = 3;
  for (my $a = 0; $a < @nl-$min_nicks+1; $a++) {
    if (uc($ml[$a]) eq uc($ml[$a+$min_nicks-1])) {
      for (my $b = 0; $b < $min_nicks; $b++) {
        push(@botlist, $nl[$a+$b]);
      }
      my $c = $a + $min_nicks;
      while ($c < @nl and uc($ml[$c]) eq uc($ml[$a])) {
        push(@botlist, $nl[$c]);
	$c += 1;
      }
      $a = $c - 1;
    }
  }
}

# --------------------------------------------------------------------

sub print_botlist
{
  if (@botlist == 0) {
    Xchat::print("$command: Sorry, no XDCC servers found.");
    return;
  }
  Xchat::print("---  --------------------  ---  --------------------");
  Xchat::print("Nr.  Server nick           Nr.  Server nick");
  Xchat::print("---  --------------------  ---  --------------------");
  for (my $i = 0; $i < @botlist; $i += 2) {
    if (defined $botlist[$i + 1]) {
      Xchat::printf("%3s  %-20s  %3s  %-20s", $i, $botlist[$i],
          $i + 1, $botlist[$i + 1]);
    } else {
      Xchat::printf("%3s  %-20s", $i, $botlist[$i]);
    }
  }
}

# --------------------------------------------------------------------

sub request_package_lists
{
  my $range = $_[0];
  my @range;
  if ($range =~ /^([0-9]+)$/) {			# Normal number
    push(@range, $1);
  } elsif ($range =~ /^([0-9]+)-([0-9]+)$/) { 	# Complete range
    for (my $a = $1; $a <= $2; $a++) {
      push(@range, $a);
    }
  } elsif ($range =~ /^-([0-9]+)$/) {		# Left-open range
    for (my $a = 0; $a <= $1; $a++) {
      push(@range, $a);
    }
  } elsif ($range =~ /^([0-9]+)-$/) {		# Right-open range
    for (my $a = $1; $a < @botlist; $a++) {
      push(@range, $a);
    }
  } elsif ($range =~ /^-$/) {			# Full range
    for (my $a = 0; $a < @botlist; $a++) {
      push(@range, $a);
    }
  } else {					# Error
    Xchat::print("$command: Sorry, invalid range given.");
    return;
  }
  if (@range == 0) {
    Xchat::print("$command: Sorry, illegal range given.");
    return;
  }

  foreach (@range) {
    if ($_ < 0 or $_ > $#botlist) {
      Xchat::print("$command: Sorry, illegal range given.");
      return;
    } else {
      Xchat::command("ctcp $botlist[$_] xdcc list");
    }
  }
}

# --------------------------------------------------------------------

sub print_overview_text
{
  Xchat::print("XDCC-Client $version by Simon Fuhrmann.");
  Xchat::print("If you like XDCC-Client, please drop a mail.");
  Xchat::printf("  %-23s  %-30s", "/$command",
      "Displays this help");
  Xchat::printf("  %-23s  %-30s", "/$command help [<command>]",
      "Displays this help [or help for <command>]");
  Xchat::printf("  %-23s  %-30s", "/$command ls [<str>]",
      "Lists all packages [with <str>], short format");
  Xchat::printf("  %-23s  %-30s", "/$command list [<str>]",
      "Lists all packages [with <str>]");
  Xchat::printf("  %-23s  %-30s", "/$command show",
      "Repeat last listing");
  Xchat::printf("  %-23s  %-30s", "/$command grep <str>",
      "Search <str> in cache");
  Xchat::printf("  %-23s  %-30s", "/$command new [keep]",
      "Lists new packages [and leave them new]");
  Xchat::printf("  %-23s  %-30s", "/$command get <n>",
      "Request pack <n> from last listing");
  Xchat::printf("  %-23s  %-30s", "/$command get <str>",
      "Request pack that matches <str>");
  Xchat::printf("  %-23s  %-30s", "/$command sort [[+|-]<field>]",
      "Sort by field: name, nick, size, chan, pack, sends");
  Xchat::printf("  %-23s  %-30s", "/$command botlist [<prefix>]",
      "Try to generate a list of XDCC servers");
  Xchat::printf("  %-23s  %-30s", "/$command getlist <range>",
      "Request XDCC package list(s)");
  Xchat::printf("  %-23s  %-30s", "/$command clear",
      "Clear the cache");
  Xchat::printf("  %-23s  %-30s", "/$command save [<file>]",
      "Save the cache to default file [or <file>]");
  Xchat::printf("  %-23s  %-30s", "/$command load [<file>]",
      "Load the cache from default file [or <file>]");
  Xchat::printf("  %-23s  %-30s", "/$command dump [<file>]",
      "Dump CSV to the screen [or to <file>]");
}

# --------------------------------------------------------------------

sub print_help_text
{
  if ($_[0] eq "list") {
    Xchat::print("/$command list [<str>]");
    Xchat::print("  This command will list all packages in cache with");
    Xchat::print("  all availible informationl in a big table.");
    Xchat::print("  You may want to sort before displaying the entries.");
    Xchat::print("  You can use regular expression-like syntax in <str>.");
  } elsif ($_[0] eq "ls") {
    Xchat::print("/$command ls [<str>]");
    Xchat::print("  This command will list all packages in a slim table.");
    Xchat::print("  You may want to sort before viewing the table.");
    Xchat::print("  You can use regular expression-like syntax in <str>.");
  } elsif ($_[0] eq "show") {
    Xchat::print("/$command show");
    Xchat::print("  This command will show you the packages from your last");
    Xchat::print("  listing, generated with list, search, grep or new.");
  } elsif ($_[0] eq "grep") {
    Xchat::print("/$command grep <str>");
    Xchat::print("  This command searches for <str> in the whole cache.");
    Xchat::print("  You can use regular expression-like syntax in <str>.");
  } elsif ($_[0] eq "new") {
    Xchat::print("/$command new");
    Xchat::print("  This command lists all new packages that have been");
    Xchat::print("  collected. Use \"keep\" as parameter, to leve these");
    Xchat::print("  packages new (and don't change the last access time.");
  } elsif ($_[0] eq "get") {
    Xchat::print("/$command get <n> | <str>");
    Xchat::print("  The command \"get <n>\" is executed if the argument");
    Xchat::print("  is numeric. Otherwise \"get <str>\" is executed.");
    Xchat::print("/$command get <n>");
    Xchat::print("  Requests the package with number <n> from your last");
    Xchat::print("  listing. Note that these numbers are not consistent");
    Xchat::print("  on subsequent listings.");
    Xchat::print("/$command get <str>");
    Xchat::print("  Requests the package whose name matches <str>.");
    Xchat::print("  If <str> matches more than one package, the command");
    Xchat::print("  behaves like \"ls <str>\".");
  } elsif ($_[0] eq "sort") {
    Xchat::print("/$command sort [[+|-]<field>]");
    Xchat::print("  This command sorts the cache. You can give one of");
    Xchat::print("  name, nick, size, chan, pack, sends as field. You can");
    Xchat::print("  prefix \"+\" or \"-\" to sort ascending or descending.");
    Xchat::print("  The default is to sort ascending by name (+name).");
  } elsif ($_[0] eq "botlist") {
    Xchat::print("/$command botlist [<prefix>]");
    Xchat::print("  This command tries to create a list of XDCC servers.");
    Xchat::print("  Servers will be selected according to their nicks.");
    Xchat::print("  This means that the output may be wrong, e.g. some");
    Xchat::print("  nicks aren't servers or some servers are not listed.");
    Xchat::print("  Use <prefix> to select all bots with this prefix.");
  } elsif ($_[0] eq "getlist") {
    Xchat::print("/$command getlist <range>");
    Xchat::print("  This command requests the lists in <range>. <range> can");
    Xchat::print("  be \"5\", \"2-8\", \"4-\", \"-9\" or \"-\". The third");
    Xchat::print("  range will request all lists from 4, the forth from");
    Xchat::print("  from 0 to 9 and the last one will request all lists.");
  } elsif ($_[0] eq "clear") {
    Xchat::print("/$command clear");
    Xchat::print("  This command clears the cache and all collected");
    Xchat::print("  packages will be dropped.");
  } elsif ($_[0] eq "save") {
    Xchat::print("/$command save [<file>]");
    Xchat::print("  This command saves the cache to file. The default");
    Xchat::print("  filename is \"$default_file\".");
    Xchat::print("  Optionally, you can specify your own filename.");
  } elsif ($_[0] eq "load") {
    Xchat::print("/$command load [<file>]");
    Xchat::print("  This command loads the cache from file. The default");
    Xchat::print("  filename is \"$default_file\".");
    Xchat::print("  Optionally, you can specify your own filename.");
  } elsif ($_[0] eq "dump") {
    Xchat::print("/$command dump [<file>]");
    Xchat::print("  This command dumps the whole cache either to the");
    Xchat::print("  screen or to a file if given. The output will be");
    Xchat::print("  in the CSV format using \";\" as delimiter.");
    Xchat::print("  The cache will not be changed in any way.");
  } else {
    Xchat::print("$command: There is no help available for \"$_[0]\".");
  }
}

# IRC Color table
# --------------------------------------------------------------------
#   1 - Black        2 - Navy Blue   3 - Green       4 - Red
#   5 - Brown        6 - Purple      7 - Olive       8 - Yellow
#   9 - Lime Green  10 - Teal       11 - Aqua Light 12 - Royal Blue
#  13 - Hot Pink    14 - Dark Gray  15 - Light Gray 16 - White

