package Orangutan::User;

use strict;

use Storable;
use IO::File;
use XML::Writer;
use Data::Dumper;
use Orangutan::Task;
use Time::HiRes qw(usleep);

use base 'Exporter';
use base qw(Orangutan::Contexts);

our @EXPORT = qw(USER_UNCONFIGURED USER_AWAYISONLINE USER_NOASKTASK USER_NONOTIFY);

use constant USER_UNCONFIGURED => 0x01;
use constant USER_AWAYISONLINE => 0x02;
use constant USER_NOASKTASK    => 0x04;
use constant USER_NONOTIFY     => 0x08;

my %user_details = ( );

sub new {
    my $class = shift;

    my $self = new Orangutan::Contexts;
    $self->{'changes'} = 0;

    my %args = ( );
    while ($#_ >= 0) {
        $args{lc(pop)} = pop;
    }

    bless $self, $class;

    foreach my $arg (keys %args) {
        if ($arg eq 'jid') {
            $self->{'jabberid'} = $args{$arg};
            if ($self->{'jabberid'} =~ /^([^@]+)\@[^@]+$/) {
                $self->{'jabberuserid'} = $1;
            }
            $self->{'changes'}++;
        } else {
            #$main::logger->Log("unsupported user property: %s", $arg);
        }
    }

    $self->{'tasks'} = [ ];

    $self->{'flags'} = 0;
    $self->{'status'} = 'shutdown';

    $self->{'history'} = [ ];

    $self->{'workhours'} = 8;
    $self->{'workstart'} = 10;
    $self->{'workdays'} = [1..5];

    return $self;
}

sub Init($) {
    my $self = shift;
    foreach my $context ($main::config->Contexts) {
        my $return = undef;
        my ($out, $in, $filter) = $context->GetWeight($self);
        if (defined($out) && defined($in)) {
            $return = $self->AddRequestResponse($context, $out, $in);
        } elsif (defined($out)) {
            $return = $self->AddRequest($context, $out);
        } elsif (defined($in)) {
            $return = $self->AddResponse($context, $in);
        }
        if (defined($filter)) {
            if (defined($return)) {
                $self->AddFilter($return, $filter);
            } else {
                $self->AddFilter($context, $filter);
            }
        }
        if ($context->HasEventHandler) {
            $self->AddHandler($context);
        }
    }
}

sub UpdateCache {
    my $force = shift;
    my @logins = keys %user_details;
    if ($force || (scalar @logins == 0) || ((time - $user_details{$logins[0]}->{'time'}) > 86400)) {
        if ($main::query->UserDetails) {
            while (defined(my $details = $main::query->Next)) {
                $user_details{$details->{'login'}} = {
                    firstname => $details->{'firstname'},
                    lastname => $details->{'lastname'},
                    email => $details->{'mail'},
                    time => time
                };
                if ($main::config->Get('redmine', 'same_login')) {
                    $user_details{$details->{'login'}}->{'jabber'} =
                        $details->{'login'}.'@'.$main::config->Get('jabber', 'hostname');
                } elsif (defined($details->{'jabber'})) {
                    $user_details{$details->{'login'}}->{'jabber'} = $details->{'jabber'};
                }
                delete($user_details{$details->{'login'}}->{'displayname'});
            }
        } else {
            $main::logger->Log("could not fetch user details");
        }
    }
}

sub Save($) {
    my $self = shift;
    my $filename = sprintf("%s/%s.xml", $main::config->Get('users', 'directory'), $self->{'jabberuserid'});
    my $file = new IO::File;
    if ($file->open($filename, 'w')) {
        my %attrs;
        my $xml = new XML::Writer(OUTPUT => $file, DATA_MODE => 1, ENCODING => 'utf-8');
        $xml->xmlDecl('utf-8');
        $xml->startTag('user', jid => $self->{'jabberid'});
        if (defined($self->{'userid'}) && defined($self->{'login'})) {
            $xml->startTag('redmine', uid => $self->{'userid'}, login => $self->{'login'});
            if (defined($self->{'projectid'})) {
                $xml->startTag('project', id => $self->{'projectid'});
                if (defined($self->{'project'})) {
                    $xml->dataElement('name', $self->{'project'});
                }
                if (defined($self->{'issue'})) {
                    $xml->emptyTag('issue', id => $self->{'issue'});
                }
                if (defined($self->{'activityid'})) {
                    $xml->startTag('activity', id => $self->{'activityid'});
                    if (defined($self->{'activity'})) {
                        $xml->characters($self->{'activity'});
                    }
                    $xml->endTag('activity');
                }
                $xml->endTag('project');
            }
            $xml->endTag('redmine');
        }
        $xml->emptyTag('worktime',
            start => $self->{'workstart'},
            hours => $self->{'workhours'}
        );
        $xml->startTag('workdays');
        for (my $i = 0; $i < scalar @{$self->{'workdays'}}; $i++) {
            $xml->dataElement('weekday', $Orangutan::Date::weekdays[$self->{'workdays'}[$i]]);
        }
        $xml->endTag('workdays');
        if ($self->{'task'} || (scalar @{$self->{'tasks'}} > 0)) {
            $xml->startTag('tasks');
            if ($self->{'task'}) {
                $self->{'task'}->Save($xml);
            }
            if (scalar @{$self->{'tasks'}} > 0) {
                $xml->startTag('last');
                for (my $i = 0; $i < scalar @{$self->{'tasks'}}; $i++) {
                    $self->{'tasks'}[$i]->Save($xml);
                }
                $xml->endTag('last');
            }
            $xml->endTag('tasks');
        }
        if ($self->{'flags'} != 0) {
            $xml->startTag('flags');
            if ($self->{'flags'} & USER_UNCONFIGURED) {
                $xml->emptyTag('unconfigured');
            }
            if ($self->{'flags'} & USER_AWAYISONLINE) {
                $xml->emptyTag('awayisonline');
            }
            if ($self->{'flags'} & USER_NOASKTASK) {
                $xml->emptyTag('noasktask');
            }
            if ($self->{'flags'} & USER_NONOTIFY) {
                $xml->emptyTag('nonotify');
            }
            $xml->endTag('flags');
        }
        if (scalar keys %{$self->{'options'}} > 0) {
            $xml->startTag('options');
            foreach my $option (sort keys %{$self->{'options'}}) {
                if (ref $self->{'options'}->{$option} eq '') {
                    $xml->emptyTag('option',
                        name  => $option,
                        value => $self->{'options'}->{$option}
                    );
                } else {
                    $main::logger->Log("cannot save option %s (%s)", $option, ref $self->{'options'}->{$option});
                }
            }
            $xml->endTag('options');
        }
        $xml->startTag('history');
        for (my $i = 0; $i < scalar @{$self->{'history'}}; $i++) {
            $xml->startTag('status', time => $self->{'history'}[$i]->{'date'}->Date);
            if (defined($self->{'history'}[$i]->{'status'})) {
                $xml->characters($self->{'history'}[$i]->{'status'});
            }
            $xml->endTag('status');
        }
        $xml->endTag('history');
        $xml->endTag('user');
        $file->close;
        $self->{'changes'} = 0;
    }
}

