use strict;
use warnings;

our $VERSION = '0.3'; # 82db0c6c788f18a
our %IRSSI = (
    authors     => 'Nei',
    contact     => 'Nei @ anti@conference.jabber.teamidiot.de',
    url         => "http://anti.teamidiot.de/",
    name        => 'bridgebots',
    description => 'A generic attempt to rewrite bridge-bots into fake nicks.',
    license     => 'ISC',
   );

die 'This script requires Irssi 1.4 or later'
    if (Irssi::parse_special('$abiversion')||0) < 46;

# Usage
# =====
# This script will attempt to rewrite incoming messages and replace
# the nick of a bridge bot with the nick of the user that it is
# relaying.
#
# You have to configure it by adding the masks of the bridge bots, and
# regular expression patterns that extract the relayed nick from the
# message.
#
# Each mask and pattern has a botname. You then assign these
# configured bots to channels where they should be replaced.
#
# See /help bridgebots for the syntax and an example.
#
# Example:
# --------
#   /bridgebots add -bot discordbot!~bridge\@* -pattern
#      (`(?<relaynick>.*?)`)? -privmsg -multiline my_discord_bridge
#
#   /bridgebots channel add my_discord_bridge #my_channel
#

# Options
# =======
# /format bridgebots_is_bridged <string>
# * string : Format String shown on the $is_bridged expando when the
#   user is not a real user on channel
#
# /set bridge_user_timeout <time>
# * time : after how much time without message fake users should part
#   the channel
#
# /set bridge_nick_unicodespace <ON|OFF>
# * whether to replace spaces in nick names with a Unicode Four-Per-Em
#   space or with underscore
#
# /set binfo_in_active_window <ON|OFF>
# * whether to show the binfo output in the active window
#

# Commands
# ========
# /bridgebots ...
# * configure the bridge-bots
#
# /binfo <nick>
# * show user info from the internal channel nicklist
#
# /bridgednames [<channel>]
# * list all the relayed nicks currently joined to the channel
#
# Use /help <command> to read the detailed descriptions of the commands.
#

# Expandos
# ========
# The following expandos are available:
#
# $is_bridged
# * shows the content of the bridgebots_is_bridged format when the
#   current message relayed is from a bridge-bot
#
# $bridged_or_cumode
# * can be used in place of $2 in your pubmsg format to show the
#   is_bridged indicator in place of the channel user mode indicator
#   (@/+) for bridged nicks
#
# $bridged_bot
# * contains the name of the bridge-bot definition if the current
#   message is relayed from a bridge-bot definition
#
# You have to add them to your /format pubmsg and related formats,
# wherever you want to see the bridged indicator.
#
# Example:
#
#   /format pubmsg {pubmsgnick $2 {pubnick $0$is_bridged}}$1
#
# Do not forget to /save if you are happy with the installation.
#
# Note that due to the Irssi /format system, this script naturally
# conflicts with nickcolor.pl, nm.pl and similar scripts that use
# /format
#

use experimental 'signatures';
use List::Util qw(min max);
use Carp ();
use CPAN::Meta::YAML qw(LoadFile DumpFile);
use Encode;
use Irssi;

my $user_timeout = 24 * 60 * 60; # seconds = 1 day
my $nick_unicodespace = 1;
my $binfo_active_win = 0;
my $config_file = Irssi::get_irssi_dir() . '/bridgebots';
my $config;
my $is_bridged_format;

my %bot_defs;
my %channel_defs;

my %fakeusers;
my %multiline_last;
my %cmmap;
my $tmout;

our ($is_bridged, $bridged_bot, $cumode_expando);

sub utf8_on {
    for (@_) {
	Encode::_utf8_on($_);
    }
    @_
}

sub load_config {
    local $@;
    my $r;
    eval {
	$r = LoadFile($config_file);
    };
    $config = $r || +{};
}

sub parse_config {
    %bot_defs = ();
    for my $def (keys %{$config->{bot_defs} || +{}}) {
	local $@;
	if ($def =~ /\s/) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "loading bridge-bot definition", $def, "Invalid name");
	    next;
	}
	unless (ref $config->{bot_defs}{$def} eq 'HASH') {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg", "loading bridge-bot definition", $def);
	    next;
	}
	unless (exists $config->{bot_defs}{$def}{bot}) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "loading bridge-bot definition", $def, "Missing bot value");
	    next;
	}
	my $bot = $config->{bot_defs}{$def}{bot};
	unless (exists $config->{bot_defs}{$def}{pattern}) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "loading bridge-bot definition", $def, "Missing pattern value");
	    next;
	}
	my $pattern = eval { qr{$config->{bot_defs}{$def}{pattern}} };
	if (my $err = $@) {
	    chomp $err;
	    $err =~ s/ at .*? line \d+\.$//;
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "loading bridge-bot definition", "$def/pattern", $err);
	    next;
	}
	unless ("$pattern" =~ /\(\?<relaynick>.*?\)/) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "loading bridge-bot definition", $def, "pattern does not contain (?<relaynick>...)");
	    next;
	}
	$bot_defs{$def} = +{
	    bot	    => $bot,
	    pattern => $pattern,
	    ($config->{bot_defs}{$def}{privmsg}   ? (privmsg   => 1) : ()),
	    ($config->{bot_defs}{$def}{notice}    ? (notice    => 1) : ()),
	    ($config->{bot_defs}{$def}{multiline} ? (multiline => 1) : ()),
	   };
    }
    %channel_defs = ();
 CHANNEL: for my $def (keys %{$config->{channel_defs} || +{}}) {
	unless (ref $config->{channel_defs}{$def} eq 'ARRAY') {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "loading bridge-bot channel", $def, "Not a list");
	    next;
	}
	for my $ref (@{$config->{channel_defs}{$def}}) {
	    unless (defined find_bot($ref)) {
		Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "loading bridge-bot channel", $def, "Bridge-bot definition $ref not found");
		next CHANNEL;
	    }
	}
	$channel_defs{$def} = [ map { find_bot($_) } @{$config->{channel_defs}{$def}} ];
    }
    return;
}

