# 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: Document.pm 3233 2008-08-21 12:47:26Z josh $

package BigMed::Media::Document;
use strict;
use warnings;
use utf8;
use Carp;
use base qw(BigMed::Media);
use BigMed::DiskUtil qw(bm_file_path bm_delete_file);
use BigMed;

###########################################################
# SET DOCUMENT DATA SCHEMA
###########################################################

my @data_schema = (
    {   name   => 'filename',
        type   => 'document',
        index  => 1,
        unique => 1,
    },
);

my @editor_fields = (
    {   column   => 'title',
        label    => 'DOCUMENT_Title',
        required => 1,
    },
    {   column => 'filename',
        label  => 'DOCUMENT_Document File',
    },
    {   column       => 'shared',
        option_label => 'LIBRARY_Allow others to use this item',
        default      => 1,
    },
    {   column       => 'in_lib',
        option_label => 'LIBRARY_Display in library for reuse',
        default      => 1,
    },
);

BigMed::Media::Document->register_minicontent(
    elements      => \@data_schema,
    editor_fields => \@editor_fields,
    can_link      => 1,
    can_embed     => 1,
    preview       => { html => \&preview_html },
    post_parse    => \&process_upload,
    batch_upload  => 'filename',
);

###########################################################
# TRASH CALLBACKS
###########################################################

BigMed::Media::Document->add_callback( 'before_trash', \&_trash_doc_file );
BigMed::Media::Document->add_callback( 'before_trash_all',
    \&_trash_all_doc_files );

sub _trash_doc_file {
    my $self = shift;
    my $file = $self->filename or return 1;
    my $sid  = $self->site or return 1;

    require BigMed::Site;
    my $site = BigMed::Site->fetch( { id => $sid } );
    return ( defined $site ) if !$site;
    my $path = bm_file_path( $site->doc_path, $file );
    return bm_delete_file($path);
}

sub _trash_all_doc_files {
    my $select = shift;
    my %dir;

    my $obj;
    $select->set_index(0);
    while ( $obj = $select->next ) {
        my $file = $obj->filename or return 1;
        my $sid  = $obj->site     or next;
        my $doc_dir = $dir{$sid};
        if ( !$doc_dir ) {
            require BigMed::Site;
            my $site = BigMed::Site->fetch( { id => $sid } );
            return if !defined $site;    #error
            next   if !$site;            #not found
            $doc_dir = $dir{$sid} = $site->doc_path;
        }
        bm_delete_file( bm_file_path( $doc_dir, $file ) ) or return;
    }
    return if !defined $obj;
    return 1;
}

###########################################################
# COPY CALLBACK
###########################################################

BigMed::Media::Document->add_callback( 'after_copy', \&_after_doc_copy );

sub _after_doc_copy {
    my ( $orig, $clone, $rparam ) = @_;
    $clone->is_unique('title');    #generate unique title
    my $orig_filename = $orig->filename or return 1;    #nothing to copy

    #gather original and new sites
    my $nsite = $rparam->{target_site} || $clone->site;
    if ( !ref $nsite ) {
        my $nid = $nsite;
        defined( $nsite = BigMed::Site->fetch($nid) ) or return;
        return $orig->set_error(
            head => 'BM_No such site',
            text => ['BM_TEXT_No such site', $nid],
          )
          if !$nsite;
    }
    my $osite = $rparam->{source_site} || $orig->site;
    if ( !ref $osite ) {
        my $oid = $osite;
        $osite =
          $orig->site == $clone->site ? $nsite : BigMed::Site->fetch($oid);
        return if !defined $osite;
        return $orig->set_error(
            head => 'BM_No such site',
            text => ['BM_TEXT_No such site', $oid],
          )
          if !$osite;

    }

    #copy file from old site to new
    $clone->set_filename(undef);
    my $ofile = bm_file_path( $osite->doc_path, $orig_filename );
    return 1 if !-e $ofile;    #doesn't exist?!?
    my $filename = $clone->import_media_file(
        dir      => $nsite->doc_path,
        ext_file => $ofile,
        filename => $orig_filename,

        #no replace, because we want a new, unique file
    ) or return;
    $clone->set_filename($filename);
    return 1;
}

