# Copyright (c) vhffs project and its contributors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions 
# are met:
#
# 1. Redistributions of source code must retain the above copyright 
#   notice, this list of conditions and the following disclaimer.
#2. Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in 
#   the documentation and/or other materials provided with the 
#   distribution.
#3. Neither the name of vhffs nor the names of its contributors 
#   may be used to endorse or promote products derived from this 
#   software without specific prior written permission.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
#"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
#LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
#FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
#COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
#INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
# POSSIBILITY OF SUCH DAMAGE.

=head1 SYNOPSIS

Vhffs::Object - The generic object type in VHFFS

=head1 DESCRIPTION

The Vhffs::Object type is the base of each Vhffs::Services::*, Vhffs::User
or Vhffs::Group objects. So, you never have to create an instance of Vhffs::Object,
this class is only used as a super-class for services (or user or group).

So, the methods of Vhffs::Object can be applied to all classes which inherit of it.
You can have all the history of an object with a method, get the status
of a service, ...

This type stored informations about state, history, owner group/user.

=head1 METHODS

=cut

use strict;
use utf8;

package Vhffs::Object;

use Vhffs::Constants;
use POSIX qw(locale_h);
use locale;
use Locale::gettext;
use Encode;

sub new
{
    my ( $this , $main , $id , $owner_uid , $owner_gid ) = @_;
    
    my $self = {};

    my $class = ref($this) || $this;
    bless($self , $class );
  
    return undef if ( ! defined $main );
    
    $self->{'main'} = $main;
    $self->{'db'} = $main->get_db_object;
    $self->{'owner_uid'} = $owner_uid;
    $self->{'owner_gid'} = $owner_gid;
    $self->{'description'} = '';
    
    if ( defined $id )
    {
        $self->{'object_id'} = $id;
    }
    else
    {
        $self->{'object_id'} = 0;
    }
   
    return $self;
}

sub _new {
    my ($class, $main, $oid, $owner_uid, $owner_gid, $date_creation, $description, $refuse_reason, $state, $type) = @_;
    
    my $self = {};

    bless($self, $class);

    return undef unless(defined $main);

    $self->{main} = $main;
    $self->{db} = $main->get_db_object;
    $self->{object_id} = $oid;
    $self->{owner_uid} = $owner_uid;
    $self->{owner_gid} = $owner_gid;
    $self->{date_creation} = $date_creation;
    $self->{description} = $description;
    $self->{refuse_reason} = $refuse_reason;
    $self->{state} = $state;
    $self->{type} = $type;
    
    return $self;
}

sub get_main
{
    my $self = shift;
    return $self->{'main'};
}

sub get_db_object {
    my $self = shift;
    return $self->{main}->get_db_object;
}

sub delete
{
    my $self = shift;

	my $query;
	my $request;


    # Foreign key constraints are in 'ON DELETE CASCADE' mode
    # we don't have to bother with foreign tables deletion.
    $query = 'DELETE FROM vhffs_object WHERE object_id=?';
    $request = $self->{'db'}->prepare($query);
    $request->execute( $self->{'object_id'} ) or return -1;

    return 1;
}

sub toString
{
    my ($self) = @_;

    foreach ( keys %$self )
    {
        print "$_: $self->{$_} ";
    }
}

=head2 create

    my $object = Vhffs::Object::create($main, [$owner_uid, $owner_gid, $description, $state, $type])

Create (in database) and return a new object.

=over 4

=item C<$main>: C<Vhffs::Main> instance

=item C<$owner_uid>: UID of the owner.

=item C<$description>: Description of the object

=item C<$state>: state of the object. Can be undef, if it is, object will be
waiting for validation or waiting for creation depending on wether
moderation is active or not.

=item C<$type>: Object's type (C<Vhffs::Constants::TYPE_*>)

=back

=cut

sub create {
   my ($vhffs, $owner_uid, $owner_gid, $description, $state, $type) = @_;
    $description = '' unless (defined $description);
    $state = ($vhffs->get_config->get_moderation ? Vhffs::Constants::WAITING_FOR_VALIDATION : Vhffs::Constants::WAITING_FOR_CREATION) unless defined $state;
    my $sth = $vhffs->get_db_object->prepare('INSERT INTO vhffs_object(owner_uid, owner_gid, date_creation, state, description, type) VALUES ( ?, ?, ?, ?, ?, ?)');
    $sth->execute($owner_uid, $owner_gid, time(), $state, $description, $type) or return undef;
    my $oid = $vhffs->get_db_object->last_insert_id(undef, undef, 'vhffs_object', undef);

    my $res = get_by_oid($vhffs, $oid);

    $res->add_history('Object created');
    return $res;
}

