The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Elive::Entity::ParticipantList;
use warnings; use strict;

use Mouse;
use Mouse::Util::TypeConstraints;

extends 'Elive::Entity';

use Elive::Entity::Participant;
use Elive::Entity::Participants;
use Elive::Entity::User;
use Elive::Entity::Group;
use Elive::Entity::Role;
use Elive::Entity::Meeting;
use Elive::Entity::InvitedGuest;
use Elive::Util;

use Carp;

__PACKAGE__->entity_name('ParticipantList');

coerce 'Elive::Entity::ParticipantList' => from 'HashRef'
          => via {Elive::Entity::ParticipantList->new($_) };

has 'meetingId' => (is => 'rw', isa => 'Int', required => 1);
__PACKAGE__->primary_key('meetingId');
__PACKAGE__->params(users => 'Str');

has 'participants' => (is => 'rw', isa => 'Elive::Entity::Participants', coerce => 1);
__PACKAGE__->_alias(users => 'participants', freeze => 1);

=head1 NAME

Elive::Entity::ParticipantList - Meeting Participants entity class

=head1 DESCRIPTION

This is the entity class for meeting participants.

The participants property is an array of type Elive::Entity::Participant.

Note: the C<insert()> and C<update()> methods are depreciated. For alternatives,
please see L<Elive::Entity::Session>.

=head2 User Participants

The I<participants> property may be specified in the format: userId[=roleId],
where the role is 3 for a normal participant or 2 for a meeting moderator.

Participants may be specified as a ';' separated string:

    my $participant_list = $meeting->participant_list;

    $participant_list->participants('111111=2;222222');
    $participant_list->update;

Participants may also be specified as an array of scalars:

    $participant_list->participants(['111111=2', 222222]);
    $participant_list->update;

Or an array of hashrefs:

    $participant_list->participants([{user => 111111, role => 2},
                                     {user => 222222}]);
    $participant_list->update;

=head2 Groups of Participants

Groups of users may also be assigned to a meeting. All users that are member of
that group are then able to participate in the meeting, and are assigned the
given role.

By convention, a leading '*' indicates a group:

    #
    # Set alice and bob as moderators. Then add all students in the
    # cookery class:
    #
    $participant_list->participants('alice=2;bob=2;*cookery_class=3');
    $participant_list->update;

Similar to the above:

    $participant_list->participants(['alice=2', 'bob=2', '*cookery_class=3']);
    $participant_list->update;

As a list of hashrefs:

    $participant_list->participants([
              {user => 'alice', role => 2},
              {user => 'bob', role => 2},
              {group => 'cookery_class', role => 3},
    ]);
    $participant_list->update;

=head2 Command Selection

By default this command uses the C<setParticipantList> SOAP command, which
doesn't handle groups. If any groups are specified, it will switch to using
C<updateSession>, which does handle groups.

=cut

=head1 METHODS

=cut

sub _retrieve_all {
    my ($class, $vals, %opt) = @_;

    #
    # No getXxxx command use listXxxx
    #
    return $class->SUPER::_retrieve_all($vals,
				       command => 'listParticipants',
				       %opt);
}

=head2 retrieve

    my $meeting_participants = Elive::Entity::ParticipantList->retrieve($meeting_id);

Retrieves the participant list for a meeting.

=head2 update

This method updates meeting participants.

    my $participant_list
         = Elive::Entity::ParticipantList->retrieve($meeting_id);
    $participant_list->participants->add($alice->userId, $bob->userId);
    $participant_list->update;

Note:

=over 4

=item if you specify an empty list, C<reset> method will be called. The
resultant list wont be empty, but will have the moderator as the sole
participant.

=back

=cut

sub update {
    my ($self, $update_data, %opt) = @_;

    if (defined $update_data) {

	die 'usage: $obj->update( \%data, %opt )'
	    unless (Elive::Util::_reftype($update_data) eq 'HASH');

	$self->set( %$update_data )
	    if (keys %$update_data);
    }

    my $meeting_id = $self->meetingId
	or die "unable to get meetingId";

    my $meeting = Elive::Entity::Meeting
	->retrieve($meeting_id,
		   reuse => 1,
		   connection => $self->connection,
	) or die "meeting not found: ".$meeting_id;

    my ($users, $groups, $guests) = $self->participants->_group_by_type;

    $self->_build_elm2x_participants ($users, $groups, $guests);
    #
    # make sure that the facilitator is included with a moderator role
    #
    $users->{ $meeting->facilitatorId } = ${Elive::Entity::Role::MODERATOR};

    my $participants = $self->_set_participant_list( $users, $groups, $guests );
    #
    # do our own readback and reread from the database (The setParticipantList
    # command doesn't return a response object.)
    #
    $self->revert;
    my $class = ref($self);
    $self = $class->retrieve($self->id, connection => $self->connection);

    $class->_readback_check({meetingId => $self->meetingId,
			     participants => $participants},
			    [$self]);

    return $self;
}

#
# _readback_check reimplementation of our validation rule
#  -- warn when input participants are omitted
#  -- error when output contains participants that weren't in the input
#