sub preview_html {
    my ( $app, $obj, $roptions ) = @_;

    my ( $filename, $file_url, $doc_class, $fs );
    if ( $filename = $obj->filename ) {    # *should* always be true...
        $fs = $obj->filesize( $app->current_site );
        $fs &&= "($fs)";
        my $url_dir = $app->current_site->doc_url;
        $file_url  = "$url_dir/$filename";
        $doc_class =
          ( $filename =~ /[.]([a-zA-Z0-9]+)$/ms )
          ? 'docIconSm_' . lc $1
          : q{};
    }
    my $html = $app->html_template(
        'wi_document_preview.tmpl',
        TITLE      => $obj->title,
        CLASS_NAME => $doc_class,
        FILE_URL   => $file_url,
        FILENAME   => $filename,
        FILESIZE   => $fs,
    );
    return ( PREVIEW_HTML => $html );
}

sub filesize {
    my $obj  = shift;
    my $site = shift;
    croak 'usage: $document->filesize($site_obj);'
      if !$site || !$site->isa('BigMed::Site');
    my $path = bm_file_path( $site->doc_path, $obj->filename );
    my $fs = -e $path ? -s $path : 0;
    $fs =
        $fs >= 1_048_576 ? sprintf( '%.1f', $fs / 1_048_576 ) . ' MB'
      : $fs ? ( int( $fs / 1024 ) + 1 ) . ' KB'
      : q{};
    return $fs;
}

sub process_upload {
    my ( $self, $rfields ) = @_;
    my $rdoc = $rfields->{filename} or return;    #there was probably an error
    my ( $filename, $tmpfile, $handle ) = @{$rdoc};
    my $old = $self->filename || q{};
    if ( !$filename || $rfields->{_ERROR} ) { #don't do a thing on parse error
        $rfields->{filename} = $old;
        $rfields->{_ERROR}->{filename} = 'PARSE_ERR_Please provide a value.'
          if !$rfields->{filename};
        return;
    }

    #load site
    my $app  = BigMed->bigmed->app;
    my $site =
        $app->can('current_site')
      ? $app->current_site
      : BigMed::Site->fetch( $self->site );
    return if !defined $site;

    $filename = $self->import_media_file(
        dir      => $site->doc_path,
        ext_file => $tmpfile,
        filename => $filename,
        replace  => $old,
    );
    close($handle);
    return _set_field_error($rfields) if !$filename;

    $rfields->{filename} = $filename;
    $self->mark_modified;
    return;
}

sub _set_field_error {
    my $rfields = shift;
    my $msg = shift || 'PARSE_Could not save file.';
    $rfields->{filename} = undef;
    $rfields->{_ERROR}->{filename} = $msg;
    $rfields->{_ERROR}->{_FIRST_ERR} ||= 'filename';
    return;
}

