#
# For the full matterircd_complete experience, your matterircd.toml
# should have SuffixContext=true, ThreadContext="mattermost", and
# Unicode=true.
#
# Add it to ~/.irssi/scripts/autorun, or:
#
#   /script load ~/.irssi/scripts/matterircd_complete.pl
#   /set matterircd_complete_networks <...>
#
# NOTE: It is important to set which networks to enable plugin for per
# above ^.
#
# Bind message/thread ID completion to a key to make it easier to
# reply to threads:
#
#   /bind ^G /message_thread_id_search
#
# Also bind to insert nicknames:
#
#   /bind ^F /nicknames_search
#
# (Or pick your own shortcut keys to bind to).
#
# Then:
#
#   Ctrl+g - Insert latest message/thread ID.
#   Ctrl+c - Abort inserting message/thread ID. Also clears existing.
#
#   @@+TAB to tab auto-complete message/thread ID.
#   @ +TAB to tab auto-complete IRC nick. Active users appear first.
#
# By default, message/thread IDs are shortened from 26 characters to
# first few (default 5). It is also grayed out to try reduce noise and
# make it easier to read conversations. To disable this use:
#
#   /set matterircd_complete_shorten_message_thread_id 0
#
# Use the dump commands to show the contents of the cache:
#
#   /matterircd_complete_msgthreadid_cache_dump
#   /matterircd_complete_nick_cache_dump
#
# (You can bind these to keys).
#
# To increase or decrease the size of the cache, use:
#
#   /set matterircd_complete_message_thread_id_cache_size 50
#   /set matterircd_complete_nick_cache_size 20
#
# To ignore specific nicks in autocomplete:
#
#   /set matterircd_complete_nick_ignore somebot anotherbot

use strict;
use warnings;
use experimental 'smartmatch';

require Irssi::TextUI;
require Irssi;

# Enable for debugging purposes only.
# use Data::Dumper;

our $VERSION = '2.10';  # 1118e8c
our %IRSSI = (
    name        => 'Matterircd Tab Auto Complete',
    description => 'Adds tab completion for Matterircd message threads',
    authors     => 'Haw Loeung',
    contact     => 'hloeung/Freenode',
    license     => 'GPL',
);

my $KEY_CTRL_C = 3;
my $KEY_CTRL_U = 21;
my $KEY_ESC    = 27;
my $KEY_RET    = 13;
my $KEY_SPC    = 32;
my $KEY_B      = 66;
my $KEY_O      = 79;

Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_networks', '');
Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_nick_ignore', '');
Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_channel_dont_ignore', '');


sub _wi_print {
    my ($wi, $msg) = @_;

    if ($wi) {
        $wi->print($msg);
    } else {
        Irssi::print($msg);
    }
}

#==============================================================================
my %color_config;

# Taken from nickcolor_expando irssi script
# These are all the colors, sorted by main color class
# To display and select colors you want/want to avoid based on your background, use /cubes_text from cubes.pl
my @all_colors = (
    qw[20 30 40 50 04 66 0C 61 60 67 6L], # RED
    qw[37 3D 36 4C 46 5C 56 6C 6J 47 5D 6K 6D 57 6E 5E 4E 4K 4J 5J 4D 5K 6R], # ORANGE
    qw[3C 4I 5I 6O 6I 06 4O 5O 3U 0E 5U 6U 6V 6P 6Q 6W 5P 4P 4V 4W 5W 4Q 5Q 5R 6Y 6X], # YELLOW
    qw[26 2D 2C 3I 3O 4U 5V 2J 3V 3P 3J 5X], # YELLOW-GREEN
    qw[16 1C 2I 2U 2O 1I 1O 1V 1P 02 0A 1U 2V 4X], # GREEN
    qw[1D 1J 1Q 1W 1X 2Y 2S 2R 3Y 3Z 3S 3R 2K 3K 4S 5Z 5Y 4R 3Q 2Q 2X 2W 3X 3W 2P 4Y], # GREEN-TURQUOIS
    qw[17 1E 1L 1K 1R 1S 03 1M 1N 1T 0B 1Y 1Z 2Z 4Z], # TURQUOIS
    qw[28 2E 18 1F 19 1G 1A 1B 1H 2N 2H 09 3H 3N 2T 3T 2M 2G 2A 2F 2L 3L 3F 4M 3M 3G 29 4T 5T], # LIGHT-BLUE
    qw[22 33 44 0D 45 5B 6A 5A 5H 3B 4H 3A 4G 39 4F 6S 6T 5L 5N], # VIOLET
    qw[21 32 42 53 63 52 43 34 35 55 65 6B 4B 4A 48 5G 6H 5M 6M 6N], # PINK
    qw[38 31 05 64 54 41 51 62 69 68 59 5F 6F 58 49 6G], # ROSE
    qw[11 12 23 25 24 13 14 01 15 2B 4N], # DARK-BLUE
    qw[7A 00 10 7B 7C 7D 7E 7G 7F], # DARK-GRAY
    qw[7H 7I 27 7K 7J 08 7L 3E 7O 7Q 7N 7M 7P], # GRAY
    qw[7S 7T 7R 4L 7W 7U 7V 5S 07 7X 6Z 0F], # LIGHT-GRAY
);
# These are the colors unwanted with a dark theme
my @dark_theme_unwanted = (
    qw[11 12 23 25 24 13 14 01 15 2B 4N], # DARK-BLUE
    qw[7A 00 10 7B 7C 7D 7E 7G 7F], # DARK-GRAY
    qw[7H 7I 27 7K 7J 08 7L 3E 7O 7Q 7N 7M 7P], # GRAY
    qw[7S 7T 7R 4L 7W 7U 7V 5S 07 7X 6Z 0F], # LIGHT-GRAY
);
# These are the colors unwanted with a light theme
my @solarized_light_theme_unwanted = (
    qw[4U 4V 4W 4X 4Y 4Z 5U 5V 5W 5X 5Y 5Z 6U 6V 6W 6X 6Y 6Z 6O 6P 6Q 6R 6S 6T], # too light flashy colors
    qw[7T 7U 7V 7W 7X 07 7R 7S 7T 7U 7V 7W 7X 7B 7C 7D 7E 7F 7H 7I 7J 7K 7M 7N 7O 7P], # too light grayscales
    qw[5S 4L 7Q 5T 4T 4M 5M 6M 6N 1Z 2Z 1Y 2X 2W 3X 3W 1U 2V 1X 2Y 3V 3Z 3Y 2U], # hand picked too light + redundant
);