sub SendMessage($$) {
    my ($self, $message) = @_;
    if (ref $message eq 'ARRAY') {
        $message = Orangutan::Context::Random($message);
    }
    if ($message && ($message !~ /^\s*$/)) {
        $main::jabber->MessageSend(
            to   => $self->GetJID('full'),
            type => 'chat',
            body => $message
        );
        $self->LogMessage($message);
        usleep(1000); # otherwise the next message may go first
    }
}

sub LogMessage($$) {
    my ($self, $message) = @_;
    $main::logger->LogUser($self, $message);
}

sub SubmitTask($) {
    my $self = shift;
    if ($self->{'task'} && $self->{'task'}->GetEnd) {
        my $task = $self->{'task'};
        $self->{'task'} = undef;

        # Move to previous tasks array
        unshift(@{$self->{'tasks'}}, $task);

        # Clean up tasks array
        for (my $i = $#{$self->{'tasks'}}; $i > 0; $i--) {
            if ($self->{'tasks'}[$i]->IsSet(TASK_SUBMITTED)) {
                pop(@{$self->{'tasks'}});
            } else {
                last;
            }
        }

        # Submit
        my $eventid = $main::query->SubmitEntry($self, $task);
        if ($eventid) {
            $task->SetID($eventid);
            $task->Set(TASK_SUBMITTED);
            $self->SubmitPending;
        }

        $self->{'changes'}++;
        return $eventid;
    }
    return undef;
}

sub ResubmitLastTask($) {
    my $self = shift;
    if ($self->{'tasks'}[0]) {
        if ($self->{'tasks'}[0]->IsSet(TASK_SUBMITTED)) {
            $self->{'tasks'}[0]->Unset(TASK_SUBMITTED);
        }
        my $eventid = $main::query->SubmitEntry($self, $self->{'tasks'}[0]);
        if ($eventid) {
            if (!$self->{'tasks'}[0]->GetID) {
                $self->{'tasks'}[0]->SetID($eventid);
            }
            $self->{'tasks'}[0]->Set(TASK_SUBMITTED);
            $self->{'changes'}++;

            $self->SubmitPending;
        }
        return $eventid;
    }
    return undef;
}

sub SubmitPreviousTask($$) {
    my ($self, $task) = @_;

    my $eventid = $main::query->SubmitEntry($self, $task);
    if ($eventid) {
        $task->SetID($eventid);
        $task->Set(TASK_SUBMITTED);
        $self->SubmitPending;
    }

    # Put it as a last task (if it is last)
    if ($task->GetStart && $task->GetEnd &&
        (!$self->{'tasks'}[0] || ($self->{'tasks'}[0]->GetEnd->Date < $task->GetEnd->Date))) {
        unshift(@{$self->{'tasks'}}, $task);

        # Clean up array
        for (my $i = $#{$self->{'tasks'}}; $i > 0; $i--) {
            if ($self->{'tasks'}[$i]->IsSet(TASK_SUBMITTED)) {
                pop(@{$self->{'tasks'}});
            } else {
                last;
            }
        }

        $self->{'changes'}++;

    # Failed to add - append to tasks array (will add unordered)
    } elsif (!$eventid) {
        push(@{$self->{'tasks'}}, $task);
        $self->{'changes'}++;
    }

    return $eventid;
}

