#!/usr/bin/perl -w

use strict;

use Image::ExifTool;
use Date::Parse;
use Time::Local;

my $debug= 0;

my $timetag= "DateTimeOriginal";


##############################################################################
# Subroutines

# Convert date/time to string in the format in which EXIF requires it.
# -> Time in seconds since epoch
# <- Date/Time string in the right format
sub timestr
{
  my ($tm)= @_;

  my ($s, $min, $h, $d, $m, $y)= localtime($tm);
  return sprintf("%04d:%02d:%02d %02d:%02d:%02d", $y+1900, $m+1, $d, $h, $min, $s);
}


# To be called when an error occurred when trying to extract the time tag in
# gettagtime().  Prints the second part of the error message and exits if the
# error concerned the reference file.
# -> Flag which is true for the ref file
# <- undef unless ref file
sub abortifref
{
  my ($isref)= @_;

  if( $isref ) {
    print STDERR "  Aborting.\n";
    exit 1;
  }
  else {
    print STDERR "  Skipping.\n";
    return undef;
  }
}


# Extracts the time tag from a file.  If the file is the reference, exits on
# error, otherwise returns undef on error.
# -> Reference to ExifTool object
#    File name
#    Flag: true if this is the reference file
# <- Time value (seconds since epoch); undef if an error occurs and this is not
#    the reference file.  The ExifTool object now contains the time tag.
sub gettagtime
{
  my ($ed, $fname, $isref)= @_;

  unless( $ed->ExtractInfo($fname) ) {
    print STDERR "Could not get EXIF data from $fname.";
    return abortifref();
  }
  my $rawval= $ed->GetValue($timetag, "Raw");
  if( !defined($rawval) ) {
    print STDERR "EXIF tag $timetag not found in $fname.";
    return abortifref();
  }
  elsif( $rawval =~ /(\d+):(\d+):(\d+)\s+(\d+):(\d+):(\d+)/ ) {
    return timelocal($6, $5, $4, $3, $2-1, $1-1900);
  }
  else {
    print STDERR "Value of EXIF tag $timetag does not look like a time/date in $fname.";
    return abortifref();
  }
}


##############################################################################
# Parse command line

if( @ARGV && $ARGV[0] eq "-t" ) {
  shift @ARGV;
  $timetag= shift @ARGV;
}

if( @ARGV < 3 ) {
  print STDERR <<EOF;
Usage: imgtime.pl [ -t <tag> ] <image> <time> <images> ...
Shifts the time and date embedded in <images> by the same amount so that
<image> would receive time <time>.  The EXIF tag to be shifted is <tag> if the
-t option is given, otherwise DateTimeOriginal by default.  <time> has to be in
a format that Date::Parse can read.  The date defaults to no change.  <image>
is processed only if it is given again among the target <images>.  imgtime.pl
writes the file name, previous time and updated time to the log file
imgtime.log for all successful conversions.
EOF
  exit 1;
}

my $ed= new Image::ExifTool;
unless( $ed  ) {
  print STDERR "Could not create ExifTool object.  Aborting.\n";
  exit 1;
}
$ed->Options( Binary => 0, Composite => 0, FastScan => 1,
		DateFormat => "%s", PrintConv => 1, IgnoreMinorErrors => 1 );

##############################################################################
# Get time from reference file and compute shift in seconds

my $reffile= shift @ARGV;
my $origtime;
my $reftime;
my ($refsec, $refmin, $refhour, $refday, $refmonth, $refyear)=
	strptime shift @ARGV;
$refsec ||= 0;
if( !defined($refmin) || !defined($refhour) ) {
  print STDERR "Error parsing reference time.  It has to contain at least hour and minute.  Aborting.\n";
  exit 1;
}

$origtime= gettagtime($ed, $reffile, 1);

if( !defined($refday) || !defined($refmonth) || !defined($refyear) ) {
  my (undef, undef, undef, $origday, $origmonth, $origyear)=
  	localtime $origtime;
  $refday= $origday unless defined($refday);
  $refmonth= $origmonth unless defined($refmonth);
  $refyear= $origyear unless defined($refyear);
}

$reftime= timelocal($refsec, $refmin, $refhour, $refday, $refmonth, $refyear);

my $timediff= $reftime - $origtime;

unless( $timediff ) {
  print STDERR "Nothing to be done - time offset is 0.\n";
  exit 0;
}

print "Offset $timediff seconds\n" if $debug;


##############################################################################
# Process target image files

# Remove duplicates, since duplicate processing would shift a file by the wrong
# amount:
my %fileh;
@fileh{@ARGV}= (1) x @ARGV;
my @files= keys %fileh;
# This is not perfect, as the same file can be referred to by different paths,
# such as absolute, relative, via symbolic links...  We could use
# Cwd::abs_path() to resolve this, but that would mean another module
# dependency.  This simple check catches the most likely copy&paste duplicates.

my $total= @files;
my $done= 0;
my $skipped= 0;
my $err= 0;
my $warn= 0;
open my $log, ">imgtime.log";

for my $file (@files)
{
  my $time= gettagtime($ed, $file, 0);
  unless( defined($time) ) {
    ++$skipped;
    next;
  }
  my ($success, $msg)=
  	$ed->SetNewValue($timetag, timestr($time+$timediff), Type => "Raw");
  unless( $success ) {
    print STDERR "Error updating $timetag tag for $file:\n$msg\nSkipping.\n";
    ++$err;
    next;
  }
  unless( $ed->WriteInfo($file) ) {
    $msg= $ed->GetValue("Error");
    print STDERR "Error writing $timetag tag for $file:\n$msg\n";
    ++$err;
    next;
  }
  elsif( $msg= $ed->GetValue("Warning") ) {
    print STDERR "Warning writing $timetag tag for $file:\n$msg\n";
    ++$warn;
  }
  print $log "$file\t\t", timestr($time), "\t\t", timestr($time+$timediff), "\n";
  ++$done;
}

close $log;

print "$done completed", ($skipped? ", $skipped skipped":""),
	($err? ", $err errors":""), ($warn? ", $warn warnings":""),
	" out of a total of $total.\n";