sub pattern_to_str ($pattern) {
    my $str = "$pattern";
    $str =~ s/^\(\?\^\w*:(.*?)\)$/$1/s;
    $str
}

sub save_config ($fname, $autosave) {
    $config->{bot_defs} = +{ map { $_ => +{%{$bot_defs{$_}}} } keys %bot_defs };
    $config->{channel_defs} = +{ map { $_ => [@{$channel_defs{$_}}] } keys %channel_defs };
    for my $def (keys %{$config->{bot_defs}}) {
	$config->{bot_defs}{$def}{pattern} = pattern_to_str($config->{bot_defs}{$def}{pattern});
    }
    local $@;
    eval {
	DumpFile($config_file => $config);
	Irssi::printformat(MSGLEVEL_CLIENTNOTICE, "bridgebots_saved", $config_file)
		unless $autosave;
    };
    if (my $err = $@) {
	chomp $err;
	$err =~ s/ at .*? line \d+\.$//;
	Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_info", "saving bridge-bot config", $err)
		unless $autosave;
    }
    return;
}

sub parse_data ($data) {
    my ($data1, @data) = split ' :', $data, 2;
    unshift @data, split ' ', $data1;
    return @data;
}

my %nick_repl = (
    '!' => "\x{ff01}",
    '@' => "\x{ff20}",
    ' ' => "\x{2005}",
   );

sub valid_nick ($nick) {
    my $tmp = $nick;
    if ($nick_unicodespace) {
	$nick =~ s/\s/$nick_repl{" "}/g;
    } else {
	$nick =~ s/\s/_/g;
    }
    $nick =~ s/([!@])/$nick_repl{$1}/g;
    $tmp
}

sub start_expire {
    return if defined $tmout;
    $tmout = Irssi::timeout_add(
	59 * 1000,
	sub ($data) {
	    my $expired = time - $user_timeout;
	    for my $tag (keys %fakeusers) {
		my $server = Irssi::server_find_tag($tag);
		unless ($server) {
		    delete $fakeusers{$tag};
		    next;
		}
		for my $target (keys %{ $fakeusers{$tag} }) {
		    my $chobj = $server->channel_find($target);
		    unless ($chobj) {
			delete $fakeusers{$tag}{$target};
			next;
		    }
		    for my $nuh (keys %{ $fakeusers{$tag}{$target} }) {
			if ($fakeusers{$tag}{$target}{$nuh} < $expired) {
			    my ($nick, $address) = split '!', $nuh, 2;
			    if (my $nickobj = $chobj->nick_find($nick)) {
				utf8_on(@{$nickobj}{qw(nick host)});
				if ($address eq $nickobj->{host}) {
				    Irssi::signal_emit("server event", $server, "PART $target :[Relay Timed out]", $nick, $address);
				}
			    }
			    delete $fakeusers{$tag}{$target}{$nuh};
			}
		    }
		    delete $fakeusers{$tag}{$target} unless %{$fakeusers{$tag}{$target}};
		}
		delete $fakeusers{$tag} unless %{$fakeusers{$tag}};
	    }
	    unless (%fakeusers) {
		Irssi::timeout_remove($tmout);
		$tmout = undef;
	    }
	},
	''
       );
}

sub UNLOAD {
    Irssi::timeout_remove($tmout) if defined $tmout;
    $tmout = undef;
    for my $tag (keys %fakeusers) {
	my $server = Irssi::server_find_tag($tag);
	unless ($server) {
	    delete $fakeusers{$tag};
	    next;
	}
	my %quit;
	for my $target (keys %{ $fakeusers{$tag} }) {
	    my $chobj = $server->channel_find($target);
	    unless ($chobj) {
		delete $fakeusers{$tag}{$target};
		next;
	    }
	    for my $nuh (keys %{ $fakeusers{$tag}{$target} }) {
		my ($nick, $address) = split '!', $nuh, 2;
		if (my $nickobj = $chobj->nick_find($nick)) {
		    utf8_on(@{$nickobj}{qw(nick host)});
		    if ($address eq $nick->{host}) {
			$quit{$nuh}++;
		    }
		}
		delete $fakeusers{$tag}{$target}{$nuh};
	    }
	    delete $fakeusers{$tag}{$target};
	}
	for my $nuh (keys %quit) {
	    my ($nick, $address) = split '!', $nuh, 2;
	    Irssi::signal_emit("server event", $server, "QUIT :*.relay *.split", $nick, $address);
	}
	delete $fakeusers{$tag};
    }
}

#########################################################################################################################