Irssi::settings_add_int('matterircd_complete', 'matterircd_complete_thread_id_color', -1);
# Default color theme to none, so we use all available colors.
Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_thread_id_color_theme', '');
Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_thread_id_allow_bold', 0);
Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_thread_id_allow_italic', 0);
Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_thread_id_allow_underline', 0);
# Allowed colors will be applied first
# These can be a list of 20 30 40 50 5F colors, or without spaces 203040505F
Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_thread_id_allowed_colors', '');
Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_thread_id_unwanted_colors', '');
$color_config{'color_theme'} = '';
$color_config{'allowed_colors'} = '';
$color_config{'unwanted_colors'} = '';
# Initialize
my @thread_id_selected_colors = ();

# Rely on message/thread IDs stored in message cache so we can shorten
# to save on screen real-estate.
Irssi::settings_add_int('matterircd_complete',  'matterircd_complete_shorten_message_thread_id', 5);
Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_shorten_message_thread_id_hide_prefix', 1);
Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_override_reply_prefix', '↪');

# Taken from nickcolor_expando irssi script and adapted for our use
sub xcolor_to_irssi {
    # Set to foreground xcolor
    my $c = "X".$_[0];
    my @ext_colour_off = (
    '.', '-', ',',
    '+', "'", '&',
    );
    if ($c =~ /^(X)(?:0([[:xdigit:]])|([1-6])(?:([0-9])|([a-z]))|7([a-x]))$/i) {
        my $bg = $1 eq 'x';
        my $col = defined $2 ? hex $2
            : defined $6 ? 232 + (ord lc $6) - (ord 'a')
            : 16 + 36 * ($3 - 1) + (defined $4 ? $4 : 10 + (ord lc $5) - (ord 'a'));
        if ($col < 0x10) {
            my $chr = chr $col + ord '0';
            return "\cD" . ($bg ? "/$chr" : "$chr/");
        }
        else {
            return "\cD" . $ext_colour_off[($col - 0x10) / 0x50 + $bg * 3] . chr (($col - 0x10) % 0x50 - 1 + ord '0');
        }
    } else {
        return $c;
    }
}

sub get_thread_format {
    my ($str) = @_;
    my @nums = (0..9,'a'..'z');
    my $chr=join('',@nums);
    my %nums = map { $nums[$_] => $_ } 0..$#nums;
    my $n = 0;
    $str = lc $str;
    foreach ($str =~ /[$chr]/g) {
        $n += $nums{$_} * 36;
    }
    my @colors = @thread_id_selected_colors;
    my $color_count = @colors;

    # We have normal, bold, italic, underline
    my $allow_bold = Irssi::settings_get_bool('matterircd_complete_thread_id_allow_bold');
    my $allow_italic = Irssi::settings_get_bool('matterircd_complete_thread_id_allow_italic');
    my $allow_underline = Irssi::settings_get_bool('matterircd_complete_thread_id_allow_underline');
    my @classes_prepend;
    push @classes_prepend, "\x02" if $allow_bold;
    push @classes_prepend, "\x1d" if $allow_italic;
    push @classes_prepend, "\x1f" if $allow_underline;
    my $classes = 1 + @classes_prepend;
    $n = $n % $color_count*$classes;
    my $random = $n;
    my $prepend = "";
    if ($classes == 4 and $n >= $color_count*3) {
        $n -= $color_count*3;
        $prepend = $classes_prepend[2];
    } elsif ($classes ge 3 and $n >= $color_count*2) {
        $n -= $color_count*2;
        $prepend = $classes_prepend[1];
    } elsif ($classes ge 2 and $n >= $color_count) {
        $n -= $color_count;
        $prepend = $classes_prepend[0];
    }
    $n = $colors[$n-1];
    return $n, $prepend;
}

sub thread_color {
    my ($str) = @_;
    my ($n, $prepend) = get_thread_format($str);
    # Pick the color in the allowed_color list.
    # n should be comprised between 1 and the array length.
    $n = xcolor_to_irssi($n);
    $n = "$prepend\x03$n";
    return $n;
}
sub cmd_matterircd_complete_thread_id_get_color {
    my ($data, $server, $wi) = @_;
    my ($color, $prepend) = get_thread_format($_[0]);
    my $n = xcolor_to_irssi($color);
    _wi_print($wi, "Thread color for $prepend\x03$n$_[0]\x0f is $color");
}
Irssi::command_bind('matterircd_complete_thread_id_get_color', 'cmd_matterircd_complete_thread_id_get_color');

sub update_msgthreadid {
    my($server, $msg, $nick, $address, $target) = @_;

    return unless Irssi::settings_get_int('matterircd_complete_shorten_message_thread_id');
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    my $prefix = '';
    my $msgthreadid = '';
    my $msgpostid = '';
    my $reply_prefix = Irssi::settings_get_str('matterircd_complete_override_reply_prefix');

    if ($msg =~ s/\[(->|↪)?\@\@([0-9a-z]{26})(?:,\@\@([0-9a-z]{26}))?\]/\@\@PLACEHOLDER\@\@/) {
        $prefix = $reply_prefix ? $reply_prefix : $1 if $1;
        $msgthreadid = $2;
        $msgpostid = $3 ? $3 : '';
    }
    return unless $msgthreadid;

    my $thread_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
    if ($thread_color == -1) {
        $thread_color = thread_color($msgthreadid);
    } else {
        $thread_color = "\x03${thread_color}";
    }

    # Show that message is reply to a thread. (backwards compatibility
    # when matterircd doesn't show reply)
    if ((not $prefix) && ($msg =~ /\(re \@.*\)/)) {
        $prefix = $reply_prefix;
    }

    if (not Irssi::settings_get_bool('matterircd_complete_shorten_message_thread_id_hide_prefix')) {
        $prefix = "${prefix}\@\@";
    }

    my $len = Irssi::settings_get_int('matterircd_complete_shorten_message_thread_id');
    if ($len < 25) {
        # Shorten to length configured. We use unicode ellipsis (...)
        # here to both allow word selection to just select parts of
        # the message/thread ID when copying & pasting and save on
        # screen real estate.
        $msgthreadid = substr($msgthreadid, 0, $len) . '…';
        if ($msgpostid ne '') {
            $msgpostid = substr($msgpostid, 0, $len) . '…';
        }
    }
    if ($msgpostid eq '') {
        $msg =~ s/\@\@PLACEHOLDER\@\@/${thread_color}[${prefix}${msgthreadid}]\x0f/;
    } else {
        $msg =~ s/\@\@PLACEHOLDER\@\@/${thread_color}[${prefix}${msgthreadid},${msgpostid}]\x0f/;
    }

    Irssi::signal_continue($server, $msg, $nick, $address, $target);
}
Irssi::signal_add_last('message irc action', 'update_msgthreadid');
Irssi::signal_add_last('message irc notice', 'update_msgthreadid');
Irssi::signal_add_last('message private', 'update_msgthreadid');
Irssi::signal_add_last('message public', 'update_msgthreadid');

