package Slim::Web::Cometd::Manager;


# Logitech Media Server Copyright 2001-2024 Logitech.
# Lyrion Music Server Copyright 2024 Lyrion Community.
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# version 2.

# This class manages clients and subscriptions

use strict;

use Scalar::Util qw(weaken);
use Tie::RegexpHash;

use Slim::Utils::Log;
use Slim::Utils::Timers;

my $log = logger('network.cometd');

sub new {
	my ( $class, %args ) = @_;

	tie my %channels, 'Tie::RegexpHash';

	my $self = {
		conn     => {},         # client connection(s)
		events   => {},         # clients and their pending events
		channels => \%channels, # all channels and who is subscribed to them
	};

	bless $self, ref $class || $class;
}

# Add a new client and connection created during handshake
sub add_client {
	my ( $self, $clid ) = @_;

	# Pending events for this clid
	$self->{events}->{$clid} = [];

	main::DEBUGLOG && $log->debug("add_client: $clid");

	return $clid;
}

# Update the connection, i.e. if the client reconnected
sub register_connection {
	my ( $self, $clid, $conn ) = @_;

	$self->{conn}->{$clid} = $conn;

	main::DEBUGLOG && $log->debug("register_connection: $clid");
}

sub remove_connection {
	my ( $self, $clid ) = @_;

	delete $self->{conn}->{$clid};

	main::DEBUGLOG && $log->debug("remove_connection: $clid");
}

sub get_connection {
	my ( $self, $clid ) = @_;

	return $self->{conn}->{$clid};
}

sub clid_for_connection {
	my ( $self, $conn ) = @_;

	my $result;

	while ( my ($clid, $c) = each %{ $self->{conn} } ) {
		if ( $conn eq $c->[0] ) {
			$result = $clid;
		}
	}

	return $result;
}

sub remove_client {
	my ( $self, $clid ) = @_;

	delete $self->{conn}->{$clid};
	delete $self->{events}->{$clid};

	$self->remove_channels( $clid );

	main::DEBUGLOG && $log->debug("remove_client: $clid");
}

sub is_valid_clid {
	my ( $self, $clid ) = @_;

	return exists $self->{events}->{$clid};
}

sub add_channels {
	my ( $self, $clid, $subs ) = @_;

	my $isDebug = main::DEBUGLOG && $log->is_debug;

	for my $sub ( @{$subs} ) {

		my $re_sub = $sub;

		# Turn channel globs into regexes
		# /foo/**, Matches /foo/bar, /foo/boo and /foo/bar/boo. Does not match /foo, /foobar or /foobar/boo
		if ( $re_sub =~ m{^/(.+)/\*\*$} ) {
			$re_sub = qr{^/$1/};
		}
		# /foo/*, Matches /foo/bar and /foo/boo. Does not match /foo, /foobar or /foo/bar/boo.
		elsif ( $re_sub =~ m{^/(.+)/\*$} ) {
			$re_sub = qr{^/$1/[^/]+};
		}

		$self->{channels}->{$re_sub} ||= {};
		$self->{channels}->{$re_sub}->{$clid} = 1;

		main::DEBUGLOG && $isDebug && $log->debug("add_channels: $sub ($re_sub) for $clid");
	}

	return 1;
}

sub remove_channels {
	my ( $self, $clid, $subs ) = @_;

	if ( !$subs ) {
		# remove all channels for this client
		for my $channel ( keys %{ $self->{channels} } ) {
			for my $sub_clid ( keys %{ $self->{channels}->{$channel} } ) {
				if ( $clid eq $sub_clid ) {
					delete $self->{channels}->{$channel}->{$clid};

					if ( !scalar keys %{ $self->{channels}->{$channel} } ) {
						delete $self->{channels}->{$channel};
					}

					main::DEBUGLOG && $log->debug("remove_channels for $clid: $channel");
				}
			}
		}
	}
	else {
		for my $sub ( @{$subs} ) {

			my $re_sub = $sub;

			# Turn channel globs into regexes
			# /foo/**, Matches /foo/bar, /foo/boo and /foo/bar/boo. Does not match /foo, /foobar or /foobar/boo
			if ( $re_sub =~ m{^/(.+)/\*\*$} ) {
				$re_sub = qr{^/$1/};
			}
			# /foo/*, Matches /foo/bar and /foo/boo. Does not match /foo, /foobar or /foo/bar/boo.
			elsif ( $re_sub =~ m{^/(.+)/\*$} ) {
				$re_sub = qr{^/$1/[^/]+};
			}

			for my $channel ( keys %{ $self->{channels} } ) {
				if ( $re_sub eq $channel ) {
					delete $self->{channels}->{$channel}->{$clid};

					if ( !scalar keys %{ $self->{channels}->{$channel} } ) {
						delete $self->{channels}->{$channel};
					}
				}
			}

			main::DEBUGLOG && $log->debug("remove_channels for $clid: $sub ($re_sub)");
		}
	}

	return 1;
}

sub queue_events {
	my ( $self, $clid, $events ) = @_;

	my $e = $self->{events}->{$clid};

	for my $event ( @{$events} ) {
		push @{$e}, $event;
	}
}

sub get_pending_events {
	my ( $self, $clid ) = @_;

	my $events = [];

	my $e = $self->{events}->{$clid} || [];

	if ( scalar @{$e} ) {
		for ( @{$e} ) {
			push @{$events}, $_;
		}

		# Clear all pending events
		$self->{events}->{$clid} = [];
	}

	return wantarray ? @{$events} : $events;
}

sub has_pending_events {
	my ( $self, $clid ) = @_;

	return scalar @{ $self->{events}->{$clid} };
}

sub deliver_events {
	my ( $self, $events, $clid ) = @_;

	if ( ref $events ne 'ARRAY' ) {
		$events = [ $events ];
	}

	my @to_send;

	# If a specific clid is provided, always try to deliver to them
	if ( $clid ) {
		push @to_send, $clid;
	}

	for my $event ( @{$events} ) {
		# Find subscriber(s) to this event
		my $channel = $event->{channel};

		if ( exists $self->{channels}->{$channel} ) {
			# Queue up all events for all subscribers
			# Since channels is a regexphash it will automatically match
			# globbed channels
			for my $clid ( keys %{ $self->{channels}->{$channel} } ) {
				push @to_send, $clid;

				main::DEBUGLOG && $log->debug("Sending event on channel $channel to $clid");
			}
		}
	}

	# Send everything
	for my $clid ( @to_send ) {
		my $conn = $self->{conn}->{$clid};

		# If we have an active connection to send to...
		if ( $conn ) {
			# Prepend any pending events
			unshift @{$events}, ( $self->get_pending_events( $clid ) );

			if ( main::DEBUGLOG && $log->is_debug ) {
				$log->debug(
					  "Delivering events to $clid:\n"
					. Data::Dump::dump( $events )
				);
			}

			Slim::Web::Cometd::sendResponse( @{$conn}, $events );
		}
		else {
			# queue the events for later
			$self->queue_events( $clid, $events );

			if ( main::DEBUGLOG && $log->is_debug ) {
				$log->debug( 'Queued ' . scalar @{$events} . " event(s) for $clid" );
			}
		}
	}

	return 1;
}

1;
