#!/usr/bin/perl

eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}' if 0; # not running under some shell

# @file autosave.pl

# @desc script to periodically save all the open documents to a temporary file

# @author Meshach Mitchell <meshach DOT mitchell AT gmail DOT com>

# @copyright 2009 Meshach Mitchell

# @license

# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.

# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.

# @info

#  Open images that haven't been saved to at all yet, will be saved in the home
#  directory (or the cwd, or a configurable directory, using the time the image
#  was started, and some random/distinguishing property)

use Gimp; qw(main :auto __ N_);
use Gtk2;
use strict 'vars';
use Cwd 'abs_path';
use File::Path 'make_path';
use File::Basename;

my $dir = dirname abs_path $0;

my %conf;
my $lock = "$dir/autosave.lock"; # lockfile, may need to include gimp pid

# need to unbuffer the files we open, or for some reason they don't get written
# until after the program exits'

sub unbuffer {
  my $fh = shift // select;
  my $ofh = select;
  select $fh;
  $|++;
  select $ofh;
}

if (-e "$dir/autosave.conf") {
  if (open (my $cfile, '<', "$dir/autosave.conf")) {
    while (<$cfile>) {
      unless (/^\s+#/) {
	if (/=/) {
	  my ($var, $val) = split (/=/, $_, 2);
	  $var =~ s/^\s+|\s+$//g;
	  $val =~ s/^\s+|\s+$//g;
	  $conf{$var} = $val;
	}}}
    $conf{active} = (defined ($conf{active})
		     && ($conf{active} eq 'no' || !$conf{active}) ? 0: 1)
  } else { die "could not open conf file for reading"};
}