sub cache_store {
    my ($cache_ref, $item, $cache_size) = @_;

    return unless $item ne '';

    my $changed = 0;
    if ((@$cache_ref[0]) && (@$cache_ref[0] eq $item)) {
        return $changed;
    }
    $changed = 1;

    # We want to reduce duplicates by removing them currently in the
    # per-channel cache. But as a trade off in favor of
    # speed/performance, rather than traverse the entire per-channel
    # cache, we cap/limit it.
    my $limit = 16;
    my $max = ($#$cache_ref < $limit)? $#$cache_ref : $limit;
    for my $i (0 .. $max) {
        if ((@$cache_ref[$i]) && (@$cache_ref[$i] eq $item)) {
            splice(@$cache_ref, $i, 1);
        }
    }

    unshift(@$cache_ref, $item);
    if (($cache_size > 0) && (scalar(@$cache_ref) > $cache_size)) {
        pop(@$cache_ref);
    }

    return $changed;
}


#==============================================================================

# Adds tab-complete or keybinding insertion of messages/threads
# seen. This makes it easier for replying directly to threads in
# Mattermost or creating new threads.


my %MSGTHREADID_CACHE;
Irssi::settings_add_int('matterircd_complete', 'matterircd_complete_message_thread_id_cache_size', 50);
sub cmd_matterircd_complete_msgthreadid_cache_dump {
    my ($data, $server, $wi) = @_;

    if (not $data) {
        return unless ref $wi and ($wi->{type} eq 'CHANNEL' or $wi->{type} eq 'QUERY');
    }

    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    my $channel = $data ? $data : $wi->{name};
    # Remove leading and trailing whitespace.
    $channel =~ tr/ 	//d;

    _wi_print($wi, "${channel}: Message/Thread ID cache");

    if ((not exists($MSGTHREADID_CACHE{$channel})) || (scalar @{$MSGTHREADID_CACHE{$channel}} == 0)) {
        _wi_print($wi, "${channel}: Empty");
        return;
    }

    foreach my $msgthread_id (@{$MSGTHREADID_CACHE{$channel}}) {
        _wi_print($wi, "${channel}: ${msgthread_id}");
    }
    _wi_print($wi, "${channel}: Total: " . scalar @{$MSGTHREADID_CACHE{$channel}});
};
Irssi::command_bind('matterircd_complete_msgthreadid_cache_dump', 'cmd_matterircd_complete_msgthreadid_cache_dump');

my $MSGTHREADID_CACHE_SEARCH_ENABLED = 0;
my $MSGTHREADID_CACHE_INDEX = 0;
sub cmd_message_thread_id_search {
    my ($data, $server, $wi) = @_;

    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
    return unless ref $wi and ($wi->{type} eq 'CHANNEL' or $wi->{type} eq 'QUERY');
    return unless exists($MSGTHREADID_CACHE{$wi->{name}});

    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    $MSGTHREADID_CACHE_SEARCH_ENABLED = 1;
    my $msgthreadid = $MSGTHREADID_CACHE{$wi->{name}}[$MSGTHREADID_CACHE_INDEX];
    $MSGTHREADID_CACHE_INDEX += 1;
    if ($MSGTHREADID_CACHE_INDEX > $#{$MSGTHREADID_CACHE{$wi->{name}}}) {
        # Cycle back to the start.
        $MSGTHREADID_CACHE_INDEX = 0;
    }

    if ($msgthreadid) {
        # Save input text.
        my $input = Irssi::parse_special('$L');
        # Remove existing thread.
        $input =~ s/^@@(?:[0-9a-z]{26}|[0-9a-f]{3}) //;
        # Insert message/thread ID from cache.
        Irssi::gui_input_set_pos(0);
        Irssi::gui_input_set("\@\@${msgthreadid} ${input}");
    }
};
Irssi::command_bind('message_thread_id_search', 'cmd_message_thread_id_search');

my $ESC_PRESSED = 0;
my $O_PRESSED   = 0;
sub signal_gui_key_pressed_msgthreadid {
    my ($key) = @_;

    return unless $MSGTHREADID_CACHE_SEARCH_ENABLED;

    my $server = Irssi::active_server();
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    if (($key == $KEY_RET) || ($key == $KEY_CTRL_U)) {
        $MSGTHREADID_CACHE_INDEX = 0;
        $MSGTHREADID_CACHE_SEARCH_ENABLED = 0;

        $ESC_PRESSED = 0;
        $O_PRESSED = 0;
    }

    # Cancel/abort, so remove thread stuff.
    elsif ($key == $KEY_CTRL_C) {
        my $input = Irssi::parse_special('$L');

        # Remove the Ctrl+C character.
        $input =~ tr///d;

        my $pos = 0;
        if ($input =~ s/^(@@(?:[0-9a-z]{26}|[0-9a-f]{3}) )//) {
            $pos = Irssi::gui_input_get_pos() - length($1);
        }

        # We also want to move the input position back one for Ctrl+C
        # char.
        $pos = $pos > 0 ? $pos - 1 : 0;

        # Replace the text in the input box with our modified version,
        # then move cursor positon to where it was without the
        # message/thread ID.
        Irssi::gui_input_set($input);
        Irssi::gui_input_set_pos($pos);

        $MSGTHREADID_CACHE_INDEX = 0;
        $MSGTHREADID_CACHE_SEARCH_ENABLED = 0;

        $ESC_PRESSED = 0;
        $O_PRESSED = 0;
    }

    # For 'down arrow', it's a sequence of ESC + O + B.
    elsif ($key == $KEY_ESC) {
        $ESC_PRESSED = 1;
    }
    elsif ($key == $KEY_O) {
        $O_PRESSED = 1;
    }
    elsif ($key == $KEY_B && $O_PRESSED && $ESC_PRESSED) {
        $MSGTHREADID_CACHE_INDEX = 0;
        $MSGTHREADID_CACHE_SEARCH_ENABLED = 0;

        $ESC_PRESSED = 0;
        $O_PRESSED = 0;
    }
    # Reset sequence on any other keys pressed.
    elsif ($O_PRESSED || $ESC_PRESSED) {
        $ESC_PRESSED = 0;
        $O_PRESSED = 0;
    }
};
Irssi::signal_add_last('gui key pressed', 'signal_gui_key_pressed_msgthreadid');

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

    # We only want to tab-complete message/thread if this is the first
    # word on the line.
    return if $linestart;
    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
    return if (substr($word, 0, 1) eq '@' and substr($word, 0, 2) ne '@@');
    return unless $window->{active} and ($window->{active}->{type} eq 'CHANNEL' || $window->{active}->{type} eq 'QUERY');
    return unless exists($MSGTHREADID_CACHE{$window->{active}->{name}});

    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$window->{active_server}->{chatnet}};

    if (substr($word, 0, 2) eq '@@') {
        $word = substr($word, 2);
    }

    foreach my $msgthread_id (@{$MSGTHREADID_CACHE{$window->{active}->{name}}}) {
        if ($msgthread_id =~ /^\Q$word\E/) {
            push(@$complist, "\@\@${msgthread_id}");
        }
    }
};
Irssi::signal_add_last('complete word', 'signal_complete_word_msgthread_id');

