package lib::cgi::Session;
use lib::Exception;
use lib::cgi::CGI;
use lib::sql::Table;
use strict;

use vars qw
    /
    $USER__NAME $USER__ID $USER__LOGIN $USER__PASSWORD
    $SESSION__NAME $SESSION__ID $SESSION__TIME $SESSION__IP $SESSION__USER_ID
    $MAX_TIMEOUT $KEY_LENGTH $TIMEOUT_CHECK $IP_CHECK
    $AUTOLOAD $ETERNAL
    /;

$USER__NAME       = "USER";
$USER__ID         = "ID";
$USER__LOGIN      = "LOGIN";
$USER__PASSWORD   = "PASSWORD";

$SESSION__NAME    = "SESSION";
$SESSION__ID      = "ID";
$SESSION__TIME    = "TIME";
$SESSION__IP      = "IP";
$SESSION__USER_ID = "USER_ID";

$MAX_TIMEOUT      = 2 * 3600;
$KEY_LENGTH       = 50;

$TIMEOUT_CHECK    = 1;
$IP_CHECK         = 0;

$ETERNAL          = undef;


##
# __PACKAGE__->new ($cookie_name, $login, $password);
# __PACKAGE__->new ($cookie_name);
##
sub new
{
    my $class       = shift;
    $class          = ref $class || $class;
    
    # if the session has been instanciated already,
    # returns the object.
    $ETERNAL and return $ETERNAL;

    my $cookie_name = shift or
	throw ( new lib::Exception ( code => "NO_COOKIE_NAME",
				     info => "The first argument should be a cookie name." ) );
    
    my $session_table = lib::sql::Table->table ($SESSION__NAME) or
	throw ( new lib::Exception ( code => "NO_SESSION_TABLE",
				     info => "$SESSION__NAME table does not seem to exist." ) );
    
    my $user_table    = lib::sql::Table->table ($USER__NAME) or
	throw ( new lib::Exception ( code => "NO_USER_TABLE",
				     info => "$USER__NAME table does not seem to exist." ) );

    if (@_ == 0)
    {
	# retrieve info from the web
	my $cgi = lib::cgi::CGI->get;

	my $session_id = $cgi->cookie ($cookie_name) or
	    throw ( new lib::Exception ( code => "NO_COOKIE_SENT",
					 info => "The browser sent no session cookie related info." ) );

	# retrieve session info from the database
	my $session = $session_table->search ($SESSION__ID => $session_id )->next or
	    throw ( new lib::Exception ( code   => "SESSION_EXPIRED",
					 info   => "That session did not seem to be in the database." ) );

	# make sure that this session is valid
	(defined $session) or return;
	if ($IP_CHECK)
	{
	    ($session->{$SESSION__IP} eq $ENV{REMOTE_ADDR} ) or
		throw ( new lib::Exception ( code => "UNVALID_IP_ADDRESS",
					     info => "Session IP is $session->{$SESSION__IP} whilst current IP is $ENV{REMOTE_ADDR}." ) );
	}
	if ($TIMEOUT_CHECK)
	{
	    ($session->{$SESSION__TIME} + $MAX_TIMEOUT > time) or
		throw ( new lib::Exception ( code => "SESSION_EXPIRED",
					     info =>
					     "Maximum time was $session->{$SESSION__TIME} + $MAX_TIMEOUT " .
					     "whilst current time is " . time . "." ) );
	}

	# update session time
	$session->{$SESSION__TIME} = time;
	$session_table->modify ($session);

	# if it is, retrieve user info which is associated with it
	my $user = $user_table->search ( $USER__ID => $session->{$SESSION__USER_ID} )->next or
	    throw ( new lib::Exception ( code => "CANNOT_FIND_USER",
					 info => "The user that this session references does not seem to exist." ) );
	
	# let us merge user and session attributes
	my $res = {};
	for my $key (keys %{$user})    { $res->{$USER__NAME    . '_' . $key} = $user->{$key}    }
	for my $key (keys %{$session}) { $res->{$SESSION__NAME . '_' . $key} = $session->{$key} }
	$res->{cookie_name} = $cookie_name;
	
	$ETERNAL = bless $res, $class;
	
	# Clean up the session table by removing timed out session
	my $condition = new lib::sql::Condition;
	$condition->add ($SESSION__TIME, '<', time - $MAX_TIMEOUT);
	$session_table->delete ($condition);
	
	$res->{cookie_name} = $cookie_name;   
	
	return $ETERNAL;
    }
    else
    {
	# Clean up the session table by removing timed out session
	my $condition = new lib::sql::Condition;
	$condition->add ($SESSION__TIME, '<', time - $MAX_TIMEOUT);
	$session_table->delete ($condition);
	
	my $login = shift or
	    throw ( new lib::Exception ( code => "LOGIN_IS_UNDEF",
					      info => "The user login is not defined." ) );
	
	my $password = shift or
	    throw ( new lib::Exception ( code => "PASSWORD_IS_UNDEF",
					      info => "The user password is not defined." ) );
	
	# retrieves the user info and checks if the user exists.
	my $user = $user_table->search ( $USER__LOGIN => $login )->next;
	(defined $user) or
	    throw ( new lib::Exception ( code => "CANNOT_FIND_USER",
					 info => "$login does not exist." ) );
	
	# checks that the user password is correct
	($user->{$USER__PASSWORD} eq $password) or
	    throw ( new lib::Exception ( code => "INCORRECT_PASSWORD",
					 info => "The password which is provided is not correct." ) );	
	
	# creates the session objects, inserts its info into
	# the database and returns it.
	my $session_id = join '', map { chr int rand (26) + ord 'A' } 1..$KEY_LENGTH;
	my $session_time = time;
	
	# first of all, delete any previous session for that user
	$session_table->delete ( $SESSION__USER_ID => $user->{$USER__ID} );
	
	$session_table->insert ( $SESSION__ID      => $session_id,
				 $SESSION__TIME    => $session_time,
				 $SESSION__IP      => $ENV{REMOTE_ADDR},
				 $SESSION__USER_ID => $user->{$USER__ID} );

	# let us merge user and session attributes
	my $res = {};
	for my $key (keys %{$user})    { $res->{$USER__NAME    . '_' . $key} = $user->{$key}    }
	$res->{$SESSION__NAME . '_' . $SESSION__ID}      = $session_id;
	$res->{$SESSION__NAME . '_' . $SESSION__TIME}    = $session_time;
	$res->{$SESSION__NAME . '_' . $SESSION__IP}      = $ENV{REMOTE_ADDR};
	$res->{$SESSION__NAME . '_' . $SESSION__USER_ID} = $user->{$USER__ID};
	$res->{cookie_name} = $cookie_name;   

	$ETERNAL = bless $res, $class;
	return $ETERNAL;
    }
}


##
# $obj->header;
# -------------
#   Prints the html header to store the proper cookie
#   into the user's browser.
##
sub header
{
    my $self = shift;
    my $cgi  = lib::cgi::CGI->get;
    return $cgi->header (-cookie => $cgi->cookie ( -name => $self->{cookie_name}, -value => $self->{ID}) );
}

##
# Accessor function for the package attributes :
# ----------------------------------------------
# user_id     user_login    user_password
# session_id  session_time  session_ip
##
sub AUTOLOAD
{
    my $self = shift;
    my ($attrib) = $AUTOLOAD =~ /.*::(.*)/;
    $self->{$attrib} = shift || return $self->{$attrib};
}


1;