sub get_owner_uid
{
	my $self = shift;
	return $self->{'owner_uid'};
}

sub get_owner_gid
{
	my $self = shift;
	return $self->{'owner_gid'};
}

sub get_type
{
    my $self = shift;
    return $self->{type};
}

#The modify method for Vhffs::Object only tells if the API can modify some values
#It protects some importants fields such as object_id
sub modify
{
    my $self;
    my $field;
    my $value;

    ($self , $field , $value ) =  @_;

    return -1 if( $field eq "object_id" );

    return 0;
}


sub commit
{
    my $self = shift;
    my $request;

    $request = 'UPDATE vhffs_object SET state=?, description=?, refuse_reason=?, owner_uid=?, owner_gid=? WHERE object_id=?';
    my $result = $self->{'db'}->prepare($request);
    $result->execute( $self->{'state'} , $self->{'description'} , $self->{'refuse_reason'}, $self->{'owner_uid'} , $self->{'owner_gid'} , $self->{'object_id'} );

}

sub get_date
{
	my $self = shift;
	return $self->{'date_creation'};
}

sub get_oid
{
    my $self = shift;
    return $self->{'object_id'};
}

sub get_description
{
    my $self = shift;
    return $self->{'description'};
}

sub set_owner_uid
{
   my ( $self , $value ) = @_;
   $self->{'owner_uid'} = $value;
}

sub set_owner_gid
{
   my ( $self , $value ) = @_;
   $self->{'owner_gid'} = $value;
}


sub get_status
{
	my $self = shift;
	return $self->{'state'};
}

=head2 get_label

Returns a label for this object that can be used to display information.
Should be redefined in every subclasses (for example, Web returns the
servername).

=cut

sub get_label {
    return '????';
}

sub set_status
{
	my ($self , $value);
	$self = shift;
	$value = shift;
	$self->{'state'} = $value;
	
	if( $value == Vhffs::Constants::TO_DELETE )
	{
		$self->add_history( "Will be deleted" );
	}
	elsif( $value == Vhffs::Constants::WAITING_FOR_CREATION )
	{
		$self->add_history( "Moderated. Will be created" );
	}
	elsif( $value == Vhffs::Constants::CREATING_ERROR )
	{
		$self->add_history( "Creating error ! Robots can't create it" );
	}
	elsif( $value == Vhffs::Constants::CREATED )
	{
		$self->add_history( "Object is created" );
	}
	elsif( $value == Vhffs::Constants::WAITING_FOR_VALIDATION )
	{
		$self->add_history( "Object is under moderation");
	}
	elsif( $value == Vhffs::Constants::VALIDATION_REFUSED )
	{
		$self->add_history( "Validation refused");
	}
	elsif( $value == Vhffs::Constants::ACTIVATED )
	{
		$self->add_history( "Is now active for production");
	}
	
	
}


sub set_default_state
{
	my $self = shift;
	my $vhffs = $self->{'main'};

	if( $vhffs->get_config->get_moderation == 1 )
	{
		$self->set_status( Vhffs::Constants::WAITING_FOR_VALIDATION );
	}
	else
	{
		$self->set_status( Vhffs::Constants::WAITING_FOR_CREATION );
	}
	return( 1 );
}

sub set_description
{
	my ($self , $value) = @_;
	$value =~ s/\?/ \?/g;
	$self->{'description'} = $value ;
}


sub add_history
{
	use Vhffs::Main;
	my $self = shift;
	my $message = shift;
	my $source = Vhffs::Main::current_user_uid;
	$message = $message;

	my $query = 'INSERT INTO vhffs_history(object_id, date, message, source_uid)  VALUES(?, ?, ?, ?)';
	my $request = $self->{'db'}->prepare( $query );
	$request->execute( $self->{'object_id'}, time(), $message, $source ) or return -2;

	return $self->{'db'}->last_insert_id(undef, undef, 'vhffs_history', undef);;
}

# Returns a hashref wich contains all the history
sub get_history
{
	use Vhffs::Functions;
	my $self = shift;

    my $dbh = $self->get_db_object;
    my $sql = 'SELECT history_id, message, date, source.username as source FROM vhffs_history h LEFT JOIN vhffs_users source ON source.uid = h.source_uid WHERE h.object_id = ? ORDER BY date DESC';
    return $dbh->selectall_arrayref($sql, {Slice => {}}, $self->{object_id});
}


sub set_refuse_reason {
        my ($self , $value) = @_;
	$self->{'refuse_reason'} = $value;
}