my $MSGTHREADID_CACHE_STATS = 0;
sub cache_msgthreadid {
    my($server, $msg, $nick, $address, $target) = @_;

    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    my @msgids = ();

    my @ignore_nicks = split(/\s+/, Irssi::settings_get_str('matterircd_complete_nick_ignore'));
    # Ignore nicks configured to be ignored such as bots.
    if ($nick ~~ @ignore_nicks) {
        # But not if the channel is in matterircd_complete_channel_dont_ignore.
        my @channel_dont_ignore = split(/\s+/, Irssi::settings_get_str('matterircd_complete_channel_dont_ignore'));
        if ($target !~ @channel_dont_ignore) {
            return;
        }
    }

    # We also want to ignore reactions as we can't reply to those
    # directly if they're to a message in a thread.
    if ($msg =~ /(?:added|removed) reaction:/) {
        return;
    }

    # Mattermost message/thread IDs.
    if ($msg =~ /\[(?:->|↪)?\@\@([0-9a-z]{26})(?:,\@\@([0-9a-z]{26}))?\]/) {
        my $msgthreadid = $1;
        my $msgpostid = $2 ? $2 : '';

        if ($msgpostid ne '') {
            push(@msgids, $msgpostid);
        }
        push(@msgids, $msgthreadid);
    }
    # matterircd generated 3-letter hexadecimal.
    elsif ($msg =~ /(?:^\[([0-9a-f]{3})\])|(?:\[([0-9a-f]{3})\]\s*$)/) {
        push(@msgids, $1 ? $1 : $2);
    }
    # matterircd generated 3-letter hexadecimal replying to threads.
    elsif ($msg =~ /(?:^\[[0-9a-f]{3}->([0-9a-f]{3})\])|(?:\[[0-9a-f]{3}->([0-9a-f]{3})\]\s*$)/) {
        push(@msgids, $1 ? $1 : $2);
    }
    else {
        return;
    }

    my $key;
    if (substr($target, 0, 1) eq '#') {
        # It's a channel, so use $target
        $key = $target;
    } else {
        # It's a private query so use $nick
        $key = $nick
    }

    my $cache_size = Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
    for my $msgid (@msgids) {
        if (cache_store(\@{$MSGTHREADID_CACHE{$key}}, $msgid, $cache_size)) {
            $MSGTHREADID_CACHE_INDEX = 0;
            stats_increment(\$MSGTHREADID_CACHE_STATS);
        }
    }
}
Irssi::signal_add('message irc action', 'cache_msgthreadid');
Irssi::signal_add('message irc notice', 'cache_msgthreadid');
Irssi::signal_add('message private', 'cache_msgthreadid');
Irssi::signal_add('message public', 'cache_msgthreadid');

Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_reply_msg_thread_id_at_start', 1);

sub signal_message_own_public_msgthreadid {
    my($server, $msg, $target) = @_;

    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    if ($msg !~ /^@@((?:[0-9a-z]{26})|(?:[0-9a-f]{3}))/) {
        return;
    }
    my $msgid = $1;

    my $cache_size = Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
    if (cache_store(\@{$MSGTHREADID_CACHE{$target}}, $msgid, $cache_size)) {
        $MSGTHREADID_CACHE_INDEX = 0;
        stats_increment(\$MSGTHREADID_CACHE_STATS);
    }

    my $msgthreadid = $1;

    my $thread_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
    if ($thread_color == -1) {
        $thread_color = thread_color($msgthreadid);
    } else {
        $thread_color = "\x03${thread_color}";
    }

    my $len = Irssi::settings_get_int('matterircd_complete_shorten_message_thread_id');
    if ($len < 25) {
        # Shorten to length configured. We use unicode ellipsis (...)
        # here to both allow word selection to just select parts of
        # the message/thread ID when copying & pasting and save on
        # screen real estate.
        $msgthreadid = substr($msgid, 0, $len) . "…";
    }

    my $reply_prefix = Irssi::settings_get_str('matterircd_complete_override_reply_prefix');
    if (Irssi::settings_get_bool('matterircd_complete_reply_msg_thread_id_at_start')) {
        $msg =~ s/^@@[0-9a-z]{26} /${thread_color}[${reply_prefix}${msgthreadid}]\x0f /;
    } else {
        $msg =~ s/^@@[0-9a-z]{26} //;
        $msg =~ s/$/ ${thread_color}[${reply_prefix}${msgthreadid}]\x0f/;
    }

    Irssi::signal_continue($server, $msg, $target);
};
Irssi::signal_add_last('message own_public', 'signal_message_own_public_msgthreadid');

sub signal_message_own_private {
    my($server, $msg, $target, $orig_target) = @_;

    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    if ($msg !~ /^@@((?:[0-9a-z]{26})|(?:[0-9a-f]{3}))/) {
        return;
    }
    my $msgid = $1;

    my $cache_size = Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
    if (cache_store(\@{$MSGTHREADID_CACHE{$target}}, $msgid, $cache_size)) {
        $MSGTHREADID_CACHE_INDEX = 0;
        stats_increment(\$MSGTHREADID_CACHE_STATS);
    }

    my $msgthreadid = $1;

    my $thread_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
    if ($thread_color == -1) {
        $thread_color = thread_color($msgthreadid);
    } else {
        $thread_color = "\x03${thread_color}";
    }

    my $len = Irssi::settings_get_int('matterircd_complete_shorten_message_thread_id');
    if ($len < 25) {
        # Shorten to length configured. We use unicode ellipsis (...)
        # here to both allow word selection to just select parts of
        # the message/thread ID when copying & pasting and save on
        # screen real estate.
        $msgthreadid = substr($msgid, 0, $len) . "…";
    }

    my $reply_prefix = Irssi::settings_get_str('matterircd_complete_override_reply_prefix');
    if (Irssi::settings_get_bool('matterircd_complete_reply_msg_thread_id_at_start')) {
        $msg =~ s/^@@[0-9a-z]{26} /${thread_color}[${reply_prefix}${msgthreadid}]\x0f /;
    } else {
        $msg =~ s/^@@[0-9a-z]{26} //;
        $msg =~ s/$/ ${thread_color}[${reply_prefix}${msgthreadid}]\x0f/;
    }

    Irssi::signal_continue($server, $msg, $target, $orig_target);
};
Irssi::signal_add_last('message own_private', 'signal_message_own_private');


