package NGCP::Rtpclient::ICE;

use strict;
use warnings;
use Socket;
use Socket6;
use IO::Socket;
use IO::Multiplex;
use Math::BigInt;
use Digest::HMAC_SHA1 qw(hmac_sha1);
use Digest::CRC qw(crc32);
use Time::HiRes qw(time);

my @ice_chars = ('A' .. 'Z', 'a' .. 'z', '0' .. '9');
my %type_preferences = (
	host => 126,
	srflx => 100,
	prflx => 110,
	relay => 0,
);
my %protocols = ( 17 => 'UDP' );

sub random_string {
	my ($len) = @_;
	return join('', (map {$ice_chars[rand(@ice_chars)]} (1 .. $len)));
}

sub new {
	my ($class, $components, $controlling) = @_;

	my $self = {};
	bless $self, $class;

	$self->{my_ufrag} = random_string(8);
	$self->{my_pwd} = random_string(26);
	$self->{controlling} = $controlling;
	$self->{components} = $components;
	$self->{tie_breaker} = i64from32(rand(2**32), rand(2**32));

	$self->{candidates} = {}; # foundation -> candidate
	$self->{remote_candidates} = {}; # foundation -> candidate
	$self->{candidate_pairs} = {}; # foundation pairs -> pair
	$self->{remote_peers} = {}; # peer_hash_key -> component
	$self->{changed_foundations} = {}; # old -> new

	$self->{triggered_checks} = [];
	$self->{last_timer} = 0;
	$self->{start_nominating} = 0;
	$self->{completed} = 0;
	$self->{keepalives} = 0;

	$self->debug("created, control" . ($controlling ? "ing" : "ed")
		. ", tie breaker " . $self->{tie_breaker}->bstr() . "\n");

	return $self;
}

sub i64from32 {
	my ($hi, $lo) = @_;
	my $i = Math::BigInt->new(int($hi));
	$i->blsft(32);
	$i->badd(int($lo));
	return $i;
}

sub calc_priority {
	my ($type, $local_pref, $component) = @_;
	defined($type_preferences{$type}) or die;
	return (2 ** 24) * $type_preferences{$type} + (2 ** 8) * $local_pref + (256 - $component);
}

sub add_candidate {
	my ($self, $local_pref, $type, @components) = @_;
	# highest local pref = 65535, lowest = 0

	@components == $self->{components} or die;
	defined($type_preferences{$type}) or die;

	my $foundation = random_string(16);
	my $cands = $self->{candidates};
	$cands->{$foundation} and die;

	my $comps = [];
	my $comp_id = 1;
	for my $c (@components) {
		my $comp = bless { socket => $c, component => $comp_id,
				priority => calc_priority($type, $local_pref, $comp_id),
				foundation => $foundation,
				protocol => 'UDP', af => $c->sockdomain(),
				address => $c->sockhost(), port => $c->sockport(),
				agent => $self }, 'NGCP::Rtpclient::ICE::Component';
		push(@$comps, $comp);
		$comp->debug("is $comp->{address}/$comp->{port}\n");

		$comp_id++;
	}

	$cands->{$foundation} = bless { foundation => $foundation, preference => $local_pref,
		base_priority => calc_priority($type, $local_pref, 0),
		type => $type, components => $comps, protocol => 'UDP',
		af => $comps->[0]->{af}, address => $comps->[0]->{address},
		agent => $self }, 'NGCP::Rtpclient::ICE::Candidate';

	$self->pair_candidates();
}

sub encode {
	my ($self) = @_;

	my @ret;

	push(@ret, "a=ice-ufrag:$self->{my_ufrag}");
	push(@ret, "a=ice-pwd:$self->{my_pwd}");

	for my $cand (values(%{$self->{candidates}})) {
		for my $comp (@{$cand->{components}}) {
			my $prot = $comp->{socket}->protocol();
			my $sa = $comp->{socket}->sockhost();
			my $sp = $comp->{socket}->sockport();
			push(@ret, "a=candidate:$cand->{foundation} $comp->{component} $protocols{$prot} $comp->{priority} $sa $sp typ $cand->{type}");
		}
	}

	return @ret;
}

