#!/usr/bin/perl
package Elive::script::elive_raise_meeting;
use warnings; use strict;
use version;

use Getopt::Long;
use Date::Parse qw{};
use Pod::Usage;
use File::Basename qw{};

use Carp;

use Elive;
use Elive::Util;
use Elive::Entity::Preload;
use Elive::Entity::User;
use Elive::Entity::Participant;
use Elive::View::Session;

use URI;
use URI::Escape;

use YAML;

=head1 NAME

elive_raise_meeting

=head1 SYNOPSIS

  elive_raise_meeting [url] [options] [participants]

=head2 Authentication Options

 [url]                        # web address and site instance path,
                              # E.g.: http://myserver.com/mysite 
 -user someAdminUser          # SDK/SOAP username
 -pass somePass               # SDK/SOAP password

=head2 Basic Options

 -facilitator  userId
 -name         'meeting name'
 -meeting_pass otherpass      # meeting password
 -start  '[YYYY-MM-DD ]HH:MM' # start time
 -end    '[YYYY-MM-DD ]HH:MM' # end time
 -occurs {days|weeks}=n       # repeat over n days/weeks (elm2)

=head2 Moderators and Participants

 -moderators user|*group|guest-name(guest-id) ...
 -participants user|*group|guest-name(guest-id) ...

 Where each participant or moderator can be:

    'user'        - a single Elluminate user. This can be either the
                    user's username or user-id.

    '*group'      - a group of users. This can be either the group-name
                    or group-id.

    'Display Name(loginName)'
                  - a guest login.

=head2 Meeting Setup Options

 -boundary     0|15|30|..     # minutes participants can arrive before or
                              # leave after the scheduled meeting times.
 -max_talkers  n              # max no. of simultaneous talkers
 -max_cameras  n              # max no. of simultaneous cameras (elm3)
 -profile_display      none|mod|all       # profiles to display (elm3)
                              # none, mod (moderators only), or all
 -recording_status     off|on|manual      # set recording status
                              #  - off:    recording disabled
                              #  - on:     starts automatically
                              #  - manual: recording started by moderator
 -recording_resolution cg|cc|mg|mc|fg|fc  # recording resolution (elm3)
                              #  - cg:course gray, cc:course color,
                              #  - mg:medium gray, mc:medium color,
                              #  - fg:fine gray,   fc:fine color
 -seats        count          # number of seats to reserve on server
 -[no]invites                 # allow in-session invitations
 -[no]follow                  # lock whiteboard to moderator view (elm3)
 -[no]private                 # hide from public schedule
 -[no]restricted              # restrict entry to registered users (elm3)
 -[no]permissions             # let participants perform activities
 -[no]raise_hands             # automatically raise hands on entry
 -[no]supervised              # moderator can see private messages

 -user_notes      'some text'
 -moderator_notes 'some text'
 -cost_center     'code'
 -exit_url=http://somesite.com  # URL to visit on meeting exit (elm3)

=head2 Preload Options

 -upload              local_file     # upload a local file
 -import_from_server  remote_file    # import a server side file
 -add_preload         preload_id,... # associate with existing preloads

=head2 Compatibility Options

 -use elm2                  # ELM 2.x compat (via Elive::View::Session)
 -use elm3                  # ELM 3.x compat (via Elive::Entity::Session)
 -use Some::Custom::Class   # create session via a custom session class

=head2 Information

 -? --help                  # print this help
 -v --version               # print version and exit
 --dump=yaml                # out created sessions as YAML
 --debug=n                  # set debug level

=head1 DESCRIPTION

Creates a meeting (session) on an Elluminate I<Live!> server

=head1 SEE ALSO

perldoc Elive

http://search.cpan.org/dist/Elive

=cut

my $class;

my $username;
my $password;
my $debug;
my $facilitator_id;
my $start_str;
my $end_str;
my $meeting_name = 'elive test meeting';
my $meeting_password;
my $max_talkers;
my $max_cameras;
my $profile_display;
my $recording_status;
my $recording_resolution;
my $raise_hands;
my $import;
my $private;
my $restricted;
my $upload;
my $preload_opt;
my $url;
my $help;
my $boundary = 15;
my $seats;
my $permissions;
my $supervised;
my $invites;
my $follow_moderator;
my $moderator_notes;
my $user_notes;
my $cost_center;
my @moderators;
my @participants;
my $exit_url;
my $dump;

my %occurs;
my $version;

main(@ARGV) unless caller;