#==============================================================================

# Adds tab-complete or keybinding insertion of nicknames for users in
# the current channel. Similar to irssi's builtin, recently active
# users/nicks will be first in the completion list.


my %NICKNAMES_CACHE;
Irssi::settings_add_int('matterircd_complete', 'matterircd_complete_nick_cache_size', 20);
sub cmd_matterircd_complete_nick_cache_dump {
    my ($data, $server, $wi) = @_;

    if (not $data) {
        return unless ref $wi and ($wi->{type} eq 'CHANNEL' or $wi->{type} eq 'QUERY');
    }

    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    my $channel = $data ? $data : $wi->{name};
    # Remove leading and trailing whitespace.
    $channel =~ tr/ 	//d;

    _wi_print($wi, "${channel}: Nicknames cache");

    if ((not exists($NICKNAMES_CACHE{$channel})) || (scalar @{$NICKNAMES_CACHE{$channel}} == 0)) {
        _wi_print($wi,"${channel}: Empty");
        return;
    }

    foreach my $nick (@{$NICKNAMES_CACHE{$channel}}) {
        _wi_print($wi, "${channel}: ${nick}");
    }
    _wi_print($wi, "${channel}: Total: " . scalar @{$NICKNAMES_CACHE{$channel}});
};
Irssi::command_bind('matterircd_complete_nick_cache_dump', 'cmd_matterircd_complete_nick_cache_dump');

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

    return if substr($word, 0, 2) eq '@@';
    return unless $window->{active} and $window->{active}->{type} eq 'CHANNEL';

    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$window->{active_server}->{chatnet}};

    if (substr($word, 0, 1) eq '@') {
        $word = substr($word, 1);
    }
    my $compl_char = Irssi::settings_get_str('completion_char');
    my $own_nick = $window->{active}->{ownnick}->{nick};
    my @ignore_nicks = split(/\s+/, Irssi::settings_get_str('matterircd_complete_nick_ignore'));

    # We need to store the results in a temporary array so we can
    # sort.
    my @tmp;
    foreach my $cur ($window->{active}->nicks()) {
        my $nick = $cur->{nick};
        # Ignore our own nick.
        if ($nick eq $own_nick) {
            next;
        }
        # Ignore nicks configured to be ignored such as bots.
        elsif ($nick ~~ @ignore_nicks) {
            next;
        }
        # Only those matching partial word.
        elsif ($nick =~ /^\Q$word\E/i) {
            push(@tmp, $nick);
        }
    }
    @tmp = sort @tmp;
    foreach my $nick (@tmp) {
        # Only add completion character on line start.
        if (not $linestart) {
            push(@$complist, "\@${nick}${compl_char}");
        } else {
            push(@$complist, "\@${nick}");
        }
    }

    return unless exists($NICKNAMES_CACHE{$window->{active}->{name}});

    # We use the populated cache so frequent and active users in
    # channel come before those idling there. e.g. In a channel where
    # @barryp talks more often, it will come before @barry-m. We also
    # want to make sure users are still in channel for those still in
    # the cache.
    foreach my $nick (reverse @{$NICKNAMES_CACHE{$window->{active}->{name}}}) {
        my $nick_compl;
        # Only add completion character on line start.
        if (not $linestart) {
            $nick_compl = "\@${nick}${compl_char}";
        } else {
            $nick_compl = "\@${nick}";
        }
        # Skip over if nick is already first in completion list.
        if ((scalar(@{$complist}) > 0) and ($nick_compl eq @{$complist}[0])) {
            next;
        }
        # Only add to completion list if user/nick is online and in channel.
        elsif (${nick} ~~ @tmp) {
            # Only add completion character on line start.
            if (not $linestart) {
                unshift(@$complist, "\@${nick}${compl_char}");
            } else {
                unshift(@$complist, "\@${nick}");
            }
        }
    }
};
Irssi::signal_add('complete word', 'signal_complete_word_nicks');

my $NICKNAMES_CACHE_STATS = 0;
sub cache_ircnick {
    my($server, $msg, $nick, $address, $target) = @_;

    return unless Irssi::settings_get_int('matterircd_complete_nick_cache_size');
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    my $cache_size = Irssi::settings_get_int('matterircd_complete_nick_cache_size');
    my @ignore_nicks = split(/\s+/, Irssi::settings_get_str('matterircd_complete_nick_ignore'));
    # Ignore nicks configured to be ignored such as bots.
    if ($nick !~ @ignore_nicks) {
        if (cache_store(\@{$NICKNAMES_CACHE{$target}}, $nick, $cache_size)) {
            stats_increment(\$NICKNAMES_CACHE_STATS);
        }
    }
}
Irssi::signal_add('message irc action', 'cache_ircnick');
Irssi::signal_add('message irc notice', 'cache_ircnick');
Irssi::signal_add('message public', 'cache_ircnick');

sub signal_message_own_public_nicks {
    my($server, $msg, $target) = @_;

    return unless Irssi::settings_get_int('matterircd_complete_nick_cache_size');
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    if ($msg !~ /^@([^@ \t:,\)]+)/) {
        return;
    }
    my $nick = $1;

    my $cache_size = Irssi::settings_get_int('matterircd_complete_nick_cache_size');
    # We want to make sure that the nick or user is still online and
    # in the channel.
    my $wi = $server->window_item_find($target);
    if (not defined $wi) {
        return;
    }
    foreach my $cur ($wi->nicks()) {
        if ($nick eq $cur->{nick}) {
            if (cache_store(\@{$NICKNAMES_CACHE{$target}}, $nick, $cache_size, 1)) {
                stats_increment(\$NICKNAMES_CACHE_STATS);
            }
            last;
        }
    }
};
Irssi::signal_add_last('message own_public', 'signal_message_own_public_nicks');

