# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: Library.pm 3046 2008-03-31 16:42:06Z josh $

package BigMed::Library;
use strict;
use warnings;
use utf8;
use Carp;
use base qw(BigMed::MiniContent);
use BigMed::Plugin;
use BigMed::Pointer;

my %minicontent;
BigMed::Library->set_schema(
    lib_superclass => 1,
    elements       => [
        {   name  => 'owner',
            type  => 'system_id',
            index => 1,
        },
        {   name    => 'shared',
            type    => 'boolean',
            default => 1,
            index   => 1,
        },
        {   name    => 'in_lib',
            type    => 'boolean',
            default => 1,
            index   => 1,
        },
    ],
);

###########################################################
# LIBRARY TYPE MANAGEMENT
###########################################################

my $types_loaded;

sub _load_libraries {
    return 1 if $types_loaded;
    $types_loaded = 1;
    return BigMed::Plugin->load_library_types();
}

my %LIBRARY_TYPES = ();
my %BATCH_UPLOAD  = ();

sub register_minicontent {
    my $class = shift;
    my %param = @_;
    $class->SUPER::register_minicontent(@_);
    $LIBRARY_TYPES{ $class->data_label } = $class if !$param{lib_superclass};
    $BATCH_UPLOAD{$class} = $param{batch_upload};
    return 1;
}

sub library_types {
    _load_libraries();
    return sort { lc $a cmp lc $b } keys %LIBRARY_TYPES;
}

sub library_classes {
    _load_libraries();
    return sort { lc $a cmp lc $b } values %LIBRARY_TYPES;
}

sub can_batch_upload {
    return $_[0]->batch_upload_column ? 1 : 0;
}

sub batch_upload_column {
    return $BATCH_UPLOAD{ ref $_[0] || $_[0] };
}

sub batch_upload_title {
    my $obj = shift;
    my $col = $BATCH_UPLOAD{ ref $obj };
    return $col && $obj->$col ? $obj->$col : 'Untitled';
}

###########################################################
# FETCH LIBRARY ITEMS
###########################################################