Irssi::theme_register([
  'binfo_not_found'	=> 'Nick not found: $0',
  'binfo_info'		=> '{nick $0} {nickhost $1}%:{whois ircname $2}',
  'binfo_channel'	=> '{whois channel %|$1}',
  'binfo_account'	=> '{whois account %|$1}',
  'binfo_is_fake'	=> '{whois  %|$1}',
  'binfo_fake_idle'	=> '{whois fakeidle %|$1 days $2 hours $3 mins $4 secs}',
  'end_of_binfo'	=> 'End of BINFO',
  'bridgednames'        => '{names_users Bridged nicks on {names_channel $0}}',
  'bridgednames_name'   => '{names_nick $0 $1}',
  'end_of_bridgednames'	=> '{channel $0}: Total of {hilight $1} bridged nicks',
  'bridgebots_not_connected'   => 'Not connected to server',
  'bridgebots_header'	       => 'Bridge-bots:',
  'bridgebots_bot_line'	       => '{hilight $0}: $1',
  'bridgebots_channel_header'  => 'Bridge-bot channels:',
  'bridgebots_channel_line'    => '{hilight $0}: $1',
  'bridgebots_errors_arg'      => 'Error $0 {hilight $1}',
  'bridgebots_errors_arg_info' => 'Error $0 {hilight $1}: $2',
  'bridgebots_errors_info'     => 'Error $0: $1',
  'bridgebots_saved'	       => 'Bridge-bot config saved to $0',
  'bridgebots_added'	       => 'Bridge-bot definition {hilight $0} added',
  'bridgebots_removed'	       => 'Bridge-bot definition {hilight $0} removed',
  'bridgebots_channel_added'   => 'Bridge-bot added to channel {hilight $0}',
  'bridgebots_channel_removed' => 'Bridge-bot removed from channel {hilight $0}',
  'bridgebots_is_bridged'      => '%Kᵇ%n',
  'bridgebots_custom_modes'    => '&%B&%n | @%g@%n | +%y+%n | b%Kᵇ%n',
 ]);

Irssi::settings_add_time('misc', 'bridge_user_timeout', '1day');
Irssi::settings_add_bool('misc', 'bridge_nick_unicodespace', 1);
Irssi::settings_add_bool('misc', 'binfo_in_active_window', 0);

sub idchan ($server, $target) {
    my @isupp = split ',', $server->isupport('idchan') // '';
    for my $i (@isupp) {
	my ($type, $len) = split ':', $i, 2;
	if ($target =~ /^\Q$type/) {
	    return ((substr $target, 0, 1) . (substr $target, $len + 1));
	}
    }
    return $target;
}

Irssi::signal_add_first(
    "server event tags" => sub ($server, $data, $nick, $address, $tags_str) {
	return unless defined $address;

	my ($command, $target, @args) = parse_data($data);

	return unless $target;
	return unless @args && defined $args[0];

	utf8_on($target, $nick, $address, @args);
	my $defs = $channel_defs{ idchan($server, $target) };
	$defs //= $channel_defs{'*'};
	if ($defs) {
	    for my $def (@$defs) {
		my $bot_def = $bot_defs{$def} or next;
		$bot_def->{ lc $command } or next;
		$server->mask_match_address($bot_def->{bot}, $nick, $address) or next;
		my ($ident, $host) = split '@', $address, 2;
		my $tmp = $args[0];
		$tmp =~ s/^$bot_def->{pattern}(?:\s|$)// or next;
		my %pat = %+;

		my $relaynick;
		if (length $pat{relaynick}) {
		    $relaynick = valid_nick($pat{relaynick});
		    if ($bot_def->{multiline}) {
			$multiline_last{ $server->{tag} }{$target} = $relaynick;
		    }
		} else {
		    if ($bot_def->{multiline}) {
			$relaynick = $multiline_last{ $server->{tag} }{$target};
		    }
		}
		defined $relaynick or next;

		$args[0] = $tmp;

		my $relayaddress = join "/", "$def\@bridge", 'bot', $nick, $host;

		my $rninfo = '';
		if (defined $pat{realname}) {
		    $rninfo = "$pat{realname} ";
		}

		local $is_bridged = 1;
		local $bridged_bot = $def;
		local $cumode_expando = exists $cmmap{b} ? Irssi::format_string_expand($cmmap{b}) : 'b';
		if (lc $command eq 'privmsg') {
		    if (my $chobj = $server->channel_find($target)) {
			unless ($chobj->nick_find($relaynick)) {
			    Irssi::signal_emit("server event", $server,
					       "JOIN $target * :$rninfo($pat{relaynick}) \[Relay: $def] [Bot: $nick]",
					       $relaynick, $relayaddress);
			}
		    }
		    $fakeusers{$server->{tag}}{$target}{"$relaynick!$relayaddress"} = time;
		    start_expire();
		}
		Irssi::signal_continue($server, "$command $target :@args", $relaynick, $relayaddress, $tags_str);
		return;
	    }
	}

	my $tg = $server->{tag};
	if (my $ch = $server->channel_find($target)) {
	    $target = $ch->{name};
	    my $nickobj = $ch->nick_find($nick);
	    if ($nickobj) {
		$nick = $nickobj->{nick};
		my $mode = substr $nickobj->{prefixes}.' ', 0, 1;
		local $cumode_expando = exists $cmmap{$mode} ? Irssi::format_string_expand($cmmap{$mode}) : $mode;
		Irssi::signal_continue($server, $data, $nick, $address, $tags_str);
		return;
	    }
	}
    }
   );

sub expando_is_bridged {
    $is_bridged ? $is_bridged_format : ''
}

sub expando_bridged_bot {
    $bridged_bot // ''
}

sub expando_bridged_or_cumode {
    $cumode_expando // ''
}