# start the perl server if it isn't running
sub autosave_enable {
  if (defined ($conf{active}) && ($conf{active} eq 'no' || !$conf{active})) {
    unlink $lock if -e $lock;
  } else {
    my $pid;
    if (-e $lock && ($pid = qx{cat "$lock"} + 0) && kill $pid) {
      return;
    } else {
      if (open my $lockfh, '>', $lock) {
	unbuffer $lockfh;
	print $lockfh $$;
	close $lockfh;
      } else {
	die "could not create lockfile '$lock'";
      }}}

  if (-e $lock) {
    my $int = ($conf{interval}||5) * 60;
    my $sfp = ($conf{saved_file_pattern} || '%F~');
    my $nfp = ($conf{new_file_pattern} || '%~/.gimp-autosave/%N.%D.%B.%Wx%H');
    my $i = 0;

    # make file to keep track of untitled documents
    unless (-e "$dir/autosave.count") {
      open (my $ifile, '>', "$dir/autosave.count");

      # disable output buffering
      unbuffer $ifile;
      print $ifile '1';
      close $ifile;
    }
    while (-e $lock) {
      if ($i > $int) {
    # set message handler to console. Otherwise we get these horribly
    # frightening, but completely useless messages about procudures not found
	my $old_handler = Gimp->message_get_handler;
	Gimp->message_set_handler (CONSOLE);

	for my $img (Image->list) {
	  bless $img, 'Image';
	  next unless $img->is_valid;
	  my $saved=0;
	  my $file;
	  if (($file = $img->get_filename) && index ($file, 'Untitled') == -1) {
	    # then we use the saved file
	    $saved=1;
	  } else {
	    # unsaved image
	    $file = ($img->get_name || $$img);
	  }
	  if ($img->is_dirty) {
	    my @date = (localtime) [5,4,3];
	    $date[0]+=1900; $date[1]+=1;
	    # okay, here we do the thing
	    # first, fix the filename
	    my $fname;

	    if ($saved) {
	      $fname = $sfp;
	      $fname =~ s{%([%BC~DFHNW]|base|cwd|date|file|height|home|name
			 |width)}
	      {	($1 eq 'C' || $1 eq 'cwd'
		 ? dirname ($file)
		 : ($1 eq '~' || $1 eq 'home'
		    ? $ENV{HOME}
		    : ($1 eq 'D' || $1 eq 'date'
		       ? join ('', @date)
		       : ($1 eq 'F' || $1 eq 'file'
			  ? (index ($file, '.xcf') == (length ($file) - 4)
			     ? $file : "$file.xcf")
			  : ($1 eq 'N' || $1 eq 'name'
			     ? $img->get_name
			     : ($1 eq 'W' || $1 eq 'width'
				? $img->width
				: ($1 eq 'H' || $1 eq 'height'
				   ? $img->height
				   :($1 eq 'B' || $1 eq 'base'
				     ? qw(RGB GRAY INDEXED) [$img->base_type]
				     : ($1 eq '%'? '%': $1))))))))) }xeg;
	    } else {
	      if ($img->get_filename){
		$fname = $img->get_filename;
	      } else {
		$fname = $nfp;
		$fname =~ s{%([~BDHNW]|base|date|height|home|name|width)}
		{ ($1 eq '~' || $1 eq 'home'
		   ? $ENV{HOME}
		   : ($1 eq 'D' || $1 eq 'date'
		      ? join ('', @date)
		      : ($1 eq 'N' || $1 eq 'name'
			 ? ($img->get_name().'-<x>')
			 : ($1 eq 'W' || $1 eq 'width'
			    ? $img->width
			    : ($1 eq 'H' || $1 eq 'height'
			       ? $img->height
			       : ($1 eq 'B' || $1 eq 'base'
				  ? qw(RGB GRAY INDEXED) [$img->base_type]
				  : ($1 eq '%'? '%': $1))))))) }xeg;
		$fname = "$fname.xcf" unless $fname =~ /\.xcf$/;

		# make sure we have a unique image/file name
		unless ($img->get_name =~ /Untitled-\d+/) {
		  my $count = qx{cat "$dir/autosave.count"};
		  # make sure we get a number for count
		  $count = (grep $_, split (/\s+/, $count))[0];
		  my @fname = split '<x>', $fname;
		  $count++ while -e "$fname[0]".$count."$fname[1]";
		  $fname = "$fname[0]".$count."$fname[1]";
		  $count++;
		  $img->set_filename ($fname);

		  open (my $ifile, '>', "$dir/autosave.count")
		   or die "couldn't open count-keeper file";

		  # disable output buffering
		  unbuffer $ifile;
		  print $ifile $count;
		  close $ifile;
		}}}

	    my $savedir = dirname $fname;
	    unless (-e $savedir) {
	      die "could not make directory '$savedir'"
	       unless make_path $savedir;
	    } else {
	      die "'$savedir' exists, but isn't a directory, can't save"
	       unless -d $savedir;
	    }
	    $img = $img->duplicate;
	    Gimp->xcf_save(0, $img, ($img->get_layers)[0], $fname, $fname);
	    $img->delete;
	  }
	}
	# reset message handler
	Gimp->message_set_handler ($old_handler);
	# reset counter
	$i = 0;
      }
      sleep (1);
    } continue {$i++}
  }
  unlink $lock if -e $lock;
}

sub autosave_disable {
  # just remove the lockfile
  unlink $lock if -e $lock;
  # waitabit
  sleep 2;
}

