# -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*-
#
# This file is MPL/GPL dual-licensed under the following terms:
#
# The contents of this file are subject to the Mozilla Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
# the License for the specific language governing rights and
# limitations under the License.
#
# The Original Code is PLIF 1.0.
# The Initial Developer of the Original Code is Ian Hickson.
#
# Alternatively, the contents of this file may be used under the terms
# of the GNU General Public License Version 2 or later (the "GPL"), in
# which case the provisions of the GPL are applicable instead of those
# above. If you wish to allow use of your version of this file only
# under the terms of the GPL and not to allow others to use your
# version of this file under the MPL, indicate your decision by
# deleting the provisions above and replace them with the notice and
# other provisions required by the GPL. If you do not delete the
# provisions above, a recipient may use your version of this file
# under either the MPL or the GPL.

package PLIF::Service::User;
use strict;
use vars qw(@ISA);
use PLIF::Service::Session;
@ISA = qw(PLIF::Service::Session);
1;

# This class implements an object and its associated factory service.
# Compare this with the UserFieldFactory class which implements a
# factory service only, and the various UserField descendant classes
# which implement Service Instances.

# XXX It would be interesting to implement crack detection (time since
# last incorrect login, number of login attempts performed with time
# since last incorrect login < a global delta, address changing
# timeout, etc).

sub provides {
    my $class = shift;
    my($service) = @_;
    return ($service eq 'user.factory' or $class->SUPER::provides($service));
}

sub getUserByCredentials {
    my $self = shift;
    my($app, $username, $password) = @_;
    my $object = $self->getUserByUsername($app, $username);
    if (defined($object) and ($object->checkPassword($password))) {
        return $object;
    } else {
        return undef;
    }
}

sub getUserByUsername {
    my $self = shift;
    my($app, $username) = @_;
    my(@data) = $app->getService('dataSource.user')->getUserByUsername($app, $username);
    if (@data) {
        return $self->objectCreate($app, @data);
    } else {
        return undef;
    }
}

sub getUserByContactDetails {
    my $self = shift;
    my($app, $contactName, $address) = @_;
    my(@data) = $app->getService('dataSource.user')->getUserByContactDetails($app, $contactName, $address);
    if (@data) {
        return $self->objectCreate($app, @data);
    } else {
        return undef;
    }
}

sub getUserByID {
    my $self = shift;
    my($app, $userID) = @_;
    # do we already have that user?
    my $user = $app->getObject("user.$userID");
    if (defined($user)) {
        # got a user created already, return that
        return $user;
    } else {
        # that user isn't in our system yet, look it up
        my(@data) = $app->getService('dataSource.user')->getUserByID($app, $userID);
        if (@data) {
            return $self->objectCreate($app, @data);
        } else {
            return undef;
        }
    }
}

sub getNewUser {
    my $self = shift;
    my($app, $password) = @_;
    return $self->objectCreate($app, undef, 0, $password, '', {}, [], []);
}

sub objectProvides {
    my $self = shift;
    my($service) = @_;
    return ($service eq 'user' or
            $service eq 'user.'.($self->userID) or
            $self->SUPER::objectProvides($service));
}

# We don't need this yet, so it's commented out as an optimisation.
# # Override the default constructor to check to see if the user already
# # has an object associated with them in the system, and if they do,
# # use that instead of creating a new one.
# sub objectCreate {
#     my $class = shift;
#     my($app, $userID, @data) @_;
#     return $app->getObject("user.$userID") || $class->SUPER::objectCreate($app, $userID, @data);
#     # more strictly:
#     #
#     # my $user = $app->getObject("user.$userID");
#     # if (not defined($user)) {
#     #     $user = $class->SUPER::objectCreate($app, $userID, @data);
#     # }
#     # return $user;
# }

sub objectInit {
    my $self = shift;
    my($app, $userID, $mode, $password, $adminMessage, $fields, $groups, $rights) = @_;
    $self->{'_DIRTY'} = {}; # make sure propertySet is happy
    $self->SUPER::objectInit(@_);
    $self->userID($userID);
    $self->mode($mode); # 0=active, 1=disabled   XXX need a way to make this extensible
    $self->password($password);
    $self->adminMessage($adminMessage);
    $self->fields({});
    $self->fieldsByID({});
    # don't forget to update the 'hash' function if you add more properties/field whatever you want to call them
    my $fieldFactory = $app->getService('user.fieldFactory');
    foreach my $fieldID (keys(%$fields)) {
        $self->insertField($fieldFactory->createFieldByID($app, $self, $fieldID, $fields->{$fieldID}));
    }
    # $groups is an array of arrays containing groupID, groupName, user level in group
    my $groupsByID = {};
    my $groupsByName = {};
    foreach my $group (@$groups) {
        $groupsByID->{$group->[0]} = {'name' => $group->[1], 'level' => $group->[2], }; # id => name, level
        $groupsByName->{$group->[1]} = {'groupID' => $group->[0], 'level' => $group->[2], }; # name => id, level
    }
    $self->groupsByID($groupsByID); # authoritative version
    $self->originalGroupsByID({%{$groupsByID}}); # a backup used to make a comparison when saving the groups
    $self->groupsByName($groupsByName); # helpful version for output purposes only
    # rights
    $self->rights({ map {$_ => 1} @$rights }); # map a list of strings into a hash for easy access
    $self->{'_DIRTY'}->{'properties'} = not(defined($userID));
}

