# $Id: Schedule.pm,v 1.16 2004/01/13 19:54:31 gwolf Exp $
######################################
# Comas - Conference Management System
######################################
# Copyright 2003 CONSOL
# Congreso Nacional de Software Libre (http://www.consol.org.mx/)
#   Gunnar Wolf <gwolf@gwolf.cx>
#   Manuel Rabade <mig@mig-29.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
######################################

######################################
# Module: Comas::Schedule
# Creating and modifying the schedule for the accepted proposals
######################################
# Depends on:
#
# Comas::Common - Common functions for various Comas modules
# Comas::Schedule::Room - Represents the schedule for a given room
# Comas::Schedule::Timeslot - represents a given timeslot in a Comas schedule
# Comas::Proposal - Handles the interaction with a proposal for Comas

package Comas::Schedule;

use strict;
use warnings;
use Carp;
use Comas::Common qw(valid_hash);
use Comas::Schedule::Room;
use Comas::Schedule::Timeslot;
use Comas::Proposal;

=head1 NAME

Comas::Schedule - Creating and modifying the schedule for the accepted 
proposals

=head1 SYNOPSIS

=head2 OBJECT CONSTRUCTOR

  $sched = Comas::Schedule->new($db);

When the object is created, it reads from the database (the Comas::DB object
passed as the only parameter) the current scheduling information. 

=head2 MAINTENANCE

  $ok = $sched->refresh();

Refreshes the working object with the information found in the database. This
method is most likely to be used only internally, but it can still be used, 
i.e., if a object exists for a long time.

=head2 SCHEDULING PROPOSALS

  $tslot_id = $sched->schedule(-prop=>$prop_id, [ -room=>$room_id ],
                               [ -day => $day ], [ -start_hr=>$start_hr ],
                               [ -timeslot => $timeslot_id ]);

  $ok = $sched->unschedule(-prop=>$prop_id);

  $ok = $sched->unschedule(-tslot=>$tslot_id);

=head2 RETRIEVING THE SCHEDULE

  $data = $sched->get_schedule;

Returns a structure representing the whole current schedule. The resulting 
schedule is sorted both by day and by room, as this example shows. It might 
appear as there is much redundancy in the rows in the different ways of 
showing them, but they are really many references to the same array:

  {by_day => {
    $date1 => [ 
      [$room_1, $date_1, $start_time_1, $end_time_1, $prop_id_1],
      [$room_2, $date_2, $start_time_2, $end_time_2, $prop_id_2], 
      (...) ],
    $date2 => [ 
      [$room_3, $date_3, $start_time_3, $end_time_3, $prop_id_3],
      [$room_4, $date_4, $start_time_4, $end_time_4, $prop_id_4], 
      (...) ], 
    (...)},
   by_room => {
    $room1 => [
      [$room_1, $date_1, $start_time_1, $end_time_1, $prop_id_1],
      [$room_2, $date_2, $start_time_2, $end_time_2, $prop_id_2], 
      [$room_3, $date_3, $start_time_3, $end_time_3, $prop_id_3],
      (...) ],
    $room2 => [
      [$room_4, $date_4, $start_time_4, $end_time_4, $prop_id_4], 
      (...) ]
    (...)}
  }

=head1 REQUIRES

Comas::Common - Common functions for various Comas modules
Comas::Schedule::Room - Represents the schedule for a given room
Comas::Schedule::Timeslot - represents a given timeslot in a Comas schedule

=head1 SEE ALSO

L<Comas::Schedule::Room> and L<Comas::Schedule::Timeslot> for more detailed 
information on how proposals are really scheduled

=head1 AUTHOR

Gunnar Wolf, gwolf@gwolf.cx

Manuel Rabade, mig@mig-29.net

Comas has been developed for CONSOL, Congreso Nacional de Software Libre,
http://www.consol.org.mx/

=head1 COPYRIGHT

Copyright 2003 Gunnar Wolf and Manuel Rabade

This library is free software, you can redistribute it and/or modify it
under the terms of the GPL version 2 or later.

=cut

