if (word(2 $loadinfo()) != [pf]) {
	load -pf $word(1 $loadinfo());
	return;
};

## We depend on 'reconnect_required' hook and '$serverctl(GET $server OPEN)',
## and '$serverctl(GET x NEXT_SERVER_IN_GROUP)' which are not available in
## previous versions.

if (info(i) < 1999 || J !~ [EPIC5*]) {
	xecho -b EPIC5 (rev. 1999) or newer is required;
	return;
};

load addset;

package reconnect;

## Reconnect script for EPIC5.
## Written by zlonix@efnet with help of hop@efnet, public domain.
## Help with testing by skered@efnet.
##
## Version: 1.0 (September, 2021)
##  - Initial roll-out
##
## Version 2.0 (Febrary, 2022)
##  - Major rewrite of script logic - now we don't use infinite timers,
##    but use re-scheduling - the timer schedules itself if appropriate.
##    Idea by hop@efnet.
##
## Version 3.0 (September, 2022)
##  - Uppon reconnect send one join line with all channels and keys instead
##    of bunch separate joins.

## This script do basic reconnection procedure for you, it hooks on newly
## implemented hook 'reconnect_required', it is thrown when network connection
## experience problems. For more details read UPDATES document.
##
## The script doesn't have complicated logic, for example if you fiddle with
## your windows while being in process of reconnection (killing them for
## example, or changing server association) unexpected things may happen,
## channels be joined not in correct windows, or not joined at all, be aware of
## that. Also don't change server refnums while in reconnect process - we rely
## on its consistency.
##
## Do not connect to more than one server per group. It's recommended to have
## sane ircII.servers file in your IRCLIB environment variable, with groups and
## all of that (on disconnect the script tries to reconnect to the server
## $auto_reconnect_retries times before trying next server in the group, looping
## over when tried the last one), also, note that Libera.chat supports supplying
## your password for the account upon connect, through password token in server
## description so you don't need to use sasl_auth script anymore. For details
## about server description and its tokens visit
## http://help.epicsol.org/server_description.
##
## If you use group <default> - reconnects will be made only to original server,
## no switching to other servers in the default group will happen.  Reconnection
## will unconditionally abort after $auto_reconnect_retries. To reconnect
## manually use /reconnect command or change the server with /server.
##
## Another limitation is that the script won't catch 'Connection refused', or
## any DNS errors, since it's not considered as action for 'reconnect_required'
## hook. It means, that if you start the client, and it can't connect to the
## specified server, reconnect won't happen.
##
## All of this has been sacrificed for simplicity of the implementation.
##
## If you encounter any bugs - please save the log with '/lastlog -file <path>'
## and report it to #epic@efnet.

## TODO
##
## Currently there is not much testing done with servers, which have round
## robin DNS, like irc.libera.chat.

addset auto_reconnect_delay int;
set auto_reconnect_delay 75;

addset auto_reconnect_join_delay int;
set auto_reconnect_join_delay 5;

addset auto_reconnect_delay_method bool;
set auto_reconnect_delay_method off;

addset auto_reconnect_retries int;
set auto_reconnect_retries 5;

addset auto_reconnect_try_other_servers bool;
set auto_reconnect_try_other_servers on;