sub main {

    local(@ARGV) = @_;

    GetOptions(
	'name|meeting_name=s' => \$meeting_name,
	'username|user=s' => \$username,
	'password|pass=s' => \$password,
	'facilitator=s' => \$facilitator_id,
	'start=s' => \$start_str,
	'end=s' => \$end_str,
	'moderators|moderator=s{,}' => \@moderators,
	'participants|participant|others|other=s{,}' => \@participants,
	'meeting_password|meeting_pass=s' => \$meeting_password,
	'max_talkers=i' => \$max_talkers,
	'max_cameras=i' => \$max_cameras,
	'raise_hands!' => \$raise_hands,
	'private!' => \$private,
	'restricted!' => \$restricted,
	'supervised!' => \$supervised,
	'permissions!' => \$permissions,
	'debug=i' => \$debug,
	'help|?' => \$help,
	'upload=s' => \$upload,
	'import_from_server|import=s' => \$import,
	'add_preload|use_preload=i' => \$preload_opt,
	'profile|profile_display=s' => \$profile_display,
	'recording|recording_status=s' => \$recording_status,
	'recording_resolution=s' => \$recording_resolution,
	'boundary=i' => \$boundary,
	'seats=i' => \$seats,
	'invites|invitations!' => \$invites,
	'follow|follow_moderator!' => \$follow_moderator,
        'occurs=i%' => \%occurs,
	'v|version' => \$version,
	'user_notes=s' => \$user_notes,
	'moderator_notes=s' => \$moderator_notes,
	'cost_center|cost_centre=s' => \$cost_center,
	'use=s' => \$class,
	'exit_url=s' => \$exit_url,
	'dump=s' => \$dump,
    )
	or die pod2usage(1);

    pod2usage(0) if $help;

    do {
	print "Elive v${Elive::VERSION} (c) 2009-2011\n";
	exit(0);
    } if $version;

    ($url = shift @ARGV)
	or pod2usage("missing url argument");

    if (defined $recording_status) {
	$recording_status = lc($recording_status);
	#
	# accept some of the other symonymns for the various modes as
	# seen in documentation and elluminate's web interface.
	#
	$recording_status =~ s{^none|disabled$}{off}x;
	$recording_status =~ s{^manual$}{remote}x;
	$recording_status =~ s{^auto(matic)?$}{on}x;

	die pod2usage("-recording_status must be on/auto, off/none/disabled or manual/remote\n")
	    unless $recording_status =~ m{^on|off|remote$}x;
    }

    if (defined $recording_resolution) {
	$recording_resolution = uc $recording_resolution;

	die ("-recording_resolution must be: cg|cc|mg|mc|fg|fc\n")
	    unless $recording_resolution =~ m{^[CMF][GC]$}x;
    }

    if (defined $profile_display) {
	$profile_display = lc $profile_display;

	die ("-profile_display must be: none|mod|all\n")
	    unless $profile_display =~ m{^(none|mod|all)$}x;
    }

    my ($recurrence_count, $recurrence_days) = _get_occurrences(\%occurs);

    Elive->debug($debug) if defined $debug;
    # debug may also be set via $ENV{ELIVE_DEBUG}
    $debug = Elive->debug;

    if ($debug) {
	$SIG{__WARN__} = \&Carp::cluck if $debug > 1;
	$SIG{__DIE__} = \&Carp::confess;
    }

    $url ||= Elive::Util::prompt("Url ('http://...'): ");

    unless ($username && $password) {
	#
	# look for credentials encoded in the uri
	#
	my $uri_obj = URI->new($url, 'http');
	my $userinfo = $uri_obj->userinfo; # credentials supplied in URI

	if ($userinfo) {
	    my ($uri_user, $uri_pass) = split(':', $userinfo, 2);
	    $username ||= URI::Escape::uri_unescape($uri_user);
	    $password ||= URI::Escape::uri_unescape($uri_pass)
		if $uri_pass;
	}
    }

    $username ||= Elive::Util::prompt('Username: ');
    $password ||= Elive::Util::prompt('Password: ', password => 1);

    our $connection = Elive->connect($url, $username, $password);

    my $server_version = $connection->server_details->version;
    my $server_version_num = version->new($server_version)->numify;

    $class ||= 'elm3';
    $class = {elm2 => 'Elive::View::Session',
	      elm3 => 'Elive::Entity::Session'}->{$class} || $class;

    warn "Session class: $class\n" if $debug;

    my $can_insert = eval "use $class";
    die "unable to load class $class: $@" if $@;

    die "class $class: does not implement the 'insert' method"
	unless eval{ $class->can('insert') };

    my $start = $start_str
	? Date::Parse::str2time($start_str)
	: time() + 15 * 60;

    my $end = $end_str
	? Date::Parse::str2time($end_str)
	: $start + 30 * 60;

    die "end time must be later than start time\n"
	unless ($end > $start);

    my $upload_data;

    my $facilitator = $facilitator_id
	? _get_user($facilitator_id)
	: Elive->login;

    $facilitator_id ||= $facilitator->userId;
    #
    # Ensure that the facilitator is included as a moderator, but not as
    # a regular participant
    #
    push (@moderators, $facilitator_id)
	unless grep {$_ eq $facilitator_id} @moderators;

    @participants = grep {$_ ne $facilitator_id} @participants;

    # participants can also be passed in argv (old usage)
    my @args = grep {$_ ne $facilitator_id} @ARGV;

    my @participants = (
	parse_participants(\@moderators, role => 2),
	parse_participants(\@participants, role => 3),
	parse_participants(\@args),
	);
    #
    # collate by type
    #
    my @users;
    my @groups;
    my @guests;

    foreach my $participant (@participants) {

	my $type = $participant->type;

	if (! $type ) {
	    push (@users, $participant);
	}
	elsif ($type == 1) {
	    push (@groups, $participant);
	}
	elsif ($type == 2) {
	    push (@guests, $participant); 
	}
	else {
	    warn "don't know how to handle participant type: $type";
	}
    }
    #
    # Vet participants
    #
    @users = _get_users(@users) if @users;
    @groups = _get_groups(@groups) if @groups;

    my %session_data = (
	name => $meeting_name,
	facilitatorId => $facilitator,
	start => $start . '000',
	end => $end . '000',
	);

    $session_data{recordingStatus} = $recording_status
	if defined $recording_status;

    $session_data{recordingResolution} = $recording_resolution
	if defined $recording_resolution;

    $session_data{profile} = $profile_display
	if defined $profile_display;

    $session_data{password} = $meeting_password
	if defined $meeting_password;

    $session_data{seats} = $seats
	if defined $seats;

    $session_data{privateMeeting} = $private
	if defined $private;

    $session_data{maxTalkers}  = $max_talkers
	if defined $max_talkers;

    $session_data{videoWindow}  = $max_cameras
	if defined $max_cameras;

    $session_data{raiseHandOnEnter} = $raise_hands
	if defined $raise_hands;

    $session_data{inSessionInvitation}  = $invites
	if defined $invites;

    $session_data{followModerator}  = $follow_moderator
	if defined $follow_moderator;

    $session_data{fullPermissions} = $permissions
	if defined $permissions;

    $session_data{supervised} = $supervised
	if defined $supervised;

    $session_data{boundaryMinutes} = $boundary
	if defined $boundary;

    $session_data{user_notes} = $user_notes
	if defined $user_notes;

    $session_data{moderator_notes} = $moderator_notes
	if defined $moderator_notes;

    $session_data{cost_center} = $cost_center
	if defined $cost_center;

    $session_data{restrictedMeeting} = $restricted
	if defined $restricted;

    $session_data{redirectURL} = $exit_url
	if $exit_url;

    if ($recurrence_count > 1) {
	$session_data{recurrenceCount} = $recurrence_count;
	$session_data{recurrenceDays} = $recurrence_days || 1;
    }

    $session_data{participants} = [@users, @groups, @guests];

    my $uploaded_preload;
    $uploaded_preload = Elive::Entity::Preload->upload($upload)
	if $upload;

    my $existing_preload;

    if ($preload_opt) {

	pod2usage("non numeric preload id")
	    unless $preload_opt =~ m{^\d+$}x;

	$existing_preload = Elive::Entity::Preload->retrieve([$preload_opt]);
	
	die "no existing preload: $preload_opt\n"
	    unless $existing_preload;
    }

    my $imported_preload;
    if ($import) {

	print "importing server-side preload: $import\n";

	$imported_preload = Elive::Entity::Preload->import_from_server({
	    fileName => $import,
	    ownerId => $facilitator,
								      });

	printf("imported '%s' preload: %s (%s)\n",
	       $imported_preload->type, $imported_preload->name, $imported_preload->mimeType);
    }

    my @preloads = grep {$_} ($existing_preload, $uploaded_preload, $imported_preload);

    $session_data{add_preload} = \@preloads if @preloads;
    my $ptmp = Elive::Entity::Participants->new($session_data{participants});

    my @sessions = $class->insert(\%session_data);

    if ($dump && $dump =~ m{yaml}i) {
	_yaml_dump_sessions( 'Elive::View::Session' => @sessions );
    }
    else {
	warn "ignoring option: -dump=$dump" if $dump;
	_echo_sessions( @sessions );
    }

    Elive->disconnect;

    return @sessions;
}

