package PhotoPub::Cache;
require Exporter;
@ISA = qw(Exporter);
@EXPORT_OK = qw(&imagefile &imagesize &exifinfo);

use PhotoPub::Global qw(&$error);
use PhotoPub::Handlers qw(&handlephoto);
use PhotoPub::Options qw(&getoption &isdefault &optionentries);
use PhotoPub::Images qw(&picfile);
use PhotoPub::Ages qw(&updateagefromfiles);
use PhotoPub::FileInfo qw(&mtime);
use PhotoPub::Utils qw(&getparallellimit);
use PhotoPub::ParallelLimit qw(&enter &leave LIM_MOGRIFY);

use Image::Magick;
use Image::Size qw(&imgsize);
use Math::Trig qw(pi);
use POSIX qw(&ceil &floor);
use Storable qw(lock_store lock_retrieve);
use Image::Info qw(&image_info);
use Fcntl ':flock';

use strict;

sub needcache {
  my ($photo,$options)=@_;
  my $needcache=0;
  my $cachefile=&handlephoto('cachefileprefix',$photo);

  &$error(type=>'config',
	  message=>"No cache handler installed for $photo")
    unless defined $cachefile;
  
  foreach my $entry (sort keys %$options) {
    if(!isdefault($options,$entry)) {
      $needcache=1;
      $cachefile.='-'.$entry.getoption($options,$entry);
    }
  }
  if(!$needcache) {
    my $forcemogrify=&handlephoto('forcemogrify',$photo);
    $needcache=1 if defined $forcemogrify && $forcemogrify;
  }
  return ($needcache,$needcache ? $cachefile : undef);
}

sub imagefile {
  my ($num,$options)=@_;
  my $picfile=&picfile($num);
  if(!defined $picfile) {
     &$error(type=>'userinput',
             message=>"Unknown picture $num");
  }
  updateagefromfiles($picfile);
  my ($needcache,$cachefile)=needcache($num,$options);

  if($needcache) {
# this should all be modularised, but we need to maintain the order things
# are processed

    my ($red,$green,$blue,$shrink,$rotate,$thumb,$crop,$sharpen,$quality)=
      map { getoption($options,$_) } ('red','green','blue',
				      'shrink','rotate','thumb','crop',
				      'sharpen','quality');
    
    # Move this to Options.pm or something
    &$error(type=>'userinput',
	    message=>"Can't specify both shrink and thumbnail") 
      if $thumb ne 'none' && $shrink!=1;

    if(! -f $cachefile || &mtime($cachefile) < &mtime($picfile)) {
      &enter(LIM_MOGRIFY,&getparallellimit('mogrify'));

      my $pic = new Image::Magick;
      if(my $res = $pic->Read('JPG:'.$picfile)) { 
	&$error(type => 'picture',
		message => $res);
      }
      $pic->Gamma(red => $red,
		  green => $green,
		  blue => $blue)
	if $red!=1 || $green!=1 || $blue!=1;
      if($rotate ne '0') {
	my ($degrees,$opts)=($rotate=~/^([\d\.]+)(a?)$/);
	my ($width,$height)=$pic->Get('width','height');
	$pic->Rotate(degrees => $degrees);
	if($opts eq 'a') {
	  my ($bigwidth,$bigheight)=$pic->Get('width','height');

	  my $sinval=sin($degrees*pi/180);
	  my $cosval=cos($degrees*pi/180);
	  my $sinsqval=$sinval*$sinval;
	  my $cossqval=$cosval*$cosval;

	  my $xoffset=ceil(($height*$cossqval*$sinval-$width*$sinsqval*$cosval)
			   /($cossqval-$sinsqval));
	  my $yoffset=ceil(($width*$cossqval*$sinval-$height*$sinsqval*$cosval)
			   /($cossqval-$sinsqval));

	  my $newwidth=$bigwidth-2*$xoffset;
	  my $newheight=$bigheight-2*$yoffset;

	  $pic->Crop(geometry => $newwidth.'x'.$newheight.
		     '+'.$xoffset.'+'.$yoffset);
	}
      }
      $pic->Crop(geometry => $crop)
	if $crop ne 'none';
      $pic->Scale(width => $pic->Get('width')/$shrink,
		  height => $pic->Get('height')/$shrink)
	if $shrink!=1;
      $pic->Scale(geometry => $thumb)
	if $thumb ne 'none';
      $pic->Sharpen(factor => $sharpen)
	if $sharpen ne 0;
      $pic->Set(quality => $quality)
	if $quality ne 75;

      $pic->Strip();

      my ($blob)=$pic->ImageToBlob(magick=>'jpg');

      open OUTFILE,">$cachefile" 
	or &$error(type=>'config',
		   message=>"Couldn't open cache file $cachefile for writing");
      flock OUTFILE,LOCK_EX;
      if(!print OUTFILE $blob) {
	# $cachefile is derived from verified parameters and
        # from trusted config options, so we untaint it unconditionally
	my ($cfile)=($cachefile=~/^(.*)$/);
	flock OUTFILE,LOCK_UN;
	close OUTFILE;
	unlink($cfile);
	&$error(type => 'config',
		message => "Couldn't write cache file $cachefile to disk");
      }
      flock OUTFILE,LOCK_UN;
      close OUTFILE;

      undef $pic;

      &leave(LIM_MOGRIFY);
    }
    return $cachefile;
  } else {
    return $picfile;
  }
}