Irssi::expando_create('is_bridged'  => \&expando_is_bridged, { 'server event' => 'none' });
Irssi::expando_create('bridged_or_cumode'  => \&expando_bridged_or_cumode, { 'server event' => 'none' });
Irssi::expando_create('bridged_bot' => \&expando_bridged_bot, { 'server event' => 'none' });

Irssi::signal_add(
    "gui exit" => sub {
	save_config() if Irssi::settings_get_bool('settings_autosave');
    }
   );

Irssi::signal_add("setup saved" => \&save_config);

sub update_settings {
    $user_timeout = Irssi::settings_get_time('bridge_user_timeout') / 1000;
    $nick_unicodespace = Irssi::settings_get_bool('bridge_nick_unicodespace');
    $binfo_active_win = Irssi::settings_get_bool('binfo_in_active_window');
}

Irssi::signal_add_last(
    "setup changed" => sub {
	update_settings();
	load_config();
	parse_config();
    }
   );

sub _get_format {
    Irssi::current_theme->get_format(__PACKAGE__, @_);
}

sub update_formats {
    $is_bridged_format = Irssi::format_string_expand(_get_format('bridgebots_is_bridged'));
    my $custom_modes = _get_format('bridgebots_custom_modes');
    %cmmap = map { (substr $_, 0, 1), (substr $_, 1) }
	$custom_modes =~ /(?:^\s?|\G\s?\|\s?)((?!\s\|)(?:[^\\|[:space:]]|\\.|\s(?!\||$))*)/sg;
}

Irssi::signal_add_last({
    'theme changed'  => 'update_formats',
    'command format' => 'update_formats',
});

sub quote ($str) {
    if ($str =~ /"/ || $str =~ /\s/) {
	$str =~ s/(["\\])/\\$1/g;
	$str = "\"$str\"";
    }
    $str
}
sub bot_def_to_string ($bot_def) {
    my $pat = quote(pattern_to_str($bot_def->{pattern}));
    join " ", ("-bot $bot_def->{bot} -pattern $pat",
	       ($bot_def->{privmsg} ? "-privmsg" : ()),
	       ($bot_def->{notice} ? "-notice" : ()),
	       ($bot_def->{multiline} ? "-multiline" : ()),
	      )
}

sub list_bots {
    Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'bridgebots_header');
    foreach my $def (sort keys %bot_defs) {
	Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'bridgebots_bot_line', $def, bot_def_to_string($bot_defs{$def}));
    }
}

sub list_channels {
    Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'bridgebots_channel_header');
    foreach my $def (sort keys %channel_defs) {
	Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'bridgebots_channel_line', $def, join " ", @{$channel_defs{$def}});
    }
}

Irssi::command_bind(
    "bridgebots" => sub ($data, $server, $witem) {
	$data =~ s/\s+$//;
	if (length $data) {
	    Irssi::command_runsub("bridgebots", $data, $server, $witem);
	} else {
	    list_bots();
	}
    },
   );

Irssi::command_bind(
    "bridgebots channel" => sub ($data, $server, $witem) {
	$data =~ s/\s+$//;
	if (length $data) {
	    Irssi::command_runsub("bridgebots channel", $data, $server, $witem);
	} else {
	    list_channels();
	}
    },
   );

sub find_bot ($name) {
    my ($found) = grep { lc $_ eq lc $name } sort keys %bot_defs;
    $found
}

sub find_channel ($name) {
    my ($found) = grep { lc $_ eq lc $name } sort keys %channel_defs;
    $found
}

sub cmd_addmodify ($add, $data, $server, $witem) {
    my ($opts, $arg) = Irssi::command_parse_options("bridgebots $add", $data);
    return unless $opts;
    $arg =~ s/\s+$//;
    utf8_on($arg, values %$opts);
    my @args = split ' ', $arg;
    my $is_add = $add eq 'add';
    my $is_modify = !$is_add;
    my $adding = $is_add ? 'adding' : 'modifying';
    if (@args > 1) {
	Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "$adding bridge-bot definition", $args[0], "Too many arguments");
	return;
    } elsif (!@args) {
	Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_info", "$adding bridge-bot definition", "Bridge-bot name missing");
	return;
    }
    my %new_def;
    %new_def = %{$bot_defs{find_bot($arg)}} if defined find_bot($arg);
    if (exists $opts->{pattern}) {
	my $pattern = eval { qr{$opts->{pattern}} };
	if (my $err = $@) {
	    chomp $err;
	    $err =~ s/ at .*? line \d+\.$//;
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "$adding bridge-bot definition", "$arg/pattern", $err);
	    return;
	}
	unless ("$pattern" =~ /\(\?<relaynick>.*?\)/) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "$adding bridge-bot definition", $arg, "pattern does not contain (?<relaynick>...)");
	    return;
	}
	$new_def{pattern} = $pattern;
    }
    if (exists $opts->{bot}) {
	$new_def{bot} = $opts->{bot};
    }
    for my $flag (qw(privmsg notice multiline)) {
	if (exists $opts->{"no$flag"}) {
	    delete $new_def{$flag};
	} elsif (exists $opts->{$flag}) {
	    $new_def{$flag} = 1
	}
    }
    if ($is_modify) {
	unless (defined find_bot($arg)) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "$adding bridge-bot definition", $arg, "Bridge-bot definition not found");
	    return;
	}
    }
    if (!$new_def{bot} || !$new_def{pattern}) {
	my $s;
	Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "$adding bridge-bot definition", $arg,
			   ((join " and ", map { $s = defined $s ? 's' : '';  "-$_" } grep { !$new_def{$_} } qw(bot pattern))
				. " argument$s missing"));
	return;
    }
    delete $bot_defs{find_bot($arg)} if defined find_bot($arg);
    $bot_defs{$arg} = \%new_def;
    Irssi::printformat(MSGLEVEL_CLIENTNOTICE, "bridgebots_added", $arg);
}

