# $Id: Cache.pm,v 1.11 2003/06/18 14:11:56 jwalt Exp $

package Apache::AxKit::Cache;
use strict;

use Apache;
use Apache::Constants qw(OK DECLINED SERVER_ERROR);
use Apache::AxKit::Exception;
use Digest::MD5 ();
use Compress::Zlib qw(gzopen);
use Fcntl qw(:flock O_RDWR O_WRONLY O_CREAT O_RDONLY);
BEGIN { $INC{'bytes.pm'}++ if $] < 5.006 }
use bytes;

# use vars qw/$COUNT/;

sub new {
    my $class = shift;
    my ($r, $xmlfile, @extras) = @_;
    
    my $gzip = 0;
    if ($xmlfile =~ /\.gzip/) {
        $gzip++;
#        @extras = grep(!/gzip/, @extras);
    }
    
    local $^W; # suppress "Use of uninitialized value" warnings
    my $key = Digest::MD5->new->add(
            join(':', 
                $r->hostname,
                $r->get_server_port,
                $xmlfile,
                @extras
            ))->hexdigest;
    
    AxKit::Debug(7, "Cache: key = $key");

    my $primary = substr($key,0,2,'');
    my $secondary = substr($key,0,2,'');
    
#    warn "New for: $xmlfile:" . join(':', @extras). "\n";
    
    my $cachedir = $AxKit::Cfg->CacheDir();
    
    my $no_cache;

    if ($AxKit::Cfg->NoCache()) {
        $no_cache = 1;
    }
    
    if (!$no_cache) {
       if (!-e $cachedir) {
           if (!mkdir($cachedir, 0777)) {
               AxKit::Debug(2, "Can't create cache directory '$cachedir': $!");
               $no_cache = 1;
           }
       }

       if (!-e "$cachedir/$primary") {
           if (!mkdir("$cachedir/$primary", 0777)) {
               AxKit::Debug(1, "Can't create cache directory '$cachedir/$primary': $!");
               $no_cache = 1;
           }
       }
       
       if (!-e "$cachedir/$primary/$secondary") {
           if (!mkdir("$cachedir/$primary/$secondary", 0777)) {
               AxKit::Debug(1, "Can't create cache directory '$cachedir/$primary/$secondary': $!");
               $no_cache = 1;
           }
       }
   }

    my $self = bless { 
        apache => $r,
        key => $key, 
        no_cache => $no_cache, 
        dir => $cachedir,
        file => "$cachedir/$primary/$secondary/$key",
        gzip => $gzip,
#        extras => \@extras,
        }, $class;

    if (my $alternate = $AxKit::Cfg->CacheModule()) {
        AxKit::reconsecrate($self, $alternate);
    }
    
#     AxKit::Debug(7, "Cache->new Count: ".++$COUNT);
    
    return $self;
}

sub _get_stats {
    my $self = shift;
    return if $self->{mtime};
    my @stats = stat($self->{file});
    my $exists = -e _ && -r _;
    if ($exists and $self->{gzip}) {
        $exists = -e $self->{file} . '.gz' and -r _;
    }
    $self->{file_exists} = $exists;
    $self->{mtime} = $stats[9];
}

# sub DESTROY {
#     AxKit::Debug(7, "Cache->DESTROY Count: ".--$COUNT);
# }

sub write {
    my $self = shift;
    return if $self->{no_cache};
    AxKit::Debug(7, "[Cache] writing cache file $self->{file}");
    my $fh = Apache->gensym();
    my $tmp_filename = $self->{file}."new$$";
    if (AxKit::sysopen($fh, $tmp_filename, O_WRONLY|O_CREAT, 'raw')) {
        # flock($fh, LOCK_EX);
        # seek($fh, 0, 0);
        # truncate($fh, 0);
        print $fh $_[0];
        close $fh;
        rename($tmp_filename, $self->{file}) 
                || throw Apache::AxKit::Exception::IO( -text => "Couldn't rename cachefile: $!");
    }
    else {
        throw Apache::AxKit::Exception::IO( -text => "Couldn't open cachefile for writing: $!");
    }
    
    if ($self->{gzip} && $AxKit::Cfg->GzipOutput) {
        AxKit::Debug(3, "Creating gzip output cache: $self->{file}.gz");
        if (my $gz = gzopen($self->{file}.'new.gz', "wb")) {
            $gz->gzwrite($_[0]);
            $gz->gzclose();
            rename($self->{file}.'new.gz', $self->{file}.'.gz')
                    || throw Apache::AxKit::Exception::IO( -text => "Couldn't rename gzipped cachefile: $!");
        }
        else {
            throw Apache::AxKit::Exception::IO( -text => "Couldn't open gzipped cachefile for writing: $!");
        }
    }
}

