package Shelf;

=head1 NAME

Shelf - Perl extension for Virtual Bookshelves

=cut

use strict;
use C4::Context;
use Cache::FileCache;

=head1 VERSION

  $Id$

=cut

=head1 DESCRIPTION

Module for querying and stocking Virtual Bookshelves

   1. can contain a list of items, a list of biblioitems, or a list of biblios
   2. can have an arbitrary name, and will have a unique numerical identifier
   3. will have arbitrary metadata (properties) associated with it
	  * Sharing information (private, only visible by the owner of the
	    shelf; shared with a group of patrons; public, viewable by anybody)
	  * Special circulation rules - Do not return to home branch, do not
	    circulate, reduced loan time (ie 3 day loan)
	  * Search query term - if the shelf is the result of a query, the
	    query itself can be stored with the list of books that resulted
          * Creation date - useful for 'retiring' a stale cached query result
          * Access information - who has "write" or "read" access to the shelf.
	  * Searchable - If a patron can perform a search query on the contents
	    of this shelf


Patrons typically will only use "biblioitem" bookshelves, and will not need to
be presented with the differences between biblioitem and item bookshelves.


Some uses for VirtualBookshelves

   1. Cache search results for faster response on popular searches
   2. Name search results so that patrons can pull up saved searches
   3. Creation of sub-collections within a library or branch
   4. replacing "itemtypes" field... this would allow an individual item to be
	a member of more than one itemtype
   5. store a patron's reading record (if he chooses to store such data)
   6. store a patron's "To be read" list
   7. A teacher of a course could add a list of books to a shelf for his course
	and ask that those items be marked non-circulating so students always
	have access to them at the library.
	  * The teacher creates the list of materials that she wants to be
	    non-circulating (or reduced to 3-day loan) and marks them as such
	  * A librarian receives a notice that a shelf requires her attention.
	    He can pull up a list of the contents of the shelf, the owner of
	    the shelf, and the reason the owner is requesting this change in
	    circulation rules. The librarian can approve or deny the request.
	  * Optionally, create an access flag that grants teachers the right to
	    put items on modified circulation shelves without librarian
	    intervention.


=cut

=head1 METHODS

=head2 C<new()>

Base constructor for the class.

  my $shelf=Shelf->new(56);
      will load bookshelf 56.
  my $shelf=Shelf->new(-name => 'Fiction');
  my $shelf=Shelf->new('Fiction');
      will load the internal 'Fiction' shelf
  my $shelf=Shelf->new('Favourite Books', 'sjohnson');
  my $shelf=Shelf->new(-name => 'Favourite Books', -owner => 'sjohnson');
      will load sjohnson's "Favourite Books" bookshelf
  
  Any of the last four invocations will create a new shelf with the name and
  owner given if one doesn't already exist.


=cut

sub new {
    my $self = {};
    $self->{ID}	= undef;
    $self->{NAME}=undef;
    $self->{OWNDER}=undef;
    $self->{BIBLIOCONTENTS}={};
    $self->{BIBLIOITEMCONTENTS}={};
    $self->{ITEMCONTENTS}={};
    $self->{ATTRIBUTES}={};
    $self->{CACHE}=new Cache::FileCache( { 'namespace' => 'KohaShelves' } );

    if (@_) {
	my $dbh=C4::Context->dbh();
	shift;
	if ($#_ == 0) {
	    $self->{ID}=shift;
	    # load attributes of shelf #ID
	    my $sth;
	    $sth=$dbh->prepare("select bookshelfname,bookshelfowner from bookshelves where bookshelfid=?");
	    $sth->execute($self->{ID});
	    ($self->{NAME},$self->{OWNER}) = $sth->fetchrow;
	    $sth=$dbh->prepare("select attribute,value from bookshelfattributes where bookshelfid=?");
	    $sth->execute($self->{ID});
	    while (my ($attribute,$value) = $sth->fetchrow) {
		$self->{ATTRIBUTES}->{$attribute}=$value;
	    }
	} elsif ($#_) {
	    my ($name,$owner,$attributes);
	    if ($_[0] =~/^-/) {
		my %params=@_;
		$name=$params{name};
		$owner=$params{owner};
		$attributes=$params{attributes};
	    } else {
		$name=shift;
		$owner=shift;
		$attributes=shift;
	    }
	    my $sth=$dbh->prepare("select bookshelfid from bookshelves where bookshelfname=? and bookshelfowner=?");
	    $sth->execute($name, $owner);
	    if ($sth->rows) {
		($self->{ID})=$sth->fetchrow;
		$sth=$dbh->prepare("select attribute,value from bookshelfattributes where bookshelfid=?");
		$sth->execute($self->{ID});
		while (my ($attribute,$value) = $sth->fetchrow) {
		    $self->{ATTRIBUTES}->{$attribute}=$value;
		}
	    } else {
		$sth=$dbh->prepare("insert into bookshelves (bookshelfname, bookshelfowner) values (?, ?)");
		$sth->execute($name,$owner);
		$sth=$dbh->prepare("select bookshelfid from bookshelves where bookshelfname=? and bookshelfowner=?");
		$sth->execute($name,$owner);
		($self->{ID})=$sth->fetchrow();
		foreach my $attribute (keys %$attributes) {
		    my $value=$attributes->{$attribute};
		    $self->attribute($attribute,$value);
		}
	    }
	}
    }
    bless($self);
    return $self;
}