sub remote_foundation_change {
	my ($self, $old, $new, $type) = @_;

	if ($self->{changed_foundations}->{$old}) {
		$self->{changed_foundations}->{$old} eq $new or die;
		return;
	}
	$self->debug("changing remote candidate foundation from $old to $new\n");
	my $old_cand = $self->{remote_candidates}->{$old} or die;
	$old_cand->{type} = $type;
	$old_cand->{foundation} = $new;

	for my $comp (@{$old_cand->{components}}) {
		$comp->{foundation} = $new;
	}

	for my $foundation_pair (keys(%{$self->{candidate_pairs}})) {
		my $pair = $self->{candidate_pairs}->{$foundation_pair};
		$pair->{remote} == $old_cand or next;

		my $new_foundation = $pair->{local}->{foundation} . $new;
		delete($self->{candidate_pairs}->{$foundation_pair});
		$self->{candidate_pairs}->{$new_foundation} = $pair;
		$pair->{foundation} = $new_foundation;

		for my $comp (@{$pair->{components}}) {
			$comp->{foundation} = $new_foundation;
		}
	}

	$self->{remote_candidates}->{$new} = $old_cand;
	delete($self->{remote_candidates}->{$old});
	$self->{changed_foundations}->{$old} = $new;
}

sub new_remote_candidate {
	my ($self, $cand_str) = @_;
	$self->_new_remote_candidates_start();
	my $ret = $self->_new_remote_candidate($cand_str);
	$self->_got_new_candidates();
	return $ret;
}

sub _new_remote_candidates_start {
	my ($self) = @_;
	$self->{new_candidates} = {};
}

sub _new_remote_candidate {
	my ($self, $c) = @_;

	$self->debug("adding remote candidate $c\n");
	my ($foundation, $component, $protocol, $priority, $address, $port, $type)
		= $c =~ /^(\w+) (\d) (\w+) (\d+) ([0-9a-fA-F:.]+) (\d+) typ (\w+)/ or die $c;

	$protocol = uc($protocol);
	my $phk = "$protocol/$address/$port";

	if (my $old = $self->{remote_peers}->{$phk}) {
		# must be a previously learned prflx candidate
		$old = $old->{candidate};
		$old->{type} eq 'prflx' or die;
		# replace the learned prflx candidate with the new one
		$self->remote_foundation_change($old->{foundation}, $foundation, $type);
		return;
	}

	my $f = ($self->{new_candidates}->{$foundation} // (
		$self->{new_candidates}->{$foundation} = {
			foundation => $foundation,
			type => $type,
			protocol => $protocol,
			components => [],
		}));

	$f->{type} eq $type or die;
	$f->{protocol} eq $protocol or die;

	$f->{components}->[$component - 1] and die;
	my $comp = $f->{components}->[$component - 1] = {
		candidate => $f,
		foundation => $foundation,
		component => $component,
		priority => $priority,
		address => $address,
		port => $port,
		peer_hash_key => $phk,
	};

	if ($address =~ /^\d+\.\d+\.\d+\.\d+$/) {
		$f->{af} = $comp->{af} = &AF_INET;
		$comp->{packed_peer} = pack_sockaddr_in($port, inet_pton(&AF_INET, $address));
	}
	elsif ($address =~ /^[0-9a-fA-F:]+$/) {
		$f->{af} = $comp->{af} = &AF_INET6;
		$comp->{packed_peer} = pack_sockaddr_in6($port, inet_pton(&AF_INET6, $address));
	}
	else {
		die;
	}

	$self->{remote_peers}->{$phk} = $comp;

	return $comp;
}