sub read {
    my $self = shift;
    return if $self->{no_cache};
    my $fh = Apache->gensym();
    if (AxKit::sysopen($fh, $self->{file}, O_RDONLY, 'raw')) {
        flock($fh, LOCK_SH);
        local $/;
        return <$fh>;
        # close($fh);
        # close unlocks automatically
    }
    return '';
}

sub get_fh {
    my $self = shift;
    return if $self->{no_cache};
    my $fh = Apache->gensym();
    if (AxKit::sysopen($fh, $self->{file}, O_RDONLY, 'raw')) {
        flock($fh, LOCK_SH);
        return $fh;
    }
    else {
        throw Apache::AxKit::Exception::IO( -text => "Cannot open cache file for reading: $!");
    }
}

sub set_type {
    my $self = shift;
    return if $self->{no_cache};
    
    my $fh = Apache->gensym();
    if (AxKit::sysopen($fh, $self->{file}.'newtype', O_RDWR|O_CREAT, 'raw')) {
        flock($fh, LOCK_EX);
        seek($fh, 0, 0);
        truncate($fh, 0);
        print $fh $_[0];
        close $fh;
        rename($self->{file}.'newtype', $self->{file}.'.type') 
                || throw Apache::AxKit::Exception::IO( -text => "Couldn't rename type cachefile: $!");
    }
    else {
        throw Apache::AxKit::Exception::IO( -text => "Couldn't open type cachefile for writing: $!");
    }
}

sub get_type {
    my $self = shift;
    return if $self->{no_cache};
    my $fh = Apache->gensym();
    if (AxKit::sysopen($fh, $self->{file}.'.type', O_RDONLY, 'raw')) {
        flock($fh, LOCK_SH);
        local $/;
        return <$fh>;
        # close($fh);
        # close unlocks automatically
    }
    return '';
}

sub deliver {
    my $self = shift;
    return SERVER_ERROR if $self->{no_cache};
    my $r = $self->{apache};

    {
        # get content-type
        AxKit::Debug(4, "Cache: Getting content-type");
        if (my $type = $self->get_type) {
            AxKit::Debug(4, "Cache: setting content-type: $type");
            $r->content_type($type);
        }
    }

    if ($r->content_type eq 'changeme' && !$r->notes('axkit_passthru_type')) {
        $AxKit::Cfg->AllowOutputCharset(1);
        $r->content_type('text/html; charset=' . ($AxKit::Cfg->OutputCharset || "UTF-8"));
    }
    elsif ($r->notes('axkit_passthru_type')) {
        $r->content_type($AxKit::OrigType);
    }


    my ($transformer, $doit) = AxKit::get_output_transformer();

    if ($doit) {
        AxKit::Debug(4, "Cache: Transforming content and printing to browser");
        $r->pnotes('xml_string',$self->read());
        return OK; # upstream deliver_to_browser should handle the rest
    }
    else {
        AxKit::Debug(4, "Cache: Sending untransformed content to browser");

        # Make sure we unset PATH_INFO or wierd things can happen!
        $ENV{PATH_INFO} = '';
        $r->path_info('');

        if ($self->{gzip} && $AxKit::Cfg->DoGzip) {
            AxKit::Debug(4, 'Cache: Delivering gzipped output');
            $r->filename($self->{file}.'.gz');
        }
        else {
            $r->filename($self->{file});
        }

        return DECLINED;
    }

}

sub reset {
    my $self = shift;
    unlink $self->{file};
}

sub mtime {
    my $self = shift;
    $self->_get_stats;
    return $self->{mtime} if exists $self->{mtime};
    return ($self->{mtime} = (stat($self->{file}))[9]);
}

sub has_changed {
    my $self = shift;
    my $time = shift;
    return $self->mtime > $time;
}

sub exists {
    my $self = shift;
    return if $self->{no_cache};
    $self->_get_stats;
    return $self->{file_exists} if exists $self->{file_exists};
    return ($self->{file_exists} = -e $self->{file});
}

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

sub no_cache {
    my $self = shift;

    return $self->{no_cache} unless @_;

    if ($_[0]) {
        AxKit::Debug(8, "Turning off cache!");
        $self->{no_cache} = 1;
        $self->reset();
    }
    
    return $self->{no_cache};
}

1;