sub new {
    my ($class, $db, $sched);
    $class = shift;
    $db = shift;
    $sched = {-db => $db};

    if (ref $sched->{-db} ne 'Comas::DB') {
	carp 'Invocation error - Mandatory field not specified or wrong';
	return undef;
    }

    bless ($sched, $class);

    unless ($sched->refresh) {
	carp 'Unable to retreive scheduling information from the database';
	return undef;
    }

    return $sched;
}

sub refresh {
    my ($sched, $sth, %days, %rooms);
    $sched = shift;

    # We create the following structures:
    #
    # To represent the timeslots, $sched->{room}{$id} will contain each of
    # the Comas::Schedule::Room objects making up the schedule.
    #
    $sched->{room} = {};
    unless ($sth = $sched->{-db}->prepare('SELECT id FROM room') and
	    $sth->execute) {
	carp 'Unable to retrieve the list of rooms';
	return undef;
    }
    while ((my $room) = $sth->fetchrow_array) {
	$sched->{room}{$room} = Comas::Schedule::Room->new(-db=>$sched->{-db},
							   -id=>$room);
    }

    return 1;
}

sub schedule {
    my ($sched, %par);
    $sched = shift;
    unless (%par = valid_hash(@_) and exists $par{-prop} and
	    !grep {$_ !~ /^-(?:prop|room|day|start_hr|timeslot)$/} keys %par) {
	carp 'Invocation error - Wrong number/type of parameters';
	return undef;
    }

    if (exists $par{-timeslot}) {
	# Schedule it in the specified timeslot
	my $tslot;
	if (scalar(keys %par) > 2) {
	    carp 'Timeslot specified - Cannot take any other parameters';
	    return undef;
	}
	$tslot = Comas::Schedule::Timeslot->new(-db => $sched->{-db},
						-id => $par{-timeslot})
	    or carp 'Could not create Timeslot object' && return undef;
	if ($tslot->schedule($par{-prop})) {
	    return $par{-timeslot};
	} else {
	    return undef;
	}

    } elsif (exists $par{-room}) {
	# Schedule it in any available timeslot in the specified room
	my $room;
	if (scalar(keys %par) > 2) {
	    carp 'Room specified - Cannot take any other parameters';
	    return undef;
	}

	$room = $sched->{room}{$par{-room}} or
	    carp 'Invalid room specified';

	if ($room->schedule_proposal(-id=>$par{-prop})) {
	    for my $tslot ($room->get_timeslots) {
		my ($prop_id) = $tslot->get_proposal;
		if (defined $prop_id and $prop_id == $par{-prop}) {
		    return $tslot->get_id;
		}
	    }
	} 

	return undef;
    } else {
	my (@tslots, $prop, $prop_type);
	unless ($prop = Comas::Proposal->new(-db=>$sched->{-db}, 
					     -id=>$par{-prop})) {
	    carp 'Could not create the proposal object';
	    return undef;
	}
	$prop_type = $prop->get_prop_type_id;
	for my $room (values %{$sched->{room}}) {
	    push(@tslots, $room->get_available_timeslots($prop_type));
	}

	if (!@tslots) {
	    carp 'No timeslots available';
	    return undef;
	}

	my $t = $sched->_choose_from_tslots($prop->get_track_id, @tslots);
	return $t->schedule($par{-prop});
    }

    # How did we get here?!
    return undef;
}