my %mime_img = (
    'generic' => '/img/docIconLg_generic.gif',

    'gif'  => '/img/docIconLg_image.gif',
    'png'  => '/img/docIconLg_image.gif',
    'jpg'  => '/img/docIconLg_image.gif',
    'jpeg' => '/img/docIconLg_image.gif',
    'ico'  => '/img/docIconLg_image.gif',
    'bmp'  => '/img/docIconLg_image.gif',
    'jfif' => '/img/docIconLg_image.gif',
    'tif'  => '/img/docIconLg_image.gif',
    'tiff' => '/img/docIconLg_image.gif',
    'psd'  => '/img/docIconLg_image.gif',
    'eps'  => '/img/docIconLg_image.gif',

    'pdf' => '/img/docIconLg_pdf.gif',

    'doc' => '/img/docIconLg_doc.gif',

    'xls' => '/img/docIconLg_excel.gif',
    'xlw' => '/img/docIconLg_excel.gif',

    'rtf' => '/img/docIconLg_rtf.gif',

    'txt' => '/img/docIconLg_txt.gif',

    'xml' => '/img/docIconLg_xml.gif',

    'zip'  => '/img/docIconLg_zip.gif',
    'gz'   => '/img/docIconLg_zip.gif',
    'gzip' => '/img/docIconLg_zip.gif',
    'taz'  => '/img/docIconLg_zip.gif',
    'tgz'  => '/img/docIconLg_zip.gif',
    'hqx'  => '/img/docIconLg_zip.gif',
    'bin'  => '/img/docIconLg_zip.gif',
    'tar'  => '/img/docIconLg_zip.gif',

    'sit' => '/img/docIconLg_sit.gif',

    'dmg' => '/img/docIconLg_dmg.gif',

    'ppt' => '/img/docIconLg_powerpoint.gif',
    'pps' => '/img/docIconLg_powerpoint.gif',

    'mpp' => '/img/docIconLg_project.gif',

    'sxw' => '/img/docIconLg_oo-write.gif',
    'stw' => '/img/docIconLg_oo-write.gif',
    'sxg' => '/img/docIconLg_oo-write.gif',
    'sdw' => '/img/docIconLg_oo-write.gif',
    'sgl' => '/img/docIconLg_oo-write.gif',

    'sxc'  => '/img/docIconLg_oo-calc.gif',
    'stcm' => '/img/docIconLg_oo-calc.gif',
    'sdc'  => '/img/docIconLg_oo-calc.gif',

    'sxi' => '/img/docIconLg_oo-impress.gif',
    'sti' => '/img/docIconLg_oo-impress.gif',
    'sdd' => '/img/docIconLg_oo-impress.gif',
    'sdp' => '/img/docIconLg_oo-impress.gif',

    'sxd' => '/img/docIconLg_oo-draw.gif',
    'std' => '/img/docIconLg_oo-draw.gif',
    'sda' => '/img/docIconLg_oo-draw.gif',

    'mp3'  => '/img/docIconLg_audio.gif',
    'mpu'  => '/img/docIconLg_audio.gif',
    'm4a'  => '/img/docIconLg_audio.gif',
    'mid'  => '/img/docIconLg_audio.gif',
    'midi' => '/img/docIconLg_audio.gif',
    'rmi'  => '/img/docIconLg_audio.gif',
    'm4p'  => '/img/docIconLg_audio.gif',
    'aac'  => '/img/docIconLg_audio.gif',
    'aif'  => '/img/docIconLg_audio.gif',
    'aiff' => '/img/docIconLg_audio.gif',
    'aifc' => '/img/docIconLg_audio.gif',
    'wav'  => '/img/docIconLg_audio.gif',
    'wma'  => '/img/docIconLg_audio.gif',
    'ram'  => '/img/docIconLg_audio.gif',
    'rm'  => '/img/docIconLg_audio.gif',

    'wmv'  => '/img/docIconLg_winvideo.gif',
    'avi'  => '/img/docIconLg_winvideo.gif',
    'mpeg' => '/img/docIconLg_winvideo.gif',
    'mpe'  => '/img/docIconLg_winvideo.gif',
    'mpg'  => '/img/docIconLg_winvideo.gif',
    'mp4'  => '/img/docIconLg_winvideo.gif',

    'swf' => '/img/docIconLg_flash.gif',
    'flv' => '/img/docIconLg_flash.gif',

    'mov'  => '/img/docIconLg_quicktime.gif',
    'qt'   => '/img/docIconLg_quicktime.gif',
    'moov' => '/img/docIconLg_quicktime.gif',
    'qtm'  => '/img/docIconLg_quicktime.gif',
);

sub lib_preview_image {
    my ( $obj, $site ) = @_;
    my ( $filename, $suffix );
    if ( $filename = $obj->filename ) {    # *should* always be true...
        $suffix = ( $filename =~ /[.]([a-zA-Z0-9]+)\z/ms ) ? lc $1 : q{};
    }
    else {
        $suffix = q{};
    }
    return BigMed->bigmed->env('BMADMINURL')
      . ( $mime_img{$suffix} || $mime_img{'generic'} );
}

sub lib_preview_url {
    my ( $obj, $site ) = @_;
    croak 'lib_preview_img requires site object'
      if ref $site ne 'BigMed::Site';

    defined( my $filename = $obj->filename ) or return q{};
    return $site->doc_url . "/$filename";
}