Irssi::command_bind("bridgebots add" => sub { cmd_addmodify("add", @_) });
Irssi::command_bind("bridgebots modify" => sub { cmd_addmodify("modify", @_) });

Irssi::command_bind(
    "bridgebots remove" => sub ($data, $server, $witem) {
	$data =~ s/\s+$//;
	utf8_on($data);
	my @args = split ' ', $data;
	if (@args > 1) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "removing bridge-bot definition", $args[0], "Too many arguments");
	    return;
	} elsif (!@args) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_info", "removing bridge-bot definition", "Bridge-bot name missing");
	    return;
	}
	unless (defined find_bot($data)) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "removing bridge-bot definition", $data, "Bridge-bot definition not found");
	    return;
	}
	delete $bot_defs{find_bot($data)};
	for my $ch (sort keys %channel_defs) {
	    @{$channel_defs{$ch}} = grep { $_ ne $data } @{$channel_defs{$ch}};
	    unless (@{$channel_defs{$ch}}) {
		delete $channel_defs{$ch};
	    }
	}
	Irssi::printformat(MSGLEVEL_CLIENTNOTICE, "bridgebots_removed", $data);
    }
   );

Irssi::command_bind(
    "bridgebots channel add" => sub ($data, $server, $witem) {
	my ($opts, $arg) = Irssi::command_parse_options("bridgebots channel add", $data);
	return unless $opts;
	$arg =~ s/\s+$//;
	utf8_on($arg, values %$opts);
	my @args = split ' ', $arg;
	if (@args == 1) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "adding bridge-bot to channel", $args[-1], "Bridge-bot name missing");
	    return;
	} elsif (!@args) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_info", "adding bridge-bot to channel", "Channel name missing");
	    return;
	}
	my $channel = pop @args;
	my @channel_bots;
	@channel_bots = @{ $channel_defs{find_channel($channel)} } if defined find_channel($channel);
	my @bb_not_found;
	for my $arg (@args) {
	    unless (defined find_bot($arg)) {
		push @bb_not_found, $arg;
	    }
	}
	if (@bb_not_found) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "adding bridge-bot to channel", $channel, "Bridge-bot definitions not found: @bb_not_found");
	    return;
	}
	my $idx = @channel_bots;
	if (exists $opts->{before}) {
	    for ($idx = $#channel_bots; $idx >= 0; $idx--) {
		if (lc $channel_bots[$idx] eq lc $opts->{before}) {
		    last;
		}
	    }
	} elsif (exists $opts->{after}) {
	    for ($idx = 0; $idx < @channel_bots; $idx++) {
		if (lc $channel_bots[$idx] eq lc $opts->{after}) {
		    $idx++;
		    last;
		}
	    }
	}
	splice @channel_bots, $idx, 0, @args;
	delete $channel_defs{find_channel($channel)} if defined find_channel($channel);
	$channel_defs{$channel} = [ map { find_bot($_) } @channel_bots ];
	Irssi::printformat(MSGLEVEL_CLIENTNOTICE, "bridgebots_channel_added", $channel);
    }
   );

Irssi::command_bind(
    "bridgebots channel remove" => sub ($data, $server, $witem) {
	$data =~ s/\s+$//;
	utf8_on($data);
	my @args = split ' ', $data;
	if (@args == 1) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "removing bridge-bot from channel", $args[-1], "Bridge-bot name missing");
	    return;
	} elsif (!@args) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_info", "removing bridge-bot from channel", "Channel name missing");
	    return;
	}
	my $channel = pop @args;
	my @channel_bots;
	@channel_bots = @{ $channel_defs{find_channel($channel)} } if defined find_channel($channel);
	my @old = @channel_bots;
	for my $arg (@args) {
	    @channel_bots = grep { lc $_ ne lc $arg } @channel_bots;
	}
	if (@old == @channel_bots) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "removing bridge-bots from channel", $channel, "Bridge-bot not assigned to channel: @args");
	    return;
	}
	delete $channel_defs{find_channel($channel)} if defined find_channel($channel);
	if (@channel_bots) {
	    $channel_defs{$channel} = [ map { find_bot($_) } @channel_bots ];
	}
	Irssi::printformat(MSGLEVEL_CLIENTNOTICE, "bridgebots_channel_removed", $channel);
    }
   );

Irssi::command_bind(
    "bridgebots channel set" => sub ($data, $server, $witem) {
	$data =~ s/\s+$//;
	utf8_on($data);
	my @args = split ' ', $data;
	if (!@args) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_info", "assigning bridge-bots to channel", "Channel name missing");
	    return;
	}
	my $channel = pop @args;
	my @bb_not_found;
	for my $arg (@args) {
	    unless (defined find_bot($arg)) {
		push @bb_not_found, $arg;
	    }
	}
	if (@bb_not_found) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_errors_arg_info", "assigning bridge-bots to channel", $channel, "Bridge-bot definitions not found: @bb_not_found");
	    return;
	}
	my @channel_bots;
	delete $channel_defs{find_channel($channel)} if defined find_channel($channel);
	if (@channel_bots) {
	    $channel_defs{$channel} = [ map { find_bot($_) } @channel_bots ];
	    Irssi::printformat(MSGLEVEL_CLIENTNOTICE, "bridgebots_channel_added", $channel);
	} else {
	    Irssi::printformat(MSGLEVEL_CLIENTNOTICE, "bridgebots_channel_removed", $channel);
	}
    }
   );