sub unschedule {
    my ($sched, %par, $sth);
    $sched = shift;
    unless (%par = valid_hash(@_) and scalar(keys %par) == 1) {
	carp 'Invocation error - Wrong number of parameters';
	return undef;
    }

    if (exists $par{-prop}) {
	unless ($sth = $sched->{-db}->prepare('UPDATE proposal SET 
                timeslot_id = NULL WHERE id = ?') and 
		$sth->execute($par{-prop})) {
	    carp 'Could not unschedule specified proposal';
	    return undef;
	}
    } elsif (exists $par{-tslot}) {
	unless ($sth = $sched->{-db}->prepare('UPDATE proposal SET 
                timeslot_id = NULL WHERE timeslot_id = ?') and 
		$sth->execute($par{-tslot})) {
	    carp 'Could not unschedule from specified timeslot';
	    return undef;
	}
    } else {
	carp 'Invocation error - Wrong type of parameters';
	return undef;
    }

    return 1;
}

sub get_schedule {
    my ($sched, $ret, $sth);
    $sched = shift;
    $ret = { by_day => {}, by_room => {} };

    # Don't worry about the order - we are querying a view that gives its
    # results in our desired order
    unless ($sth = $sched->{-db}->prepare('SELECT room_id, day, start_hr,
            start_hr+duration, id FROM scheduled_proposals') 
	    and $sth->execute) {
	carp 'Could not get the schedule from the database';
	return undef;
    }

    while (my @row = $sth->fetchrow_array) {
	my $data = \@row;
	$ret->{by_room}{$row[0]} = [] unless defined $ret->{by_room}{$row[0]};
	$ret->{by_day}{$row[1]} = [] unless defined $ret->{by_day}{$row[1]};

	push(@{$ret->{by_room}{$row[0]}}, $data);
	push(@{$ret->{by_day}{$row[1]}}, $data);
    }
    return $ret;
}

sub _choose_from_tslots {
    # Chooses from a list of timeslots the one which overlaps with the least
    # simultaneous proposals belonging to the same track.
    # Should be called as a method (i.e., with a Comas::Schedule object as its
    # implicit first parameter). 
    # Receives a track ID as the first parameter, and one or more 
    # Comas::Schedule::Timeslot objects as parameters.
    # Returns the chosen timeslot object.
    my ($sched, $track, @tslots, %density, $lowest_num, @lowest_data);
    $sched = shift;
    $track = shift;
    @tslots = @_;

    return undef unless @tslots;

    %density = ();
    for my $tslot (@tslots) {
	my $sim_with_same_track = 0;
	for my $t_id ($tslot->simultaneous_used_timeslots) {
	    my ($sim_ts, $prop_id, $sim_prop, $sim_track);
	    # We have the ID of each simultaneous used timeslot. From here, we
	    # create the timeslot object to query for the proposal it hosts,
	    # create the proposal object, and finally query it for its track 
	    # ID - All just to know whether to add 1 to $sim_with_same_track :-/
	    unless ($sim_ts=Comas::Schedule::Timeslot->new(-db=>$sched->{-db},
							   -id=>$t_id)) {
		carp "Could not verify timeslot $sim_ts - Aborting";
		return undef;
	    }

	    ($prop_id) = $sim_ts->get_proposal;
	    $sim_prop = Comas::Proposal->new(-db=>$sched->{-db}, -id=>$prop_id);

	    $sim_track = $sim_prop->get_track_id;

	    $sim_with_same_track++ if $track == $sim_track;
	}

	if (ref $density{$sim_with_same_track}) {
	    push(@{$density{$sim_with_same_track}}, $tslot);
	} else {
	    $density{$sim_with_same_track} = [$tslot];
	}
    }

    # We now just select, from the timeslots in the lowest-numbered entry of
    # %density, any one at random.
    ($lowest_num) = sort {$a<=>$b} keys %density;
    @lowest_data = @{$density{$lowest_num}};

    return $lowest_data[rand @lowest_data];
}


1;

# $Log: Schedule.pm,v $
# Revision 1.16  2004/01/13 19:54:31  gwolf
# Corrijo la corrección que recién mandé, así como la otra parte del reporte de Mig (concerniente a get_schedule)
#
# Revision 1.15  2004/01/13 18:58:05  gwolf
# Corrijo el comportamiento de schedule cuando no hay timeslots disponibles. (Revisé en Room - ahí el caso sí está ya contemplado)
#
# Revision 1.14  2004/01/07 20:24:08  gwolf
# Cambio el nombre de una variable que se me hacía poco clara
#
# Revision 1.13  2004/01/07 20:18:12  gwolf
# Listo el agendador basado en tracks!
#
# Revision 1.12  2004/01/05 19:36:32  gwolf
# - Schedule.pm, Schedule/Room.pm: Por fin ya agendo sobre cualquier criterio!
#   (sólo falta hacer que evite sobrecargar de ponencais con el mismo track un
#   mismo espacio)
# - Admin/academic_committee.pm: Agrego unas funcioncillas que pidió Mig
# - Admin.pm: Quito todas las referencias a db_param, que ya no usaremos
#
# Revision 1.11  2003/12/20 04:14:51  mig
# - Agrego tags Id y Log que expanda el CVS
#