sub hasRight {
    my $self = shift;
    my($right) = @_;
    return (defined($self->rights->{$right}) or $self->levelInGroup(1)); # group 1 is a magical group
}

sub hasField {
    my $self = shift;
    my($category, $name) = @_;
    if (defined($self->fields->{$category})) {
        return $self->fields->{$category}->{$name};
    }
    return undef;
}

# returns a field *even if it does not exist yet*
# -- so if you want to add a field by name, you do:
#     $user->getField('category', 'name')->data($myData);
sub getField {
    my $self = shift;
    my($category, $name) = @_;
    my $field = $self->hasField($category, $name);
    if (not defined($field)) {
        $field = $self->insertField($self->app->getService('user.fieldFactory')->createFieldByName($self->app, $self, $category, $name));
    }
    return $field;
}

# returns a field *even if it does not exist yet*
# -- so if you want to add a field by ID, you do:
#     $user->getField($fieldID)->data($myData);
sub getFieldByID {
    my $self = shift;
    my($ID) = @_;
    my $field = $self->fieldsByID->{$ID};
    if (not defined($field)) {
        $field = $self->insertField($self->app->getService('user.fieldFactory')->createFieldByID($self->app, $self, $ID));
    }
    return $field;
}

sub getAddress {
    my $self = shift;
    my($protocol) = @_;
    my $field = $self->hasField('contact', $protocol);
    if (defined($field)) {
        return $field->address;
    } else {
        return undef;
    }
}

sub addFieldChange {
    my $self = shift;
    my($field, $newData, $password, $type) = @_;
    $field->prepareChange($newData);
    return $self->app->getService('dataSource.user')->setUserFieldChange($self->app, $self->userID, $field->fieldID, $newData, $password, $type);
}

sub performFieldChange {
    my $self = shift;
    my($changeID, $candidatePassword, $minTime) = @_;
    my $dataSource = $self->app->getService('dataSource.user');
    my($userID, $fieldID, $newData, $password, $createTime, $type) = $dataSource->getUserFieldChangeFromChangeID($self->app, $changeID);
    # check for valid change
    if ((not defined($userID)) or # invalid change ID
        ($userID != $self->userID) or # wrong change ID
        (not $self->app->getService('service.password')->checkPassword($candidatePassword, $password)) or # wrong password
        ($createTime < $minTime)) { # expired change
        return 0;
    }
    # perform the change
    $self->getFieldByID($fieldID)->data($newData);
    # remove the change from the list of pending changes
    if ($type == 1) { # XXX HARDCODED CONSTANT ALERT
        # this is an override change
        # remove all pending changes for this field (including this one)
        $dataSource->removeUserFieldChangesByUserIDAndFieldID($self->app, $userID, $fieldID);
    } else {
        # this is a normal change
        # remove just this change
        $dataSource->removeUserFieldChangesByChangeID($self->app, $changeID);
    }
    return 1;
}

# a convenience method for either setting a user setting from a new
# value or getting the user's prefs
sub setting {
    my $self = shift;
    my($variable, $setting) = @_;
    $self->assert(ref($variable) eq 'SCALAR', 1, 'Internal Error: User object was expecting a scalar ref for setting() but didn\'t get one');
    if (defined($$variable)) {
        $self->getField('settings', $setting)->data($$variable);
    } else {
        my $field = $self->hasField('settings', $setting);
        if (defined($field)) {
            $$variable = $field->data;
        }
    }
}

sub hash {
    my $self = shift;
    my $result = $self->SUPER::hash();
    $result->{'userID'} = $self->userID,
    $result->{'mode'} = $self->mode,
    $result->{'adminMessage'} = $self->adminMessage,
    $result->{'groupsByID'} = $self->groupsByID;
    $result->{'groupsByName'} = $self->groupsByName;
    $result->{'rights'} = [keys(%{$self->rights})];
    if ($self->levelInGroup(1)) {
        # has all rights
        $result->{'right'} = {};
        foreach my $right (@{$self->app->getService('dataSource.user')->getAllRights($self->app)}) {
            $result->{'right'}->{$right} = 1;
        }
    } else {
        $result->{'right'} = $self->rights;
    }
    $result->{'fields'} = {};
    foreach my $field (values(%{$self->fieldsByID})) {
        # XXX should we also pass the field metadata on? (e.g. typeData)
        $result->{'fields'}->{$field->fieldID} = $field->hash; # (not an array btw: could have holes)
        $result->{'fields'}->{$field->category.'.'.$field->name} = $field->hash;
    }
    return $result;
}

sub checkPassword {
    my $self = shift;
    my($password) = @_;
    return $self->app->getService('service.passwords')->checkPassword($self->password, $password);
}