my $addmofify_options = "+bot +pattern privmsg notice multiline noprivmsg nonotice nomultiline";
Irssi::command_set_options("bridgebots add", $addmofify_options);
Irssi::command_set_options("bridgebots modify", $addmofify_options);

Irssi::command_bind(
    "binfo" => sub ($data, $server, $witem) {
	$data =~ s/\s+$//;
	utf8_on($data);
	return unless length $data;
	my $dest;
	if ($binfo_active_win) {
	    my $win = Irssi::active_win();
	    $dest = sub { Irssi::Server::format_create_dest($server, $witem ? $witem->{name} : '', $_[0], $win) };
	} else {
	    $dest = sub { Irssi::Server::format_create_dest($server, '', $_[0]) };
	}
	unless ($server) {
	    $dest->(MSGLEVEL_CLIENTERROR)->printformat("bridgebots_not_connected");
	    return;
	}
	my $nickobj;
	my @ngs = $server->nicks_get_same($data);
	if ($witem && $witem->{type} eq 'CHANNEL') {
	    $nickobj = $witem->nick_find($data);
	}
	unless ($nickobj || @ngs) {
	    $dest->(MSGLEVEL_CRAP)->printformat("binfo_not_found", $data);
	    return;
	}
	unless ($nickobj) {
	    $nickobj = $ngs[1];
	}
	utf8_on(@{$nickobj}{qw(nick host realname account)});
	for (map { $_ * 2 } 0 .. $#ngs>>1) {
	    utf8_on($ngs[$_]{visible_name}, $ngs[$_ + 1]{prefixes});
	}
	$dest->(MSGLEVEL_CRAP)->printformat("binfo_info", $nickobj->{nick}, $nickobj->{host}, $nickobj->{realname});
	my @cns = sort { lc $a->[0]{visible_name} cmp lc $b->[0]{visible_name} } map { [ $ngs[$_], $ngs[$_ + 1] ] } map { $_ * 2 } 0 .. $#ngs>>1;
	my $channel = join ' ', map { $_->[1]{prefixes} . $_->[0]{visible_name} } @cns;
	$dest->(MSGLEVEL_CRAP)->printformat("binfo_channel", $nickobj->{nick}, $channel);
	$dest->(MSGLEVEL_CRAP)->printformat("binfo_account", $nickobj->{nick}, $nickobj->{account});
	my $current_time = time;
	my $least_time = 0;
	my $fake;
	my @fake_chans;
	for my $target (keys %{ $fakeusers{ $server->{tag} } }) {
	    my $chobj = $server->channel_find($target) or next;
	    utf8_on($chobj->{visible_name});
	    my $time = $fakeusers{ $server->{tag} }{$target}{$nickobj->{nick} . '!' . $nickobj->{host}} or next;
	    $least_time = max($least_time, $time);
	    $fake = 1;
	    push @fake_chans, $chobj->{visible_name};
	}
	if ($fake) {
	    my $channel = "[" . (join ' ', @fake_chans) . "]";
	    $dest->(MSGLEVEL_CRAP)->printformat("binfo_is_fake", $nickobj->{nick}, "is a fake user on $channel");
	    my $tdiff = $current_time - $least_time;
	    my $days = int($tdiff / 3600 / 24);
	    my $hours = int(($tdiff % (3600 * 24)) / 3600);
	    my $mins = int(($tdiff % 3600) / 60);
	    my $secs = $tdiff % 60;
	    $dest->(MSGLEVEL_CRAP)->printformat("binfo_fake_idle", $nickobj->{nick}, $days, $hours, $mins, $secs);
	}
	$dest->(MSGLEVEL_CRAP)->printformat("end_of_binfo", $nickobj->{nick});
    }
   );

# src/core/misc.c
sub get_max_column_count {
    my $max_width = pop(@_) - 1;
    my @item_info = @_;

    my $items_count = @item_info;
    if ($items_count == 0) {
	return;
    }

    my $min_len = max 1, min map { $_->[-1] } @item_info;
    my $max_columns = max 1, int($max_width/$min_len);

    my (@columns, @columns_width, @columns_rows);

    for my $n (1 .. $max_columns - 1) {
	$columns_rows[$n] = $items_count <= $n+1 ? 1 :
	    ($items_count+$n)/($n+1);
    }

    # for each possible column count, save the column widths and
    # find the biggest column count that fits to screen.
    my $item_pos = 0;
    my $max_len = max 1, map { $_->[-1] } @item_info;
    for my $tmp (@item_info) {
	my $len = $tmp->[-1];

	for my $n (1 .. $max_columns - 1) {
	    no warnings 'uninitialized';
	    if ($columns_width[$n] > $max_width) {
		next; # too wide
	    }

	    my $col = $item_pos/$columns_rows[$n];
	    if ($columns[$n][$col] < $len) {
		$columns_width[$n] += $len - $columns[$n][$col];
		$columns[$n][$col] = $len;
	    }
	}

	$item_pos++;
    }

    for my $n (reverse 1 .. $max_columns - 1) {
	no warnings 'uninitialized';
	if ($columns_width[$n] <= $max_width &&
		$columns[$n][$n] > 0) {
	    return $n + 1;
	}
    }

    return 1;
}

sub fill_spaces ($length, $max_length) {
    return " " x max(0, $max_length - $length);
}

