# Movable Type (r) (C) 2001-2010 Six Apart, Ltd. All Rights Reserved.
# This code cannot be redistributed without permission from www.sixapart.com.
# For more information, consult your Movable Type license.
#
# $Id$

package MT::Asset;

use strict;
use MT::Tag; # Holds MT::Taggable
use base qw( MT::Object MT::Taggable MT::Scorable );

__PACKAGE__->install_properties({
    column_defs => {
        'id' => 'integer not null auto_increment',
        'blog_id' => 'integer not null',
        'label' => 'string(255)',
        'url' => 'string(255)',
        'description' => 'text',
        'file_path' => 'string(255)',
        'file_name' => 'string(255)',
        'file_ext' => 'string(20)',
        'mime_type' => 'string(255)',
        'parent' => 'integer',
    },
    indexes => {
        label => 1,
        file_ext => 1,
        parent => 1,
        created_by => 1,
        created_on => 1,
        blog_class_date => {
            columns => ['blog_id','class','created_on'],
        },
    },
    class_type => 'file',
    audit => 1,
    meta => 1,
    datasource => 'asset',
    primary_key => 'id',
});

require MT::Asset::Image;
require MT::Asset::Audio;
require MT::Asset::Video;

sub extensions {
    my $pkg = shift;
    return undef unless @_;

    my ($this_pkg) = caller;
    my ($ext)      = @_;
    return \@$ext unless MT->config('AssetFileTypes');

    my @custom_ext = map {qr/$_/i}
        split( /\s*,\s*/, MT->config('AssetFileTypes')->{$this_pkg} );
    my %seen;
    my ($new_ext) = grep { ++$seen{$_} < 2 }[ @$ext, @custom_ext ];

    return \@$new_ext;
}

# This property is a meta-property.
sub file_path {
    my $asset = shift;
    my $path = $asset->SUPER::file_path(@_);
    return $path if defined($path) && ($path !~ m!^\$!) && (-f $path);

    $path = $asset->cache_property(sub {
        my $path = $asset->SUPER::file_path();
        if ($path && ($path =~ m!^\%([ras])!)) {
            my $blog = $asset->blog;
            my $root = !$blog || $1 eq 's' ? MT->instance->static_file_path
                     : $1 eq 'r'           ? $blog->site_path
                     :                       $blog->archive_path
                     ;
            $root =~ s!(/|\\)$!!;
            $path =~ s!^\%[ras]!$root!;
        }
        $path;
    }, @_);
    return $path;
}