sub parse_participants {
    my $spec = shift;
    my %opt = @_;

    my @participants;

    foreach my $spec (map {split(';',$_)} @$spec) {

	my $participant = eval {
	    Elive::Entity::Participant->new($spec)
	};

	if ($@) {
	    warn "$@\n";
	    next;
	}
	#
	# Ignore the facilitator. we're going to add them later
	#
	$participant->role( $opt{role} ) if $opt{role};

	push (@participants, $participant);
    }

    return @participants;
}

########################################################################

sub show_participants {
    my ($session) = @_;

    my $participant_objs = $session->participants;

    my @participants_display = map  {
	my $type = $_->type;
	my $str;

	if (! $type)  {
	    my $user_obj = $_->user;
	    my $loginName = $user_obj->loginName;
	    my $email = $user_obj->email;

	    $str = ($loginName || $user_obj->userId);
	    $str .= ' <'.$email.'>'
		if $email;
	}
	elsif ($type == 1) {
	    my $group_obj = $_->group;
	    my $id = $group_obj->groupId;
	    my $name = $group_obj->name;

	    $str = '*'.$id;
	    $str .= ' <group:'.$name.'>'
		if $name;
	}
	elsif ($type == 2) {
	    my $guest_obj = $_->guest;
	    my $loginName = $guest_obj->loginName;
	    my $displayName = $guest_obj->displayName;

	    $str = $displayName;
	    $str .= ' ('.$loginName.')'
		if $loginName;
	}
	else {
	    warn "unknown participant type $type: ignored";
	    $str = ''
	}
	$str;
    } @$participant_objs;

    print "participants: ".join(', ', @participants_display)."\n";
    return;
}