sub columnize_nicks ($channel, @nicks) {
    my $total = @nicks;

    # determine max columns
    my $cols = Irssi::settings_get_int("names_max_columns");
    my $width = $channel->window->{width};
    {
	my $ts_format = Irssi::settings_get_str('timestamp_format');
	my $render_str = Irssi::current_theme->format_expand(
	    Irssi::current_theme->get_format('fe-common/core', 'timestamp'));
	(my $ts_escaped = $ts_format) =~ s/([%\$])/$1$1/g;
	$render_str =~ s/(?|\$(.)(?!\w)|\$\{(\w+)\})/$1 eq 'Z' ? $ts_escaped : $1/ge;
	$render_str = Irssi::strip_codes(Irssi::format_string_expand($render_str));
	$width -= Irssi::string_width($render_str);
    }
    $width = max 10, $width;
    my $max_cols = get_max_column_count(@nicks, $width - 1);
    return unless $max_cols;
    if ($cols < 1) {
	$cols = $max_cols;
    }
    $cols = min $max_cols, $cols;

    # determine number of rows
    my $rows = int($total / $cols) + !!($total % $cols);

    # array of rows
    my @r;
    for (my $i = 0; $i < $cols; $i++) {
	# peek at next $rows items, determine max length
	my $max_length = max map { $_->[-1] } grep { defined } @nicks[0 .. $rows - 1];

	# fill rows
	for (my $j = 0; $j < $rows; $j++) {
	    my $n = shift @nicks;  # single nick
	    if ($n->[-1]) {
		$r[$j] .= $channel->window->format_get_text(
		    __PACKAGE__, $channel->{server}, $channel->{visible_name},
		    'bridgednames_name', '', $n->[0] . fill_spaces($n->[-1], $max_length) );
	    }
	}
    }

    for (my $m = 0; $m < $rows; $m++) {
	chomp $r[$m];
	$r[$m] =~ s/%/%%/g;
	$channel->print($r[$m], MSGLEVEL_CLIENTCRAP);
    }
}

Irssi::command_bind(
    "bridgednames" => sub ($args, $server, $witem) {
	my ($opts, $arg) = Irssi::command_parse_options("bridgednames", $args);
	return unless $opts;
	my $chan = $witem->{name} if $witem;
	$arg =~ s/\s+$//;
	$chan = $arg if length $arg;
	utf8_on($chan,  values %$opts);
	unless ($server) {
	    Irssi::printformat(MSGLEVEL_CLIENTERROR, "bridgebots_not_connected");
	    return;
	}
	my $chobj = defined $chan ? $server->channel_find($chan) : undef;
	unless ($chobj) {
	    # no nicklist
	    Irssi::UI::Window::format_create_dest(undef, MSGLEVEL_CLIENTERROR)->printformat_module('fe-common/core', 'not_joined');
	    return;
	}
	utf8_on($chobj->{visible_name});
	($chan) = grep { lc $_ eq lc $chan } sort keys %{ $fakeusers{ $server->{tag} } };
	my @nicks;
	if (defined $chan) {
	    for my $nuh (sort { lc $a cmp lc $b } keys %{ $fakeusers{ $server->{tag} }{$chan} }) {
		my ($nick) = split '!', $nuh, 2;
		my $text = $chobj->window->format_get_text(__PACKAGE__, $server, $chobj->{visible_name}, 'bridgednames_name', '', $nick);
		my $bleak = Irssi::strip_codes($text);
		push @nicks, [ $nick, Irssi::string_width($bleak) ];
	    }
	}
	if (@nicks && !exists $opts->{count}) {
	    $chobj->printformat(MSGLEVEL_CLIENTCRAP, 'bridgednames', $chobj->{visible_name});
	    columnize_nicks($chobj, @nicks);
	}
	$chobj->printformat(MSGLEVEL_CLIENTNOTICE, 'end_of_bridgednames', $chobj->{visible_name}, scalar @nicks);
    }
   );

Irssi::command_set_options("bridgednames", "count");

Irssi::signal_register({'complete command ' => [qw[glistptr_char* Irssi::UI::Window string string intptr]]});

sub complete_cmd_addmodify ($cl, $win, $word, $start, $ws) {
    my $line = Irssi::parse_special('$L');
    $line =~ s/\s+$//;
    utf8_on($line, $start, $word);
    my @line = split ' ', $line;
    my $exists = defined find_bot($line[-1]);
    my @start = split ' ', $start;
    if ($exists && @start) {
	my $pattern = lc $start[-1] eq '-pattern';
	my $bot = lc $start[-1] eq '-bot';
	if (!length $word && ($pattern || $bot)) {
	    my $bot_def = $bot_defs{find_bot($line[-1])};
	    if ($pattern) {
		unshift @$cl, quote(pattern_to_str($bot_def->{pattern}));
	    } elsif ($bot) {
		unshift @$cl, $bot_def->{bot};
	    }
	}
    }
    if (!$exists) {
	push @$cl, grep { /^\Q$word/i } sort keys %bot_defs;
    }
}

sub complete_cmd_remove ($cl, $win, $word, $start, $ws) {
    unless (length $start) {
	utf8_on($word);
	push @$cl, grep { /^\Q$word/i } sort keys %bot_defs;
    }
}