sub _readback_check {
    my $class = shift;
    my $_data = shift;
    my $rows = shift;
    my %opt = @_;

    my %data = %{ $_data };

    if (my $participants = delete $data{participants}) {

	my $participants_in = Elive::Entity::Participants->new( $participants );
	my %requested = map {lc Elive::Entity::Participant->stringify($_) => 1} @$participants_in;
	my $requested_count = scalar keys %requested;

	foreach my $row (@$rows) {

	    if ($participants = $row->{participants}) {
		my $participants_out = Elive::Entity::Participants->new( $participants );
		my %actual = map {lc Elive::Entity::Participant->stringify($_) => 1} @$participants_out;
		my %rejected = %requested;
		delete @rejected{ keys %actual };

		my @rejected_participants = sort keys %rejected;
		my $rejected_count = scalar @rejected_participants;

		Carp::croak "unable to add $rejected_count of $requested_count participants; rejected participants: @rejected_participants"
		    if $rejected_count;

		my %extra = %actual;
		delete @extra{ keys %requested };
		my @extra_participants = sort keys %extra;
		my $extra_count = scalar @extra_participants;

		Carp::croak "found $extra_count extra participants: @extra_participants"
		    if $extra_count;

	    }
	}
    }

    $class->SUPER::_readback_check( \%data, $rows, %opt);
}

sub _build_elm2x_participants {
    my ($self, $users, $groups, $guests) = @_;
    #
    # Take our best shot at passing participants via the elm 2.x
    # setParticipantList and updateParticipantList commands. These have
    # some restrictions on the handling of groups and invited guests.
    #
    #
   if (keys %$guests) {
       # no can do invited guests
	carp join(' ', "ignoring invited guests:", sort keys %$guests);
	%$guests = ();
    }

    foreach my $group_spec (keys %$groups) {
	#
	# Best we can do with groups is to expand members
	#
	carp "client side expansion of group: $group_spec";
	my $role = delete $groups->{ $group_spec };
	(my $group_id = $group_spec) =~ s{^\*}{};

	my $group = Elive::Entity::Group->retrieve($group_id,
						   connection => $self->connection,
						   reuse => 1,
	    );

	my @members = $group->expand_members;

	foreach (@members) {
	    #
	    # member names may be in the format <ldap-domain>:userId
	    #
	    my $member = $_;
	    $member =~ s{^ [^:]* :}{}x;
            $users->{ $member } ||= $role;
        }
    }
}

sub _set_participant_list {
    my $self = shift;
    my $users = shift;
    my $groups = shift;
    my $guests = shift;

    my $som;

    my @participants;

    foreach (keys %$users) {
	push(@participants, Elive::Entity::Participant->new({user => $_, role => $users->{$_}, type => ${Elive::Entity::Participant::TYPE_USER}}) )
    }

    foreach (keys %$groups) {
	push(@participants, Elive::Entity::Participant->new({group => $_, role => $groups->{$_}, type => ${Elive::Entity::Participant::TYPE_GROUP}}) )
    }

    foreach (keys %$guests) {
	push(@participants, Elive::Entity::Participant->new({guest => $_, role => $guests->{$_}, type => ${Elive::Entity::Participant::TYPE_GUEST}}) )
    }

    my %params;
    $params{meetingId} = $self;
    $params{participants} = \@participants;
    $som = $self->connection->call('setParticipantList' => %{$self->_freeze(\%params)});
    $self->connection->_check_for_errors( $som );

    return \@participants;
}

=head2 reset 

    $participant_list->reset

Reset the participant list. This will set the meeting facilitator as
the only participant, with a role of 2 (moderator).

=cut

sub reset {
    my ($self, %opt) = @_;
    return $self->update({participants => []}, %opt);
}

=head2 insert
 
    my $participant_list = Elive::Entity::ParticipantList->insert({
       meetingId => $meeting_id,
       participants => '111111=2;33333'
       });

Note that if you empty the participant list, C<reset> will be called.

=cut

sub insert {
    my ($class, $data, %opt) = @_;

    my $meeting_id = delete $data->{meetingId}
    or die "can't insert participant list without meetingId";
    my $self = $class->retrieve($meeting_id, reuse => 1)
	or die "No such meeting: $meeting_id";

    $self->update($data, %opt);

    return $self;
}

=head2 list

The list method is not available for participant lists.

=cut

sub list {return shift->_not_available}

sub _thaw {
    my ($class, $db_data, @args) = @_;

    $db_data = Elive::Util::_clone( $db_data );  # take a copy
    #
    # the SOAP record has a Participant property that can either
    # be of type user or group. However, Elive has separate 'user'
    # and 'group' properties. Resolve here.
    #
    if ($db_data->{ParticipantListAdapter}) {
	if (my $participants = $db_data->{ParticipantListAdapter}{Participants}) {
	    $participants = [$participants]
		unless Elive::Util::_reftype($participants) eq 'ARRAY';

	    foreach (@$participants) {
		my $p = $_->{ParticipantAdapter};
		if ($p && $p->{Participant}) {
		    #
		    # peek at the the type property. 0 => user, 1 => group
		    # a group record, rename to Group, otherwise treat
		    #
		    if (!defined $p->{Type} || $p->{Type} == ${Elive::Entity::Participant::TYPE_USER}) {
			$p->{User} = delete $p->{Participant}
		    }
		    elsif ($p->{Type} == ${Elive::Entity::Participant::TYPE_GROUP}) {
			$p->{Group} = delete $p->{Participant}
		    }
		    elsif ($p->{Type} == ${Elive::Entity::Participant::TYPE_GUEST}) {
			$p->{Guest} = delete $p->{Participant}
		    }
		    else {
			warn "unknown participant type: $p->{Type}";
		    }
		}
	    }
	}
    }

    return $class->SUPER::_thaw($db_data, @args);
}

=head1 SEE ALSO

L<Elive::Entity::Meeting>

L<Elive::View::Session>

L<Elive::Entity::Participants>

L<Elive::Entity::Participant>

=cut

1;