=head2 C<itemcontents()>

retrieve a slice of itemnumbers from a shelf.

    my $arrayref = $shelf->itemcontents(-orderby=>'title', 
    					-startat=>50,
					-number=>10	);

=cut

sub itemcontents {
    my $self=shift;
    my ($orderby,$startat,$number);
    if ($_[0]=~/^\-/) {
	my %params=@_;
	$orderby=$params{'-orderby'};
	$startat=$params{'-startat'};
	$number=$params{'-number'};
    } else {
	($orderby,$startat,$number)=@_;
    }
    $number--;
    unless ($self->{ITEMCONTENTS}->{orderby}->{$orderby}) {
	$self->loadcontents(-orderby=>$orderby, -startat=>$startat, -number=>$number);
    }
    my $endat=$startat+$number;
    my @return;
    foreach (@{$self->{ITEMCONTENTS}->{orderby}->{$orderby}}[$startat..$endat]) {
	push @return,$_;
    }
    return \@return;
}

=head2 C<biblioitemcontents()>

retrieve a slice of biblioitemnumbers from a shelf.

    my $arrayref = $shelf->biblioitemcontents(-orderby=>'title', 
    					      -startat=>50,
					      -number=>10	);

=cut

sub biblioitemcontents {
    my $self=shift;
    my ($orderby,$startat,$number);
    if ($_[0]=~/^\-/) {
	my %params=@_;
	$orderby=$params{'-orderby'};
	$startat=$params{'-startat'};
	$number=$params{'-number'};
    } else {
	($orderby,$startat,$number)=@_;
    }
    unless ($self->{BIBLIOITEMCONTENTS}->{orderby}->{$orderby}) {
	$self->loadcontents(-orderby=>$orderby, -startat=>$startat, -number=>$number);
    }
    my $endat=$startat+$number;
    my @return;
    foreach (@{$self->{BIBLIOITEMCONTENTS}->{orderby}->{$orderby}}[$startat..$endat]) {
	push @return,$_;
    }
    return \@return;
}

=head2 C<biblioitemcontents()>

retrieve a slice of biblionumbers from a shelf.

    my $arrayref = $shelf->bibliocontents(-orderby=>'title', 
    					  -startat=>50,
					  -number=>10	);

=cut

sub bibliocontents {
    my $self=shift;
    my ($orderby,$startat,$number);
    if ($_[0]=~/^\-/) {
	my %params=@_;
	$orderby=$params{'-orderby'};
	$startat=$params{'-startat'};
	$number=$params{'-number'};
    } else {
	($orderby,$startat,$number)=@_;
    }
    unless ($self->{BIBLIOCONTENTS}->{orderby}->{$orderby}) {
	$self->loadcontents(-orderby=>$orderby, -startat=>$startat, -number=>$number);
    }
    my $endat=$startat+$number;
    my @return;
    foreach (@{$self->{BIBLIOCONTENTS}->{orderby}->{$orderby}}[$startat..$endat]) {
	push @return,$_;
    }
    return \@return;
}