sub complete_cmd_channel_addset ($cl, $win, $word, $start, $ws) {
    utf8_on($start, $word);
    my @start = split ' ', $start;
    my %seen = map { $_ => 1 } @start;
    push @$cl, grep { /^\Q$word/i }
	grep { !$seen{$_}++ }
	sort keys %bot_defs;
    push @$cl, grep { /^\Q$word/i }
	grep { !$seen{$_}++ }
	map { utf8_on($_) }
	map { $_->{visible_name} }
	grep { $_->isa('Irssi::Irc::Channel') }
	map { ( $_->items ) } Irssi::windows;
    return;
}

sub complete_cmd_channel_remove ($cl, $win, $word, $start, $ws) {
    my $line = Irssi::parse_special('$L');
    $line =~ s/\s+$//;
    utf8_on($line, $start, $word);
    my @line = split ' ', $line;
    my $exists = defined find_channel($line[-1]);
    my @start = split ' ', $start;
    my %seen = map { $_ => 1 } @start;
    my @bots;
    if ($exists) {
	@bots = @{ $channel_defs{find_channel($line[-1])} };
    } else {
	@bots = sort keys %bot_defs;
    }
    unless ($exists) {
	push @$cl, grep { /^\Q$word/i }
	    grep { defined find_channel($_) }
	    (map { utf8_on($_) }
	     map { $_->{visible_name} }
	     grep { $_->isa('Irssi::Irc::Channel') }
	     map { ( $_->items ) } Irssi::windows),
	     sort keys %channel_defs;
    }
    push @$cl, grep { /^\Q$word/i }
	grep { !$seen{$_}++ }
	@bots;
    return;
}

Irssi::signal_add({
    "complete command bridgebots add"		 => \&complete_cmd_addmodify,
    "complete command bridgebots modify"	 => \&complete_cmd_addmodify,
    "complete command bridgebots remove"	 => \&complete_cmd_remove,
    "complete command bridgebots channel add"	 => \&complete_cmd_channel_addset,
    "complete command bridgebots channel set"	 => \&complete_cmd_channel_addset,
    "complete command bridgebots channel remove" => \&complete_cmd_channel_remove,
});

Irssi::command_bind_last(
    'help' => sub ($args, $server, $witem) {
	if ($args =~ /^binfo *$/i) {
	    print CLIENTCRAP <<HELP
%9Syntax:%9

BINFO <nick>

%9Description:%9

    Show information about nick, including if it is a fake nick added
    by bridgebots.

%9Example:%9

    /BINFO mike

%9See also:%9 WHOIS, BRIDGEDNAMES
HELP
	} elsif ($args =~ /^bridgednames *$/i) {
	    print CLIENTCRAP <<HELP
%9Syntax:%9

BRIDGEDNAMES [-count] [<channel>]

%9Parameters:%9

    -count:      Only display the total number of bridged names in the channel.

%9Description:%9

    List bridged names in a channel.

%9Examples:%9

    /BRIDGEDNAMES
    /BRIDGEDNAMES -count
    /BRIDGEDNAMES #my_channel

%9See also:%9 NAMES, BRIDGEBOTS, BINFO
HELP
	} elsif ($args =~ /^bridgebots *$/i) {
	    print CLIENTCRAP <<HELP
%9Syntax:%9

BRIDGEBOTS
BRIDGEBOTS ADD|MODIFY [-bot <mask>] [-pattern <pattern>] [-privmsg] [-notice] [-multiline] <botname>
BRIDGEBOTS REMOVE <botname>
BRIDGEBOTS CHANNEL ADD|SET [-before <botname>] [-after <botname>] <botname...> <channel>
BRIDGEBOTS CHANNEL REMOVE <botname...> <channel>

%9Parameters:%9

    ADD:             Add a bridge-bot definition.
    MODIFY:          Modify an existing bridge-bot definition.
    REMOVE:          Remove a bridge-bot definition.
    CHANNEL:         List the channels with assigned bridge-bot definitions.
    CHANNEL ADD:     Add a bridge-bot definition to a channel.
    CHANNEL REMOVE:  Remove a bridge-bot definition from a channel.
    CHANNEL SET:     Set the bridge-bot definitions of a channel.

    -bot:            The mask (nick!user\@host) to identify the bridge-bot.
    -pattern:        A regular expression to extract and remove the
                     relay nick. It %Imust%I contain a (?<relaynick>...)
                     capture group. It can optionally contain a
                     (?<realname>...) capture group.
    -privmsg:        Enable processing for IRC PRIVMSG commands (also
                     known as PUBLIC messages to the channel).
    -notice:         Enable processing for IRC NOTICE commands.
    -multiline:      Enable multiline nick repeat.
    -before|-after:  Evaluate this bridge-bot definition before or
                     after another bridge-bot definition in a channel.

%9Description:%9

    Manage bridge-bots.  Without arguments, it will list the current definitions.

%9Examples:%9

    /BRIDGEBOTS ADD -bot discordbot!~bridge\@* -pattern (`(?<relaynick>.*?)`)? -privmsg -multiline my_discord_bridge
    /BRIDGEBOTS CHANNEL ADD my_discord_bridge #my_channel
    /BRIDGEBOTS CHANNEL REMOVE my_discord_bridge #my_channel
    /BRIDGEBOTS MODIFY -noprivmsg my_discord_bridge

%9See also:%9 BRIDGEDNAMES
HELP
	}
    });

sub init {
    update_formats();
    update_settings();
    load_config();
    parse_config();
}

init();

# Changelog
# =========
# 0.3
# - run bridgebots signal first
# - remove some legacy code
# - add $bridged_or_cumode expando
# 0.2
# - some unicode fixes
# - do not part the wrong users
#
# 0.1
# - initial release