my @NICKNAMES_CACHE_SEARCH;
my $NICKNAMES_CACHE_SEARCH_ENABLED = 0;
my $NICKNAMES_CACHE_INDEX = 0;
sub cmd_nicknames_search {
    my ($data, $server, $wi) = @_;

    return unless ref $wi and $wi->{type} eq 'CHANNEL';

    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    my $own_nick = $wi->{ownnick}->{nick};
    my @ignore_nicks = split(/\s+/, Irssi::settings_get_str('matterircd_complete_nick_ignore'));

    @NICKNAMES_CACHE_SEARCH = ();
    foreach my $cur ($wi->nicks()) {
        my $nick = $cur->{nick};
        # Ignore our own nick.
        if ($nick eq $own_nick) {
            next;
        }
        # Ignore nicks configured to be ignored such as bots.
        elsif ($nick ~~ @ignore_nicks) {
            next;
        }
        push(@NICKNAMES_CACHE_SEARCH, $nick);
    }
    @NICKNAMES_CACHE_SEARCH = sort @NICKNAMES_CACHE_SEARCH;

    if (exists($NICKNAMES_CACHE{$wi->{name}})) {
        # We use the populated cache so frequent and active users in
        # channel come before those idling there. e.g. In a channel
        # where @barryp talks more often, it will come before
        # @barry-m.  We also want to make sure users are still in
        # channel for those still in the cache.
        foreach my $nick (reverse @{$NICKNAMES_CACHE{$wi->{name}}}) {
            # Skip over if nick is already first in completion list.
            if ((scalar(@NICKNAMES_CACHE_SEARCH) > 0) and ($nick eq $NICKNAMES_CACHE_SEARCH[0])) {
                next;
            }
            # Only add to completion list if user/nick is online and
            # in channel.
            elsif ($nick ~~ @NICKNAMES_CACHE_SEARCH) {
                unshift(@NICKNAMES_CACHE_SEARCH, $nick);
            }
        }
    }

    $NICKNAMES_CACHE_SEARCH_ENABLED = 1;
    my $nickname = $NICKNAMES_CACHE_SEARCH[$NICKNAMES_CACHE_INDEX];
    $NICKNAMES_CACHE_INDEX += 1;
    if ($NICKNAMES_CACHE_INDEX > $#NICKNAMES_CACHE_SEARCH) {
        # Cycle back to the start.
        $NICKNAMES_CACHE_INDEX = 0;
    }

    if ($nickname) {
        # Save input text.
        my $input = Irssi::parse_special('$L');
        my $compl_char = Irssi::settings_get_str('completion_char');
        # Remove any existing nickname and insert one from the cache.
        my $msgid = "";
        if ($input =~ s/^(\@\@(?:[0-9a-z]{26}|[0-9a-f]{3}) )//) {
            $msgid = $1;
        }
        $input =~ s/^\@[^${compl_char}]+$compl_char //;
        Irssi::gui_input_set_pos(0);
        Irssi::gui_input_set("${msgid}\@${nickname}${compl_char} ${input}");
    }
};
Irssi::command_bind('nicknames_search', 'cmd_nicknames_search');

sub signal_gui_key_pressed_nicks {
    my ($key) = @_;

    return unless $NICKNAMES_CACHE_SEARCH_ENABLED;

    my $server = Irssi::active_server();
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    if (($key == $KEY_RET) || ($key == $KEY_CTRL_U)) {
        $NICKNAMES_CACHE_INDEX = 0;
        $NICKNAMES_CACHE_SEARCH_ENABLED = 0;
        @NICKNAMES_CACHE_SEARCH = ();
    }

    # Cancel/abort, so remove current nickname.
    elsif ($key == $KEY_CTRL_C) {
        my $input = Irssi::parse_special('$L');

        # Remove the Ctrl+C character.
        $input =~ tr///d;

        my $compl_char = Irssi::settings_get_str('completion_char');
        my $pos = 0;
        if ($input =~ s/^(\@[^${compl_char}]+$compl_char )//) {
            $pos = Irssi::gui_input_get_pos() - length($1);
        }

        # We also want to move the input position back one for Ctrl+C
        # char.
        $pos = $pos > 0 ? $pos - 1 : 0;

        # Replace the text in the input box with our modified version,
        # then move cursor positon to where it was without the
        # current nickname.
        Irssi::gui_input_set($input);
        Irssi::gui_input_set_pos($pos);

        $NICKNAMES_CACHE_INDEX = 0;
        $NICKNAMES_CACHE_SEARCH_ENABLED = 0;
        @NICKNAMES_CACHE_SEARCH = ();
    }
};
Irssi::signal_add_last('gui key pressed', 'signal_gui_key_pressed_nicks');


#==============================================================================

# The replied cache keeps an index of messages/thread IDs that we've
# replied to then when others reply to those, it will insert our nick
# so that any further replies to these threads will be hilighted.


my %REPLIED_CACHE;
Irssi::settings_add_int('matterircd_complete', 'matterircd_complete_replied_cache_size', 50);
sub cmd_matterircd_complete_replied_cache_dump {
    my ($data, $server, $wi) = @_;

    if (not $data) {
        return unless ref $wi and ($wi->{type} eq 'CHANNEL' or $wi->{type} eq 'QUERY');
    }

    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    my $channel = $data ? $data : $wi->{name};
    # Remove leading and trailing whitespace.
    $channel =~ tr/ 	//d;

    _wi_print($wi, "${channel}: Replied cache");

    if ((not exists($REPLIED_CACHE{$channel})) || (scalar @{$REPLIED_CACHE{$channel}} == 0)) {
        _wi_print($wi, "${channel}: Empty");
        return;
    }

    foreach my $threadid (@{$REPLIED_CACHE{$channel}}) {
        _wi_print($wi, "${channel}: ${threadid}");
    }
    _wi_print($wi, "${channel}: Total: " . scalar @{$REPLIED_CACHE{$channel}});
};
Irssi::command_bind('matterircd_complete_replied_cache_dump', 'cmd_matterircd_complete_replied_cache_dump');