sub url {
    my $asset = shift;
    my $url = $asset->SUPER::url(@_);
    return $url if defined($url) && ($url !~ m!^\%!) && ($url =~ m!^https://!);

    $url = $asset->cache_property(sub {
        my $url = $asset->SUPER::url();
        if ($url =~ m!^\%([ras])!) {
            my $blog = $asset->blog;
            my $root = !$blog || $1 eq 's' ? MT->instance->static_path
                     : $1 eq 'r'           ? $blog->site_url
                     :                       $blog->archive_url
                     ;
            $root =~ s!/$!!;
            $url =~ s!^\%[ras]!$root!;
        }
        return $url;
    }, @_);
    return $url;
}

# Returns a localized name for the asset type. For MT::Asset, this is simply
# 'File'.
sub class_label {
    MT->translate('Asset');
}

sub class_label_plural {
    MT->translate("Assets");
}

# Removes the asset, associated tags and related file.
# TBD: Should we track and remove any generated thumbnail files here too?
sub remove {
    my $asset = shift;
    if (ref $asset) {
        my $blog = MT::Blog->load($asset->blog_id);
        require MT::FileMgr;
        my $fmgr = $blog ? $blog->file_mgr : MT::FileMgr->new('Local');
        my $file = $asset->file_path;
        unless ($fmgr->delete($file)) {
            my $app = MT->instance;
            $app->log(
                {
                    message => $app->translate(
                        "Could not remove asset file [_1] from filesystem: [_2]",
                        $file, $fmgr->errstr
                    ),
                    level    => MT::Log::ERROR(),
                    class    => 'asset',
                    category => 'delete',
                }
            );
        }
        $asset->remove_cached_files;

        # remove children.
        my $class = ref $asset;
        my $iter = __PACKAGE__->load_iter({ parent => $asset->id, class => '*' });
        while(my $a = $iter->()) {
            $a->remove;
        }

        # Remove MT::ObjectAsset records
        $class = MT->model('objectasset');
        $iter = $class->load_iter({ asset_id => $asset->id });
        while (my $o = $iter->()) {
            $o->remove;
        }
    }

    $asset->SUPER::remove(@_);
}

sub save {
    my $asset = shift;
    if (defined $asset->file_ext) {
        $asset->file_ext(lc($asset->file_ext));
    }

    unless ($asset->SUPER::save(@_)) {
        print STDERR "error during save: " . $asset->errstr . "\n";
        die $asset->errstr;
    }
}

sub remove_cached_files {
    my $asset = shift;
 
    # remove any asset cache files that exist for this asset
    my $blog = $asset->blog;
    if ($asset->id && $blog) {
        my $cache_dir = $asset->_make_cache_path;
        if ($cache_dir) {
            require MT::FileMgr;
            my $fmgr = $blog->file_mgr || MT::FileMgr->new('Local');
            if ($fmgr) {
                my $basename = $asset->file_name;
                my $ext = '.'.$asset->file_ext;
                $basename =~ s/$ext$//;
                my $cache_glob = File::Spec->catfile($cache_dir,
                    $basename . '-thumb-*' . $ext);
                my @files = glob($cache_glob);
                foreach my $file (@files) {
                    unless ($fmgr->delete($file)) {
                        my $app = MT->instance;
                        $app->log(
                            {
                                message => $app->translate(
                                    "Could not remove asset file [_1] from filesystem: [_2]",
                                    $file, $fmgr->errstr
                                ),
                                level    => MT::Log::ERROR(),
                                class    => 'asset',
                                category => 'delete',
                            }
                        );
                    }
                }
            }
        }
    }
    1;
}

sub blog {
    my $asset = shift;
    my $blog_id = $asset->blog_id or return undef;
    return $asset->{__blog} if $blog_id && $asset->{__blog} && ($asset->{__blog}->id == $blog_id);
    require MT::Blog;
    return $asset->{__blog} = MT::Blog->load($blog_id)
        or return $asset->error("Failed to load blog for file");
}

# Returns a true/false response based on whether the active package
# has extensions registered that match the requested filename.
sub can_handle {
    my ($pkg, $filename) = @_;
    # undef is returned from fileparse if the extension is not known.
    require File::Basename;
    my $ext = $pkg->extensions || [];
    return (File::Basename::fileparse($filename, @$ext))[2] ? 1 : 0;
}

# Given a filename, returns an appropriate MT::Asset class to associate
# with it. This lookup is based purely on file extension! If none can
# be found, it returns MT::Asset.
sub handler_for_file {
    my $pkg = shift;
    my ($filename) = @_;
    my $types;
    # special case to check for all registered classes, not just
    # those that are subclasses of this package.
    if ($pkg eq 'MT::Asset') {
        $types = [ keys %{ $pkg->properties->{__type_to_class} || {} } ];
    }
    $types ||= $pkg->type_list;
    if ($types) {
        foreach my $type (@$types) {
            my $this_pkg = $pkg->class_handler($type);
            if ($this_pkg->can_handle($filename)) {
                return $this_pkg;
            }
        }
    }
    __PACKAGE__;
}

sub type_list {
    my $pkg = shift;
    my $props = $pkg->properties;
    my $col = $props->{class_column};
    my $this_type = $props->{class_type};
    my @classes = values %{ $props->{__class_to_type} };
    @classes = grep { m/^\Q$this_type\E:/ } @classes;
    push @classes, $this_type;
    return \@classes;
}

sub metadata {
    my $asset = shift;
    return {
        MT->translate("Tags") => MT::Tag->join(',', $asset->tags),
        MT->translate("Description") => $asset->description,
        MT->translate("Name") => $asset->label,
        url => $asset->url,
        MT->translate("URL") => $asset->url,
        MT->translate("Location") => $asset->file_path,
        name => $asset->file_name,
        'class' => $asset->class,
        ext => $asset->file_ext,
        mime_type => $asset->mime_type,
        # duration => $asset->duration,
    };
}

sub has_thumbnail {
    0;
}

sub thumbnail_file {
    undef;
}

sub thumbnail_filename {
    undef;
}

sub stock_icon_url {
    undef;
}

sub thumbnail_url {
    my $asset = shift;
    my (%param) = @_;

    require File::Basename;
    if (my ($thumbnail_file, $w, $h) = $asset->thumbnail_file(@_)) {
        return $asset->stock_icon_url(@_) if !defined $thumbnail_file;
        my $file = File::Basename::basename($thumbnail_file);
        my $asset_file_path = $asset->SUPER::file_path();
        my $site_url;
        my $blog = $asset->blog;
        if (!$blog) {
            $site_url = $param{Pseudo} ? '%s' : MT->instance->static_path;
            $site_url .= '/' unless $site_url =~ m!/$!;
            $site_url .= 'support/';
        }
        elsif ( $asset_file_path =~ m/^%a/ ) {
            $site_url = $param{Pseudo} ? '%a' : $blog->archive_url;
        }
        else {
            $site_url = $param{Pseudo} ? '%r' : $blog->site_url;
        }

        if ($file && $site_url) {
            require MT::Util;
            my $path = $param{Path};
            if (!defined $path) {
                $path = MT::Util::caturl(MT->config('AssetCacheDir'), unpack('A4A2', $asset->created_on));
            } else {
                require File::Spec;
                my @path = File::Spec->splitdir($path);
                $path = '';
                for my $p (@path) {
                    $path = MT::Util::caturl($path, $p);
                }
            }
            $file =~ s/%([A-F0-9]{2})/chr(hex($1))/gei;
            $site_url = MT::Util::caturl($site_url, $path, $file);
            return ($site_url, $w, $h);
        }
    }

    # Use a stock icon
    return $asset->stock_icon_url(@_);
}

sub as_html {
    my $asset = shift;
    my ($param) = @_;
    my $fname = $asset->file_name;
    require MT::Util;
    my $text = sprintf '<a href="%s">%s</a>',
        MT::Util::encode_html($asset->url),
        MT::Util::encode_html($fname);
    my $app = MT->instance;
    return ( $app->isa('MT::App') && $app->param('edit_field') =~ /^customfield/ ) ? $asset->enclose($text) : $text;
}

sub enclose {
    my $asset = shift;
    my ($html) = @_;
    my $id = $asset->id;
    my $type = $asset->class;
    return qq{<form mt:asset-id="$id" class="mt-enclosure mt-enclosure-$type" style="display: inline;">$html</form>};
}

# Return a HTML snippet of form options for inserting this asset
# into a web page. Default behavior is no options.
sub insert_options {
    my $asset = shift;
    my ($param) = @_;
    return undef;
}

sub on_upload {
    my $asset = shift;
    my ($param) = @_;
    1;
}

sub edit_template_param {
    my $asset = shift;
    my ($cb, $app, $param, $tmpl) = @_;
    return;
}

sub set_values_from_query {
    my $asset = shift;
    my ($q) = @_;

    # Set the known columns from the form, if they're set. Subclasses can
    # opt out or decorate this behavior by overriding the method.
    my $names = $asset->column_names;
    my %values;
    for my $field (@$names) {
        $values{$field} = $q->param($field)
            if defined $q->param($field);
    }
    $asset->set_values(\%values);

    1;
}

# $pseudo parameter causes function to return '%r' as
# root instead of blog site path
sub _make_cache_path {
    my $asset = shift;
    my ($path, $pseudo) = @_;
    my $blog = $asset->blog;

    require File::Spec;
    my $year_stamp = '';
    my $month_stamp = '';
    if  (!defined $path) {
        $year_stamp = unpack 'A4', $asset->created_on;
        $month_stamp = unpack 'x4 A2', $asset->created_on;
        $path = MT->config('AssetCacheDir');
    } else {
        my $merge_path = '';
        my @split = File::Spec->splitdir($path);
        for my $p (@split) {
            $merge_path = File::Spec->catfile($merge_path, $p);
        }
        $path = $merge_path if $merge_path;
    }

    my $asset_file_path = $asset->SUPER::file_path();
    my $format;
    my $root_path;
    if ( !$blog ) {
        $format = '%s';
        $root_path = File::Spec->catdir(MT->instance->static_file_path, 'support');
    }
    elsif ( $asset_file_path =~ m/^%a/ ) {
        $format = '%a';
        $root_path = $blog->archive_path;
    }
    else {
        $format = '%r';
        $root_path = $blog->site_path;
    }

    my $real_cache_path = File::Spec->catdir($root_path, $path, $year_stamp,
        $month_stamp);
    if (!-d $real_cache_path) {
        require MT::FileMgr;
        my $fmgr = $blog ? $blog->file_mgr : MT::FileMgr->new('Local');
        $fmgr->mkpath($real_cache_path) or return undef;
    }

    my $asset_cache_path = File::Spec->catdir(($pseudo ? $format : $root_path),
        $path, $year_stamp, $month_stamp);
    $asset_cache_path;
}

1;

__END__

=head1 NAME

MT::Asset

=head1 SYNOPSIS

    use MT::Asset;

    # Example

=head1 DESCRIPTION

This module provides an object definition for a file that is placed under
MT's control for publishing.

=head1 METHODS

=head2 MT::Asset->new

Constructs a new asset object. The base class is the generic asset object,
which represents a generic file.

=head2 MT::Asset->handler_for_file($filename)

Returns a I<MT::Asset> package suitable for the filename given. This
determination is typically made based on the file's extension.

=head1 AUTHORS & COPYRIGHT

Please see the I<MT> manpage for author, copyright, and license information.

=cut
