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

package BigMed::Theme;
use strict;
use warnings;
use utf8;
use Carp;
$Carp::Verbose = 1;

use BigMed;
use base qw(BigMed::Error);
use BigMed::DiskUtil
  qw(bm_file_path bm_untaint_filepath bm_load_file bm_delete_file bm_copy_dir bm_delete_dir bm_copy_file bm_write_file bm_confirm_dir bm_dir_permissions);
use BigMed::Template;
use BigMed::CSS;

my %META_KEY = (
    'NAME:'        => 'name',
    'DESCRIPTION:' => 'description',
    'AUTHOR:'      => 'author',
    'URL:'         => 'url',
    'EMAIL:'       => 'email',
);

sub new {
    my $class    = shift;
    my $bm       = BigMed->bigmed;
    my $themedir =
      bm_untaint_filepath( bm_file_path( $bm->env('BMADMINDIR'), 'themes' ) )
      or croak 'Bad file path for bmadmin: ' . $bm->env('BMADMINDIR');
    croak "Could not locate theme directory at $themedir" if !-e $themedir;
    my $self = bless {
        _themedir => $themedir,
        _themeurl => $bm->env('BMADMINURL') . '/themes',
    }, $class;
    return $self;
}

sub categories {
    my $self     = shift;
    my $themedir = $self->{_themedir};
    my ( $THEMES, $cat, @categories );
    opendir( $THEMES, $themedir )
      or $self->set_io_error( $THEMES, 'opendir', $themedir, $! )
      or $self->error_stop;
    while ( defined( $cat = readdir($THEMES) ) ) {
        push @categories, $cat
          if -d bm_file_path( $themedir, $cat )
          && substr( $cat, 0, 1 ) ne q{.};

    }
    closedir($THEMES);
    return ( sort @categories );
}

sub themes {
    my ( $self, $category ) = @_;
    return () if !$category;
    my $themedir = $self->{_themedir};
    my $catdir = bm_untaint_filepath( bm_file_path( $themedir, $category ) )
      or return ();
    return () if !-e $catdir;
    my @themes;
    my ( $CATEGORY, $theme );
    opendir( $CATEGORY, $catdir )
      or $self->set_io_error( $CATEGORY, 'opendir', $catdir, $! )
      or return ();

    while ( defined( $theme = readdir($CATEGORY) ) ) {
        next
          if substr( $theme, 0, 1 ) eq q{.}
          || !-d ( my $dir = bm_file_path( $catdir, $theme ) );
        my %info = $self->theme_metadata( $category, $theme, $dir );
        next if !%info;
        push( @themes, { %info, theme => $theme } );
    }
    closedir($CATEGORY);
    return ( sort { $a->{name} cmp $b->{name} } @themes );
}

sub apply_theme {
    my ( $self, $site, $category, $theme ) = @_;
    croak 'apply_theme requires a site object'
      if !$site || !$site->isa('BigMed::Site');

    #make sure the theme exists
    my $detail_dir = $self->detail_dir( $category, $theme )
      or return $self->set_error(
        head => 'THEME_Could not load theme',
        text => ['THEME_TEXT_Could not load theme', $theme, $category]
      );

    #delete the existing theme, and move new theme's assets into place
    $self->clear_site_theme($site) or return;

    #theme css
    my $tmpl_path = $self->site_template_path($site);
    my $theme_css = bm_file_path( $detail_dir, 'theme.css' );
    if ( -e $theme_css ) {
        my $site_css = $self->site_css_filepath($site) or return undef;
        bm_copy_file( $theme_css, $site_css, { data => 1, build_path => 1 } )
          or return;
    }

    #template files
    my $theme_templates = bm_file_path( $detail_dir, 'templates' );
    if ( -d $theme_templates ) {
        my $site_templates = bm_file_path( $tmpl_path, 'HTML' );    #html only
        bm_copy_dir( $theme_templates, $site_templates,
            { data => 1, build_path => 1 } )
          or return;
    }

    #web assets
    my $theme_assets = bm_file_path( $detail_dir, 'assets' );
    if ( -d $theme_assets ) {
        my $site_assets = $self->site_asset_path($site);
        bm_copy_dir( $theme_assets, $site_assets, { build_path => 1 } )
          or return;
    }
    return 1;
}