sub collect_lib {
    my $self = shift;
    my %arg  = @_;

    my $site = $arg{site} || ( ref $self ? $self->site : undef );
    croak 'site unspecified for collect request'
      if !( my $sid = ( ref $site ? $site->id : $site ) );

    my $user = $arg{user};
    if ($user) {
        croak 'user parameter must be a BigMed::User object'
          if !$user->isa('BigMed::User');
        if ( !$user->privilege_level($sid) ) {
            return BigMed::Error->set_error(
                head => 'LIBRARY_ERR_HEAD_No privileges at site',
                text =>
                  ['LIBRARY_ERR_TEXT_No privileges at site', $user->name],
            );
        }
    }

    #assemble search info
    my %terms = ( site => $sid );
    $terms{in_lib} = 1 if $arg{in_lib};
    my %args = ( offset => $arg{offset}, limit => $arg{limit} );
    my $order = $arg{order};
    $order = 'alpha' if !$order || $order ne 'recent';    #defaults to alpha
    ( $args{sort}, $args{order} ) = $self->_lib_sort_info($order);

    #if no user, or if user is webmaster or admin, they get all objects;
    #otherwise, limited to subset of objects
    if ( $user && $user->privilege_level($sid) < 5 ) {
        $terms{shared} = 1 if !$arg{editable};
        $terms{owner} = $user->id;
        $args{any} = ['owner', 'shared'] if $terms{owner} && $terms{shared};
    }

    #searchname -- do this search first, then winnow from there
    my $search = $arg{cache} || $self;
    if ( defined $arg{namesearch} && $arg{namesearch} ne q{} ) {
        $arg{namesearch} =~ s/[.,"]/ /msg;
        $arg{namesearch} =~ s/&quot;/ /msg;

        #build the regex; use blocks of anything other than
        #marks, punctuation and separators. Originally split the
        #words by using a regex paren collector:
        #    ( $filter{'q'} =~ /([^\p{M}\p{P}\p{Z}]+)/msg )
        #...but Perl 5.6 mangles that. Splitting does the same thing
        #but works under 5.6.
        #there is still a weird 5.8.0 bug that won't match a utf8 string
        #at the very end of a string. not a prob in 5.6.1 or 5.8.6.
        #looks like a utf8 regex bug in 5.8.0.
        my $find = join( q{|},
            map    { "\Q$_\E" }
              grep { length $_ }
              ( split( /[\p{M}\p{P}\p{Z}]+/, $arg{namesearch} ) ) );
        my $find_reg = qr/$find/msi;
        my $col      = $self->lib_namesearch_col;
        if ( ref $col eq 'ARRAY' && $args{any} ) {

            #can't do two OR searches at once; do namesearch, then winnow
            my %nterms = map { $_ => $find_reg } @{$col};
            $search =
              $search->select( { site => $sid, %nterms }, { any => $col } )
              or return;
        }
        elsif ( ref $col eq 'ARRAY' ) {

            #no pre-existing OR search, just add to existing terms
            foreach my $c ( @{$col} ) {
                $terms{$c} = $find_reg;
            }
            $args{any} = $col;
        }
        elsif ($col) {
            $terms{$col} = $find_reg;
        }
    }

    return $search->select( \%terms, \%args );
}

sub build_dependent_pages {
    my ( $self, $rparam ) = @_;
    $rparam ||= {};
    my ( $sid, $id_term );
    if ( ref $self ) {
        $sid = $rparam->{site} || $self->site;
        $sid = $sid->id if ref $sid;
        $id_term = $rparam->{id} || $self->id;
    }
    else {
        $sid = ref $rparam->{site} ? $rparam->{site}->id : $rparam->{site};
        $id_term = $rparam->{id};
    }
    croak 'build_dependent_pages requires site and object ids'
      if !$sid || !$id_term;

    ## IMPORTANT! If make changes here, probably need to mirror
    ## those changes in App::Web::LibEditor->_build_dependent_pages_dropin

    my $rcontent = $self->dependent_content($rparam) or return;
    if ( @{ $rcontent->{section} } ) {
        my $site = $rparam->{site};
        $site = BigMed::Site->fetch( { id => $sid } ) if !ref $site;

        if ( @{ $rcontent->{index_id} } ) {
            require BigMed::Search::Scheduler;
            import BigMed::Search::Scheduler;
            schedule_index( $site, $rcontent->{index_id} )
              or return;
        }

        require BigMed::Builder;
        my $builder = BigMed::Builder->new( site => $site ) or return;
        $builder->build(
            pages          => $rcontent->{page_id},
            sections       => $rcontent->{section},
            defer_overflow => 1,
          )
          or return;
    }
    return 1;
}

sub dependent_content {
    my ( $self, $rparam ) = @_;
    $rparam ||= {};
    my ( $sid, $id_term );
    if ( ref $self ) {
        $sid = $rparam->{site} || $self->site;
        $sid = $sid->id if ref $sid;
        $id_term = $rparam->{id} || $self->id;
    }
    else {
        $sid = ref $rparam->{site} ? $rparam->{site}->id : $rparam->{site};
        $id_term = $rparam->{id};
    }
    croak 'dependent_content requires site and object ids'
      if !$sid || !$id_term;

    my $pointers = BigMed::Pointer->select(
        {   target_table => $self->data_source,
            target_id    => $id_term,
            site         => $sid,
        }
      )
      or return;
    return { section => [], page_id => [], index_id => [] }
      if !$pointers->count;

    my $site = $rparam->{site};
    $site = BigMed::Site->fetch( { id => $sid } ) if !ref $site;

    BigMed::Plugin->load_content_types;
    my ( $point,   $rkids );
    my ( %page_id, %index_id );
    my %section;
    while ( $point = $pointers->next ) {
        my $source = $point->source_table;
        my $class  = $point->source_class($source);
        next if !$class->isa('BigMed::Content');

        my $obj = $class->fetch( { site => $sid, id => $point->source_id } );
        return if !defined $obj;
        next if !$obj || $obj->pub_status ne 'published';

        @section{ $obj->sections } = ();
        next if !$obj->isa('BigMed::Content::Page');
        $rkids ||= { map { $_ => 1 }
              ( $site->homepage_id, $site->all_active_descendants_ids() ) };
        $index_id{ $obj->id } = 1
          if $obj->active_page_url( $site, { rkids => $rkids } );
        $page_id{ $obj->id } = 1 if $obj->slug;
    }
    return {
        section  => [keys %section],
        page_id  => [keys %page_id],
        index_id => [keys %index_id],
    };
}

###########################################################
# BROWSE LIBRARY ITEMS
# These methods are commonly overridden by the subclass
###########################################################

#should always override in subclass
sub lib_preview_image {
    my ( $obj, $site ) = @_;

    #we don't actually need a site object here, but many subclasses
    #will -- so require it to enforce good hygiene.
    croak 'lib_preview_image requires site object'
      if ref $site ne 'BigMed::Site';
    return BigMed->bigmed->env('BMADMINURL') . '/img/bmcp_lib_unknown.png';
}

#should always override in subclass
sub lib_preview_url {
    my ( $obj, $site ) = @_;

    #we don't actually need a site object here, but many subclasses
    #will -- so require it to enforce good hygiene.
    croak 'lib_preview_image requires site object'
      if ref $site ne 'BigMed::Site';
    return q{};
}

#this will work fine for any subclass with a name or title column
sub lib_preview_title {
    my ( $obj, $site ) = @_;

    #we don't actually need a site object here, but some subclasses
    #might -- so require it to enforce good hygiene.
    croak 'lib_preview_title requires site object'
      if ref $site ne 'BigMed::Site';
    my $title =
      $obj->can('name') ? $obj->name : $obj->can('title') ? $obj->title : q{};
    $title = q{} if !defined $title;
    return $title;
}

sub lib_namesearch_col {
    return $_[0]->can('name') ? 'name'
      : $_[0]->can('title')   ? 'title'
      : undef;
}

sub _lib_sort_info {
    my $self  = shift;
    my $order = shift
      or croak '_get_sort_info requires alpha/recent argument';

    my ( $sort_column, $sort_ord );
    if ( $order eq 'alpha' ) {
        $sort_column = $self->alpha_sort_col;
        if ( !$sort_column ) {
            $sort_column =
                $self->can('title') ? ['title']
              : $self->can('name')  ? ['name']
              : [];
        }
        if ( $sort_column eq 'ARRAY' ) {
            $sort_ord = [( ('ascend') x scalar @{$sort_column} ), 'descend'];
        }
        elsif ( ref $sort_column ne 'ARRAY' ) {
            $sort_column = [$sort_column];
            $sort_ord = ['ascend', 'descend'];
        }
        push @{$sort_column}, 'mod_time';
    }
    else {    # 'recent' does mod_time
        $sort_column = 'mod_time';
        $sort_ord    = 'descend';
    }
    return ( $sort_column, $sort_ord );
}

1;

__END__

=head1 NAME

BigMed::Library - Base class for Big Medium mini-content library
items, which can be shared among users and content elements.

=head1 SYNOPSIS

    package Person;
    use base qw(BigMed::Library);
    
    #register Person as a library type
    my @data_schema = (
        {   name  => 'last_name',
            type  => 'simple_text',
            index => 1,
        },
        {   name  => 'first_name',
            type  => 'simple_text',
            index => 1,
        },
        {   name => 'email',
            type => 'email',
        },
    );
    BigMed::Person->register_minicontent(
        elements       => \@data_schema,
        alpha_sort_col => ['last_name', 'first_name'],
        editor_abbr    => [
            { column => 'first_name', required => 1, },
            { column => 'last_name',  required => 1, },
            { column => 'email' },
        ],
        preview => { html => \&preview_html },
    );
    
    #collect people that joe user is allowed to edit
    my $select = BigMed::Person->collect_lib(
        site => $site_obj,
        user => $joe_user_obj,
        editable => 1,
    );

=head1 DESCRIPTION

BigMed::Library is a subclass of BigMed::MiniContent and, like that class,
it is intended to be subclassed by classes whose objects are used to
associate additional info and content to Big Medium's primary content
objects.

BigMed::Library is not itself intended to be used to store media information
but is an abstract class to be subclassed.

Library subclasses consist of objects intended to be reused and shared
among several content objects. Examples include media objects (images,
documents, audio/video) and person objects, which can be assigned to multiple
pages.

=head1 DATA SCHEMA

BigMed::Library bestows the following data columns on its subclasses. They
can be accessed and set using the standard data access methods described
in the BigMed::Data documentation. See the L<"Searching and Sorting">
section below for details on the data columns available to search and sort
BigMed::Site objects.

All of these fields can be used for sorting and searching via the C<fetch>
and C<select> methods of BigMed::Data subclasses.

=over 4

=item * id

The content object's unique ID

=item * site

ID of the site (BigMed::Site) to which the content belongs

=item * owner

ID of the user (BigMed::User) who owns edit rights to the object

=item * shared

Boolean value indicating whether other users are allowed to add the
library item to pages (webmasters and publishers are always allowed
to do this, regardless of the value of shared).

=item * in_lib

Boolean value indicating whether the item should be displayed in the
library for reuse.

=item * mod_time

Timestamp for when the last time the object was saved.

=item * create_time

Timestamp for when the object was first created.

=back

=head1 Registration of BigMed::Library Subclasses

Like all BigMed::MiniContent classses, BigMed::Library requires its
subclasses to register via the register_minicontent method. It works
the same way as described in BigMed:MiniContent, except that it also
accepts these additional parameters:

=over 4

=item * C<< lib_superclass => 1 >>

If true, indicates that the class is intended to be a superclass of
still other library classes and should not itself be included as a
library type to be returned by the C<library_classes> and C<library_types>
methods. BigMed::Media is an example that uses lib_superclass.

=item * C<< batch_upload => $column_name >>

Option value indicating that the subclass can batch-upload files
to the server via the library editor. The value should be the name of
the column to use as the upload file (filename for BigMed::Media::Document,
formats for BigMed::Media::Images, etc).
=back

=head1 Content Methods

=head2 C<< ClassName->collect_lib( %param ) >>

This is a convenient wrapper to the BigMed::Data select method that
helps handle user permissions issues. It returns a BigMed::Data selection
for the requesting class, based on the submitted parameters:

=over 4

* site => $site_obj or $site_id

B<Required.> The site to search.

* user => $user

An optional BigMed::User object. If provided, the returned selection
object includes only items that the user is allowed to see/edit.
If not provided (or if user is a webmaster/admin), no permission filter
is applied.

* namesearch => 'keyword1 keyword2'

The string is used to find case-insensitive regex matches on the class's
name field (specified by the C<lib_namesearch_column> method).
The field is split by whitespace so that C<'keyword1 keyword2'> is
searched as C<qr/keyword1 keyword2/i>.

* editable => 1

If true, should return only objects that the user is allowed to edit.
(users with less than webmaster privileges must be the owner to edit
objects, but they can still see shared objects that they don't own).

* in_lib => 1

If true, should return only objects that have the in_lib flag set,
indicating that they may be displayed as part of the library.

* limit => 25

The maximum number of items to return.

* offset => 25

The offset number to display (e.g. 25 would return results starting
with the 26th item in the full set).

* order => alpha|recent

Indicates the sort order of items to return. Defaults to alpha, which
returns items in alphabetical order, based on the class' alpha_sort_col
column.

=back

=head2 $self->build_dependent_pages( \%param )

Triggers a page build for all pages that contain the library item(s).
Returns true on success or false on error (putting a message in
the BigMed::Error queue). Also schedules those pages for re-indexing
to update their info for the search engine

The method may be called as either a class or object method.
The method accepts a single argument, a hash reference. This argument
is optional when called as an object method but required as a
class method. The hash may contain these key/value pairs:

=over 4

=item * site => $site_id_or_obj

The site where you want to rebuild the pages. Using a site object
instead of id provides a modest performance improvement. If not
provided when called as an object method, the site id is taken
from the object itself.

=item * id => $id_or_arrayref

The id or array ref of ids of the library items whose dependent
pages should be rebuilt. If not provided when called as an object
method, the object's id is used.

=back

=head2 $self->dependent_content( \%param )

Retrieves info about the pages and sections that contain the
library item(s); used internally by build_dependent_pages to
know which pages to build.

The method may be called as either a class or object method.
The method accepts a single argument, a hash reference. This argument
is optional when called as an object method but required as a
class method. The hash may contain these key/value pairs:

=over 4

=item * site => $site_id_or_obj

The site where you want to rebuild the pages. Using a site object
instead of id provides a modest performance improvement. If not
provided when called as an object method, the site id is taken
from the object itself.

=item * id => $id_or_arrayref

The id or array ref of ids of the library items whose dependent
pages should be rebuilt. If not provided when called as an object
method, the object's id is used.

=back

The method returns a hash reference with three key/value pairs:

=over 4

=item * section => \@section_ids

An array reference of unsorted ids for sections containing dependent content.

=item * page_id => \@page_ids

An array reference of unsorted ids of pages containing dependent content.

=item * index_id => \@page_ids

An array reference of unsorted ids of pages that have active URLs and should
be updated in the search index if changes are made.

=back

=head1 Library Metadata Methods

Big Medium's library browser displays titles, thumbnail images and preview
URLs to the user. These are accessed via three methods that may be
overridden by the subclass (and probably should be):

=over 4

=item * C<< $self->lib_namesearch_column() >>

Returns the column(s) to use to search when the C<namesearch> argument
is used in the C<collect_lib> method. The method returns 'name' if the
method exists, and falls back to 'title,' otherwise undef.

Subclasses can override the method and optionally return an array reference
to indicate more than one column should be searched. The BigMed::Person
subclass does this, for example:

    sub lib_namesearch_column {
        return ['first_name', 'last_name'];
    }

=item * C<< $obj->lib_preview_title($site) >>

Returns the title of the object to display for the object. By default,
uses the object's name method if it exists or, if not, the title method.

=item * C<< $obj->lib_preview_image($site) >>

Returns the URL of a thumbnail image to display for the object; this should
almost always be overridden by the subclass. The built-in method returns
the URL of a placeholder image.

=item * C<< $obj->lib_preview_url($site) >>

Returns the URL where the object may be previewed (document URL for a
document item, or an image for an image item). This should be overridden
by the subclass; the built-in method returns an empty string.

=item * C<< $class->can_batch_upload() >>

Returns true if the subclass is able to batch-upload files via the library
editor.

=item * C<< $class->batch_upload_column >>

The name of the column containing the file upload for batch uploads.

=item * C<< $class->batch_upload_title >>

Returns a title to use for items that are not given a title when a batch
upload occurs. The built-in method uses the value of the batch_upload_column
(or 'Untitled' if there is no value in the upload column). It may make
sense to override this built-in method in some cases (see e.g.
BigMed::Media::Image).

=back

=head1 Library Type Info

=over 4

=item * BigMed::Library->library_types

Returns an array of the label names of all BigMed::Library subclasses, in
alphabetical order.

=item * BigMed::Library->library_classes

Returns an array of the class names of all BigMed::Library subclasses.

=back

=head1 SEE ALSO

=over 4

=item * BigMed::Data

=item * BigMed::MiniContent

=item * BigMed::Content

=back

=head1 AUTHOR & COPYRIGHTS

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content
management system are governed by Global Moxie's software licenses
and may not be used outside of the terms and conditions outlined
there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut

