package TVGuide::Schedule ;

=head1 NAME

TVGuide::Schedule - Manage program recording scheduling

=head1 SYNOPSIS

use TVGuide::Schedule ;


=head1 DESCRIPTION


=head1 DIAGNOSTICS

Setting the debug flag to level 1 prints out (to STDOUT) some debug messages, setting it to level 2 prints out more verbose messages.

=head1 AUTHOR

Steve Price 

=head1 BUGS

None that I know of!

=head1 INTERFACE

=over 4

=cut

use strict ;
use Carp ;

our $VERSION = "1.015" ;

#============================================================================================
# USES
#============================================================================================
use TVGuide::Base::Object ;
use TVGuide::Prog ;
use TVGuide::Crontab ;
use TVGuide::DVB ;

#============================================================================================
# OBJECT HIERARCHY
#============================================================================================
our @ISA = qw(TVGuide::Base::Object) ; 

#============================================================================================
# GLOBALS
#============================================================================================

## Schedule HASH entry eventually contains:
#
# From SQL:
#	pid 
#	channel 
#	title 
#	date   (YYYY/MM/DD for Date::Manip)
#	start 
#	duration 
#	episode 
#	num_episodes 
#	repeat 
#	adapter 
#	chan_type 
#	record
#
#   priority - from schedule/recording (1=highest; 100=lowest)
#
# Created:
#  end	= end time
#  duration_secs = duration time in seconds
#  start_datetime = Date::Manip date for start time/date
#  end_datetime = Date::Manip date for start time/date
#  start_dt_mins = minutes from 1970 for start date/time
#  end_dt_mins = minutes from 1970 for end date/time
#
#


my %FIELDS = (
	'phase'		=> '',

	## Internal
	'_tvsql'		=> undef,		# TVGuide::Sql object
	'_tvrec'		=> undef,		# TVGuide::Recording object
	'_tvreport'		=> undef,		# TVGuide::Report object
	
	'today_mins'	=> undef,		# Today's date/time in minutes
) ;


#============================================================================================
# CONSTRUCTOR 
#============================================================================================

=item C<new([%args])>

Create a new object.

The %args are specified as they would be in the B<set> method, for example:

	'mmap_handler' => $mmap_handler

The full list of possible arguments are :

	'fields'	=> Either ARRAY list of valid field names, or HASH of field names with default values 

=cut

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

	my $class = ref($obj) || $obj ;

	# Create object
	my $this = $class->SUPER::new(%args) ;
	
	my $today_dt = TVGuide::Time::today_dt() ;
	my $today_mins = TVGuide::Time::dt2mins($today_dt) ;
	$this->today_mins($today_mins) ;

	return($this) ;
}



#============================================================================================
# CLASS METHODS 
#============================================================================================

#-----------------------------------------------------------------------------

=item C<init_class([%args])>

Initialises the Cwrsync object class variables. Creates a class instance so that these
methods can also be called via the class (don't need a specific instance)

=cut

sub init_class
{
	my $class = shift ;
	my (%args) = @_ ;

	if (! keys %args)
	{
		%args = () ;
	}
	
	# Add extra fields
	$class->add_fields(\%FIELDS, \%args) ;

	# init class
	$class->SUPER::init_class(%args) ;

	# Create a class instance object - allows these methods to be called via class
	$class->class_instance(%args) ;
	
}

#============================================================================================
# OBJECT DATA METHODS 
#============================================================================================



#============================================================================================
# OBJECT METHODS 
#============================================================================================

#---------------------------------------------------------------------
sub existing_schedule
{
	my $this = shift ;
	my $tvsql = $this->_tvsql ;
	my $tvrec = $this->_tvrec ;
	
	my @schedule = $tvsql->select_scheduled() ;

	$this->fix_times(\@schedule) ;	
	
	# Set up sub-title entry
	foreach (@schedule)
	{
		$_->{'subtitle'} ||= Linux::DVB::DVBT::Utils::subtitle($_->{'text'}) ;
	}
	
	# fix any mutliplex recordings
	@schedule = $this->fix_multiplex(\@schedule) ;
	
	# sort by start
	@schedule = sort { TVGuide::Prog::start_cmp($a, $b) } @schedule ;
	
	return @schedule ;
}

#---------------------------------------------------------------------
# Gets the latest scheduled recordings list from the database and also
# sets up various date/time values for later use
sub fix_times
{
	my $this = shift ;
	my ($schedule_aref) = @_ ;

	# Set up times
	foreach (@$schedule_aref)
	{
		# Set end time etc
		# only do so if extra information hasn't already been set
		unless (exists($_->{'start_dt_mins'}))
		{
			TVGuide::Prog::set_times($_) ;
		}
	}
}


#---------------------------------------------------------------------
# Run through all the phases to get the recordings scheduled. Allows there to already
# be a schedule of recordings (e.g. for when we want to add some new recordings to a
# previous good set)
#
sub schedule_recordings
{
	my $this = shift ;
	my ($num_adapters, $recording_schedule_aref, $schedule_aref, $unscheduled_aref, %options) = @_ ;

$this->prt_data("schedule_recordings() options=", \%options) if $this->debug >= 4 ;

	## filter out any recordings that are IPLAY-only
	my @dvbt_recordings = $this->filter_dvbt($recording_schedule_aref) ;

	## remove dummy "fuzzy" recordings
	my @filtered_recordings = $this->remove_dummies(\@dvbt_recordings) ;
	$recording_schedule_aref = \@filtered_recordings ;
	
	# keep a copy for later
	my $requested_rec_aref = $recording_schedule_aref ;
	my @requested_recordings = (@$recording_schedule_aref) ;

	TVGuide::Base::DbgProf::startfn() ;

	my $tvreport = $this->_tvreport ;

	# report
	$this->_new_phase('initial') ;
	$tvreport->recordings($recording_schedule_aref) ;
	$tvreport->scheduling($schedule_aref, $unscheduled_aref) ;

TVGuide::Prog::disp_sched("P0 Recordings=", $recording_schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("P0 Schedule=", $schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("P0 Unscheduled=", $unscheduled_aref) if $this->debug >= 4 ;

	## If allowed, group into multiplex blocks
	if ($options{'enable_multirec'})
	{
#		# remove any recording duplication
#		$recording_schedule_aref = $this->remove_recording_duplicates($recording_schedule_aref, $schedule_aref) ;
#		
#		# get next available multid
#		my $next_multid = $this->next_multid($schedule_aref) ;
#		
#		## Group new recordings into multiplex blocks
#		$recording_schedule_aref = $this->block_multiplex($recording_schedule_aref, %options, 'multid' => $next_multid) ;
#
#		## Attempt to add new recordings into the existing set
#		$recording_schedule_aref = $this->schedule_block_multiplex($recording_schedule_aref, $schedule_aref, %options) ;

		# Mark any multiplexes that are currently recording (or will be recording) as 'locked'
		$this->lock_multiplexes($schedule_aref) ;

		## merge together existing schedule over to recording list (along with the requested recordings!) 
		## (This removes duplicates!)
		my $demux_schedule_aref = $this->de_multiplex($schedule_aref) ;
		my %sched_recordings = map { $_->{'pid'} => $_ } @$demux_schedule_aref ;
		my %req_recordings = map { $_->{'pid'} => $_ } @$recording_schedule_aref ;
		my %all_recordings = (%sched_recordings, %req_recordings) ;
	
		# Move existing schedule over to recording list (along with the requested recordings!)
		@$recording_schedule_aref = values %all_recordings ;
		
		# clear out
		@$schedule_aref = () ;
		@$unscheduled_aref = () ;

		## Group new recordings into multiplex blocks
		$recording_schedule_aref = $this->block_multiplex($recording_schedule_aref, %options) ;

		$this->_new_phase('multiplex') ;
TVGuide::Prog::disp_sched("Multiplex recordings:", $recording_schedule_aref) if $this->debug ;

	}
	
	## 1: Schedule programs evenly over adapters
	if (scalar(@$recording_schedule_aref) || scalar(@$unscheduled_aref))
	{
		$this->_new_phase('no adjacent programs') ;
		$this->schedule($num_adapters, $recording_schedule_aref, $schedule_aref, $unscheduled_aref, 'adjacent' => 0) ;
	
		# report
		$tvreport->scheduling($schedule_aref, $unscheduled_aref) ;
	}
	
$this->prt_data("Recordings=", $recording_schedule_aref) if $this->debug >= 4 ;
$this->prt_data("Schedule=", $schedule_aref, "Unscheduled=", $unscheduled_aref) if $this->debug >= 4 ;

TVGuide::Prog::disp_sched("P1 Recordings=", $recording_schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("P1 Schedule=", $schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("P1 Unscheduled=", $unscheduled_aref) if $this->debug >= 4 ;


if ($this->debug >= 2)
{
print "\n==[ Phase 1 ]=============================================\n" ;
$tvreport->display_unschedule_by_adap($schedule_aref, $unscheduled_aref) ;
print "\n==========================================================\n" ;	
}

	## If unscheduled programs...
	if (@$unscheduled_aref)
	{

		## 2: Attempt to add unscheduled programs into any gaps
		$this->_new_phase('insert programs into gaps') ;
		$this->schedule($num_adapters, $unscheduled_aref, $schedule_aref, $unscheduled_aref, 'adjacent' => 1) ;

TVGuide::Prog::disp_sched("P2 Recordings=", $recording_schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("P2 Schedule=", $schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("P2 Unscheduled=", $unscheduled_aref) if $this->debug >= 4 ;


if ($this->debug >= 2)
{
print "\n==[ Phase 2 ]=============================================\n" ;
$tvreport->display_unschedule_by_adap($schedule_aref, $unscheduled_aref) ;
print "\n==========================================================\n" ;	
}

		# report
		$tvreport->scheduling($schedule_aref, $unscheduled_aref) ;

$this->prt_data("Schedule2=", $schedule_aref, "Unscheduled2=", $unscheduled_aref) if $this->debug >= 4 ;

		## If failed, then re-try allowing any to be adjacent
		if (@$unscheduled_aref)
		{
		my %sched_recordings ;
		my %req_recordings ;
		my %all_recordings ;
		
			## merge together existing schedule over to recording list (along with the requested recordings!) 
			## (This removes duplicates!)
			## NOTE: Using @resuested_recordings copy of recordings array BEFORE they have been merged into multiplex
			%sched_recordings = map { $_->{'pid'} => $_ } @$schedule_aref ;
			%req_recordings = map { $_->{'pid'} => $_ } @requested_recordings ;
			%all_recordings = (%sched_recordings, %req_recordings) ;
		
			# Move existing schedule over to recording list (along with the requested recordings!)
			@$recording_schedule_aref = values %all_recordings ;
			
			# clear out
			@$schedule_aref = () ;
			@$unscheduled_aref = () ;


			## If allowed, group into multiplex blocks
			if ($options{'enable_multirec'})
			{
				## Group new recordings into multiplex blocks
				$recording_schedule_aref = $this->de_multiplex($recording_schedule_aref) ;
				$recording_schedule_aref = $this->block_multiplex($recording_schedule_aref, %options) ;
		
				$this->_new_phase('multiplex rip-up') ;
		TVGuide::Prog::disp_sched("Multiplex recordings:", $recording_schedule_aref) if $this->debug ;
			}

#TODO: Only rip up from end to (just before?) failed one(s)
#      or just rip up region around failed ones?			

TVGuide::Prog::disp_sched("Pre-P3 Recordings=", $recording_schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("Pre-P3 Requested Recordings=", \@requested_recordings) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("Pre-P3 Schedule=", $schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("Pre-P3 Unscheduled=", $unscheduled_aref) if $this->debug >= 4 ;


			## 3: Attempt to redo whole schedule allow adjacent progs
			$this->_new_phase('rip up and retry allowing adjacent progs') ;
			$this->schedule($num_adapters, $recording_schedule_aref, $schedule_aref, $unscheduled_aref, 'adjacent' => 1) ;

TVGuide::Prog::disp_sched("P3 Recordings=", $recording_schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("P3 Schedule=", $schedule_aref) if $this->debug >= 4 ;
TVGuide::Prog::disp_sched("P3 Unscheduled=", $unscheduled_aref) if $this->debug >= 4 ;


# TODO: If allowed, attempt recording using multiplex?
# TODO: If that fails, attempt multiplex recordings from just before failed one?

if ($this->debug >= 2)
{
print "\n==[ Phase 3 ]=============================================\n" ;
$tvreport->display_unschedule_by_adap($schedule_aref, $unscheduled_aref) ;
print "\n==========================================================\n" ;	
}
	
			# report
			$tvreport->scheduling($schedule_aref, $unscheduled_aref) ;
	
	$this->prt_data("Schedule3=", $schedule_aref, "Unscheduled3=", $unscheduled_aref) if $this->debug >= 4 ;
		}

	}

	## Need to propogate the DVB adapter down from the 'multiplex' container into each sub-prog
	$this->update_multiplex($schedule_aref) ;


#	## If timeslip is enabled, then flag each recording with whether it can be timeslipped or not
#	$this->flag_timeslip($schedule_aref, %options) ;


	# update the array passed in 
	@$requested_rec_aref = @$recording_schedule_aref ;

	TVGuide::Base::DbgProf::endfn() ;
	
	my $ok = scalar(@$unscheduled_aref) ? 0 : 1 ;
	return ($ok) ;	
}

#---------------------------------------------------------------------
# Remove any dummy "fuzzy" recordings
#
sub remove_dummies
{
	my $this = shift ;
	my ($recording_aref) = @_ ;

# TODO: Change scheduling to not schedule these in the first place!

	TVGuide::Base::DbgTrace::startfn('remove dummies') ;
	TVGuide::Base::DbgProf::startfn() ;

	my @removed ;
	foreach my $prog_href (@$recording_aref)
	{
		if ($prog_href->{'pid'} == $TVGuide::Base::Constants::FUZZY_PID)
		{
			# delete 
		}
		else
		{
			push  @removed, $prog_href ;
		}
	}
	
	TVGuide::Base::DbgProf::endfn() ;
	
	return @removed ;
}


#---------------------------------------------------------------------
# Update cron jobs
#
sub update_cron
{
	my $this = shift ;
	my ($schedule_aref) = @_ ;

	TVGuide::Base::DbgTrace::startfn('update cron') ;
	TVGuide::Base::DbgProf::startfn() ;

	## Set cron
	TVGuide::Crontab::update($schedule_aref) ;

	TVGuide::Base::DbgProf::endfn() ;

}

#---------------------------------------------------------------------
# Commit the final schedule (both to database and cron jobs)
#
sub commit
{
	my $this = shift ;
	my ($schedule_aref, $test) = @_ ;

	TVGuide::Base::DbgTrace::startfn('commit') ;
	TVGuide::Base::DbgProf::startfn() ;


	## Set cron
	TVGuide::Crontab::commit() unless $test ;

	## Set database
	my $tvsql = $this->_tvsql ;
	$tvsql->update_schedule_table($schedule_aref) unless $test>=2 ;
	
	## Update recorded files
	unless ($test >= 2)
	{	
		my $recorded_files_href = TVGuide::Crontab::recorded_files() ;

print "Schedule: Update recorded table\n" if $this->debug ;

		## Check for exsistence of recrded entries
		foreach my $idx (sort {
				my $a_href = $recorded_files_href->{$a} ;
				my $b_href = $recorded_files_href->{$b} ;
				$a_href->{'start_dt_mins'} <=> $b_href->{'start_dt_mins'} || 
				$a_href->{'rid'} <=> $b_href->{'rid'} || 
				$a_href->{'rectype'} cmp $b_href->{'rectype'}
			}  keys %$recorded_files_href)
		{
print " + $recorded_files_href->{$idx}{pid} ($recorded_files_href->{$idx}{rectype}) $recorded_files_href->{$idx}{title}\n" if $this->debug ;

			my $exsisting_aref = $tvsql->select_recorded($recorded_files_href->{$idx}) ;
			
			## If not exsits, then create a new one
			if (!@$exsisting_aref)
			{
print " + + Does not exist : creating new\n" if $this->debug ;
				$tvsql->insert_recorded($recorded_files_href->{$idx}) ;	
			}
			else
			{
print " + + Exists : status=\"$exsisting_aref->[0]{'status'}\"\n" if $this->debug ;

				## Check status - if set then skip this (don't alter an existing result)
				if (!$exsisting_aref->[0]{'status'})
				{
print " + + + No status : amended details\n" if $this->debug ;
					## No status (i.e. not recorded yet), so amend details
					$tvsql->update_recorded($recorded_files_href->{$idx}) ;	
				}
			}
		}
		
		
#		## delete any existing entries
#		foreach my $idx (keys %$recorded_files_href)
#		{
#			$tvsql->delete_recorded($recorded_files_href->{$idx}) ;	
#		}
#		
#		## Create new
#		foreach my $idx (keys %$recorded_files_href)
#		{
#			$tvsql->insert_recorded($recorded_files_href->{$idx}) ;	
#		}
	}


	TVGuide::Base::DbgProf::endfn() ;
}

#---------------------------------------------------------------------
# Remove any recordings matching this RID
# 
#
#Need to keep multiplex info, just remove prog from multiplex (and demote to prog if multiplex
#only holds a single prog). Otherwise the multirec table (Sql) is screwed.
#
sub unschedule
{
	my $this = shift ;
	my ($schedule_aref, $rid) = @_ ;

	my @sched = (@$schedule_aref) ;
	@$schedule_aref = () ;
	
	foreach my $href (@sched)
	{
		if ($href->{'type'} eq 'multiplex')
		{
			my @progs = @{$href->{'multiplex'}} ;
			$href->{'multiplex'} = [] ;
			foreach my $rec_href (@progs)
			{
				if ($rec_href->{'rid'} != $rid)
				{
					push @{$href->{'multiplex'}}, $rec_href ;
				}
			}
			
			# see how many progs are left inside the multiplex container
			if (scalar(@{$href->{'multiplex'}}) > 1)
			{
				# still got more than 1, so keep the multiplex
				push @$schedule_aref, $href ;
			}
			elsif (scalar(@{$href->{'multiplex'}}) > 0)
			{
				# only got 1 prog in multiplex, so just save that (delete multiplex)
				my $prog_href = $href->{'multiplex'}[0] ;
				push @$schedule_aref, $prog_href ;
				
				# Need to amend the prog to remove it from the multiplex
				$prog_href->{'multid'} = 0 ;
			}

			
		}
		else
		{
			if ($href->{'rid'} != $rid)
			{
				push @$schedule_aref, $href ;
			}
		}
	}
}

#---------------------------------------------------------------------
# Called by:
#	schedule_recordings()
#
# Input: 
#	$recording_schedule_aref - ARRAY ref of recording requests expanded
#								into schedule format and sorted by time
#
# Output:
#	$schedule_aref - ARRAY ref of scheduled recordings
#	$unscheduled_aref - ARRAY ref of any recordings not yet scheduled
# 
sub schedule
{
	my $this = shift ;
	my ($num_adapters, $recording_schedule_aref, $schedule_aref, $unscheduled_aref, %options) = @_ ;

print "\n -- schedule (adjacent=$options{adjacent}) --\n\n" if $this->debug ;	

	my $phase = $this->phase ;
	TVGuide::Base::DbgTrace::startfn("[PHASE: $phase] schedule") ;
	TVGuide::Base::DbgProf::startfn("[PHASE: $phase]") ;

	# Sort recordings into order of date / priority
	my @recordings = sort { TVGuide::Prog::prog_cmp($a, $b) } @$recording_schedule_aref ;

TVGuide::Prog::disp_sched("Sorted recordings:", \@recordings) if $this->debug ;
	
	## Keep track of the DVB adapter
	my $adapter = 0 ;
	
	# a schedule list per adapter - makes it quicker to search for overlaps
	my @adapter_schedule ;
	
	## Copy over any existing
	if (@$schedule_aref)
	{
		@$schedule_aref = sort { TVGuide::Prog::prog_cmp($a, $b) } @$schedule_aref ;
		foreach my $prog_href (@$schedule_aref)
		{
			my $adapter = $prog_href->{'adapter'} || 0 ;
			$adapter_schedule[$adapter] ||= [] ;
			push @{$adapter_schedule[$adapter]}, $prog_href ;

			TVGuide::Base::DbgTrace::add_rec($prog_href, "copied existing") ;
		}
	}

	TVGuide::Base::DbgProf::waypoint("copied any existing") ;

if ($this->debug)
{
	print "Existing schedule (Num PVR=$num_adapters):\n" ;
	for (my $adapter=0; $adapter < $num_adapters; ++$adapter)
	{
		print "  [Adapter $adapter]\n" ;
		foreach my $prog_href (@{$adapter_schedule[$adapter]})
		{
			print "    $prog_href->{title}  $prog_href->{date} $prog_href->{start} : start_dt_mins=$prog_href->{'start_dt_mins'}\n" ;	
		}
	}
	
	print "\n" ;	
}
	
		
	## Process the recordings
	@$unscheduled_aref = () ;
	foreach my $prog_href (@recordings)
	{
		TVGuide::Base::DbgTrace::add_rec($prog_href, {
			'seen'	=> 1,	
		}) ;
		
		## try adding to current adapter schedule, otherwise cycle through the other adapters
		## and leave us pointing at either the one we managed to schedule the prog on, or the one we staretd
		## with
		my $scheduled = 0 ;
		for (my $adapter_num=0; !$scheduled && ($adapter_num < $num_adapters); ++$adapter_num)
		{
			$adapter_schedule[$adapter] ||= [] ;
			
			my $dvb = sprintf "dvb%d", $adapter ;

print "-> adapter=$adapter\n" if $this->debug ;	
			
			## check to see if we can add this to the current schedule
			if ($this->can_schedule($adapter_schedule[$adapter], $prog_href))
			{
				TVGuide::Base::DbgTrace::add_rec($prog_href, "can schedule on $dvb") ;

				## iff required: only add if not adjacent
				if ($options{'adjacent'} || ! $this->prog_adjacent($adapter_schedule[$adapter], $prog_href))
				{
					## add and set adapter
					$prog_href->{'adapter'} = $adapter ;
					push @{$adapter_schedule[$adapter]}, $prog_href ;
					
					TVGuide::Base::DbgTrace::add_rec($prog_href, "** scheduled on $dvb **") ;

					++$scheduled ;
print " + + Scheduled onto adapter=$adapter!\n\n" if $this->debug ;	
				}
				else
				{
					TVGuide::Base::DbgTrace::add_rec($prog_href, "skipped adjacent on $dvb") ;
				}
			}
			else
			{
				TVGuide::Base::DbgTrace::add_rec($prog_href, "cannot schedule on $dvb") ;
			}
			
			if (!$scheduled)
			{		
				## next adapter
				$adapter = ($adapter+1) % $num_adapters ;

print " + + FAILED to schedule\n\n" if $this->debug ;	
			}
		}

		## if failed, add to unscheduled list
		if (!$scheduled)
		{
			## add to unscheduled list
			push @$unscheduled_aref, $prog_href ;
		}
	}

	TVGuide::Base::DbgProf::waypoint("processed recordings") ;

if ($this->debug)
{
	print "Final schedule:\n" ;
	for (my $adapter=0; $adapter < $num_adapters; ++$adapter)
	{
		print "  [Adapter $adapter]\n" ;
		foreach my $prog_href (@{$adapter_schedule[$adapter]})
		{
			print "    $prog_href->{title}  : $prog_href->{'start_dt_mins'}\n" ;	
		}
	}
	
	print "\n" ;	
}
	
	
	## Create final schedule
	@$schedule_aref = () ;
	foreach (@adapter_schedule)
	{
		push @$schedule_aref, @$_ ;
	}
	
	# sort into date order
	@$schedule_aref = sort { TVGuide::Prog::start_cmp($a, $b) } @$schedule_aref ;

	TVGuide::Base::DbgProf::waypoint("created final schedule") ;


print "\n -- schedule END --\n\n" if $this->debug ;	
	TVGuide::Base::DbgProf::endfn() ;
	
}



#---------------------------------------------------------------------
# Create a multiplex recording entry based on a single program
sub _new_multiplex
{
	my $this = shift ;
	my ($prog_href, $multid) = @_ ;
	
	my $mux_href = {
		%$prog_href,
		
		'type'			=> 'multiplex',
		'multiplex'		=> [ $prog_href ],
		'title'			=> 'Multiplex',
		'channel'		=> '(multiple)',
		'multid'		=> $multid,
	} ;

	return $mux_href ;	
}

#---------------------------------------------------------------------
# Input: 
#	$recording_schedule_aref - ARRAY ref of recording requests expanded
#								into schedule format and sorted by time
#
# Output:
#	$recording_schedule_aref - ARRAY ref of recording requests grouped
#								into multiplexes and sorted by time
# 
sub block_multiplex
{
	my $this = shift ;
	my ($recording_schedule_aref, %options) = @_ ;
	
	# used to track the multiplex record grouping (in sql) - i.e. which group a channel recording is in
	my $multid = $options{'multid'} || 1 ;

print "\n -- block_multiplex --\n\n" if $this->debug ;	
	TVGuide::Base::DbgProf::startfn("block_multiplex") ;

	# Sort recordings into order of date (ignore priority)
	my @recordings = sort { TVGuide::Prog::start_cmp($a, $b) } @$recording_schedule_aref ;

TVGuide::Prog::disp_sched("Sorted recordings:", \@recordings) if $this->debug ;
	
		
	## Process each recording
	my $mux_recording_schedule_aref = [] ;
	do
	{
		## get this recording
		my $prog_href = shift @recordings ;

		#                                        timeslip
		#                   |-----------------|..............|
		#                     prog_href               |-------------------------|
		#                                               next_prog_href

		## get the TV channels that are in the same multiplex as this program
		my %multiplex_chans = TVGuide::DVB::multiplex_channels($prog_href->{'channel'}) ;
		my $end_mins = $prog_href->{'end_dt_mins'} ;
		my $priority = $prog_href->{'priority'} ;
		my $max_start = $end_mins + $options{'max_timeslip'} ;
		my $mux_locked = $prog_href->{'locked'} ;

TVGuide::Prog::disp_sched("Looking at:", [$prog_href]) if $this->debug ;
print " * start_dt=$prog_href->{start_dt_mins} end_dt=$prog_href->{end_dt_mins} : max=$max_start\n" if $this->debug ;
print " * mux lock = $mux_locked\n" if $this->debug ;
$this->prt_data("Multiplex chans:", \%multiplex_chans) if $this->debug ;
		
		# Compare all subsequent recordings and compare with this one to see if they are in the same multiplex
		my @temp = @recordings ;
		@recordings = () ;
		my @multiplex ;
		foreach my $next_prog_href (@temp)
		{
			my $this_locked = $next_prog_href->{'locked'} ;
			
TVGuide::Prog::disp_sched(" + sub prog:", [$next_prog_href]) if $this->debug >= 2 ;
print " * * start_dt=$next_prog_href->{start_dt_mins} end_dt=$next_prog_href->{end_dt_mins} : max=$max_start\n" if $this->debug >= 2;
print " * * locked = $this_locked\n" if $this->debug >= 2;

			# check within  "range" of first prog
			if ($next_prog_href->{'start_dt_mins'} > $max_start)
			{
				# nowt within range, so not in multiplex...
				push @recordings, $next_prog_href ;
print " + start > max\n" if $this->debug >= 2 ;
				next ;
			}
			
			# if the multiplex is locked (i.e. can't add any new items) then check that this new prog 
			# is also locked (i.e. part of the same mux)
			#
			# Actually 2 cases: 
			# 1. prog_href is part of locked mux and next prog is new
			# 2. prog_href is new and next prog is part of locked mux
			#
			# In either case we can't add them together into a mux
			#
			if ($mux_locked != $this_locked)
			{
				# can't add to the mux, so treat as normal prog
print " + + skip next prog due to lock mismatch (this=$mux_locked, next=$this_locked)\n" if $this->debug;
				push @recordings, $next_prog_href ;
				next ;
			}
			
			# see if in the same multiplex
			if (exists($multiplex_chans{$next_prog_href->{'channel'}}))
			{
TVGuide::Prog::disp_sched(" + found mux:", [$next_prog_href]) if $this->debug ;
##print " + found mux\n" if $this->debug ;
				push @multiplex, $next_prog_href ;
				
				# update end time
				if ($end_mins < $next_prog_href->{'end_dt_mins'})
				{
					$end_mins = $next_prog_href->{'end_dt_mins'} ;
					$max_start = $end_mins + $options{'max_timeslip'} ;
print " * * * updated end: end_dt=$end_mins : max=$max_start\n" if $this->debug ;
				}
				
				# update priority
				if ($priority > $next_prog_href->{'priority'})
				{
					$priority = $next_prog_href->{'priority'} ;
				}

				next ;
			}
			
			
print " + normal\n" if $this->debug >= 2;
			push @recordings, $next_prog_href ;
		}
		
		# see if we have multiple entries
		if (@multiplex)
		{
			my $duration = $end_mins - $prog_href->{'start_dt_mins'} ;
			
			# create multiplex entry
			my $entry_href = $this->_new_multiplex($prog_href, $multid++) ;
			$entry_href->{'duration'} = TVGuide::Time::mins2time($duration) ; ;
			$entry_href->{'priority'} = $priority ;
			push @{$entry_href->{'multiplex'}}, @multiplex ;

			TVGuide::Prog::set_times($entry_href) ;
			push @$mux_recording_schedule_aref, $entry_href ;
			
TVGuide::Prog::disp_sched("final Multiplex:", [$entry_href]) if $this->debug ;
			
		}
		else
		{
			# simple single prog
			push @$mux_recording_schedule_aref, $prog_href ;
		}
		
	}
	while (@recordings) ;
	
	TVGuide::Base::DbgProf::waypoint("created mulriplex recordings") ;


print "\n -- schedule END --\n\n" if $this->debug ;	
	TVGuide::Base::DbgProf::endfn() ;

TVGuide::Prog::disp_sched("Multiplex recs:", $mux_recording_schedule_aref) if $this->debug ;

	# sort into date order
	@$mux_recording_schedule_aref = sort { TVGuide::Prog::start_cmp($a, $b) } @$mux_recording_schedule_aref ;

TVGuide::Prog::disp_sched("Sorted mulriplex recs:", $mux_recording_schedule_aref) if $this->debug ;
	
	return $mux_recording_schedule_aref ;
}


##---------------------------------------------------------------------
## Add new recordings to the existing set
##
## Input: 
##	$mux_recording_schedule_aref - ARRAY ref of recording requests grouped
##								into multiplexes and sorted by time
##
##	$mux_schedule_aref - ARRAY ref of existing schedule grouped
##								into multiplexes and sorted by time
##
## Output:
##	$mux_recording_schedule_aref - ARRAY ref of recording requests grouped
##								into multiplexes and sorted by time
## 
#sub schedule_block_multiplex
#{
#	my $this = shift ;
#	my ($mux_recording_schedule_aref, $mux_schedule_aref, %options) = @_ ;
#	
#	## get HASH of existing recordings and new recordings
#	my $demux_schedule_aref = $this->de_multiplex($schedule_aref) ;
#	my %sched_recordings = map { $_->{'pid'} => $_ } @$demux_schedule_aref ;
#	my %req_recordings = map { $_->{'pid'} => $_ } @$recording_schedule_aref ;
#
#	
#	return $mux_recording_schedule_aref ;
#}
#
#
##---------------------------------------------------------------------
## Remove any recordings that are duplicates
##
#sub remove_recording_duplicates
#{
#	my $this = shift ;
#	my ($recording_schedule_aref, $mux_schedule_aref) = @_ ;
#	
#	## get HASH of existing recordings and new recordings
#	my $demux_schedule_aref = $this->de_multiplex($schedule_aref) ;
#	my %sched_recordings = map { $_->{'pid'} => $_ } @$demux_schedule_aref ;
#	my %req_recordings = map { $_->{'pid'} => $_ } @$recording_schedule_aref ;
#	
#	## Strip out duplicates
#	my @recordings ;
#	foreach my $pid (keys %req_recordings)
#	{
#		if (!exists($sched_recordings{$pid}))
#		{
#			push @recordings, $req_recordings{$pid} ;
#		}
#	}
#	return \@recordings ;
#}
#
#		
#
##---------------------------------------------------------------------
## Get the next available multiplex id
## 
#sub next_multid
#{
#	my $this = shift ;
#	my ($schedule_aref) = @_ ;
#
#	my $next_multid = 0 ;
#	
#	foreach my $rec_href (@$schedule_aref)
#	{
#		if ($rec_href->{'type'} eq 'multiplex')
#		{
#			if ($next_multid < $rec_href->{'multid'})
#			{
#				$next_multid = $rec_href->{'multid'} ;
#			}
#		}
#	}
#	
#	return ++$next_multid ;
#}


#---------------------------------------------------------------------
# Mark any multiplex that is about to be recorded (or is being recorded) 
# as "locked". This then means we can't schedule recordings that will change 
# the multiplex.
#
# Adds the 'locked'=>1 field to multiplex AND the program
# 
sub mark_locked_recordings
{
	my $this = shift ;
	my ($recording_aref, $schedule_aref) = @_ ;

$this->prt_data("mark_locked_recordings() existing:", $schedule_aref) if $this->debug>=2 ;
	
	## First mark the schedule
	$this->lock_multiplexes($schedule_aref) ;
	
	## Get the list
	my $demux_schedule_aref = $this->de_multiplex($schedule_aref) ;
	my %sched_recordings = map { $_->{'pid'} => $_ } @$demux_schedule_aref ;
	my %req_recordings = map { $_->{'pid'} => $_ } @$recording_aref ;
	
	## Copy the locked flag to the recordings
	foreach my $pid (keys %req_recordings)
	{
		if (exists($sched_recordings{$pid}))
		{
			if ($sched_recordings{$pid}{'locked'})
			{
				$req_recordings{$pid}{'locked'} = 1 ;
			}
		}
	}
}

#---------------------------------------------------------------------
# Mark any multiplex that is about to be recorded (or is being recorded) 
# as "locked". This then means we can't schedule recordings that will change 
# the multiplex.
#
# Adds the 'locked'=>1 field to multiplex AND the program
# 
sub lock_multiplexes
{
	my $this = shift ;
	my ($schedule_aref) = @_ ;
	
print "lock_multiplexes()\n" if $this->debug ;
#$this->prt_data("Schedule to lock:", $schedule_aref) if $this->debug>=2 ;
	
	# get current time with some margin
	#
	#	mux -----------| ok (s<C e<A)
	#	
	#	mux ---------------| lock (s<C e>A) - ending recording, can't change
	#	
	#	mux                 |--------------| lock (s<C e>A) - started recording, can't change
	#	
	#	mux                    |--------------| lock (s<C e>A) - imminent start, can't change
	#	
	#	mux                      |------------------| ok (s>=C e>A) - start far enough away to change
	#	
	#	now------------------^B
	#	 - margin  ......^A	
	#	 + margin  .............^C	
	#
	#
	my $today_mins = $this->today_mins ;
	my $MARGIN = 5 ;
	my $min_time = $today_mins - $MARGIN ;
	my $max_time = $today_mins + $MARGIN ;

print " * now=$today_mins (-margin=$min_time, +margin=$max_time)\n" if $this->debug ;
	
	# check each multiplex compared with the current time
	foreach my $rec_href (@$schedule_aref)
	{
print " + $rec_href->{'title'}\n" if $this->debug ;
		if ($rec_href->{'type'} eq 'multiplex')
		{
print " + mux start=$rec_href->{'start_dt_mins'}, end=$rec_href->{'end_dt_mins'}\n" if $this->debug ;

			# see if this is locked
			my $locked = 0 ;
			if ( ($rec_href->{'start_dt_mins'} < $max_time) && ($rec_href->{'end_dt_mins'} >= $min_time) ) 
			{
				++$locked ;
print " + + locked\n" if $this->debug ;
			}
			
			if ($locked)
			{
				$rec_href->{'locked'} = 1 ;
				foreach my $mux_href (@{$rec_href->{'multiplex'}})
				{
					$mux_href->{'locked'} = 1 ;
				}
			}
		}
	}
	
#$this->prt_data("Schedule locked:", $schedule_aref) if $this->debug>=2 ;
print "lock_multiplexes() - DONE\n" if $this->debug ;
	
}



#---------------------------------------------------------------------
# Input: 
#	$schedule_aref - ARRAY ref of scheduled recordings (with any multiplex recordings)
#
# Output:
#	$schedule_aref - ARRAY ref of scheduled recordings (without any multiplex recordings)
# 
sub de_multiplex
{
	my $this = shift ;
	my ($schedule_aref, %options) = @_ ;
	my $demux_aref = [] ;
	
	foreach my $rec_href (@$schedule_aref)
	{
		if ($rec_href->{'type'} eq 'multiplex')
		{
			push @$demux_aref, @{$rec_href->{'multiplex'}} ;
		}
		else
		{
			push @$demux_aref, $rec_href ;
		}
	}
	
	return $demux_aref ;
}


#---------------------------------------------------------------------
# Input: 
#	$schedule_aref - ARRAY ref of schedule format
#
# Propogates the DVB adapter number & the multiplex ID down into all progs contained
# in a multiplex recording
#
sub update_multiplex
{
	my $this = shift ;
	my ($schedule_aref, %options) = @_ ;

	my @FIELDS = qw/multid adapter timeslip/ ;
	
	foreach my $rec_href (@$schedule_aref)
	{
		# ensure defaults are set
		foreach my $field (@FIELDS)
		{
			$rec_href->{$field} ||= 0 ;
		}
		
		# propogate from top-level multiplex container down to the individual programs
		if ($rec_href->{'type'} eq 'multiplex')
		{
			foreach my $mux_href (@{$rec_href->{'multiplex'}})
			{
				foreach my $field (@FIELDS)
				{
					$mux_href->{$field} = $rec_href->{$field} ;
				}
			}
		}
	}
}

#---------------------------------------------------------------------
# Input: 
#	$schedule_aref - ARRAY ref in schedule format (read from SQL table)
#
# Output: 
#	$schedule_aref - ARRAY ref of schedule format (with multiplex recordings fixed)
#
#
sub fix_multiplex
{
	my $this = shift ;
	my ($schedule_aref) = @_ ;
	
	my @mux_schedule = () ;
	my %multiplex ;

	foreach my $rec_href (@$schedule_aref)
	{
		my $multid = $rec_href->{'multid'} ;
		if ($multid > 0)
		{
			# create new entry if required
			if (!exists($multiplex{$multid}))
			{
				# create new multiplex entry based on this prog's info
				my $mux_href = $this->_new_multiplex($rec_href, $multid) ;
				push @mux_schedule, $mux_href ;
				
				$multiplex{$multid} = $mux_href ;
			}
			else
			{
				# get multiplex & update the start/end/duration/priority based on this new prog 
				my $mux_href = $multiplex{$multid} ;
				push @{$mux_href->{'multiplex'}}, $rec_href ;
				
				if ($mux_href->{'start_dt_mins'} > $rec_href->{'start_dt_mins'})
				{
					$mux_href->{'date'} = $rec_href->{'date'} ;
					$mux_href->{'start'} = $rec_href->{'start'} ;
					$mux_href->{'start_dt_mins'} = $rec_href->{'start_dt_mins'} ;
				}
				$mux_href->{'end_dt_mins'} = $rec_href->{'end_dt_mins'} if ($mux_href->{'end_dt_mins'} < $rec_href->{'end_dt_mins'}) ;
				$mux_href->{'priority'} = $rec_href->{'priority'} if ($mux_href->{'priority'} > $rec_href->{'priority'}) ;

			}
		}
		else
		{
			push @mux_schedule, $rec_href ;
		}
	}


	## Now ensure all multiplex times/durations etc are correct
	foreach my $rec_href (@mux_schedule)
	{
		if ($rec_href->{'type'} eq 'multiplex')
		{
			my $duration = $rec_href->{'end_dt_mins'} - $rec_href->{'start_dt_mins'} ;
			$rec_href->{'duration'} = TVGuide::Time::mins2time($duration) ;
			TVGuide::Prog::set_times($rec_href) ;
		}
	}
	
	return @mux_schedule ;
}

#---------------------------------------------------------------------
# Replace all rids set to NEW_RID with the specified new value
#
sub update_rid($new_rid, \@schedule)
{
	my $this = shift ;
	my ($new_rid, $schedule_aref) = @_ ;
	
	foreach my $href (@$schedule_aref)
	{
		my @progs = ($href) ;
		if ($href->{'type'} eq 'multiplex')
		{
			@progs = @{$href->{'multiplex'}} ;
		}
		foreach my $rec_href (@progs)
		{
			if ($rec_href->{'rid'} == $TVGuide::Base::Constants::NEW_RID)
			{
				$rec_href->{'rid'} = $new_rid ;
			}
		}
	}
}

#---------------------------------------------------------------------
# Check the schedule list for overlapped programs.  
# Assumes the other programs are non-overlapping and checks to ensure 
# that this new one doesn't interfere with existing ones. Also assumes
# that the schedule list is for the same DVB adapter
#
sub can_schedule
{
	my $this = shift ;
	my ($schedule_aref, $prog_href) = @_ ;

print "can_sched($prog_href->{title}  $prog_href->{date} $prog_href->{start})\n" if $this->debug ;			
	
	my $ok = 1 ;
	
	# Start with the new entry, moving through the schedule list to see if it overlaps 

	# For each entry, need to check 
	# (a) it starts AFTER the end of the previous program
	#
	#  (prev)  end
	#  ----------|
	#      ^
	#      |---------------.....
	#     start   (current)
	#
	#
	# (b) it ends BEFORE the start of the next program
	#
	#                     start (next)
	#                       |--------....
	#                          ^
	#      |-------------------|
	#     start   (current)   end
	#
	#
	# NOTE: May we have:
	#
	#   start                                           end
	#    |-----------------------------------------------|
	#               |----------------|
	#              start (current)  end
	#
	# Easiest approach is to create a temporary sorted list, then just look at the new program and see if it overlaps
	#
	#

	# add new to list
	my $num_entries ;
	my $new_index ;
	my @new_list = $this->_insert_prog($schedule_aref, $prog_href, \$num_entries, \$new_index) ;

	# check for overlap
	if ($new_index > 0)
	{
		# check new start is AFTER previous end
		if ($new_list[$new_index]{'start_dt_mins'} < $new_list[$new_index-1]{'end_dt_mins'})
		{
			$ok = 0 ;
print " + new prog start is not AFTER prev end  ($new_list[$new_index-1]{'title'} $new_list[$new_index-1]{'start'} - $new_list[$new_index-1]{'end'})\n" if $this->debug ;	
			my $other = TVGuide::Base::DbgTrace::format_rec($new_list[$new_index-1]) ;		
			TVGuide::Base::DbgTrace::add_rec($prog_href, "can't schedule: new prog start is not AFTER prev end [Prev: $other]") ;
		}
	}

	# if not the last one
	if ($new_index < $num_entries-1)
	{
		# check new end is BEFORE next start
		if ($new_list[$new_index]{'end_dt_mins'} > $new_list[$new_index+1]{'start_dt_mins'})
		{
			$ok = 0 ;
print " + new prog end is not BEFORE next start ($new_list[$new_index+1]{'title'} $new_list[$new_index+1]{'start'} - $new_list[$new_index+1]{'end'})\n" if $this->debug ;			
			my $other = TVGuide::Base::DbgTrace::format_rec($new_list[$new_index+1]) ;		
			TVGuide::Base::DbgTrace::add_rec($prog_href, "can't schedule: new prog end is not BEFORE next start [Next: $other]") ;
		}
	}
		
print " + can sched=$ok\n" if $this->debug ;			

	return $ok ;
}

#---------------------------------------------------------------------
# Returns true if new program is adjacent to previous or next program
#
sub prog_adjacent
{
	my $this = shift ;
	my ($schedule_aref, $prog_href) = @_ ;

print "prog_adjacent($prog_href->{title}  $prog_href->{date} $prog_href->{start})\n" if $this->debug ;			

	my $adjacent = 0 ;

	# add new to list
	my $num_entries ;
	my $new_index ;
	my @new_list = $this->_insert_prog($schedule_aref, $prog_href, \$num_entries, \$new_index) ;

	# check for adjacent to prev
	if ($new_index > 0)
	{
		# check new start is ADJACENT previous end
		if ($new_list[$new_index]{'start_dt_mins'} == $new_list[$new_index-1]{'end_dt_mins'})
		{
			$adjacent = 1 ;
print " + new prog start is ADJACENT prev end ($new_list[$new_index-1]{'title'} $new_list[$new_index-1]{'start'} - $new_list[$new_index-1]{'end'})\n" if $this->debug ;			
			my $other = TVGuide::Base::DbgTrace::format_rec($new_list[$new_index-1]) ;		
			TVGuide::Base::DbgTrace::add_rec($prog_href, "new prog start is ADJACENT to prev end [Next: $other]") ;
		}
	}

	# if not the last one
	if ($new_index < $num_entries-1)
	{
		# check new end is ADJACENT next start
		if ($new_list[$new_index]{'end_dt_mins'} == $new_list[$new_index+1]{'start_dt_mins'})
		{
			$adjacent = 1 ;
print " + new prog end is ADJACENT next start ($new_list[$new_index+1]{'title'} $new_list[$new_index+1]{'start'} - $new_list[$new_index+1]{'end'})\n" if $this->debug ;			
			my $other = TVGuide::Base::DbgTrace::format_rec($new_list[$new_index+1]) ;		
			TVGuide::Base::DbgTrace::add_rec($prog_href, "new prog end is ADJACENT to next start [Next: $other]") ;
		}
	}
		
print " + adjacent=$adjacent\n" if $this->debug ;			

	return $adjacent ;
}


#---------------------------------------------------------------------
sub _insert_prog
{
	my $this = shift ;
	my ($schedule_aref, $prog_href, $num_entries_ref, $new_index_ref) = @_ ;

print "_insert_prog($prog_href->{title}  $prog_href->{date} $prog_href->{start})\n" if $this->debug ;			

	# add new to list
	my %new_entry = (%$prog_href) ;
	TVGuide::Prog::set_times(\%new_entry) ;
	my @new_list = sort { TVGuide::Prog::start_cmp($a, $b) } (@$schedule_aref, \%new_entry) ;
	
	# find new
	$$num_entries_ref = scalar(@new_list) ;
	for(my $i=0; $i<$$num_entries_ref; ++$i)
	{
		if ($new_list[$i]{'pid'} == $prog_href->{'pid'})
		{
			$$new_index_ref = $i ;
			last ;
		}
	}
print " - num entries=$$num_entries_ref, new index=$$new_index_ref\n" if $this->debug ;			

	return @new_list ;	
}

#---------------------------------------------------------------------
# Start a new scheduling phase
sub _new_phase
{
	my $this = shift ;
	my ($phase) = @_ ;

	## save phase for tracing
	$this->phase($phase) ;
	
	## set report
	my $tvreport = $this->_tvreport ;
	$tvreport->new_phase($phase) ;
}

#---------------------------------------------------------------------
# filter out any recordings that are IPLAY only (they get handled elsewhere)
#
sub filter_dvbt
{
	my $this = shift ;
	my ($recording_schedule_aref) = @_ ;

	my @dvbt_recordings = () ;
	foreach my $prog_href (@$recording_schedule_aref)
	{
		if ($prog_href->{'type'} eq 'multiplex')
		{
			my @progs = @{$prog_href->{'multiplex'}} ;
			@{$prog_href->{'multiplex'}} = () ;
			foreach my $href (@progs)
			{
				if (!TVGuide::Base::Constants::has_dvbt($href->{'record'}))
				{
					# skip anything that doesn't have DVBT
					next
				}
				push @{$prog_href->{'multiplex'}}, $href ;
			}
			
			# save multiplex if multiplex actually contains something
			if (@{$prog_href->{'multiplex'}})
			{
				push @dvbt_recordings, $prog_href ;
			}
		}
		else
		{
			if (!TVGuide::Base::Constants::has_dvbt($prog_href->{'record'}))
			{
				# skip anything that doesn't have DVBT
				next
			}
			push @dvbt_recordings, $prog_href ;
		}

	}
	return @dvbt_recordings ;
}


#============================================================================================
# DEBUG
#============================================================================================
#



# ============================================================================================
# END OF PACKAGE
1;

__END__