sub get_refuse_reason  {
	my $self = shift;
	return gettext('no reason given') if( !defined $self->{'refuse_reason'} || length $self->{'refuse_reason'} <= 0 );
	return $self->{'refuse_reason'};
}


# Accept this object
sub moderate_accept
{
	my $self = shift;
	my $comments = shift;

	my $vhffs = $self->{main};
	my $user = $self->get_user;

	$self->set_status( Vhffs::Constants::WAITING_FOR_CREATION );
	$self->set_refuse_reason( '' );

	# TODO: write a beautiful module for INTL
	bindtextdomain('vhffs', '%localedir%');
	textdomain('vhffs');

	my $prevlocale = setlocale( LC_ALL );
	setlocale( LC_ALL , $user->get_lang );

	my $mail;

	if(defined $comments) {
		$mail = sprintf(
			gettext( "Hello %s %s,\n\nYour request for a %s (%s) on %s was accepted, however, moderators wanted to add some precision:\n%s.\nPlease wait while we are creating your object.\n\nCheers,\nThe moderator team\n\n---------\n%s\n%s\n" ) ,
		$user->get_firstname,
		$user->get_lastname,
		Vhffs::Functions::type_string_from_type_id( $self->get_type ),
		$self->get_label,
		$vhffs->get_config->get_host_name,
		$comments,
		$vhffs->get_config->get_host_name,
		$vhffs->get_config->get_panel->{url}
		);
	} else {
		$mail = sprintf(
			gettext( "Hello %s %s,\n\nYour request for a %s (%s) on %s was accepted.\nPlease wait while we are creating your object.\n\nCheers,\nThe moderator team\n\n---------\n%s\n%s\n" ) ,
		$user->get_firstname,
		$user->get_lastname,
		Vhffs::Functions::type_string_from_type_id( $self->get_type ),
		$self->get_label,
		$vhffs->get_config->get_host_name,
		$vhffs->get_config->get_host_name,
		$vhffs->get_config->get_panel->{url}
		);
	}
	my $subject = sprintf(
		gettext('Your request for a %s (%s) on %s was accepted'), 
		Vhffs::Functions::type_string_from_type_id( $self->get_type ),
		$self->get_label,
		$vhffs->get_config->get_host_name
		);

	Vhffs::Functions::send_mail( $vhffs , $vhffs->get_config->get_moderator_mail , $user->get_mail , $vhffs->get_config->get_mailtag , $subject , $mail );

	setlocale( LC_ALL , $prevlocale );

	$user->set_note( $user->get_note +1 );
	$user->commit;

	return $self->commit;
}


# Refuse this object
sub moderate_refuse
{
	my $self = shift;
	my $reason = shift;

	my $vhffs = $self->{main};
	my $user = $self->get_user;

	$self->set_status( Vhffs::Constants::VALIDATION_REFUSED );
	$self->set_refuse_reason( $reason );

	# TODO: write a beautiful module for INTL
	bindtextdomain('vhffs', '%localedir%');
	textdomain('vhffs');

	my $prevlocale = setlocale( LC_ALL );
	setlocale( LC_ALL , $user->get_lang );

	my $mail = sprintf(
		gettext( "Hello %s %s,\n\nYour request for a %s (%s) on %s was refused.\n\nThe reason of refusal given by moderators is:\n%s\n\nYou can change the description and submit it again for moderation on the\npanel. You can delete this object on the panel if you have made\na mistake. Don't be upset, if you don't understand why your request has\nbeen refused, just reply to this email !\n\nCheers,\nThe moderator team\n\n---------\n%s\n%s\n" ) ,

		$user->get_firstname,
		$user->get_lastname,
		Vhffs::Functions::type_string_from_type_id( $self->get_type ),
		$self->get_label,
		$vhffs->get_config->get_host_name,
		$self->get_refuse_reason,
		$vhffs->get_config->get_host_name,
		$vhffs->get_config->get_panel->{url}
		);

	my $subject = sprintf(
		gettext('Your request for a %s (%s) on %s was refused'),
		Vhffs::Functions::type_string_from_type_id( $self->get_type ),
		$self->get_label,
		$vhffs->get_config->get_host_name
		);

	Vhffs::Functions::send_mail( $vhffs , $vhffs->get_config->get_moderator_mail , $user->get_mail , $vhffs->get_config->get_mailtag , $subject , $mail );

	setlocale( LC_ALL , $prevlocale );

	$user->set_note( $user->get_note -1 );
	$user->commit;

	return $self->commit;
}