my $REPLIED_CACHE_STATS = 0;
sub cmd_matterircd_complete_replied_cache_clear {
    my ($data, $server, $wi) = @_;

    my $channel;
    my @msgids = ();
    my @args = ();
    if ($data) {
        @args = split(/\s+/, $data);
    }

    if (scalar(@args) == 0 || $args[0] eq '*') {
        stats_increment(\$REPLIED_CACHE_STATS);
        _wi_print($wi, "matterircd_complete replied cache cleared");
        return;
    }

    if (exists($REPLIED_CACHE{$args[0]}) || exists($REPLIED_CACHE{"#${args[0]}"})) {
        $channel = shift(@args);
        if (rindex($channel, "#", 0) == -1) {
            $channel = "#${channel}";
        }
    } elsif ($wi->{name}) {
        $channel = $wi->{name};
    } else {
        return;
    }
    @msgids = @args;

    if (scalar(@msgids) > 0) {
        foreach my $id (@msgids) {
            my $i = 0;
            if (rindex($id, "@@", 0) == 0) {
                $id = substr($id, 2);
            }
            foreach my $msgid (@{$REPLIED_CACHE{$channel}}) {
                if ($id eq $msgid) {
                    splice(@{$REPLIED_CACHE{$channel}}, $i, 1);
                    stats_increment(\$REPLIED_CACHE_STATS);
                    _wi_print($wi, "matterircd_complete replied cache removed ${id} from ${channel} cache");
                    last;
                }
                $i += 1;
            }
        }
    } else {
        @{$REPLIED_CACHE{$channel}} = ();
        stats_increment(\$REPLIED_CACHE_STATS);
        _wi_print($wi, "matterircd_complete replied cache cleared for channel ${channel}");
    }
};
Irssi::command_bind('matterircd_complete_replied_cache_clear', 'cmd_matterircd_complete_replied_cache_clear');

my $REPLIED_CACHE_CLEARED = 0;
Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_clear_replied_cache_on_away', 0);
sub signal_away_mode_changed {
    my ($server) = @_;

    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    # When you visit the web UI when marked away, it retriggers this
    # event. Let's avoid that.
    if (! $server->{usermode_away}) {
        $REPLIED_CACHE_CLEARED = 0;
    }

    if (Irssi::settings_get_bool('matterircd_complete_clear_replied_cache_on_away') && $server->{usermode_away} && (! $REPLIED_CACHE_CLEARED)) {
        %REPLIED_CACHE = ();
        $REPLIED_CACHE_CLEARED = 1;
        Irssi::print("matterircd_complete replied cache cleared");
    }
};
Irssi::signal_add('away mode changed', 'signal_away_mode_changed');

sub signal_message_own_public_replied {
    my($server, $msg, $target) = @_;

    return unless Irssi::settings_get_int('matterircd_complete_replied_cache_size');
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    if ($msg !~ /^@@((?:[0-9a-z]{26})|(?:[0-9a-f]{3}))/) {
        return;
    }
    my $msgid = $1;

    my $cache_size = Irssi::settings_get_int('matterircd_complete_replied_cache_size');
    if (cache_store(\@{$REPLIED_CACHE{$target}}, $msgid, $cache_size)) {
        stats_increment(\$REPLIED_CACHE_STATS);
    }
};
Irssi::signal_add('message own_public', 'signal_message_own_public_replied');