sub _got_new_candidates {
	my ($self) = @_;

	# validate received info and eliminate duplicates
	my $r_cand = $self->{remote_candidates};
	my $r_peers = $self->{remote_peers};
	for my $c (values(%{$self->{new_candidates}})) {
		# @{$c->{components}} == $self->{components} or die;

		if (my $exist = $r_cand->{$c->{foundation}}) {
			# duplicate. OK if this is a learned prflx
			if ($exist->{type} eq 'prflx' && $c->{type} eq 'prflx') {
				# merge components
				for my $idx (0 .. $#{$c->{components}}) {
					defined($c->{components}->[$idx]) or next;
					defined($exist->{components}->[$idx]) and die;
					$exist->{components}->[$idx] = $c->{components}->[$idx];
				}
				next;
			}
			warn;
			next;
		}
		$r_cand->{$c->{foundation}} = $c;
	}

	delete($self->{new_candidates});
	$self->pair_candidates();
};

sub decode {
	my ($self, $h) = @_;
	# $h is output of SDP::Media->decode_ice()

	$self->{other_ufrag} = $h->{ufrag} or die;
	$self->{other_pwd} = $h->{pwd} or die;

	my $cands = $h->{candidates} or die;
	$self->_new_remote_candidates_start();
	for my $c (@$cands) {
		$self->_new_remote_candidate($c);
	}
	$self->_got_new_candidates();
}

sub pair_candidates {
	my ($self) = @_;

	my $pairs = $self->{candidate_pairs};

	for my $rem (values(%{$self->{remote_candidates}})) {
		for my $loc (values(%{$self->{candidates}})) {
			$loc->{protocol} eq $rem->{protocol} or next;
			$loc->{af} == $rem->{af} or next;

			@{$loc->{components}} == $self->{components} or die;

			my $foundation = $loc->{foundation} . $rem->{foundation};
			my $pair = $pairs->{$foundation} || ($pairs->{$foundation} =
					bless { foundation => $foundation, local => $loc, remote => $rem,
					components => [], agent => $self}, 'NGCP::Rtpclient::ICE::Candidate::Pair'
				);
			my $comps = $pair->{components};

			for my $idx (0 .. ($self->{components} - 1)) {
				defined($loc->{components}->[$idx]) or next;
				defined($rem->{components}->[$idx]) or next;

				my $c = $comps->[$idx] || ($comps->[$idx] =
					bless { foundation => $foundation,
					local => $loc->{components}->[$idx],
					remote => $rem->{components}->[$idx],
					agent => $self},
					'NGCP::Rtpclient::ICE::Component::Pair');
				$c->{state} = $c->{state} || ($idx == 0 ? 'waiting' : 'frozen');
			}
		}
	}
}

sub get_pair {
	my ($self, $local, $remote, $component) = @_;
	my $found = "$local$remote";
	my $pair = $self->{candidate_pairs}->{$found} or return;
	$component or return $pair;
	return $pair->{components}->[$component - 1];
}

sub is_ice {
	my ($s) = @_;

	length($s) < 20 and return 0;
	my $c = ord(substr($s, 0, 1));
	($c & 0xb0) != 0 and return 0;
	$c = ord(substr($s, 3, 1));
	($c & 0x03) != 0 and return 0;
	$c = substr($s, 4, 4);
	$c ne "\x21\x12\xA4\x42" and return 0;
	return 1;
}

sub input {
	my ($self, $fh, $s_r, $peer) = @_;

	$$s_r eq '' and return;
	is_ice($$s_r) or return;

	for my $cands (values(%{$self->{candidates}})) {
		for my $comp (@{$cands->{components}}) {
			$fh == $comp->{socket} or next;
			$self->do_input($comp, $$s_r, $peer);
			$$s_r = '';
			return;
		}
	}
}

my %attr_handlers = (
	0x0006 => \&stun_handler_USERNAME,
	0x0008 => \&stun_handler_MESSAGE_INTEGRITY,
	0x0009 => \&stun_handler_ERROR_CODE,
	0x000a => \&stun_handler_UNKNOWN_ATTRIBUTES,
	0x0020 => \&stun_handler_XOR_MAPPED_ADDRESS,
	0x0024 => \&stun_handler_PRIORITY,
	0x0025 => \&stun_handler_USE_CANDIDATE,
	0x8022 => \&stun_handler_SOFTWARE,
	0x8028 => \&stun_handler_FINGERPRINT,
	0x8029 => \&stun_handler_ICE_CONTROLLED,
	0x802a => \&stun_handler_ICE_CONTROLLING,
);