sub autosave_settings {
  Gtk2->init();
  my $window = Gtk2::Window->new ('toplevel');
  $window->set_name("Autosave Settings");
  $window->set_wmclass("Gimp", "Dialog");

  $window->signal_connect("destroy" => \&Gtk2::main_quit);
  $window->signal_connect("delete_event" => \&Gtk2::false);

  my $box1 = Gtk2::VBox->new (0, 0);
  $box1->set_spacing (2);
  $window->add ($box1);

  my $btn_tbl = Gtk2::Table->new (4,2);
  $btn_tbl->set_border_width (6);
  $btn_tbl->set_col_spacings (6);
  $box1->add($btn_tbl);

  my ($toglbl, $tog, $intlbl, $int, $sfplbl, $nfplbl, $sfp, $nfp);

  my @elems = (
    [$toglbl = Gtk2::Label->new ("Autosave enabled"),
     $intlbl = Gtk2::Label->new ("Autosave interval")],
    [$tog = Gtk2::CheckButton->new,
     $int = Gtk2::SpinButton->new_with_range (5,60,5)],
    [$sfplbl = Gtk2::Label->new ("Saved file pattern"),
     $nfplbl = Gtk2::Label->new ("New file pattern")],
    [$sfp = Gtk2::Entry->new,
     $nfp = Gtk2::Entry->new]);

  $tog->set_active ($conf{active});
  $int->set_value ($conf{interval} || 5);
  $sfp->set_text ($conf{saved_file_pattern} || '%F~');
  $nfp->set_text ($conf{new_file_pattern} ||
		  '%~/.gimp-autosave/%N.%D.%B.%Wx%H');

  # tooltips
  my $tips = Gtk2::Tooltips->new;
  my @tips = (
    'Set whether or not autosave is active',
    'Autosave interval in minutes between 5 and 60',
    'Path and filename pattern for saved files.
Defaults to "%F~" (filename with \'~\' appended, in the same directory as the file).
You can use the special intentifiers:
%~|%home => user\'s home directory
%B|%base => image base type (RGB/GRAY/INDEXED)
%C|%cwd => current directory of the file
%D|%date => date file was opened
%F|%file => filename (full path)
%N|%name => filename
%W|%width => image width
%H|%height => image height

The only available extension is xcf, since that is the format of the working project.
',
    'Path and filename pattern for new (unsaved) files.
Defaults to %~/.gimp-autosave/%N.%D.%M.%Wx%H
You can use the special intentifiers:
%~|%home => user\'s home directory
%B|%base => image base type (RGB/GRAY/INDEXED)
%D|%date => date file was opened
%N|%name => filename
%W|%width => image width
%H|%height => image height

The only available extension is xcf, since that is the format of the working project.
'
   );

  my $k = 0;
  for my $i (0..3) {
    for my $j (0,1) {
      $btn_tbl->attach_defaults ($elems[$i][$j], $i, $i+1, $j, $j+1);
      $tips->set_tip ($elems[$i][$j], $tips[$k]);
      $k++ if $i % 2;
      $elems[$i][$j]->show;
    }}

  my $hsep = Gtk2::HSeparator->new;
  $box1->pack_start($hsep, 0, 0, 0);
  $hsep->show;

  my $hbox = Gtk2::HButtonBox->new;
  $hbox->set_border_width(2);
  $hbox->set_spacing(4);
  $box1->pack_start($hbox, 0, 0, 0);
  $hbox->show;

  my $do_autosave=0;

  (my $btn = Gtk2::Button->new ("OK"))->can_default(1);
  $btn->signal_connect (
    clicked => sub {
      $tog = $tog->get_active();
      $int = $int->get_value();
      $sfp = ($sfp->get_text() || '%file~');
      $nfp = ($nfp->get_text() || '%~/.gimp-autosave/%N.%D.%B.%Wx%H');

      $window->hide;
      Gtk2->main_quit;

      if (open my $cfile, '>', "$dir/autosave.conf") {
	# disable output buffering
	unbuffer $cfile;
	print $cfile
	 "active = ".($tog? 'yes': 'no')."
interval = $int
saved_file_pattern = $sfp
new_file_pattern = $nfp
";
	close $cfile;

	my ($pid, @pids);
	# do stuff to change running state
	my $changes2;
	my $changes = ($tog != $conf{active}
		       || ($changes2 =
			   ($int != $conf{interval}
			    || $sfp ne $conf{saved_file_pattern}
			    || $nfp ne $conf{new_file_pattern})));

	if ($changes2 || !$tog) {
	  if (-e $lock) {
	    # stop it
	    autosave_disable;
	  } elsif (($pid = qx{pgrep -fl autosave.pl})
		   && (grep $_ != $$, ($pid =~ /^(\d+)/))) {
	    # shouldn't happen
	    kill 2, qx{pgrep -fl autosave.pl};
	  }}

	if ($changes && $tog) {
	  unless (-e $lock) {
	    if (($pid = qx{pgrep -fl autosave.pl})
		&& (grep $_ != $$, ($pid =~ /^(\d+)/))) {
	      # shouldn't happen
	      kill 2, qx{pgrep -fl autosave.pl};
	    }}
	  # set autosave to start
	  $do_autosave = 1;
	}
      } else { warn "could not open conf file for reading"};
    });
  $btn->show;
  $hbox->pack_start($btn, 0, 0, 0);
  $btn->grab_default;

  ($btn = Gtk2::Button->new ("Cancel"))->can_default(1);
  $btn->signal_connect(clicked => \&Gtk2::main_quit);
  $btn->show;
  $hbox->pack_start($btn, 0, 0, 0);

  $btn_tbl->show;
  $box1->show;
  $window->show;
  Gtk2->main();

  goto \&autosave_enable if $do_autosave;

  return
}

Gimp::register_callback autosave => \&autosave_enable;
Gimp::register_callback autosave_enable => \&autosave_enable;
Gimp::register_callback autosave_disable => \&autosave_disable;
Gimp::register_callback autosave_settings => \&autosave_settings;

Gimp::on_query {
  # if there's an autosave lock-file here, it's left over, and should be removed
  unlink $lock if -e $lock;

  Gimp->install_procedure (
    "autosave_settings",					 # name
    "Edit autosave settings",					 # blurb
    "Update the settings of the autosave script.
There's no non-interactive mode yet, but it's on the 'roadmap' ;)", # help
    "Meshach Mitchell",						 # Author
    "Meshach Mitchell",						 # Copyright
    "2009-11-26",						 # Date
    N_"<Image>/File/Autosave/Settings",				 # Menu
    undef,							 # Image types
    PLUGIN,							 # Type
    [[PDB_INT32, "run-mode", "interactive, [non-interactive]"]], # Params
    []);							 # Return values

  Gimp->install_procedure (
    "autosave_enable",						 # name
    "start the autosave process",				 # blurb
    "Enable periodic saving of files. This script starts the process, unless it thinks there's already an autosave process running.", # help
    "Meshach Mitchell",						 # Author
    "Meshach Mitchell",						 # Copyright
    "2009-11-26",						 # Date
    N_"<Image>/File/Autosave/Enable",				 # Menu
    undef,							 # Image types
    PLUGIN,							 # Type
    [[PDB_INT32, "run-mode", "interactive, [non-interactive]"]], # Params
    []);							 # Return values

  Gimp->install_procedure (
    "autosave",							 # name
    "Start the autosave process",				 # blurb
    "Alias for autosave-enable",				 # help
    "Meshach Mitchell",						 # Author
    "Meshach Mitchell",						 # Copyright
    "2009-11-26",						 # Date
    N_"<none>",							 # Menu
    undef,							 # Image types
    PLUGIN,							 # Type
    [[PDB_INT32, "run-mode", "interactive, [non-interactive]"]], # Params
    []);							 # Return values

  Gimp->install_procedure (
    "autosave_disable",						 # name
    "A plugin to periodically save open images",		 # blurb
    "Stop autosaving. If you find that it's interfering too much with your work, you can stop it, until the next time you start gimp. If you want to disable it altogether, use autosave-settings", # help
    "Meshach Mitchell",						 # Author
    "Meshach Mitchell",						 # Copyright
    "2009-11-26",						 # Date
    N_"<Image>/File/Autosave/Disable",				 # Menu
    undef,							 # Image types
    PLUGIN,							 # Type
    [[PDB_INT32, "run-mode", "interactive, [non-interactive]"]], # Params
    []);							 # Return values
};

exit main;