=head2 C<itemcounter()>

returns the number of items on the shelf

    my $itemcount=$shelf->itemcounter();

=cut
sub itemcounter {
    my $self=shift;
    unless ($self->{ITEMCONTENTS}->{orderby}->{'natural'}) {
	$self->loadcontents();
    }
    my @temparray=@{$self->{ITEMCONTENTS}->{orderby}->{'natural'}};
    return $#temparray+1;
}

sub shelfcontents {
    my $self=shift;
}


=head2 C<clearcontents()>

Removes all contents from the shelf.

    $shelf->clearcontents();

=cut

sub clearcontents {
    my $self=shift;
    my $dbh=C4::Context->dbh();
    my $sth=$dbh->prepare("delete from bookshelfcontents where bookshelfid=?");
    $sth->execute($self->{ID});
    foreach my $level ('ITEM', 'BIBLIOITEM', 'BIBLIO') {
	delete $self->{$level."CONTENTS"};
	$self->{$level."CONTENTS"}={};
    }
    $self->clearcache();

}



=head2 C<addtoshelf()>

adds an array of items to a shelf.  If any modifications are actually made to
the shelf then the per process caches and the FileCache for that shelf are
cleared.

  $shelf->addtoshelf(-add => [[ 45, 54, 67], [69, 87, 143]]);

=cut

sub addtoshelf {
    my $self=shift;
    my $add;
    if ($_[0]=~/^\-/) {
	my %params=@_;
	$add=$params{'-add'};
    } else {
	($add)=@_;
    }
    my $dbh=C4::Context->dbh();
    my $sth;
    my $bookshelfid=$self->{ID};
    my $clearcache=0;
    foreach (@$add) {
	my ($biblionumber,$biblioitemnumber,$itemnumber) = @$_;
	$sth=$dbh->prepare("select count(*) from bookshelfcontents where bookshelfid=$bookshelfid and itemnumber=$itemnumber and biblioitemnumber=$biblioitemnumber and biblionumber=$biblionumber");
	$sth->execute;
	my $rows=$sth->fetchrow();
	if ($rows==0) {
	    $sth=$dbh->prepare("insert into bookshelfcontents (bookshelfid,biblionumber,biblioitemnumber,itemnumber) values ($bookshelfid,$biblionumber,$biblioitemnumber,$itemnumber)");
	    $sth->execute;
	    $clearcache=1;
	}
    }
    ($clearcache) && ($self->clearcache());
}


sub removefromshelf {
    my $self=shift;
}

=head2 C<attribute()>

Returns or sets the value of a given attribute for the shelf.

  my $loanlength=$shelf->attribute('loanlength');
  $shelf->attribute('loanlength', '21 days');


=cut

sub attribute {
    my $self=shift;
    my ($attribute, $value);
    $attribute=shift;
    $value=shift;
    if ($value) {
	$self->{ATTRIBUTES}->{$attribute}=$value;
	my $dbh=C4::Context->dbh();
	my $sth=$dbh->prepare("select value from bookshelfattributes where bookshelfid=? and attribute=?");
	$sth->execute($self->{ID}, $attribute);
	if ($sth->rows) {
	    my $sti=$dbh->prepare("update bookshelfattributes set value=? where bookshelfid=? and attribute=?");
	    $sti->execute($value, $self->{ID}, $attribute);
	} else {
	    my $sti=$dbh->prepare("insert into bookshelfattributes (bookshelfid, attribute, value) values (?, ?, ?)");
	    $sti->execute($self->{ID}, $attribute, $value);
	}
    }
    return $self->{ATTRIBUTES}->{$attribute};
}


=head2 C<attributes()>

Returns a hash reference of the shelf attributes

    my $attributes=$shelf->attributes();
    my $loanlength=$attributes->{loanlength};

=cut

sub attributes {
    my $self=shift;
    return $self->{ATTRIBUTES};
}

=head2 C<clearcache()>

Clears the per process in-memory cache and the FileCache if any changes are
made to a shelf.

  $shelf->clearshelf();