########################################################################

sub _get_users {
    my @users_in = @_;

    my %users;

    foreach (@users_in) {
	my $user = Elive::Entity::User->stringify($_->user);
	
	die "bad username: $user\n"
	    if $user =~ m{'}x;

	$users{$user} = $_;
    }

    my $filter = join(' OR ',
		      map {sprintf("loginName='%s' OR userId='%s'",
				   $_, $_)}
		      (keys %users)
	);

    my $db_users = Elive::Entity::User->list(filter => $filter);
    my @users_out;

    foreach my $user (@$db_users) {

	my $obj1 = delete $users{$user->loginName};
	my $obj2 =  delete $users{$user->userId};
	my $obj = $obj1 || $obj2;

	if ($obj) {
	    # replace with db user
	    $obj->user( $user );
	    push (@users_out, $obj);
	}
    }

    my @users_not_found = keys %users;

    die "unknown user(s): @users_not_found\n"
	if @users_not_found;

    return @users_out;
}

########################################################################

sub _get_groups {
    my @groups_in = @_;

    my %groups;

    foreach (@groups_in) {

	my $group_spec = Elive::Entity::Group->stringify($_->group);
	$group_spec =~ s{^\*}{};

	$groups{$group_spec} = $_;
    }

    my $filter = join(' OR ',
		      map {sprintf("groupId='%s' OR groupName='%s'",
				   $_, $_)}
		      (keys %groups)
	);

    my $db_groups = Elive::Entity::Group->list(filter => $filter);

    my @groups_out;

    foreach my $group (@$db_groups) {

	my $obj1 = delete $groups{$group->groupId};
	my $obj2 = delete $groups{$group->groupName};
	my $obj = $obj1 || $obj2;

	if ($obj) {
	    $obj->group($group);
	    push (@groups_out, $obj)
	}
    }

    my @groups_not_found = keys %groups;

    die "unknown group(s): @groups_not_found\n"
	if @groups_not_found;

    return @groups_out;
}

########################################################################

sub _get_user {
    my $user_spec = shift;

    die 'usage: _get_user($user_name_or_id)'
	unless (defined $user_spec && $user_spec ne '');

    my $user = (Elive::Entity::User->retrieve([$user_spec])
		|| Elive::Entity::User->get_by_loginName($user_spec))
	or die "unkown user: $user_spec\n";

    return $user;
}

########################################################################

sub _get_occurrences {
    my $occurs = shift;

    my $recurrence_count = 1;
    my $recurrence_days = 1;

    foreach (keys %$occurs) {

	$recurrence_count = $occurs{$_};

	$recurrence_days =
	    m{^.*day(s?).*$}i       ? 1
	  : m{^.*week(s?).*$}i      ? 7
          : m{^.*fortnight(s?).*$}i ? 14
          : die "occurs usage: --occurs days=n  or --occurs weeks=n\n";

    }

    return ($recurrence_count, $recurrence_days);
}

sub _echo_sessions {
    my @sessions = @_;

    foreach my $session (@sessions) {
	print "created meeting: ".$session->name." with id ".$session->sessionId."\n";

	if (@{ $session->participants }) {

	    show_participants($session);

	}
	else {
	    print "no participants\n";
	}

	print "session address: ".$session->web_url."\n";
    }

}

########################################################################

sub _yaml_dump_sessions {
    my $class = shift;
    my @sessions = @_;

    my @props = $class->properties;
    my %derivable = $class->derivable;
    my $entity_name = $class->entity_name;

    foreach my $session (@sessions) {
	
	my %vals = (
	    map {
		my $meth = $derivable{$_} || $_;
		my $val = $session->$meth;
		$_ => $val,
	    } (sort (@props, keys %derivable)));

	print YAML::Dump {$entity_name => \%vals};

    }

}