sub save_site_to_library {
    my ( $self, $site, $category, $theme, $rmeta ) = @_;
    croak 'apply_theme requires a site object'
      if !$site || !$site->isa('BigMed::Site');

    require BigMed::Template;
    my $tmpl = BigMed::Template->new($site);

    my $detail_dir =
      $self->detail_dir( $category, $theme, { build_path => 1 } )
      or return;

    #clear any existing theme templates and copy new ones, if any
    my $theme_tmpl_dir = bm_file_path( $detail_dir, 'templates' );
    bm_delete_dir($theme_tmpl_dir) or return;
    $tmpl->copy_format_to_dir( 'HTML', $theme_tmpl_dir ) or return;

    #clear any existing css and copy new one, if any,
    #appending custom css styles to the style sheet
    my $theme_css = bm_file_path( $detail_dir, 'theme.css' );
    if ( -e $theme_css ) {
        bm_delete_file($theme_css) or return;
    }
    my $css_text = $self->site_css_text($site);
    {
        require BigMed::CSS;
        my $custom_css = BigMed::CSS->generate_custom_styles($site);
        return if !defined $custom_css;
        $css_text .= "\n\n$custom_css";
    }
    $css_text =~ s/\A\s+//ms;
    $css_text =~ s/\s+\z//ms;
    if ($css_text) {
        bm_write_file( $theme_css, $css_text ) or return;
    }

    #write the description text
    my $meta_file = q{};
    my %lookup    = reverse %META_KEY;
    foreach my $key ( keys %lookup ) {
        next if !defined( my $value = $rmeta->{$key} );
        $meta_file .= "$lookup{$key}\n$value\n";
    }
    my $desc_path = bm_file_path( $detail_dir, 'description.txt' );
    bm_write_file( $desc_path, $meta_file ) or return;

    #make sure everything is readable
    bm_dir_permissions($detail_dir) or return;

    return 1;
}

sub update_asset_url {
    my ( $self, $site, $category, $theme, $url ) = @_;
    croak 'apply_theme requires a site object'
      if !$site || !$site->isa('BigMed::Site');
    my $detail_dir = $self->detail_dir( $category, $theme )
      or return 1;    #no such directory, just return quietly, nothing to do

    #collect files to update
    my @files;
    my $theme_path = bm_file_path( $detail_dir, 'theme.css' );
    my $tmpl_path  = bm_file_path( $detail_dir, 'templates' );
    push @files, $theme_path if -e $theme_path;
    if ( -d $tmpl_path ) {
        my $TDIR;
        opendir( $TDIR, $tmpl_path )
          or return BigMed::Error->set_io_error( undef, 'opendir', $tmpl_path,
            $! );
        while ( defined( my $file = readdir $TDIR ) ) {
            push @files, bm_file_path( $tmpl_path, $file )
              if $file =~ /\A_.*[.]txt\z/msg;
        }
        closedir($TDIR);
    }
    foreach my $file (@files) {
        my $template = join( "\n", bm_load_file($file) );
        if ($template) {
            $template = _replace_asset_url( $site, $template, $url );
            bm_write_file( $file, $template ) or return;
        }
    }
    return 1;

}