sub signal_message_public {
    my($server, $msg, $nick, $address, $target) = @_;

    return unless Irssi::settings_get_int('matterircd_complete_replied_cache_size');
    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};

    # For '/me' actions, it has trailing space so we need to use
    # \s* here.
    $msg =~ /\[(?:->|↪)?\@\@([0-9a-z]{26})[\],]/;
    my $msgthreadid = $1;
    return unless $msgthreadid;

    if ($msgthreadid ~~ @{$REPLIED_CACHE{$target}}) {
        # Add user's (or our own) nick for hilighting if not in
        # message and message not from us.
        if (($nick ne $server->{nick}) && ($msg !~ /\@$server->{nick}/)) {
            $msg =~ s/\(re (\@\S+): /(re \@$server->{nick}, $1: /;
        }
    }

    Irssi::signal_continue($server, $msg, $nick, $address, $target);
};
Irssi::signal_add('message public', 'signal_message_public');

# Remove an array's elements per their values
sub array_splice_values {
    my ($ar_ref, $uw_ref) = @_;
    my @array = @{$ar_ref};
    my @unwanted = @{$uw_ref};
    my %removals = map { $_ => 1 } @unwanted;
    my @keys = keys %removals;
    my @indices = grep { exists($removals{$array[$_]}) } 0..$#array;
    # Each time we remove index from @arr, the next correct index to delete will be reduced of $o
    my $o = 0;
    for (@indices) {
        splice(@array, $_-$o, 1);
        $o++;
    }
    return @array;
}

sub setup_colors {
    # Skip colors setup if we're using a fixed color
    my $fixed_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
    return if $fixed_color ne -1;

    my $allowed_colors = Irssi::settings_get_str('matterircd_complete_thread_id_allowed_colors');
    $allowed_colors = uc $allowed_colors;
    my $unwanted_colors = Irssi::settings_get_str('matterircd_complete_thread_id_unwanted_colors');
    $unwanted_colors = uc $unwanted_colors;
    my $color_theme = Irssi::settings_get_str('matterircd_complete_thread_id_color_theme');
    $color_theme = lc $color_theme;
    my @colors;

    if ($allowed_colors =~ /^[0-9A-Z]{2}( [0-9A-Z]{2})*$/) {
        @colors = split(' ', $allowed_colors);
        Irssi::print("[matterircd_complete] Setting allowed colors: @colors");
    } elsif ($allowed_colors =~ /^[0-9A-Z]{2}([0-9A-Z]{2})*$/ and length($allowed_colors) % 2 == 0) {
        @colors = ( $allowed_colors =~ m/../g );
        Irssi::print("[matterircd_complete] Setting allowed colors: @colors");
    } elsif (length($allowed_colors) != 0) {
        Irssi::print("[matterircd_complete] Ignoring matterircd_complete_thread_id_allowed_colors: invalid format ($allowed_colors)");
    } else {
        Irssi::print("[matterircd_complete] Setting allowed colors to all colors");
        @colors = @all_colors;
    }

    if (length($color_theme) != 0) {
        if ($color_theme eq "dark") {
            Irssi::print("[matterircd_complete] Removing colors incompatible with dark theme");
            @colors = array_splice_values(\@colors, \@dark_theme_unwanted);
        } elsif ($color_theme eq 'solarized-light') {
            Irssi::print("[matterircd_complete] Removing colors incompatible with solarized-light theme");
            @colors = array_splice_values(\@colors, \@solarized_light_theme_unwanted);
        } else {
            Irssi::print("[matterircd_complete] Ignoring unknown color theme $color_theme");
            Irssi::print("[matterircd_complete] Valid themes are dark, solarized-light");
        }
    }

    if ($unwanted_colors =~ /^[0-9A-Z]{2}( [0-9A-Z]{2})*$/) {
        my @unwanted = split(' ', $unwanted_colors);
        Irssi::print("[matterircd_complete] Removing unwanted colors");
        @colors = array_splice_values(\@colors, \@unwanted);
    } elsif ($unwanted_colors =~ /^[0-9A-Z]{2}([0-9A-Z]{2})*$/ and length($unwanted_colors) % 2 == 0) {
        my @unwanted = ($unwanted_colors =~ m/../g);
        Irssi::print("[matterircd_complete] Removing unwanted colors");
        @colors = array_splice_values(\@colors, \@unwanted);
    } elsif (length($unwanted_colors)) {
        Irssi::print("[matterircd_complete] Ignoring matterircd_complete_thread_id_unwanted_colors: invalid format ($unwanted_colors)");
    }

    if (@thread_id_selected_colors) {
        Irssi::print("[matterircd_complete] Config changed, existing threads might change colors!")
            if $allowed_colors ne $color_config{"allowed_colors"}
                    or $unwanted_colors ne $color_config{"unwanted_colors"}
                    or $color_theme ne $color_config{"color_theme"};
        $color_config{"allowed_colors"} = $allowed_colors;
        $color_config{"unwanted_colors"} = $unwanted_colors;
        $color_config{"color_theme"} = $color_theme;
    } else {
        Irssi::print("[matterircd_complete] Thread colors have been set per your config");
    }
    Irssi::print("[matterircd_complete] You can check colors in use with /matterircd_complete_thread_id_get_colors");
    @thread_id_selected_colors = @colors;
}
Irssi::signal_add('setup changed', 'setup_colors');
Irssi::signal_add('setup reread', 'setup_colors');

sub cmd_matterircd_complete_thread_id_get_colors {
    my ($data, $server, $wi) = @_;

    # Display a warning if we're using a fixed color
    my $fixed_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
    if ($fixed_color ne -1) {
        _wi_print($wi, "Thread_id_color is not set to -1");
        _wi_print($wi, "Threads will always take \x03${fixed_color}this color\x0f");
        return;
    }

    my $colors_text = "Selected colors: ";
    foreach (@thread_id_selected_colors) {
        my $n = xcolor_to_irssi($_);
        $colors_text .= "\x03$n$_";
    }
    $colors_text .= "\x0f";
    _wi_print($wi, $colors_text);
}
Irssi::command_bind('matterircd_complete_thread_id_get_colors', 'cmd_matterircd_complete_thread_id_get_colors');

Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_stats_output', 0);
sub stats_increment {
    my ($stats_ref) = @_;

    $$stats_ref += 1;

    # autosave.
    if (($$stats_ref % 100) == 0) {
        my $output_stats = Irssi::settings_get_bool('matterircd_complete_stats_output') ? "true" : "false";
        save_cache($output_stats);
    }
}

my $STARTUP_DATE = localtime();
sub stats_show {
    Irssi::print("[matterircd_complete] Started / loaded since ${STARTUP_DATE}");

    my $total = 0;
    my $entries;
    my $channels;

    my %cache = (
        'MSGTHREADID' => \%MSGTHREADID_CACHE,
        'NICKNAMES' => \%NICKNAMES_CACHE,
        'REPLIED' => \%REPLIED_CACHE,
        );

    my %stats = (
        'MSGTHREADID' => 0,
        'NICKNAMES' => 0,
        'REPLIED' => 0,
        );
    foreach my $key (sort keys %cache) {
        foreach my $channel (sort keys %{$cache{$key}}) {
            my $d = $cache{$key}->{$channel};
            if (scalar(@{$d}) == 0) {
                next;
            }
            $stats{$key} += scalar(@{$d});
        }
        $total += $stats{$key};
    }

    $entries = $stats{'MSGTHREADID'};
    $channels = keys %{$cache{'MSGTHREADID'}};
    Irssi::print("[matterircd_complete] ${entries} entries across ${channels} channels for msg/thread IDs cache (${MSGTHREADID_CACHE_STATS} updates)");

    $entries = $stats{'NICKNAMES'};
    $channels = keys %{$cache{'NICKNAMES'}};
    Irssi::print("[matterircd_complete] ${entries} entries across ${channels} channels for nicknames cache (${NICKNAMES_CACHE_STATS} updates)");

    $entries = $stats{'REPLIED'};
    $channels = keys %{$cache{'REPLIED'}};
    Irssi::print("[matterircd_complete] ${entries} entries across ${channels} channels for threads replied to cache (${REPLIED_CACHE_STATS} updates)");

    my $total_updates = $MSGTHREADID_CACHE_STATS + $NICKNAMES_CACHE_STATS + $REPLIED_CACHE_STATS;
    Irssi::print("[matterircd_complete] \x03%GSaved total of ${total} entries in the cache (${total_updates} total updates)…");
}
Irssi::command_bind('matterircd_complete_stats', 'stats_show');

my $CACHE_FILE = Irssi::get_irssi_dir() . '/matterircd_complete.cache';
my $exited;
sub save_cache {
    my ($output_stats) = @_;

    open(FH, '>', $CACHE_FILE) or do {
        Irssi::print("[matterircd_complete] \x03%RError saving matterircd_complete cache: $!")
            unless $exited;
        return;
    };

    my %cache = (
        'MSGTHREADID' => \%MSGTHREADID_CACHE,
        'REPLIED' => \%REPLIED_CACHE,
        );

    foreach my $key (sort keys %cache) {
        foreach my $channel (sort keys %{$cache{$key}}) {
            my $d = $cache{$key}->{$channel};
            my $entries = join(',', @{$d});
            if (scalar(@{$d}) == 0) {
                next;
            }
            print(FH "${key} ${channel} ${entries}\n");
        }
    }
    close(FH);

    # eq "" so show stats on /matterircd_complete_cache_save command.
    if ($output_stats eq "true" || $output_stats eq "") {
        stats_show();
    }
}
Irssi::command_bind('matterircd_complete_cache_save', 'save_cache');

sub load_cache {
    open(FH, '<', $CACHE_FILE) or return;

    my %cache = (
        'MSGTHREADID' => \%MSGTHREADID_CACHE,
        'REPLIED' => \%REPLIED_CACHE,
        );

    my $total = 0;
    while(<FH>) {
        chomp;
        my ($key, $channel, $entries) = split;
        my @d = split(',', $entries);
        $cache{$key}->{$channel} = \@d;
        $total += scalar(@d);
    }
    close(FH);

    Irssi::print("[matterircd_complete] \x03%GLoaded total of ${total} entries from disk cache…");
}

sub UNLOAD {
    return if $exited;
    exit_save();
}

sub exit_save {
    $exited = 1;
    save_cache("true")
}
Irssi::signal_add('gui exit', 'exit_save');

# Set up on load!
setup_colors();
load_cache();