sub exists_at_site {
    my ( $obj, $site, $rparam ) = @_;
    croak 'site object required' if !ref $site || !$site->isa('BigMed::Site');
    my $sid = $site->id;
    $rparam ||= {};

    my $title = $obj->title or return 0;
    $title = qr/\A\Q$title\E/ms;
    my $filename = $obj->filename or return 0;
    $filename = qr/\A\Q$filename\E/ms;
    my $seek = $rparam->{selection} || ref $obj;
    my $select =
      $seek->select(
        { title => $title, site => $site->id, filename => $filename } )
      or return;
    return 0 if !$select->count;

    my $source = $rparam->{source_site}
      || ( $obj->site == $sid ? $site : BigMed::Site->fetch( $obj->site ) )
      or return 0;
    my $filesize =
      -s bm_file_path( $source->doc_path, $obj->filename );
    my $tdir = $site->doc_path;
    my $test;

    while ( $test = $select->next ) {
        my $file = bm_file_path( $tdir, $test->filename );
        next         if !-e $file;
        return $test if ( -s $file == $filesize );
    }
    return 0;
}

1;

__END__

=head1 NAME

BigMed::Media:Document - Big Medium document objects.

=head1 DESCRIPTION

BigMed::Media::Document objects represent download documents in the Big Medium
system. The class also handles publication and unpublication of document files,
and other document manipulation.

=head1 USAGE

BigMed::Media::Document is a subclass of BigMed::Data. In addition to the
methods  documented below, please see the BigMed::Data documentation for details
about topics including:

=over 4

=item Creating a new data object

=item Saving a data object

=item Finding and sorting saved data objects

=item Data access methods

=item Error handling

=back

=head1 METHODS

=head2 Data Access Methods

BigMed::Media::Document objects hold the following pieces of data. 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.

=over 4

=item * id

The numeric ID of the pointer object

=item * site

The numeric ID of the site to which the two objects belong

=item * owner

The numeric ID of the user who "owns" the media object.

=item * shared

Boolean value indicating whether the owner allows this media to be used by other
users with privileges at the same site. (Admins and webmasters are always
allowed to use the media objects).

= item * slug

The slug name for the media object. This may be used to generate a filename
or be used as an id string in widgets. The slug must be unique within the
BigMed::Media subclass.

= item * title

The name of the media object.

= item * version

The version of the media object.

= item * description

A text description of the media object (this might be used as a caption for
some types of media).

= item * filetype

The file extension for the document type.

=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

=head3 Searching and Sorting

You can look up and sort records by any combination of the following fields.
See the C<fetch> and C<select> documentation in BigMed::Data for more info.

=over 4

=item * id

=item * mod_time

=item * site

=item * owner

=item * shared

=item * slug

=item * title

=item * version

=back

=head3 Copying document objects

BigMed::Media::Document extends BigMed::Data's C<copy> method by copying
the document file in addition to the data object itself. The method
returns the cloned, unsaved document object on success, or undef on failure.

    $clone = $doc->copy( { source_site => $site1, target_site => $site2 } );

The optional hash reference can contain two parameters. Both are optional
but offer performance gains if provided:

=over 4

=item * C<<target_site => $site_id_or_obj>>

The site to which you would like to copy the document object. This can
be a site object or id. If not provided, the document is copied to the
same site as the original.

=item * C<<source_site => $site_id_or_obj>>

The site object or id for the original document object.

=back

=head3 Checking for duplicates

BigMed::Media::Document extends BigMed::Library's C<exists_at_site> stub
method. It returns the matching object if a similar object exists at the
argument site, otherwise a false value (undef on error).

    my $test;
    $test = $doc->exists_at_site($site_obj);
    $app->error_stop if !defined $test;
    if ($test) {
        print $test->id, ' has the same content as ', $doc->id, "\n";
    }

    #providing optional parameters in the second argument can provide
    #a performance boost
    my $select = BigMed::Media::Document->select( { site => $site->id } )
      or $app->error_stop;
    $test = $doc->exists_at_site(
        $site, { selection => $select, source_site => $site_obj } );
    $app->error_stop if !defined $test;
    if ($test) {
        print $test->id, ' has the same content as ', $doc->id, "\n";
    }

The check searches for documents with similar titles and filenames and
then compares the byte size of the filename. The first match returns
the object.

The optional second argument is a hash reference of two parameters:

=over 4

=item * selection => $data_selection

A cached selection of document objects at the site to search.

=item * source_site => $site_obj

The site object for the site to which the original document object belongs

=back

=head1 SEE ALSO

=over 4

=item * BigMed::Data

=item * BigMed::Media

=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