# Delete this object with a notice mail
sub delete_withmail
{
	my $self = shift;

	my $vhffs = $self->{main};
	my $user = $self->get_user;

	# TODO: write a beautiful module for INTL
	bindtextdomain('vhffs', '%localedir%');
	textdomain('vhffs');

	my $prevlocale = setlocale( LC_ALL );
	setlocale( LC_ALL , $user->get_lang );

	my $mail = sprintf(
		gettext( "Hello %s %s,\n\nYour %s (%s) on %s has been deleted.\n\nThis is because it have been refused a long time ago and you didn't\nsubmit an update since.\n\nFor reminder, the reason of refusal was:\n%s\n\nDon't be upset, submit it again if you forgot to update it in time.\nIf you need further information, just reply to this email !\n\nCheers,\nThe moderator team\n\n---------\n%s\n%s\n" ) ,

		$user->get_firstname,
		$user->get_lastname,
		Vhffs::Functions::type_string_from_type_id( $self->get_type ),
		$self->get_label,
		$vhffs->get_config->get_host_name,
		$self->get_refuse_reason,
		$vhffs->get_config->get_host_name,
		$vhffs->get_config->get_panel->{url}
		);

	my $subject = sprintf(
		gettext('Your %s (%s) on %s has been deleted due to lack of updates'),
		Vhffs::Functions::type_string_from_type_id( $self->get_type ),
		$self->get_label,
		$vhffs->get_config->get_host_name
		);

	Vhffs::Functions::send_mail( $vhffs , $vhffs->get_config->get_moderator_mail , $user->get_mail , $vhffs->get_config->get_mailtag , $subject , $mail );

	setlocale( LC_ALL , $prevlocale );

	$self->delete;
}


sub resubmit_for_moderation
{
	my $self = shift;
	my $description = shift;

	return -1 unless defined $description;
	return -1 if( $self->get_status != Vhffs::Constants::VALIDATION_REFUSED );

	$self->set_description( $description );
	$self->set_status( Vhffs::Constants::WAITING_FOR_VALIDATION );
	return $self->commit;
}


#Get all objects of the database.
#If $name is undefined, the functions returns ALL objects.
#Else, it returns objects like the name. It searchs in oid, user and description
#The first arg is a valid instance of Vhffs::Main
sub getall
{
	my $vhffs = shift;
	my $name = shift;
	my $state = shift;
	my $age = shift; #seconds late

	my $query = 'SELECT o.object_id, o.owner_uid, o.owner_gid, o.date_creation , o.description, o.refuse_reason, o.state, o.type FROM vhffs_object o';
	my @params;

	if( defined $name )
	{
		$query .= ' INNER JOIN vhffs_users u ON o.owner_uid = u.uid INNER JOIN vhffs_groups g ON o.owner_gid = g.gid WHERE ( o.description LIKE ? ) OR ( o.object_id LIKE ? ) OR ( o.owner_uid LIKE ? ) OR ( state LIKE ? ) OR ( u.username LIKE ? ) OR ( g.groupname LIKE ? )';
		push(@params, '%'.$name.'%');
		push(@params, '%'.$name.'%');
		push(@params, '%'.$name.'%');
		push(@params, '%'.$name.'%');
		push(@params, '%'.$name.'%');
		push(@params, '%'.$name.'%');
	}
	if( defined $state )
	{
		if( $query =~ /WHERE/ )  {
			$query .= ' AND o.state = ?';
		}
		else {
			$query .= ' WHERE o.state = ?';
		}
		push(@params, $state);
	}
	if( defined $age )
	{
		my $ts = time() - $age;

		if( $query =~ /WHERE/ )  {
			$query .= ' AND date_creation < ? ';
		}
		else {
			$query .= ' WHERE date_creation < ? ';
		}
		push(@params, $ts);
	}

	$query .= ' ORDER BY object_id';

	my $request = $vhffs->get_db_object->prepare( $query ) or return -1;
	return undef unless $request->execute(@params);

	my $result;
	my $rows = $request->fetchall_arrayref();
	foreach(@$rows) {
		push(@$result, _new Vhffs::Object($vhffs, @$_));
	}
	return $result;
}


sub get_group
{
    my $self = shift;
    if( defined $self->{'group'} )
	{
		return( $self->{'group'} );
	}
	else
	{
		if( defined $self->{'owner_gid'} )
		{
			use Vhffs::Group;
			$self->{'group'} = Vhffs::Group::get_by_gid( $self->{'main'} , $self->{'owner_gid'} );
			return( $self->{'group'} ) if( defined( $self->{'group'} ) );
		}
	}
    return undef;
}