sub imagesize {
  my ($photo,$options)=@_;
  my $sizeinfo=&sizeinfo($photo);
  if(!defined $sizeinfo) {
    &$error(type=>'config',
	    message=>"Photo $photo has undefined size");
  }
  my ($width,$height)=@{&sizeinfo($photo)};

  my $picfile=&picfile($photo);

  if(1) {
    my ($shrink,$rotate,$thumb,$crop)=
      map { &getoption($options,$_) } ('shrink','rotate','thumb','crop');

    if($rotate ne 0) {
      my ($degrees,$opts)=($rotate=~/^([\d\.]+)(a?)$/);

      # calculation is broken :-(
      return undef unless $degrees==90 || $degrees==180 || $degrees==270; 
      
      my $sinval=sin($degrees*pi/180);
      my $cosval=cos($degrees*pi/180);
      
      my ($bigwidth,$bigheight)
	=($width*abs($cosval)+$height*abs($sinval),
	  $height*abs($cosval)+$width*abs($sinval));

      $bigwidth=floor(abs($bigwidth)+0.5);
      $bigheight=floor(abs($bigheight)+0.5);

      if($opts eq 'a') {
	my $sinsqval=$sinval*$sinval;
	my $cossqval=$cosval*$cosval;
	
	my $xoffset=ceil(($height*$cossqval*$sinval-$width*$sinsqval*$cosval)
			 /($cossqval-$sinsqval));
	my $yoffset=ceil(($width*$cossqval*$sinval-$height*$sinsqval*$cosval)
			 /($cossqval-$sinsqval));
	
	($width,$height)=($bigwidth-2*$xoffset,$bigheight-2*$yoffset);
      } else {
	($width,$height)=($bigwidth,$bigheight);
      }
    }
    
    if($crop ne 'none') {
      return undef; # broken too :-(
      my ($w,$h,$l,$t)=($crop=~/^(\d+)x(\d+)(?:\+(\d+)(?:\+(\d+))?)?$/)
	or &$error(type=>'config',
		   message=>"Bad crop specification '$crop'");
      $l=0 if !defined $l;
      $t=0 if !defined $t;
      if($w+$l >= $width) {
	$w = $width-$l;
      }
      if($h+$t >= $height) {
	$h = $height-$t;
      }
      ($width,$height)=($w,$h);
    }

    if($shrink!=1) {
      $width=floor($width/$shrink);
      $height=floor($height/$shrink);
    }

    if($thumb ne 'none') {
      my ($w,$h)=($thumb=~/^(\d+)x(\d+)$/);
      if ($w*$height > $width*$h) {
	($width,$height)=(floor($width*$h/$height+0.5),$h);
      } else {
	($width,$height)=($w,floor($height*$w/$width+0.5));
      }
    }
  }

  return ($width,$height);
}



sub exifinfo {
  my ($photo)=@_;
  my $cachefile=&handlephoto('exifcachefile',$photo);
  my $picfile=&picfile($photo);
  return undef unless defined $picfile;

  my $info;
  if(defined $cachefile) {
    if(! -f $cachefile || &mtime($cachefile) < &mtime($picfile)) {
      $info=&image_info($picfile);
      my $infocopy;
      for my $key ('DateTimeOriginal') {
	$infocopy->{$key} = $info->{$key};
      }
      lock_store $infocopy,$cachefile;
    } else {
      $info=lock_retrieve($cachefile)
    }
  } else {
    $info=&image_info($picfile);
  }
  return $info;
}

sub sizeinfo {
  my ($photo)=@_;
  my $cachefile=&handlephoto('sizecachefile',$photo);
  my $picfile=&picfile($photo);
  return undef unless defined $picfile;

  my $dims;

  if(defined $cachefile) {
    if(! -f $cachefile || &mtime($cachefile) < &mtime($picfile)) {
      my ($width,$height,$msg)=&imgsize($picfile);
      if(!defined $width || !defined $height) {
	&$error(type=>'picture',
		message=>"Couldn't get size of image $photo : $msg");
      }
      $dims=[$width,$height];
      lock_store $dims,$cachefile;
    } else {
      $dims=lock_retrieve($cachefile)
    }
  } else {
    my ($width,$height,$msg)=&imgsize($picfile);
    if(!defined $width || !defined $height) {
      &$error(type=>'picture',
	      message=>"Couldn't get size of image $photo : $msg");
    }
    $dims=[$width,$height];
  }
  return $dims;
}