=cut

sub clearcache {
    my $self=shift;
    foreach my $level ('ITEM','BIBLIOITEM','BIBLIO') {
	delete $self->{$level."CONTENTS"};
	foreach my $sorttype (('author', 'title')) {
	    $self->{CACHE}->remove($self->{ID}."_".$level."CONTENTS_".$sorttype);
	}
    }
}


=head2 C<loadcontents()>

loads the contents of a particular shelf and loads into a per process memory
cache as well as a shared Cache::FileCache.

This subroutine is normally only used internally (called by itemcontents,
biblioitemcontents, or bibliocontents).

  $shelf->loadcontents(-orderby => 'author', -startat => 30, -number => 10);


=cut

sub loadcontents {
    my $self=shift;
    my ($orderby,$startat,$number);
    if ($_[0]=~/^\-/) {
	my %params=@_;
	$orderby=$params{'-orderby'};
	$startat=$params{'-startat'};
	$number=$params{'-number'};
    } else {
	($orderby,$startat,$number)=@_;
    }
    my $bookshelfid=$self->{ID};
    ($orderby) || ($orderby='natural');
    $self->{ITEMCONTENTS}->{orderby}->{$orderby}=$self->{CACHE}->get( "$bookshelfid\_ITEMCONTENTS_$orderby" );
    $self->{BIBLIOITEMCONTENTS}->{orderby}->{$orderby}=$self->{CACHE}->get( "$bookshelfid\_BIBLIOITEMCONTENTS_$orderby" );
    $self->{BIBLIOCONTENTS}->{orderby}->{$orderby}=$self->{CACHE}->get( "$bookshelfid\_BIBLIOCONTENTS_$orderby" );
    if ( defined $self->{ITEMCONTENTS}->{orderby}->{$orderby}) {
	return;
    }
    my $dbh=C4::Context->dbh();
    my $sth;
    my $limit='';
    if ($startat && $number) {
	$limit="limit $startat,$number";
    }
    $limit='';
    my $biblionumbers;
    my $biblioitemnumbers;
    if ($orderby eq 'author') {
	$sth=$dbh->prepare("select itemnumber,BSC.biblionumber,BSC.biblioitemnumber from bookshelfcontents BSC, biblio B where BSC.biblionumber=B.biblionumber and bookshelfid=$bookshelfid order by B.author $limit");
    } elsif ($orderby eq 'title') {
	$sth=$dbh->prepare("select itemnumber,BSC.biblionumber,BSC.biblioitemnumber from bookshelfcontents BSC, biblio B where BSC.biblionumber=B.biblionumber and bookshelfid=$bookshelfid order by B.title $limit");
    } else {
	$sth=$dbh->prepare("select itemnumber,biblionumber,biblioitemnumber from bookshelfcontents where bookshelfid=$bookshelfid $limit");
    }
    $sth->execute;
    my @results;
    my @biblioresults;
    my @biblioitemresults;
    while (my ($itemnumber,$biblionumber,$biblioitemnumber) = $sth->fetchrow) {
	unless ($biblionumbers->{$biblionumber}) {
	    $biblionumbers->{$biblionumber}=1;
	    push @biblioresults, $biblionumber;
	}
	unless ($biblioitemnumbers->{$biblioitemnumber}) {
	    $biblioitemnumbers->{$biblioitemnumber}=1;
	    push @biblioitemresults, $biblioitemnumber;
	}
	push @results, $itemnumber;
    }
    $self->{CACHE}->set("$bookshelfid\_ITEMCONTENTS_$orderby", \@results, "3 hours");
    $self->{CACHE}->set("$bookshelfid\_BIBLIOITEMCONTENTS_$orderby", \@results, "3 hours");
    $self->{CACHE}->set("$bookshelfid\_BIBLIOCONTENTS_$orderby", \@results, "3 hours");
    $self->{ITEMCONTENTS}->{orderby}->{$orderby}=\@results;
    $self->{BIBLIOOCONTENTS}->{orderby}->{$orderby}=\@biblioresults;
    $self->{BIBLIOITEMCONTENTS}->{orderby}->{$orderby}=\@biblioitemresults;
}



1;