my %type_handlers = (
	1   => \&stun_handler_binding_request,
	17  => \&stun_handler_binding_indication,
	257 => \&stun_handler_binding_success,
	273 => \&stun_handler_binding_error,
);

sub do_input {
	my ($self, $comp, $s, $peer) = @_;

	my $hdr = substr($s, 0, 20, '');
	my ($mtype, $mlen, $cookie, $tid) = unpack('nnNa12', $hdr);
	$cookie == 0x2112A442 or return;

	my (@stack, %hash);

	while (my ($type, $len) = unpack('nn', $s)) {
		my $padding = 4 - ($len % 4);
		$padding == 4 and $padding = 0;

		my $raw = substr($s, 0, 4 + $len + $padding);

		substr($s, 0, 4) = '';
		my $data = substr($s, 0, $len, '');
		substr($s, 0, $padding) = '';

		my $handler = $attr_handlers{$type};
		if (!$handler) {
			warn("unknown STUN attribute $type data $data");
			next;
		}

		my $parsed = $handler->($data, $tid) or die;
		$parsed->{raw} = $raw;

		push(@stack, $parsed);
		$hash{$parsed->{name}} = $parsed;
	}

	$stack[$#stack]->{name} eq 'fingerprint' or die;
	$stack[$#stack - 1]->{name} eq 'integrity' or die;

	my $pwd_check = $mtype == 1 ? $self->{my_pwd} : $self->{other_pwd};
	# XXX unify these with sub integrity/fingerprint ?
	my $int_check = join('', (map {$_->{raw}} @stack[0 .. ($#stack - 2)]));
	$int_check = pack('nnNa12', $mtype, length($int_check) + 24, $cookie, $tid) . $int_check;
	my $digest = hmac_sha1($int_check, $pwd_check);
	$digest eq $hash{integrity}->{digest} or die;

	my $fp_check = join('', (map {$_->{raw}} @stack[0 .. ($#stack - 1)]));
	$fp_check = pack('nnNa12', $mtype, length($fp_check) + 8, $cookie, $tid) . $fp_check;
	my $crc = crc32($fp_check);
	($crc ^ 0x5354554e) == $hash{fingerprint}->{crc} or die;

	# decode peer address
	my $domain = $comp->{af};
	my (@peer, $address);
	if ($domain == &AF_INET) {
		@peer = unpack_sockaddr_in($peer);
	}
	elsif ($domain == &AF_INET6) {
		@peer = unpack_sockaddr_in6($peer);
	}
	else {
		die;
	}
	$address = inet_ntop($domain, $peer[1]);

	# process it
	my $handler = $type_handlers{$mtype} or die;
	my $response = $handler->($self, $comp, \@stack, \%hash, $tid, $peer, $peer[1], $address, $peer[0]);

	if ($response) {
		# construct and send response packet
		integrity($response->{attrs}, $response->{mtype}, $tid, $self->{my_pwd});
		fingerprint($response->{attrs}, $response->{mtype}, $tid);

		# XXX unify
		my $packet = join('', @{$response->{attrs}});
		$packet = pack('nnNa12', $response->{mtype}, length($packet), 0x2112A442, $tid) . $packet;
		$comp->{socket}->send($packet, 0, $peer);
	}
}

sub stun_reply {
	my ($self, $attrs, $mtype) = @_;

	unshift(@$attrs, attr(0x8022, 'perl:ICE.pm'));

	my $response = { mtype => $mtype, attrs => $attrs };
}

sub stun_success {
	my ($self, $attrs) = @_;
	return $self->stun_reply($attrs, 257);
}

sub stun_error {
	my ($self, $code, $msg) = @_;
	return $self->stun_reply([ attr(0x0009, pack('Na*', ((($code / 100) << 8) | ($code % 100)), $msg)) ], 273);
}

sub debug {
	my ($self, @rest) = @_;
	print("ICE agent", ' ', $self->{my_ufrag}, ' - ', @rest);
}

sub dummy_foundation {
	my ($protocol, $address) = @_;
	return $protocol . unpack('H*', $address);
}

sub stun_handler_binding_request {
	my ($self, $comp, $stack, $hash, $tid, $packed_peer, $packed_host, $address, $port) = @_;

	$hash->{username}->{my_ufrag} eq $self->{my_ufrag} or die;

	# check role
	if ($self->{controlling} && $hash->{controlling}) {
		if ($self->{tie_breaker}->bcmp($hash->{controlling}->{tie_breaker}) >= 0) {
			$self->debug("returning 487 role conflict\n");
			return $self->stun_error(487, "Role conflict");
		}
		$self->debug("role conflict, switching to controlled\n");
		$self->{controlling} = 0;
	}
	elsif (!$self->{controlling} && $hash->{controlled}) {
		if ($self->{tie_breaker}->bcmp($hash->{controlled}->{tie_breaker}) < 0) {
			$self->debug("returning 487 role conflict\n");
			return $self->stun_error(487, "Role conflict");
		}
		$self->debug("role conflict, switching to controlling\n");
		$self->{controlling} = 1;
	}

	$self->debug("binding request from $address/$port\n");

	# check if peer is known - learn prflx candidates
	my $cand = $self->{remote_peers}->{"UDP/$address/$port"};
	if (!$cand) {
		$cand = $self->new_remote_candidate(dummy_foundation('UDP', $packed_host)
			. " $comp->{component} UDP "
			. "$hash->{priority}->{priority} $address $port typ prflx");
		# this also pairs up the new candidate, which goes against 7.2.1.3
	}

	# get candidate pair and trigger check
	my $pair = $self->get_pair($comp->{foundation}, $cand->{foundation}, $comp->{component});
	$pair or die;
	$pair->trigger_check();

	# set and check nominations
	if ($hash->{use}) {
		$pair->{nominated} = 1;
		$self->debug("$pair->{foundation} - got nominated\n");
		$self->check_nominations();
	}

	# construct response
	my $attrs = [];

	if ($comp->{af} == &AF_INET) {
		push(@$attrs, attr(0x0020, pack('nna4', 1, $port ^ 0x2112, $packed_host ^ "\x21\x12\xa4\x42")));
	}
	elsif ($comp->{af} == &AF_INET6) {
		push(@$attrs, attr(0x0020, pack('nna16', 2, $port ^ 0x2112,
			$packed_host ^ ("\x21\x12\xa4\x42" . $tid))));
	}

	return $self->stun_success($attrs);
}

sub check_nominations {
	my ($self) = @_;

	$self->{controlling} and return;

	my @nominated;

	for my $pair (values(%{$self->{candidate_pairs}})) {
		my @comps = @{$pair->{components}};
		my @nominated_comps = grep {$_->{nominated}} @comps;
		@nominated_comps < $self->{components} and next;
		$self->debug("got fully nominated pair $pair->{foundation}\n");
		push(@nominated, $pair);
	}

	if (!@nominated) {
		$self->debug("no fully nominated pairs yet\n");
		return;
	}

	@nominated = sort_pairs(\@nominated);
	my $pair = $nominated[0];
	$self->debug("highest priority nominated pair is $pair->{foundation}\n");
	$self->{nominated_pair} = $pair;
	$self->{completed} ||= time();
}

sub stun_handler_binding_success {
	my ($self, $comp, $stack, $hash, $tid, $packed_peer, $packed_host, $address, $port) = @_;

	$self->debug("binding success from $address/$port\n");

	# check xor address
	$comp->{address} eq $hash->{address}->{address} or die("$comp->{address} $hash->{address}->{address}");
	$comp->{port} == $hash->{address}->{port} or die;

	# we must have remote candidate and a pair
	my $cand = $self->{remote_peers}->{"UDP/$address/$port"};
	$cand or die;
	my $pair = $self->get_pair($comp->{foundation}, $cand->{foundation}, $comp->{component});
	$pair or die;
	$tid eq $pair->{transaction} or die;

	$self->debug("$pair->{foundation} succeeded\n");
	$pair->{state} = 'succeeded';

	# unfreeze other components
	my $parent_pair = $self->{candidate_pairs}->{$pair->{foundation}};
	my $components = $parent_pair->{components};
	my @frozen_pairs = grep {$_->{state} eq 'frozen'} @$components;
	for my $p (@frozen_pairs) {
		$self->debug("unfreezing $p->{local}->{port}\n");
		$p->{state} = 'waiting';
	}

	$self->check_to_nominate();

	return;
}

sub check_to_nominate {
	my ($self) = @_;

	return unless $self->{controlling};
	return if $self->{start_nominating} && time() < $self->{start_nominating};
	return if $self->{nominate};
	return if @{$self->{triggered_checks}};

	my @succeeded;

	for my $pair (values(%{$self->{candidate_pairs}})) {
		my @comps = @{$pair->{components}};
		my @succeeded_comps = grep {$_->{state} eq 'succeeded'} @comps;
		next if @succeeded_comps < $self->{components};
		$self->debug("got fully succeeded pair $pair->{foundation}\n");
		push(@succeeded, $pair);
	}

	if (!@succeeded) {
		$self->debug("no fully succeeded pairs yet\n");
		return;
	}

	@succeeded = sort_pairs(\@succeeded);
	my $pair = $succeeded[0];
	$self->debug("highest priority succeeded pair is $pair->{foundation}\n");

	if (!$self->{start_nominating}) {
		$self->{start_nominating} = time() + 0.1;
		return;
	}

	$pair->{nominate} and return;

	$self->{nominate} = 1;
	$pair->{nominate} = 1;
	$self->{start_nominating} = 0;
	$self->{nominated_pair} = $pair;
	$self->{completed} ||= time();

	$pair->debug("nominating\n");
	$pair->nominate();
}

sub integrity {
	my ($attrs, $mtype, $tid, $pwd) = @_;

	my $int_check = join('', @$attrs);
	$int_check = pack('nnNa12', $mtype, length($int_check) + 24, 0x2112A442, $tid) . $int_check;
	my $digest = hmac_sha1($int_check, $pwd);
	push(@$attrs, attr(0x0008, $digest));
}

sub fingerprint {
	my ($attrs, $mtype, $tid) = @_;

	my $fp_check = join('', @$attrs);
	$fp_check = pack('nnNa12', $mtype, length($fp_check) + 8, 0x2112A442, $tid) . $fp_check;
	my $crc = crc32($fp_check);
	push(@$attrs, attr(0x8028, pack('N', ($crc ^ 0x5354554e))));
}

sub attr {
	my ($id, $data) = @_;
	my $len = length($data);
	my $padding = 4 - ($len % 4);
	$padding == 4 and $padding = 0;
	return pack('nn a*a*', $id, $len, $data, "\0" x $padding);
}

sub stun_handler_SOFTWARE {
	my ($data, $out) = @_;
	return { name => 'software', data => $data };
}
sub stun_handler_USE_CANDIDATE {
	my ($data, $out) = @_;
	return { name => 'use' };
}
sub stun_handler_ICE_CONTROLLED {
	my ($data) = @_;
	my $out = { name => 'controlled' };
	$out->{controlled} = 1;
	($out->{tie_breaker_hi}, $out->{tie_breaker_lo}) = unpack('NN', $data);
	$out->{tie_breaker} = i64from32($out->{tie_breaker_hi}, $out->{tie_breaker_lo});
	return $out;
}
sub stun_handler_ICE_CONTROLLING {
	my ($data) = @_;
	my $out = { name => 'controlling' };
	$out->{controlling} = 1;
	($out->{tie_breaker_hi}, $out->{tie_breaker_lo}) = unpack('NN', $data);
	$out->{tie_breaker} = i64from32($out->{tie_breaker_hi}, $out->{tie_breaker_lo});
	return $out;
}
sub stun_handler_USERNAME {
	my ($data) = @_;
	my $out = { name => 'username' };
	$data =~ /^(.*):(.*)$/ or die;
	$out->{my_ufrag} = $1;
	$out->{other_ufrag} = $2;
	return $out;
}
sub stun_handler_PRIORITY {
	my ($data) = @_;
	my $out = { name => 'priority' };
	($out->{priority}) = unpack('N', $data);
	return $out;
}
sub stun_handler_MESSAGE_INTEGRITY {
	my ($data) = @_;
	my $out = { name => 'integrity' };
	$out->{digest} = $data;
	return $out;
}
sub stun_handler_FINGERPRINT {
	my ($data) = @_;
	my $out = { name => 'fingerprint' };
	($out->{crc}) = unpack('N', $data);
	return $out;
}
sub stun_handler_ERROR_CODE {
	my ($data) = @_;
	my $out = { name => 'error' };
	my ($code, $msg) = unpack('Na*', $data);
	$out->{msg} = $msg;
	$out->{code} = (($code & 0x700) >> 8) * 100 + ($code & 0x0ff);
	return $out;
}
sub stun_handler_XOR_MAPPED_ADDRESS {
	my ($data, $tid) = @_;
	my $out = { name => 'address' };
	if (length($data) == 8) {
		my ($fam, $port, $addr) = unpack('nna4', $data);
		$fam == 1 or die;
		$out->{af} = &AF_INET;
		$out->{port} = $port ^ 0x2112;
		$out->{address} = $addr ^ "\x21\x12\xa4\x42";
	}
	elsif (length($data) == 20) {
		my ($fam, $port, $addr) = unpack('nna16', $data);
		$fam == 2 or die;
		$out->{af} = &AF_INET6;
		$out->{port} = $port ^ 0x2112;
		$out->{address} = $addr ^ ("\x21\x12\xa4\x42" . $tid);
	}
	else {
		die;
	}
	$out->{address} = inet_ntop($out->{af}, $out->{address});
	return $out;
}

sub timer {
	my ($self) = @_;
	my $now = time();
	$now - $self->{last_timer} < 0.02 and return;
	$self->{last_timer} = $now;

	# run checks

	# not enough info
	return if !defined($self->{other_ufrag}) || !defined($self->{other_pwd});

	if (my $pair = shift(@{$self->{triggered_checks}})) {
		$pair->debug("running triggered check\n");
		$pair->run_check();
		return;
	}

	# get all component pairs, sort by their priority and run check for the highest waiting one

	my @candidate_pairs = values(%{$self->{candidate_pairs}});
	my @component_pairs = map {@{$_->{components}}} @candidate_pairs;
	my @sorted_pairs = sort_pairs(\@component_pairs);
	my @waiting_pairs = grep {$_->{state} eq 'waiting'} @sorted_pairs;

	if (my $pair = shift(@waiting_pairs)) {
		$pair->debug("running scheduled check (waiting state)\n");
		$pair->run_check();
		return;
	}

	$self->check_to_nominate();

	if ($self->{completed}) {
		$self->{keepalives} ||= time() + 2;
		if (time() >= $self->{keepalives}) {
			$self->keepalives();
			$self->{keepalives} += 2;
		}
	}
}

sub keepalives {
	my ($self) = @_;

	$self->debug("sending keepalives");
	my $pair = $self->{nominated_pair} or return;
	$pair->nominate();
}

sub sort_pairs {
	my ($pair_list) = @_;
	my @sorted_list = sort {
		$a->priority() <=> $b->priority()
	} @{$pair_list};
	return @sorted_list;
}

sub get_send_component {
	my ($self, $component) = @_;

	my $pair = $self->{nominated_pair};

	if (!$pair) {
		my @pairs = values(%{$self->{candidate_pairs}});
		@pairs = sort_pairs(\@pairs);
		$pair = $pairs[0];
	}

	return ($pair->{components}->[$component]->{local}->{socket},
				$pair->{components}->[$component]->{remote}->{packed_peer});
}

package NGCP::Rtpclient::ICE::Candidate;

sub debug {
	my ($self, @rest) = @_;
	$self->{agent}->debug("candidate", $self->{foundation}, ' - ', @rest);
}

package NGCP::Rtpclient::ICE::Component;

sub debug {
	my ($self, @rest) = @_;
	$self->{agent}->debug("component $self->{foundation}/$self->{component}", ' - ', @rest);
}

package NGCP::Rtpclient::ICE::Candidate::Pair;

sub priority {
	my ($self) = @_;
	my $firstcomp = $self->{components}->[0];
	return $firstcomp->priority();
}

sub debug {
	my ($self, @rest) = @_;
	$self->{agent}->debug("candidate pair $self->{foundation}", ' - ', @rest);
}

sub nominate {
	my ($self) = @_;
	for my $comp (@{$self->{components}}) {
		$comp->cancel_check();
		$comp->{nominate} = 1;
		$comp->trigger_check();
	}
}

package NGCP::Rtpclient::ICE::Component::Pair;

sub debug {
	my ($self, @rest) = @_;
	$self->{agent}->debug("component pair $self->{foundation}", ' - ', @rest);
}

sub priority {
	my ($self) = @_;
	my $agent = $self->{agent};
	my $gk = $agent->{controlling} ? 'local' : 'remote';
	my $dk = $agent->{controlling} ? 'remote' : 'local';
	my $gc = $self->{$gk};
	my $dc = $self->{$dk};
	my $g = $gc->{priority};
	my $d = $dc->{priority};
	return (($g < $d ? $g : $d) << 32) + (($g > $d ? $g : $d) * 2) + ($g > $d ? 1 : 0);
}

sub trigger_check {
	my ($self) = @_;
	$self->debug("trigger check\n");
	if ($self->{state} eq 'succeeded') {
		$self->debug("already succeeded\n");
		return;
	}
	if ($self->{state} eq 'in progress') {
		$self->cancel_check();
	}
	push(@{$self->{agent}->{triggered_checks}}, $self);
}

sub run_check {
	my ($self) = @_;

	$self->{state} eq 'in progress' and return;

	$self->debug("running check\n");
	$self->{state} = 'in progress';
	$self->{transaction} = NGCP::Rtpclient::ICE::random_string(12);
	$self->send_check();
	# XXX handle retransmits
}

sub cancel_check {
	my ($self) = @_;
	$self->{transaction} or return;
	$self->debug("canceling existing check $self->{transaction}\n");
	$self->{previous_transactions}->{$self->{transaction}} = 1;
	delete $self->{transaction};
	$self->{state} = 'waiting';
}

sub send_check {
	my ($self) = @_;

	$self->debug("sending check $self->{transaction}\n");

	$self->{last_transmit} = time();
	my $local_comp = $self->{local};
	my $remote_comp = $self->{remote};
	my $local_cand = $self->{agent}->{candidates}->{$local_comp->{foundation}};

	my $attrs = [];
	unshift(@$attrs, NGCP::Rtpclient::ICE::attr(0x8022, 'perl:ICE.pm'));
	my $hexbrk = $self->{agent}->{tie_breaker}->as_hex();
	$hexbrk =~ s/^0x// or die;
	$hexbrk = ('0' x (16 - length($hexbrk))) . $hexbrk;
	unshift(@$attrs, NGCP::Rtpclient::ICE::attr($self->{agent}->{controlling} ? 0x802a : 0x8029, pack('H*', $hexbrk)));
	unshift(@$attrs, NGCP::Rtpclient::ICE::attr(0x0024, pack('N', NGCP::Rtpclient::ICE::calc_priority('prflx',
				$local_cand->{preference}, $local_comp->{component}))));
	unshift(@$attrs, NGCP::Rtpclient::ICE::attr(0x0006, "$self->{agent}->{other_ufrag}:$self->{agent}->{my_ufrag}"));
	$self->{nominate} and
		unshift(@$attrs, NGCP::Rtpclient::ICE::attr(0x0025, ''));

	integrity($attrs, 1, $self->{transaction}, $self->{agent}->{other_pwd});
	fingerprint($attrs, 1, $self->{transaction});

	my $packet = join('', @$attrs);
	$packet = pack('nnNa12', 1, length($packet), 0x2112A442, $self->{transaction}) . $packet;
	$local_comp->{socket}->send($packet, 0, $remote_comp->{packed_peer});
}

1;