sub SubmitPending($) {
    my $self = shift;
    while ($#{$self->{'tasks'}} >= 0) {
        if (!$self->{'tasks'}[$#{$self->{'tasks'}}]->IsSet(TASK_SUBMITTED)) {
            my $eventid = $main::query->SubmitEntry($self, $self->{'tasks'}[$#{$self->{'tasks'}}]);
            if ($eventid) {
                if (!$self->{'tasks'}[$#{$self->{'tasks'}}]->GetID) {
                    $self->{'tasks'}[$#{$self->{'tasks'}}]->SetID($eventid);
                }
                $self->{'tasks'}[$#{$self->{'tasks'}}]->Set(TASK_SUBMITTED);
                $self->{'changes'}++;
            } else {
                last;
            }
        }
        if ($#{$self->{'tasks'}} > 0) {
            pop(@{$self->{'tasks'}});
        } else {
            last;
        }
    }
}

sub RemovePreviousTask($$) {
    my ($self, $eventid) = @_;
    if ($main::query->RemoveEntry($eventid)) {
        if ($self->{'tasks'}[0] && ($self->{'tasks'}[0]->GetID == $eventid)) {
            shift(@{$self->{'tasks'}});
        }
    }
}

sub LoadData($$) {
    my ($self, $id) = @_;
    if ($id =~ /^[A-Za-z0-9\-_]+$/) {
        if (!defined($self->{'data'}) || !defined($self->{'data'}->{$id})) {
            my $filename = sprintf("%s/%s/%s.data", $main::config->Get('users', 'data'), $self->GetJabberUserID, $id);
            if (-f $filename) {
                my $value = retrieve($filename);
                if (defined($value)) {
                    if (!defined($self->{'data'})) {
                        $self->{'data'} = { };
                    }
                    $self->{'data'}->{$id} = $value;
                    return $self->{'data'}->{$id};
                }
            }
        } else {
            $main::logger->Log("refused to read data again: %s", $id);
        }
    }
    return undef;
}

sub FireEvent($$$) {
    my ($self, $event) = @_;

    if (defined($self->{'contexts'}->{'handlers'}->{$event})) {
        foreach my $id (keys %{$self->{'contexts'}->{'handlers'}->{$event}}) {
            my $context = $self->{'contexts'}->{'handlers'}->{$event}->{$id};
            $context->CallEventHandler(@_);
        }
    }
}

sub ShouldSave($) {
    my $self = shift;
    return $self->{'changes'};
}

sub MaySpeak($) {
    my $self = shift;
    return defined($self->{'status'}) &&
        (($self->{'status'} eq 'online') || (($self->{'status'} eq 'away') && $self->IsSet(USER_AWAYISONLINE)));
}

sub IsWorkDay($$) {
    my ($self, $date) = @_;
    my $wday = (localtime($date->Date))[6];
    if (grep(($wday == $_), @{$self->{'workdays'}})) {
        my $time = $date->DayStart->Date;
        my $holidays = $date->GetHolidays;
        foreach my $holiday (@{$holidays}) {
            if (${$holiday}[2] == $time) {
                return 0;
            } elsif (${$holiday}[2] > $time) {
                last;
            }
        }
        return 1;
    } else {
        return 0;
    }
}

sub IsDayOff($$) {
    my ($self, $date) = @_;
    if ($main::query->GetLeaveDays($self, $date, $date)) {
        return 1;
    }
    return 0;
}

sub IsWorkTime($$) {
    my ($self, $date) = @_;
    my $hour = ($date->GetTime)[0];
    my $end = $self->{'workstart'} + $self->{'workhours'};
    if ($self->{'workhours'} > 4) {
        $end++;
    }
    if ($end > 23) {
        $end = 24;
    }
    if ($self->IsWorkDay($date) && !$self->IsDayOff($date) &&
        ($hour >= $self->{'workstart'}) && ($hour < $end)) {
        return 1;
    } else {
        return 0;
    }
}

sub IsSet($$) {
    my ($self, $flag) = @_;
    return $self->{'flags'} & $flag;
}

sub IsOnline($) {
    my $self = shift;
    return defined($self->{'status'});
}

sub IsCreator($) {
    my $self = shift;
    if ($self->GetJID eq $main::config->Get('admin', 'creator')) {
        return 1;
    } else {
        return 0;
    }
}

sub IsManager() {
    my $self = shift;
    my $manager = $main::config->Get('admin', 'manager');
    if (ref $manager eq 'ARRAY') {
        foreach my $jid (@{$manager}) {
            if ($self->GetJID eq $jid) {
                return 1;
            }
        }
        return 0;
    } else {
        if ($self->GetJID eq $manager) {
            return 1;
        } else {
            return 0;
        }
    }
}

sub Set($$) {
    my ($self, $flag) = @_;
    $self->{'flags'} |= $flag;
    $self->{'changes'}++;
}

sub SetField($$$) {
    my ($self, $field, $value) = @_;
    if (!defined($self->{'custom'})) {
        $self->{'custom'} = { };
    }
    $self->{'custom'}->{$field} = $value;
}

sub SetOption($$;$) {
    my ($self, $option, $value) = @_;
    if (!defined($self->{'options'})) {
        $self->{'options'} = { };
    }
    $self->{'options'}->{$option} = $value;
    $self->{'changes'}++;
}

sub WriteData($$$) {
    my ($self, $id, $value) = @_;
    if ($id =~ /^[A-Za-z0-9\-_]+$/) {
        my $path = sprintf("%s/%s", $main::config->Get('users', 'data'), $self->GetJabberUserID);
        if (-d $path || mkdir($path)) {
            if (!defined($self->{'data'})) {
                $self->{'data'} = { };
            }
            $self->{'data'}->{$id} = $value;
            store($value, sprintf("%s/%s.data", $path, $id));
        } else {
            $main::logger->Log("could not create directory: %s", $path);
        }
    } else {
        $main::logger->Log("invalid data id: %s", $id);
    }
    return 0;
}

sub SetStatus($;$) {
    my ($self, $status) = @_;
    if (!defined($status)) {
        if (defined($self->{'status'})) {
            $self->FireEvent('status', undef);
            $self->{'status'} = undef;
            $self->HandleStatus;
            return 1;
        }
    } elsif ($status eq 'shutdown') {
        $self->{'status'} = 'shutdown';
        $self->HandleStatus;
    } elsif (($status eq 'away') || ($status eq 'xa')) {
        if (!defined($self->{'status'}) || ($self->{'status'} ne 'away')) {
            $self->FireEvent('status', 'away');
            $self->{'status'} = 'away';
            $self->HandleStatus;
            return 1;
        }
    } else {
        if (!defined($self->{'status'}) || ($self->{'status'} ne 'online')) {
            $self->FireEvent('status', 'online');
            $self->{'status'} = 'online';
            $self->HandleStatus;
            return 1;
        }
    }
    return 0;
}

sub SetResource($$) {
    my ($self, $resource) = @_;
    $self->{'resource'} = $resource;
}

sub SetUserID($$) {
    my ($self, $userid) = @_;
    $self->{'userid'} = $userid;
    $self->{'changes'}++;
}

sub SetLogin($$) {
    my ($self, $login) = @_;
    $self->{'login'} = $login;
    $self->{'changes'}++;
}

sub SetIssue($;$) {
    my ($self, $issue) = @_;
    if (defined($issue)) {
        $self->{'issue'} = $issue;
    } else {
        delete($self->{'issue'});
    }
    $self->{'changes'}++;
}

sub SetProject($$) {
    my ($self, $project) = @_;
    $self->{'project'} = $project;
    $self->{'changes'}++;
}

sub SetProjectID($$) {
    my ($self, $projectid) = @_;
    $self->{'projectid'} = $projectid;
    $self->{'changes'}++;
}

sub SetActivityID($;$) {
    my ($self, $activityid) = @_;
    if (defined($activityid)) {
        $self->{'activityid'} = $activityid;
    } else {
        delete($self->{'activityid'});
    }
    $self->{'changes'}++;
}

sub SetActivity($;$) {
    my ($self, $activity) = @_;
    if (defined($activity)) {
        $self->{'activity'} = $activity;
    } else {
        delete($self->{'activity'});
    }
    $self->{'changes'}++;
}

sub SetWorkWeekDay($$) {
    my ($self, $wday) = @_;
    for (my $i = 0; $i < scalar @{$self->{'workdays'}}; $i++) {
        if ($self->{'workdays'}[$i] == $wday) {
            return 0;
        }
    }
    push(@{$self->{'workdays'}}, $wday);
    $self->{'changes'}++;
    return 1;
}

sub SetFreeWeekDay($$) {
    my ($self, $wday) = @_;
    for (my $i = 0; $i < scalar @{$self->{'workdays'}}; $i++) {
        if ($self->{'workdays'}[$i] == $wday) {
            splice(@{$self->{'workdays'}}, $i, 1);
            $self->{'changes'}++;
            return 1;
        }
    }
    return 0;
}

sub SetWorkTimeHours($;$) {
    my ($self, $hours) = @_;
    if (defined($hours)) {
        $self->{'workhours'} = $hours;
    } else {
        $self->{'workhours'} = 0;
    }
    $self->{'changes'}++;
}

sub SetWorkTimeStart($;$) {
    my ($self, $start) = @_;
    if (defined($start)) {
        $self->{'workstart'} = $start;
    } else {
        $self->{'workstart'} = 0;
    }
    $self->{'changes'}++;
}

sub SetTask($$) {
    my ($self, $task) = @_;
    $self->{'task'} = $task;
    $self->{'changes'}++;
}

sub Unset($$) {
    my ($self, $flag) = @_;
    $self->{'flags'} &= ~$flag;
    $self->{'changes'}++;
}

sub StartTask($@) {
    my $self = shift;
    $self->{'task'} = new Orangutan::Task(@_);
    if (!$self->{'task'}->GetStart) {
        $self->{'task'}->SetStart;
    }
    $self->{'changes'}++;
    $self->{'task'}->{'changes'} = \$self->{'changes'};
    return $self->{'task'};
}

sub UnsetField($$) {
    my ($self, $field) = @_;
    if (defined($self->{'custom'}) && defined($self->{'custom'}->{$field})) {
        my $value = $self->{'custom'}->{$field};
        delete($self->{'custom'}->{$field});
        return $value;
    }
    return undef;
}

sub ClearOption($$) {
    my ($self, $option) = @_;
    if (defined($self->{'options'}) && defined($self->{'options'}->{$option})) {
        my $value = $self->{'options'}->{$option};
        delete($self->{'options'}->{$option});
        $self->{'changes'}++;
        return $value;
    }
    return undef;
}

sub HandleStatus($) {
    my $self = shift;

    if (defined($self->{'status'}) && (scalar @{$self->{'history'}} > 0)) {
        if (!defined($self->{'history'}[0]->{'status'})) {
            if ($self->{'status'} eq 'away') {
                $self->Set(USER_AWAYISONLINE);
            }
        } elsif ($self->{'history'}[0]->{'status'} eq 'online') {
            if ($self->{'status'} eq 'away') {
                $self->Unset(USER_AWAYISONLINE);
            }
        }
    }

    if (!defined($self->{'status'}) || ($self->{'status'} eq 'shutdown') ||
        (($self->{'status'} eq 'away') && !$self->IsSet(USER_AWAYISONLINE))) {
        $self->Suspend;
    } else {
        $self->Resume;
    }

    unshift(@{$self->{'history'}}, {
        date   => new Orangutan::Date,
        status => $self->{'status'}
    });

    if (scalar @{$self->{'history'}} > 10) {
        pop(@{$self->{'history'}});
    }

    $self->{'changes'}++;
}

sub HandleMessage($) {
    my $self = shift;

    if (defined($self->{'status'}) && ($self->{'status'} eq 'away')) {
        $self->Set(USER_AWAYISONLINE);
        $self->Resume;
    }

    $self->{'lastmessage'} = time;
}

sub RemoveTask($) {
    my $self = shift;
    if (defined($self->{'task'})) {
        $self->{'task'} = undef;
        $self->{'changes'}++;
        return 1;
    }
    return 0;
}

sub GetField($$) {
    my ($self, $field) = @_;
    return (defined($self->{'custom'}) && defined($self->{'custom'}->{$field})) ? $self->{'custom'}->{$field} : undef;
}

sub GetOption($$) {
    my ($self, $option) = @_;
    return (defined($self->{'options'}) && defined($self->{'options'}->{$option})) ? $self->{'options'}->{$option} : undef;
}

sub ReadData($$) {
    my ($self, $id) = @_;
    return (defined($self->{'data'}) && defined($self->{'data'}->{$id})) ? $self->{'data'}->{$id} : undef;
}

sub GetLogin($) {
    my $self = shift;
    return $self->{'login'};
}

sub GetJabberUserID($) {
    my $self = shift;
    return $self->{'jabberuserid'};
}

sub GetJID($;$) {
    my ($self, $format) = @_;

    if (ref $self ne 'Orangutan::User') {
        my $user = $main::users->GetUserLogin($self);
        if (defined($user)) {
            $self = $user;
        }
    }

    if (ref $self eq 'Orangutan::User') {
        if (defined($format) && ($format eq 'full') && defined($self->{'resource'})) {
            return $self->{'jabberid'}.'/'.$self->{'resource'};
        } else {
            return $self->{'jabberid'};
        }
    } else {
        UpdateCache(!defined($user_details{$self}));

        if (defined($user_details{$self})) {
            return $user_details{$self}->{'jabber'};
        } else {
            return undef;
        }
    }
}

sub GetDisplayName($) {
    my $self = shift;

    my $login;
    if (ref $self eq 'Orangutan::User') {
        $login = $self->GetLogin;
    } else {
        $login = $self;
    }

    UpdateCache(!defined($user_details{$self}));

    my $display_name;
    if (defined($user_details{$login})) {
        my $details = $user_details{$login};
        if (defined($details->{'displayname'})) {
            $display_name = $details->{'displayname'};
        } else {
            $display_name = $main::config->Get('orangutan', 'display_name') || '%l';
            $display_name =~ s/\%f/$details->{'firstname'}/g;
            $display_name =~ s/\%s/$details->{'lastname'}/g;
            $display_name =~ s/\%l/$login/g;
            $display_name =~ s/\%m/$details->{'email'}/g;
            if (defined($details->{'jabber'})) {
                $display_name =~ s/\%j/$details->{'jabber'}/g;
            } else {
                my $jabber = 'unknown@'.$main::config->Get('jabber', 'hostname');
                $main::logger->Log("missing jabber id: %s", $login);
                $display_name =~ s/\%j/$jabber/g;
            }
            $details->{'displayname'} = $display_name;
        }
    } else {
        $main::logger->Log("user details unavailable: %s", $login);
        $display_name = $login;
    }
    return $display_name;
}

sub GetStatus($) {
    my $self = shift;
    return $self->{'status'};
}

sub GetUserID($) {
    my $self = shift;
    return $self->{'userid'};
}

sub GetIssue($) {
    my $self = shift;
    return $self->{'issue'};
}

sub GetProject($) {
    my $self = shift;
    return $self->{'project'};
}

sub GetProjectID($) {
    my $self = shift;
    return $self->{'projectid'};
}

sub GetActivity($) {
    my $self = shift;
    return $self->{'activity'};
}

sub GetActivityID($) {
    my $self = shift;
    return $self->{'activityid'};
}

sub GetWorkTimeHours($) {
    my $self = shift;
    return $self->{'workhours'};
}

sub GetWorkTimeStart($) {
    my $self = shift;
    return $self->{'workstart'};
}

sub GetWorkTimeEnd($) {
    my $self = shift;
    my $end = $self->{'workstart'} + $self->{'workhours'};
    if ($self->{'workhours'} > 4) {
        $end++;
    }
    if ($end > 23) {
        $end = 24;
    }
    return $end;
}

sub GetTask($) {
    my $self = shift;
    return $self->{'task'};
}

sub GetLast($) {
    my $self = shift;
    if (scalar @{$self->{'tasks'}} == 0) {
        my $event = $main::query->GetLastEntry($self);
        if ($event) {
            unshift(@{$self->{'tasks'}}, new Orangutan::Task(
                id       => $event->{'id'},
                start    => new Orangutan::Date($event->{'start_time'}),
                name     => $event->{'comments'},
                issue    => $event->{'issue_id'},
                project  => {
                    id   => $event->{'project_id'},
                    name => $event->{'project'}
                },
                activity => {
                    id   => $event->{'activity_id'},
                    name => $event->{'activity'}
                },
                end      => new Orangutan::Date($event->{'end_time'})
            ));
            $self->{'tasks'}[0]->Set(TASK_SUBMITTED);
            $self->{'tasks'}[0]->{'changes'} = \$self->{'changes'};
        }
    }
    return $self->{'tasks'}[0];
}

sub GetHistoryEntry($;$) {
    my ($self, $query) = @_;
    if (scalar @{$self->{'history'}} > 0) {
        if ($query) {
            my $query = $self->GetStatusCode($query);
            my $previous = undef;
            for (my $i = 0; $i < scalar @{$self->{'history'}}; $i++) {
                my $code = $self->GetStatusCode($self->{'history'}[$i]->{'status'});
                if (($code > $query) || (defined($previous) && (($code - $previous) <= 0))) {
                    if ($i > 0) {
                        return $self->{'history'}[$i-1];
                    } else {
                        return undef;
                    }
                }
                $previous = $code;
            }
        } else {
            return $self->{'history'}[0];
        }
    }
    return undef;
}

sub GetStatusCode($$) {
    my ($self, $status) = @_;
    if (defined($status)) {
        if ($status eq 'away') {
            return 1;
        } elsif ($status eq 'online') {
            return 2;
        }
    }
    return 0;
}

sub GetLastMessageTime($) {
    my $self = shift;
    return $self->{'lastmessage'};
}

sub GetFreePeriods($$$) {
    my ($self, $start, $end) = @_;
    my @periods = ( );
    my $rows = $main::query->GetEntries($self, $start, $end);
    if (defined($rows)) {
        if ($rows > 0) {
            my $start_time = $start->Date;
            for (my $event = $main::query->First; defined($event); $event = $main::query->Next) {
                if (($event->{'start_time'} >= $start->Date) && (($event->{'start_time'} - $start_time) > 60)) {
                    push(@periods, [ $start_time, $event->{'start_time'} ]);
                }
                $start_time = $event->{'end_time'};
            }
            if (($start_time < $end->Date) && (($end->Date - $start_time) > 60)) {
                push(@periods, [ $start_time, $end->Date ]);
            }
        } else {
            push(@periods, [ $start->Date, $end->Date ]);
        }
    }
    return @periods;
}

sub GetEntries($$$$$$) {
    my ($self, $start, $end, $project, $issue, $activity) = @_;
    my @tasks = ( );
    my $user = (ref $self eq 'Orangutan::User') ? $self->GetUserID : $self;
    if ($main::query->GetEntriesEx($user, $start, $end, $project, $issue, $activity) > 0) {
        while (defined(my $task = $main::query->Next)) {
            push(@tasks, $task);
        }
    }
    if (ref $self eq 'Orangutan::User') {
        for (my $i = 0; $i < scalar @{$self->{'tasks'}}; $i++) {
            if (!$self->{'tasks'}[$i]->GetID &&
                 $self->{'tasks'}[$i]->GetStart &&
                 $self->{'tasks'}[$i]->GetEnd &&
                ($self->{'tasks'}[$i]->GetStart->Date < $end->Date) &&
                ($self->{'tasks'}[$i]->GetEnd->Date > $start->Date) &&
                (!defined($project) || # check project
                (defined($self->{'tasks'}[$i]->GetProject) && ($project == $self->{'tasks'}[$i]->GetProject('id')))) &&
                (!defined($issue) || # check issue
                (defined($self->{'tasks'}[$i]->GetIssue) && ($issue == $self->{'tasks'}[$i]->GetIssue))) &&
                (!defined($activity) || # check activity
                (defined($self->{'tasks'}[$i]->GetActivity) && ($activity == $self->{'tasks'}[$i]->GetActivity('id'))))) {
                my $j;
                for ($j = 0; $j < scalar @tasks; $j++) {
                    if ($tasks[$j]->{'order_time'} > $self->{'tasks'}[$i]->GetStart->Date) {
                        last;
                    }
                }
                my $task = {
                    user_id     => $self->GetUserID,
                    start_time  => $self->{'tasks'}[$i]->GetStart->Date,
                    end_time    => $self->{'tasks'}[$i]->GetEnd->Date,
                    duration    => $self->{'tasks'}[$i]->GetDuration,
                    issue_id    => $self->{'tasks'}[$i]->GetIssue,
                    project_id  => $self->{'tasks'}[$i]->GetProject('id'),
                    project     => $self->{'tasks'}[$i]->GetProject,
                    activity_id => $self->{'tasks'}[$i]->GetActivity('id'),
                    activity    => $self->{'tasks'}[$i]->GetActivity,
                    comments    => $self->{'tasks'}[$i]->GetName,
                    order_time  => $self->{'tasks'}[$i]->GetStart->Date
                };
                splice(@tasks, $j, 0, $task);
            }
        }
        if (defined($self->{'task'}) && ($self->{'task'}->GetStart->Date < $end->Date) &&
            (!defined($project) || # check project
            (defined($self->{'task'}->GetProject) && ($project == $self->{'task'}->GetProject('id')))) &&
            (!defined($issue) || # check issue
            (defined($self->{'task'}->GetIssue) && ($issue == $self->{'task'}->GetIssue))) &&
            (!defined($activity) || # check activity
            (defined($self->{'task'}->GetActivity) && ($activity == $self->{'task'}->GetActivity('id'))))) {
            my $task = {
                user_id     => $self->GetUserID,
                start_time  => $self->{'task'}->GetStart->Date,
                issue_id    => $self->{'task'}->GetIssue,
                project_id  => $self->{'task'}->GetProject('id'),
                project     => $self->{'task'}->GetProject,
                activity_id => $self->{'task'}->GetActivity('id'),
                activity    => $self->{'task'}->GetActivity,
                comments    => $self->{'task'}->GetName,
                order_time  => $self->{'task'}->GetStart->Date
            };
            splice(@tasks, scalar @tasks, 0, $task);
        }
    }
    return @tasks;
}

sub GetWorkHours($$$) {
    my ($self, $start, $end) = @_;
    return $main::query->GetWorkHours($self, $start, $end);
}

sub GetWorkHoursDetails($$$$$) {
    my ($self, $start, $end, $project, $activity) = @_;
    my %items = ( );
    my $user = (ref $self eq 'Orangutan::User') ? $self->GetUserID : $self;
    if ($main::query->GetWorkHoursEx($user, $start, $end, $project, $activity) > 0) {
        while (defined(my $item = $main::query->Next)) {
            if (!defined($items{$item->{'project_id'}})) {
                $items{$item->{'project_id'}} = {
                    name       => $item->{'project'},
                    activities => { }
                };
            }
            if (!defined($items{$item->{'project_id'}}->{'activities'}->{$item->{'activity_id'}})) {
                $items{$item->{'project_id'}}->{'activities'}->{$item->{'activity_id'}} = {
                    name     => $item->{'activity'},
                    duration => $item->{'duration'}
                };
            } else {
                $items{$item->{'project_id'}}->{'activities'}->{$item->{'activity_id'}}->{'duration'} += $item->{'duration'};
            }
        }
    }
    if (ref $self eq 'Orangutan::User') {
        for (my $i = 0; $i < scalar @{$self->{'tasks'}}; $i++) {
            if (!$self->{'tasks'}[$i]->GetID &&
                 $self->{'tasks'}[$i]->GetStart &&
                 $self->{'tasks'}[$i]->GetEnd &&
                ($self->{'tasks'}[$i]->GetStart->Date < $end->Date) &&
                ($self->{'tasks'}[$i]->GetEnd->Date > $start->Date)) {
                my $projectid = $self->{'tasks'}[$i]->GetProject('id');
                my $activityid = $self->{'tasks'}[$i]->GetActivity('id');
                if (!defined($items{$projectid})) {
                    $items{$projectid} = {
                        name       => $self->{'tasks'}[$i]->GetProject,
                        activities => { }
                    };
                }
                if (!defined($items{$projectid}->{'activities'}->{$activityid})) {
                    $items{$projectid}->{'activities'}->{$activityid} = {
                        name     => $self->{'tasks'}[$i]->GetActivity,
                        duration => $self->{'tasks'}[$i]->GetDuration
                    };
                } else {
                    $items{$projectid}->{'activities'}->{$activityid}->{'duration'} += $self->{'tasks'}[$i]->GetDuration;
                }
            }
        }
        if (defined($self->{'task'}) && ($self->{'task'}->GetStart->Date < $end->Date)) {
            my $projectid = $self->{'task'}->GetProject('id');
            my $activityid = $self->{'task'}->GetActivity('id');
            if (!defined($items{$projectid})) {
                $items{$projectid} = {
                    name       => $self->{'task'}->GetProject,
                    activities => { }
                };
            }
            if (!defined($items{$projectid}->{'activities'}->{$activityid})) {
                $items{$projectid}->{'activities'}->{$activityid} = {
                    name     => $self->{'task'}->GetActivity,
                    duration => $self->{'task'}->GetStart->Elapsed
                };
            } else {
                $items{$projectid}->{'activities'}->{$activityid}->{'duration'} += $self->{'task'}->GetStart->Elapsed;
            }
        }
    }
    return %items;
}

sub GetWorkHoursPerDay($$$$) {
    my ($self, $start, $end, $project) = @_;
    my %days = ( );
    my $user = (ref $self eq 'Orangutan::User') ? $self->GetUserID : $self;
    if ($main::query->GetWorkHoursPerDay($user, $start, $end, $project) > 0) {
        while (defined(my $day = $main::query->Next)) {
            $days{$day->{'spent_on'}} = $day->{'duration'};
        }
    }
    if (ref $self eq 'Orangutan::User') {
        for (my $i = 0; $i < scalar @{$self->{'tasks'}}; $i++) {
            if (!$self->{'tasks'}[$i]->GetID &&
                 $self->{'tasks'}[$i]->GetStart &&
                 $self->{'tasks'}[$i]->GetEnd &&
                ($self->{'tasks'}[$i]->GetStart->Date < $end->Date) &&
                ($self->{'tasks'}[$i]->GetEnd->Date > $start->Date)) {
                my $projectid = $self->{'tasks'}[$i]->GetProject('id');
                if ($projectid == $project) {
                    my $date = $self->{'tasks'}[$i]->GetDate;
                    if (defined($days{$date})) {
                        $days{$date} += $self->{'tasks'}[$i]->GetDuration;
                    } else {
                        $days{$date} = $self->{'tasks'}[$i]->GetDuration;
                    }
                }
            }
        }
    }
    return %days;
}

sub GetTasksReport($$$$) {
    my ($self, $start, $end, $project) = @_;
    my %tasks = ( );
    my $user = (ref $self eq 'Orangutan::User') ? $self->GetUserID : $self;
    if ($main::query->GetTasksReport($user, $start, $end, $project) > 0) {
        while (defined(my $task = $main::query->Next)) {
            utf8::decode($task->{'subject'});
            my $issueid = $task->{'issue_id'} || 0;
            if (!defined($tasks{$task->{'project_id'}})) {
                $tasks{$task->{'project_id'}} = {
                    name     => $task->{'project_name'},
                    spent_on => $task->{'spent_on'},
                    duration => 0,
                    issues   => { }
                };
            }
            if (!defined($tasks{$task->{'project_id'}}->{'issues'}->{$issueid})) {
                $tasks{$task->{'project_id'}}->{'issues'}->{$issueid} = {
                    tracker  => $task->{'tracker'},
                    subject  => $task->{'subject'},
                    spent_on => $task->{'spent_on'},
                    duration => 0,
                    tasks    => [ ]
                };
            }
            if ($task->{'comments'}) {
                my $entry = {
                    description => $task->{'comments'},
                    duration    => $task->{'hours'}
                };
                push(@{$tasks{$task->{'project_id'}}->{'issues'}->{$issueid}->{'tasks'}}, $entry);
            }
            $tasks{$task->{'project_id'}}->{'issues'}->{$issueid}->{'duration'} += $task->{'hours'};
            $tasks{$task->{'project_id'}}->{'duration'} += $task->{'hours'};
        }
    }
    return %tasks;
}

sub GetWorkDays($$$) {
    my ($self, $start, $end) = @_;
    my @days = ( );
    if (!$self->IsWorkDay($start)) {
        $start = $self->NextWorkDay($start);
    }
    for (my $date = $start->WorkDayStart; $date->Date <= $end->WorkDayStart->Date; $date = $self->NextWorkDay($date)) {
        push(@days, $date);
    }
    if ($main::query->GetLeaveDays($self, $start, $end)) {
        while (defined(my $date = $main::query->Next)) {
            my $count = 0;
            my $offset = undef;
            for (my $i = 0; $i < scalar @days; $i++) {
                if (($days[$i]->DayStart->Date >= $date->{'start_date'}) &&
                    ($days[$i]->DayStart->Date <= $date->{'end_date'})) {
                    if (!defined($offset)) {
                        $offset = $i;
                    }
                    $count++;
                } elsif (defined($offset)) {
                    last;
                }
            }
            if (defined($offset)) {
                splice(@days, $offset, $count);
            }
        }
    }
    return @days;
}

sub PreviousWorkDay($$) {
    my ($self, $date) = @_;
    do {
        $date = $date->PreviousWorkDay;
    } while (!$self->IsWorkDay($date));
    return $date;
}

sub NextWorkDay($$) {
    my ($self, $date) = @_;
    do {
        $date = $date->NextWorkDay;
    } while (!$self->IsWorkDay($date));
    return $date;
}

sub XmlInit {
    my ($self, $expat) = @_;
    $self->{'xml'} = { };
    $self->{'xml'}->{'path'} = [ ];
}

sub XmlStart {
    my ($self, $expat, $element, %attrs) = @_;
    push(@{$self->{'xml'}->{'path'}}, $element);
    my $path = join('::', @{$self->{'xml'}->{'path'}});
    if ($path eq 'user') {
        foreach my $name (keys %attrs) {
            if ($name eq 'jid') {
                $self->{'jabberid'} = $attrs{$name};
                if ($self->{'jabberid'} =~ /^([^@]+)\@[^@]+$/) {
                    $self->{'jabberuserid'} = $1;
                }
            } else {
                $main::logger->Log("unsupported xml attribute: $name");
            }
        }
    } elsif ($path eq 'user::redmine') {
        foreach my $name (keys %attrs) {
            if ($name eq 'uid') {
                $self->{'userid'} = $attrs{$name};
            } elsif ($name eq 'login') {
                $self->{'login'} = $attrs{$name};
            } else {
                $main::logger->Log("unsupported xml attribute: $name");
            }
        }
        if (!defined($attrs{'login'}) &&
            $main::config->Get('redmine', 'same_login')) {
            $self->{'login'} = $self->{'jabberuserid'};
            $main::logger->Log("login missing - using username: %s", $self->{'jabberuserid'});
        }
    } elsif ($path eq 'user::redmine::project') {
        foreach my $name (keys %attrs) {
            if ($name eq 'id') {
                $self->{'projectid'} = $attrs{$name};
            } else {
                $main::logger->Log("unsupported xml attribute: $name");
            }
        }
    } elsif ($path eq 'user::redmine::project::issue') {
        foreach my $name (keys %attrs) {
            if ($name eq 'id') {
                $self->{'issue'} = $attrs{$name};
            } else {
                $main::logger->Log("unsupported xml attribute: $name");
            }
        }
    } elsif ($path eq 'user::redmine::project::activity') {
        foreach my $name (keys %attrs) {
            if ($name eq 'id') {
                $self->{'activityid'} = $attrs{$name};
            } else {
                $main::logger->Log("unsupported xml attribute: $name");
            }
        }
    } elsif ($path eq 'user::worktime') {
        foreach my $name (keys %attrs) {
            if ($name eq 'start') {
                $self->{'workstart'} = $attrs{$name};
            } elsif ($name eq 'hours') {
                $self->{'workhours'} = $attrs{$name};
            } else {
                $main::logger->Log("unsupported xml attribute: $name");
            }
        }
    } elsif ($path eq 'user::workdays') {
        $self->{'workdays'} = [ ];
    } elsif
       (($path eq 'user::tasks::task') ||
        ($path eq 'user::tasks::last::task')) {
        my $task = new Orangutan::Task;
        $task->XmlInit($self);
        $task->XmlStart($expat, $element, %attrs);
        $expat->setHandlers(
            Start => sub { $task->XmlStart(@_); },
            Char  => sub { $task->XmlChar(@_); },
            End   => sub { $task->XmlEnd(@_); },
        );
        if ($path eq 'user::tasks::task') {
            $self->{'task'} = $task;
        } else {
            unshift(@{$self->{'tasks'}}, $task);
        }
    } elsif ($path eq 'user::history::status') {
        my $status = { };
        foreach my $name (keys %attrs) {
            if ($name eq 'time') {
                $status->{'date'} = new Orangutan::Date($attrs{$name});
            } else {
                $main::logger->Log("unsupported xml attribute: $name");
            }
        }
        push(@{$self->{'history'}}, $status);
    } elsif ($path eq 'user::options::option') {
        if (defined($attrs{'name'}) && defined($attrs{'value'})) {
            if (!defined($self->{'options'})) {
                $self->{'options'} = { };
            }
            $self->{'options'}->{$attrs{'name'}} = $attrs{'value'};
        } else {
            $main::logger->Log("invalid option xml");
        }
    } elsif ($path eq 'user::flags::unconfigured') {
        $self->{'flags'} |= USER_UNCONFIGURED;
    } elsif ($path eq 'user::flags::awayisonline') {
        $self->{'flags'} |= USER_AWAYISONLINE;
    } elsif ($path eq 'user::flags::noasktask') {
        $self->{'flags'} |= USER_NOASKTASK;
    } elsif ($path eq 'user::flags::nonotify') {
        $self->{'flags'} |= USER_NONOTIFY;
    }
}

sub XmlChar {
    my ($self, $expat, $data) = @_;
    $data =~ s/^\s+//;
    $data =~ s/\s+$//;
    if ($data ne '') {
        my $path = join('::', @{$self->{'xml'}->{'path'}});
        if ($path eq 'user::redmine::project::name') {
            $self->{'project'} = $data;
        } elsif ($path eq 'user::redmine::project::activity') {
            $self->{'activity'} = $data;
        } elsif ($path eq 'user::workdays::weekday') {
            my $wday = Orangutan::Date::ParseWeekDay($data);
            if (defined($wday)) {
                push(@{$self->{'workdays'}}, $wday);
            }
        } elsif ($path eq 'user::history::status') {
            $self->{'history'}[$#{$self->{'history'}}]->{'status'} = $data;
        }
    }
}

sub XmlEnd {
    my ($self, $expat, $element) = @_;
    pop(@{$self->{'xml'}->{'path'}});
}

sub XmlFinal {
    my ($self, $expat) = @_;
    delete($self->{'xml'});
}

1;