sub _replace_asset_url {
    my ( $site, $file, $url ) = @_;
    my ( $full_url, $slash_url, $relative_url );
    if ( $url =~ m{\Ahttps?://[^/]+(/.*)\z}msi ) {
        $slash_url = $1;
        $full_url  = $url;
    }
    elsif ( $url =~ m{\A(/.*)}ms ) {
        $slash_url = $full_url;
        my $domain;
        ( $domain = $site->html_url ) =~ s{\Ahttps?://[^/]+/?}{}msi;
        $full_url = $domain . $slash_url;
    }
    ( $relative_url = $slash_url ) =~ s{\A/}{}ms;

    #regular src
    $file =~ s{(src|background|value)   #attribute: $1
        \s*=\s*
        (["'])                    #url token: $2
        (?:                       #url match
            (?: \Q$full_url\E)  |
            (?: \Q$slash_url\E) |
            (?: (?:[.][.]/)* \Q$relative_url\E )
        ) #end url match
        /([^/"']+)                #filename: $3
        \2
    }{$1="<%pagedirurl%>/bm.theme/$3"}msixg;

    #css style
    $file =~ s{url\s*\(\s*
        (                         #url match: $1
            (?: \Q$full_url\E)  |
            (?: \Q$slash_url\E) |
            (?: (?:[.][.]/)* \Q$relative_url\E )
        ) #end url match
        /([^/]+?)                #filename: $2
        \)
    }{url(bm.theme/$2)}msixg;
    return $file;
}

sub import_theme_assets {
    my ( $self, $category, $detail, $source ) = @_;
    croak "Not a directory: $source" if !-d $source;
    my $theme_dir =
      $self->detail_dir( $category, $detail, { build_path => 1 } )
      or return;
    my $asset_dir = bm_file_path( $theme_dir, 'assets' );
    bm_confirm_dir( $asset_dir, { build_path => 1 } ) or return;

    my $DIR;
    opendir( $DIR, $source )
      or return BigMed::Error->set_io_error( undef, 'opendir', $source, $! );
    while ( defined( my $file = readdir $DIR ) ) {
        next if substr( $file, 0, 1 ) eq q{.};
        my $path = bm_file_path( $source, $file );
        next if !-f $path;    #no directories, devices, etc.
        if ( !bm_copy_file( $path, bm_file_path( $asset_dir, $file ) ) ) {
            closedir($DIR);
            return;
        }
    }
    closedir($DIR);
    return 1;
}

sub clear_site_theme {
    my ( $self, $site ) = @_;
    croak 'clear_theme requires a site object'
      if !$site || !$site->isa('BigMed::Site');

    #delete theme assets
    my $asset_dir = $self->site_asset_path($site);
    bm_delete_dir($asset_dir) or return;

    #delete custom template directory (which includes theme file)
    my $tmpl = BigMed::Template->new($site);
    $tmpl->delete_all_custom_templates() or return;

    #delete custom CSS entries
    my $all_css = BigMed::CSS->select( { site => $site->id } ) or return;
    $all_css->trash_all or return;

    return 1;
}

sub save_site_css {
    my ( $self, $site, $css ) = @_;
    croak 'save_site_css requires a site object'
      if !$site || !$site->isa('BigMed::Site');
    $css = q{} if !defined $css;
    my $css_path = $self->site_css_filepath($site) or return;
    return bm_write_file( $css_path, $css );
}

sub site_template_path {
    my ( $self, $site ) = @_;
    croak 'site_template_path requires a site object'
      if !$site || !$site->isa('BigMed::Site');

    return bm_file_path(
        BigMed->bigmed->env('MOXIEDATA'), 'templates_custom',
        'site_templates',                 'site' . $site->id
    );
}

sub site_asset_path {
    my ( $self, $site ) = @_;
    croak 'site_asset_dir requires a site object'
      if !$site || !$site->isa('BigMed::Site');
    return bm_file_path( $site->html_dir, 'bm.theme' );
}

sub site_css_text {
    my ( $self, $site ) = @_;
    croak 'site_css_text requires a site object'
      if !$site || !$site->isa('BigMed::Site');
    my $css_file = $self->site_css_filepath($site) or return;
    return join( "\n", bm_load_file($css_file) );
}

sub site_css_filepath {
    my ( $self, $site ) = @_;
    croak 'site_css_filepath requires a site object'
      if !$site || !$site->isa('BigMed::Site');
    my $dir = $self->site_template_path($site);
    bm_confirm_dir( $dir, { build_path => 1, data => 1 } ) or return;
    return bm_file_path( $dir, 'theme.css' );
}

sub category_dir {
    my ( $self, $category, $rparam ) = @_;
    return q{} if !$category;
    $rparam ||= {};
    my $themedir = $self->{_themedir};
    my $catdir = bm_untaint_filepath( bm_file_path( $themedir, $category ) )
      or return;
    if ( $rparam->{build_path} ) {
        bm_confirm_dir( $catdir, { build_path => 1 } ) or return;
    }
    return -e $catdir ? $catdir : q{};
}

sub detail_dir {
    my ( $self, $category, $theme, $rparam ) = @_;
    return q{} if !$category || !$theme;
    $rparam ||= {};

    defined( my $cat_dir = $self->category_dir( $category, $rparam ) )
      or return;
    return $cat_dir if !$cat_dir;

    my $detail_dir = bm_untaint_filepath( bm_file_path( $cat_dir, $theme ) )
      or return;
    if ( $rparam->{build_path} ) {
        bm_confirm_dir( $detail_dir, { build_path => 1 } ) or return;
    }
    return q{} if !$detail_dir || !-e $detail_dir;
    return $detail_dir;
}

sub apply_site_theme {
    my ( $self, $orig, $target, $rmap ) = @_;
    $rmap ||= {};
    croak 'apply_site_theme requires original and target site objects'
      if !ref $orig
      || !$orig->isa('BigMed::Site')
      || !$target
      || !$target->isa('BigMed::Site');

    #clear target and copy template files
    $self->clear_site_theme($target) or return;
    my $tmpl = BigMed::Template->new($orig);
    $tmpl->copy_all_to_site( $target, $rmap ) or return;

    #copy assets
    my $orig_assets = $self->site_asset_path($orig);
    if ( -d $orig_assets ) {
        bm_copy_dir(
            $orig_assets,
            $self->site_asset_path($target),
            { build_path => 1 }
          )
          or return;
    }

    #copy theme css
    my $theme_css = $self->site_css_text($orig);
    return if !defined $theme_css;
    $self->save_site_css( $target, $theme_css ) or return;

    #copy css styles
    my $all_css = BigMed::CSS->select( { site => $orig->id } ) or return;
    my $style;
    while ( $style = $all_css->next ) {
        $style->copy( { target_site => $target->id } )->save or return;
    }
    return if !defined $style;

    #build the sheet
    BigMed::CSS->build_sheet($target) or return;

    return 1;
}

sub theme_metadata {
    my ( $self, $cat_name, $theme_name, $detail_dir ) = @_;
    $detail_dir ||= $self->detail_dir( $cat_name, $theme_name );
    return () if !$detail_dir || !-e $detail_dir;

    my $desc = bm_file_path( $detail_dir, 'description.txt' );
    my @file = bm_load_file($desc);
    return () if !@file;

    my %info;
    my $current;
    foreach my $line (@file) {
        $current = $META_KEY{$line}, next if $META_KEY{$line};
        next if !$current;
        push @{ $info{$current} }, $line;
    }
    foreach my $key ( values %META_KEY ) {
        next if !$info{$key};
        $info{$key} = join( "\n", @{ $info{$key} } );
    }
    $info{name} ||= $theme_name;

    #scrub the url and email
    foreach my $key qw(url email) {
        my $url = $info{$key};
        next if !$url;
        $url =~ s/"/&quot;/msg;
        $url =~ s/>/&gt;/msg;
        $url =~ s/</&lt;/msg;
        $url =~ s/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/&amp;/msg;
        $url =~ s/j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*://msgi;
        $info{$key} = $url;
    }

    foreach my $ext qw(jpg gif png jpeg) {
        $info{thumbnail} =
          $self->{_themeurl} . "/$cat_name/$theme_name/thumbnail.$ext"
          if -e bm_file_path( $detail_dir, "thumbnail.$ext" );
        $info{screenshot} =
          $self->{_themeurl} . "/$cat_name/$theme_name/screenshot.$ext"
          if -e bm_file_path( $detail_dir, "screenshot.$ext" );
    }

    return %info;
}

1;

__END__