sub checkLogin {
    my $self = shift;
    return ($self->mode == 0);
}

sub joinGroup {
    my $self = shift;
    my($groupID, $level) = @_;
    if ($level > 0) {
        my $groupName = $self->app->getService('dataSource.user')->getGroupName($self->app, $groupID);
        $self->{'groupsByID'}->{$groupID} = {'name' => $groupName, 'level' => $level, };
        $self->{'groupsByName'}->{$groupName} = {'groupID' => $groupID, 'level' => $level, };
        $self->invalidateRights();
        $self->{'_DIRTY'}->{'groups'} = 1;
    } else {
        $self->leaveGroup($groupID, $level);
    }
}

sub leaveGroup {
    my $self = shift;
    my($groupID) = @_;
    if (defined($self->{'groupsByID'}->{$groupID})) {
        delete($self->{'groupsByName'}->{$self->{'groupsByID'}->{$groupID}->{'name'}});
        delete($self->{'groupsByID'}->{$groupID});
        $self->invalidateRights();
        $self->{'_DIRTY'}->{'groups'} = 1;
    }
}

sub levelInGroup {
    my $self = shift;
    my($groupID) = @_;
    if (defined($self->{'groupsByID'}->{$groupID})) {
        return $self->{'groupsByID'}->{$groupID}->{'level'};
    } else {
        return 0;
    }
}


# internal routines

sub insertField {
    my $self = shift;
    my($field) = @_;
    $self->assert(ref($field) and $field->provides('user.field'), 1, 'Tried to insert something that wasn\'t a field object into a user\'s field hash');
    $self->fields->{$field->category}->{$field->name} = $field;
    $self->fieldsByID->{$field->fieldID} = $field;
    return $field;
}

sub invalidateRights {
    my $self = shift;
    my $rights = $self->app->getService('dataSource.user')->getRightsForGroups($self->app, keys(%{$self->{'groupsByID'}}));
    $self->rights({ map {$_ => 1} @$rights }); # map a list of strings into a hash for easy access
    # don't set a dirty flag, because rights are merely a convenient
    # cached expansion of the rights data. Changing this externally
    # makes no sense -- what rights one has is dependent on what
    # groups one is in, and changing the rights won't magically change
    # what groups you are in (how could it).
}

sub propertySet {
    my $self = shift;
    my($name, $value) = @_;
    # check that we're not doing silly things like changing the user's ID
    my $hadUndefinedID = (($name eq 'userID') and
                          ($self->propertyExists($name)) and
                          (not defined($self->propertyGet($name))));
    my $result = $self->SUPER::propertySet(@_);
    if (($hadUndefinedID) and (defined($value))) {
        # we've just aquired an ID, so propagate the change to all fields
        foreach my $field (values(%{$self->fieldsByID})) {
            $field->userID($value);
        }
        # and mark the groups as dirty too
        $self->{'_DIRTY'}->{'groups'} = 1;
    }
    $self->{'_DIRTY'}->{'properties'} = 1;
    return $result;
}

sub propertyGet {
    my $self = shift;
    my($name) = @_;
    if ($name eq 'groupsByID') {
        return {%{$self->{'groupsByID'}}};
        # Create new hash so that they can't edit ours. This ensures
        # that they can't inadvertently bypass the DIRTY flagging by
        # propertySet(), above. This does mean that internally we have
        # to access $self->{'groupsByID'} instead of $self->groupsByID.
    } else {
        # we don't bother looking at $self->rights or
        # $self->groupsByName, but any changes made to those won't be
        # saved anyway.
        return $self->SUPER::propertyGet(@_);
    }
}

sub DESTROY {
    my $self = shift;
    if ($self->{'_DIRTY'}->{'properties'}) {
        $self->writeProperties();
    }
    if ($self->{'_DIRTY'}->{'groups'}) {
        $self->writeGroups();
    }
    $self->SUPER::DESTROY(@_);
}

sub writeProperties {
    my $self = shift;
    $self->userID($self->app->getService('dataSource.user')->setUser($self->app, $self->userID, $self->mode,
                                                                     $self->password, $self->adminMessage,
                                                                     $self->newFieldID, $self->newFieldValue, $self->newFieldKey));
}

sub writeGroups {
    my $self = shift;
    # compare the group lists before and after and see which got added or changed and which got removed
    my $dataSource = $self->app->getService('dataSource.user');
    foreach my $group (keys(%{$self->{'groupsByID'}})) {
        if ((not defined($self->{'originalGroupsByID'}->{$group})) or
            ($self->{'groupsByID'}->{$group}->{'level'} != $self->{'originalGroupsByID'}->{$group}->{'level'})) {
            $dataSource->addUserGroup($self->app, $self->userID, $group, $self->{'groupsByID'}->{$group}->{'level'});
        }
    }
    foreach my $group (keys(%{$self->{'originalGroupsByID'}})) {
        if (not defined($self->{'groupsByID'}->{$group})) {
            $dataSource->removeUserGroup($self->app, $self->userID, $group);
        }
    }
}

# fields write themselves out