addset auto_reconnect bool {
	if (*0 == [on]) {
		on #-server_state 100 '% % ACTIVE' {
			@ :group = serverctl(GET $0 GROUP);

			## If we use default group for more than one server
			## we need to encode server name as a tag for our
			## counters and variables, since user may be connected
			## to more than one server in default group, and when
			## we will experience disconnect for more than one server,
			## timers and variables will share encoded "<default>"
			## tag.
			##
			## We encode(), because default "<default>" group
			## isn't accepted as lval for auto_reconnect array
			if (group == [<default>]) {
				@ :group_enc = encode($serverctl(GET $0 NAME));
			} else {
				@ :group_enc = encode($serverctl(GET $0 GROUP));
			};

			## Stop reconnection timers on successful connect
			timer -delete auto_reconnect.$group_enc;
			^assign -auto_reconnect.failures[$group_enc];

			## Send one line with all channels to join
			## https://www.rfc-editor.org/rfc/rfc1459#section-4.2.1
			@ :chans = auto_reconnect[$group_enc][chans];
			@ :keys = auto_reconnect[$group_enc][keys];

			if (@chans) {
				if (auto_reconnect_delay_method == [on]) {
					@i = auto_reconnect_join_delay;
					fe chans chan {
						timer $i quote join $chan;
						@i++;
					};
				} else {
					quote join $sar(g/ /,/$chans) $sar(g/ /,/$keys);
				};
			};

		};
		on #-channel_claim 100 * {
			@ :group = serverctl(GET $0 GROUP);

			if (group == [<default>]) {
				@ :group_enc = encode($serverctl(GET $0 NAME));
			} else {
				@ :group_enc = encode($serverctl(GET $0 GROUP));
			};

			## If we can't find the channel in auto_reconnect array for the
			## group - we're not in reconnect situation, user just has joined
			## a channel on his own - do not proceed.
			@ :idx = findw($1 $auto_reconnect[$group_enc][chans]);
			if (idx == -1) {
				return;
			};

			@ :key = word($idx $auto_reconnect[$group_enc][keys]);
			@ :uuid = word($idx $auto_reconnect[$group_enc][wins]);
			@ :win = windowctl(REFNUM $uuid);

			if (windowctl(GET $win SERVER) == *0 && windowctl(GET $2 uuid) != uuid) {
				window $uuid claim $1;
			};

			@ ::auto_reconnect[$group_enc][chans] = notw($idx $auto_reconnect[$group_enc][chans]);
			@ ::auto_reconnect[$group_enc][keys] = notw($idx $auto_reconnect[$group_enc][keys]);
			@ ::auto_reconnect[$group_enc][wins] = notw($idx $auto_reconnect[$group_enc][wins]);

		};
		alias auto_reconnect.handler (server, void) {
			@ :server_name = serverctl(GET $server NAME);
			@ :group = serverctl(GET $server GROUP);

			if (group == [<default>]) {
				@ :group_enc = encode($serverctl(GET $server NAME));
			} else {
				@ :group_enc = encode($serverctl(GET $server GROUP));
			};

			## Check if we're reconnecting already
			if (timerctl(REFNUM auto_reconnect.$group_enc)) {
				return;
			};

			if (!serverctl(GET $server OPEN)) {
				@ :attempt = (auto_reconnect.failures[$group_enc] % auto_reconnect_retries) + 1;
				@ auto_reconnect.failures[$group_enc]++;

				if (attempt <= auto_reconnect_retries && auto_reconnect.overflow != 1) {
					## This is ugly hack for detecting when we must
					## try next server in a group
					@ auto_reconnect.overflow = (attempt == auto_reconnect_retries);

					xecho -b Autoreconnecting to $server_name [group $group] - attempt $attempt;
					server $server;
				} else {
					@ auto_reconnect.overflow = 0;

					if (auto_reconnect_try_other_servers == [on] && group != [<default>]) {
						@ :old_server_name = serverctl(GET $server NAME);
						@ :server = serverctl(GET $server NEXT_SERVER_IN_GROUP);
						@ :new_server_name = serverctl(GET $server NAME);

						xecho -b Maximum auto reconnect retries to server $old_server_name has been reached;
						xecho -b Switching to the next server in group $group - $new_server_name - attempt $attempt;
						server $server;
					} else {
						xecho -b Maximum auto reconnect retries failed - Use /RECONNECT to reconnect;
						^assign -auto_reconnect.failures[$group_enc];
						return;
					};
				};
			} else {
				xecho -b Timeout for autoreconnect retry has been reached, but you're still in the process of connection;
				xecho -b Scheduling another try in $auto_reconnect_delay seconds;
			};

			timer -server $server -refnum auto_reconnect.$group_enc $auto_reconnect_delay auto_reconnect.handler $server;
		};
		on #-reconnect_required 100 * {
			@ :server = serverctl(GET $0 NAME);
			@ :group = serverctl(GET $0 GROUP);

			if (group == [<default>]) {
				@ :group_enc = encode($serverctl(GET $0 NAME));
			} else {
				@ :group_enc = encode($serverctl(GET $0 GROUP));
			};

			xecho -b Connection to server $server [group $group] has been lost, autoreconnecting in $auto_reconnect_delay seconds;

			timer -server $0 -refnum auto_reconnect.$group_enc $auto_reconnect_delay auto_reconnect.handler $0;
		};
		on #-channel_lost 100 * {
			if (serverctl(GET $0 STATE) == [CLOSING]) {
				@ :group = serverctl(GET $0 GROUP);
				@ :uuid = windowctl(GET $2 UUID);

				if (group == [<default>]) {
					@ :group_enc = encode($serverctl(GET $0 NAME));
				} else {
					@ :group_enc = encode($group);
				};

				push auto_reconnect[$group_enc][chans] $1;
				push auto_reconnect[$group_enc][keys] $key($1);
				push auto_reconnect[$group_enc][wins] $uuid;
			};
		};
		alias lsreconnects (void) {
			xecho -v -b Currently reconnecting to following groups:;

			fe ($timerctl(REFNUMS)) timer {
				if (timer =~ [auto_reconnect.*]) {
					@ :group = decode($after(1 . $timer));

					xecho -v -b $group;
				};
			};
		};
		alias rmreconnect (void) {
			@ :server = windowctl(GET $winnum() SERVER);
			@ :group = serverctl(GET $server GROUP);

			if (group == [<default>]) {
				@ :group_enc = encode($serverctl(GET $server NAME));
			} else {
				@ :group_enc = encode($group);
			};

			xecho -v -b Canceling reconnect to $decode($group_enc);
			timer -delete auto_reconnect.$group_enc;
		};
	} else {
		fe ($timerctl(REFNUMS)) timer {
			if (timer =~ [auto_reconnect.*]) {
				timer -delete $timer;
			};
		};
		on #-server_state 100 -'% % ACTIVE';
		on #-reconnect_required 100 -*;
		on #-channel_lost 100 -*;
		alais -lsreconnects;
		alias -rmreconnect;
		alias -auto_reconnect.handler;
	};
};

set auto_reconnect on;