sub get_user
{
    my $self = shift;
    if( defined $self->{'user'} )
	{
		return( $self->{'user'} );
	}
	else
	{
		if( defined $self->{'owner_uid'} )
		{
			use Vhffs::User;
			$self->{'user'} = Vhffs::User::get_by_uid( $self->{'main'} , $self->{'owner_uid'} );
			return( $self->{'user'} ) if( defined ( $self->{'user'} ) );
		}
	}
    return undef;
}

=pod

=head2 get_by_oid

    my $obj = Vhffs::Object::get_by_oid($main, $oid);

Fetches an object using its object ID. Returned object is
fully functional.

=cut

sub get_by_oid
{
    my ($vhffs, $oid) = @_;
    return undef unless ( defined $oid && $oid =~ /^\d+$/ );

    my $query = 'SELECT owner_uid, owner_gid, date_creation, description, refuse_reason, state, type FROM vhffs_object WHERE object_id =?';
    my $sth = $vhffs->get_db_object->prepare( $query );
    my $rows = $sth->execute( $oid );

    return undef unless $rows == 1;

    my @result = $sth->fetchrow_array();

    my $object = _new Vhffs::Object($vhffs, $oid, @result);

    return $object;
}


=head2 fill_object

    my $svc = Vhffs::Service::XXX::fill_object($obj);

This method should be overloaded in every subclasses.
Its goal is to transform a given object into a more
specialized subclass.
=cut
sub fill_object {
    my ($class, $obj) = @_;
    warn "Unimplemented fill_object method\n";
    return $obj;
}

=head2 _fill_object

    $svc = $class->SUPER::_fill_object($obj, $sql);

Convenience method to implement fill_object in subclasses.

Adds fields returned by C<$sql> query to object $obj and, if the query succeed,
bless the object whith C<$class>.

C<$sql> must contain a placeholder (?) which will be filled with the object
OID. See subclasses implementation of C<fill_object> for more details.

=cut

sub _fill_object {
    my ($class, $obj, $sql) = @_;
    my $dbh = $obj->get_db_object();
    my $res = $dbh->selectrow_hashref($sql, undef, $obj->get_oid);
    return $obj unless(defined $res);
    foreach(keys %$res) {
        $obj->{$_} = $res->{$_};
    }
    return bless($obj, $class);
}

1;
			    
__END__

=head2 get_main( )

This method returns the Vhffs::Main object contained in this object.
This method can be useful if you have an object and you need a Vhffs::Main instance.

=head2 delete( )

Delete the object. This method is called from inherited class.
Note that it destroy the object-part (history, ...), but not the inherited class.
Returns negative value if fails, positive if success.

=head2 commit( )

Apply all changes that were made on this object. Returns negative value if failed, positive if success.

=head2 fetch( )

Get informations for this object from the database. Typically, you create an instance of Vhffs::Object with a valid object-id and when you call fetch, it takes all informations about this object in the database according to the object-id.

=head2 fetch_object( )
Same as fetch()

=head2 get_owner_uid( )

Returns the uid that own this Object.

=head2 modify( $field , $value )

Modify a propertiy of this object. For example, modify( "description" , "my_description") will change the description of this Object. If you want update those modified informations on the database, you need to call commit() to update informations in the database. Note that you never had to change object-id of an object.

=head2 get_date( )

Returns the date of the creation for this object.

=head2 get_oid( )

Returns the object_id of this Object.

=head2 get_description( )

Returns the description of this object.

=head2 set_owner( )

Change the uid of the owner of this object.

=head2 get_status( )

Get the status of this object. The status are given in the Vhffs::Constants class.

=head2 set_status( )

Change the status of an object. The status are available as constants in Vhffs::Constants class. When you change the status of an object, a message is added in its history.

=head2 set_description( $string )

Change the description of an object. As all accessors, you need to call commit() method after.

=head2 add_history( $string )

Add a message in the object's history.

=head2 get_history( )

Returns an hashref that represent the history of this object. This object has as key the date of each history message. For exemple :

$history = $object->get_history();

if( defined $history )

{

	foreach $key ( keys %{$history} )

	{

		print "At date " . $key . " message: " . $history->{$key}{'message'};

	}

}

=head2 get_group( )

Returns the Vhffs::Group instance that own this object. Returns undef is fails.

=head2 get_user( )

Returns the Vhffs::User instance that own this object. Returns undef is fails.

=head2 getall( Vhffs::Main , $name )

The getall is very important and defined in every service. In Vhffs::Object,
it returns all object if $name is not defined (undef). Return all objects that matches with $name if $name is defined.

=head1 SEE ALSO
Vhffs::Constants Vhffs::User Vhffs::Group

=head1 AUTHOR

	Julien Delange <julien at gunnm dot org>

=head1 COPYRIGHT

	Julien Delange <julien at gunnm dot org>
