#! /usr/bin/env perl

###################################################
#
#  Copyright (C) 2008, 2009 Mario Kemper <mario.kemper@googlemail.com> and Shutter Team
#
#  This file is part of Shutter.
#
#  Shutter 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.
#
#  Shutter 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 Shutter; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
###################################################

###################################################
#
#  Based on a script by Fred Weinhaus
#
#  Fred Weinhaus and http://www.fmwconcepts.com/imagemagick/index.html
#
#  http://www.fmwconcepts.com/imagemagick/3Drotate/index.php
#
###################################################

use utf8;
use strict;
use warnings;
use Gtk2 '-init';
use Image::Magick;
use POSIX qw/setlocale strftime/;
use Locale::gettext;
use Glib qw/TRUE FALSE/;
use FindBin '$Bin';    #path where plugin is located
use File::Temp qw/ tempfile tempdir /;
use Time::HiRes qw/usleep/;
use Data::Dumper;

#load modules at custom path at runtime
#--------------------------------------
require lib;
import lib $ENV{'SHUTTER_ROOT'}."/share/shutter/resources/modules";

#proc (Thanks to Michael Schilli)
require Proc::Simple;

#load shutter's modules
#--------------------------------------
require Shutter::App::SimpleDialogs;

#configure gettext using ENV Variable (setup during shutter start)
setlocale( LC_MESSAGES, "" );
my $d = Locale::gettext->domain("shutter-plugins");
$d->dir( $ENV{'SHUTTER_INTL'} );

#icontheme to determine if icons exist or not
#in some cases we deliver fallback icons
my $icontheme = Gtk2::IconTheme->get_default;
$icontheme->append_search_path($ENV{'SHUTTER_ROOT'} . "/share/icons");

#shutter will ask for some infos
my %plugin_info = 	( 	'name'	=> $d->get( "3D rotate" ),
						'sort'	=> $d->get( "Effect" ),
						'tip'	=> $d->get("Applies a perspective distortion to an image\n\nBased on a script by Fred Weinhaus\n\nhttp://www.fmwconcepts.com/imagemagick/3Drotate/index.php"),
					);

binmode( STDOUT, ":utf8" );
if ( exists $plugin_info{$ARGV[ 0 ]} ) {
	print $plugin_info{$ARGV[ 0 ]};
	exit;
}

#these variables are passed to the plugin
my $socket_id = $ARGV[ 0 ];
my $filename  = $ARGV[ 1 ];
my $width     = $ARGV[ 2 ];
my $height    = $ARGV[ 3 ];
my $filetype  = $ARGV[ 4 ];

#decode filename
utf8::decode $filename;

my $plug = Gtk2::Plug->new( $socket_id );
$plug->set_default_icon_name( 'shutter' );
$plug->set_border_width( 10 );

$plug->signal_connect( destroy => sub { Gtk2->main_quit } );

#tooltips
my $tooltips = Gtk2::Tooltips->new;

#configure buttons and other needed controls
my $pan_label = Gtk2::Label->new( "Pan:" );
my $pan_sbutton = Gtk2::SpinButton->new_with_range( -180, 180, 1 );
$pan_sbutton->set_value( 40 );

$tooltips->set_tip(
	$pan_label,
	$d->get(
		"Rotation about image vertical centerline"
	)
);

$tooltips->set_tip(
	$pan_sbutton,
	$d->get(
		"Rotation about image vertical centerline"
	)
);

my $tilt_label = Gtk2::Label->new( "Tilt:" );
my $tilt_sbutton = Gtk2::SpinButton->new_with_range( -180, 180, 1 );
$tilt_sbutton->set_value( 0 );

$tooltips->set_tip(
	$tilt_label,
	$d->get(
		"Rotation about image horizontal centerline"
	)
);

$tooltips->set_tip(
	$tilt_sbutton,
	$d->get(
		"Rotation about image horizontal centerline"
	)
);

my $roll_label = Gtk2::Label->new( "Roll:" );
my $roll_sbutton = Gtk2::SpinButton->new_with_range( -180, 180, 1 );
$roll_sbutton->set_value( 0 );

$tooltips->set_tip(
	$roll_label,
	$d->get(
		"Rotation about the image center"
	)
);

$tooltips->set_tip(
	$roll_sbutton,
	$d->get(
		"Rotation about the image center"
	)
);

my $pef_label = Gtk2::Label->new( "Pef:" );
my $pef_sbutton = Gtk2::SpinButton->new_with_range( 0, 3.19, 0.01 );
$pef_sbutton->set_value( 1 );

$tooltips->set_tip(
	$pef_label,
	$d->get(
		"Perspective exaggeration factor"
	)
);

$tooltips->set_tip(
	$pef_sbutton,
	$d->get(
		"Perspective exaggeration factor"
	)
);

my $zoom_label = Gtk2::Label->new( $d->get("Zoom:") );
my $zoom_sbutton = Gtk2::SpinButton->new_with_range( -10, 10, 0.10 );
$zoom_sbutton->set_value( -1.5 );

$tooltips->set_tip(
	$zoom_label,
	$d->get(
		"Output zoom factor; where value > 1 means zoom in and < 1 means zoom out"
	)
);

$tooltips->set_tip(
	$zoom_sbutton,
	$d->get(
		"Output zoom factor; where value > 1 means zoom in and < 1 means zoom out"
	)
);

#AUTO can be either c, zc or out. 
#If auto is c, then the resulting perspective of the input image will have its bounding 
#box centered in the output image whose size will be the same as the input image. 
#
#If auto is zc, then the resulting perspective of the input image will have its bounding box 
#zoomed to fill its largest dimension to match the size of the the input image 
#and the other dimension will be centered in the output. 
#
#If auto is out, then the output image will be made as large or as small as needed 
#to just fill out the transformed input image
my $auto_label = Gtk2::Label->new( $d->get( "Auto:" ) );
my $auto_combo = Gtk2::ComboBox->new_text;
$auto_combo->insert_text( 0, "off" );
$auto_combo->insert_text( 1, "c" );
$auto_combo->insert_text( 2, "zc" );
$auto_combo->insert_text( 3, "out" );

#if auto is zc or auto is out => zoom is ignored
$auto_combo->signal_connect('changed' => sub {
	my $state = $auto_combo->get_active_text;
	if($state eq "zc" or $state eq "out"){
		$zoom_sbutton->set_sensitive(FALSE);	
	}else{
		$zoom_sbutton->set_sensitive(TRUE);		
	}
});

$auto_combo->set_active( 1 );

$tooltips->set_tip(
	$auto_label,
	$d->get(
		"off - No automatic adjustment\n\nc - Center bounding box in output\n\nzc - Zoom to fill and center bounding box in output\n\nout - Creates an output image of size needed to hold the transformed image"
	)
);

$tooltips->set_tip(
	$auto_combo,
	$d->get(
		"off - No automatic adjustment\n\nc - Center bounding box in output\n\nzc - Zoom to fill and center bounding box in output\n\nout - Creates an output image of size needed to hold the transformed image"	)
);

my $back_color_label = Gtk2::Label->new( $d->get("Background color"). ":" );
my $back_color = Gtk2::ColorButton->new();
$back_color->set_color( Gtk2::Gdk::Color->parse('gray') );
$back_color->set_alpha( int( 0 * 65535 ) );
$back_color->set_use_alpha(TRUE);
$back_color->set_title( $d->get("Choose background color") );

my $sky_color_label = Gtk2::Label->new( $d->get("Sky color"). ":" );
my $sky_color = Gtk2::ColorButton->new();
$sky_color->set_color( Gtk2::Gdk::Color->parse('blue') );
$sky_color->set_alpha( int( 0.9 * 65535 ) );
$sky_color->set_use_alpha(TRUE);
$sky_color->set_title( $d->get("Choose sky color") );

#all labels on the left side 
#need to have the same size
my $sgl = Gtk2::SizeGroup->new ('both');
$sgl->set ('ignore-hidden' => FALSE);
$sgl->add_widget($pan_label);
$sgl->add_widget($roll_label);
$sgl->add_widget($tilt_label);
$sgl->add_widget($pef_label);
$sgl->add_widget($auto_label);
$sgl->add_widget($zoom_label);

#we define two Gtk2::Image widgets
#to store the screenshot
#and a throbber that is shown while the changes are processed
my $preview =
	Gtk2::Image->new_from_pixbuf(
	   Gtk2::Gdk::Pixbuf->new_from_file_at_scale( $filename, 300, 300, TRUE ) );

my $preview_throb =
	Gtk2::Image->new_from_file( $ENV{'SHUTTER_ROOT'}."/share/shutter/resources/icons/throbber.gif" );

my $sg = Gtk2::SizeGroup->new ('both');
$sg->set ('ignore-hidden' => FALSE);
$sg->add_widget($preview);
$sg->add_widget($preview_throb);


#we define three Gtk2::Button widgets
#to refresh, save and cancel the plugin's work
my $refresh_btn = Gtk2::Button->new_from_stock( 'gtk-refresh' );
$refresh_btn->signal_connect( 'clicked', \&fct_imagemagick_3Drotate,
							  'refresh' );

my $save_btn = Gtk2::Button->new_from_stock( 'gtk-save' );
$save_btn->signal_connect( 'clicked', \&fct_imagemagick_3Drotate, 'save' );

my $cancel_btn = Gtk2::Button->new_from_stock( 'gtk-cancel' );
$cancel_btn->signal_connect( 'clicked' => sub { Gtk2->main_quit; exit 2; }, 'cancel' );

#define the gui layout
my $hbox1      = Gtk2::HBox->new( FALSE, 8 );
my $hbox2      = Gtk2::HBox->new( FALSE, 8 );
my $hbox3      = Gtk2::HBox->new( FALSE, 8 );
my $hbox4      = Gtk2::HBox->new( FALSE, 8 );
my $hbox5      = Gtk2::HBox->new( FALSE, 8 );
my $hbox6      = Gtk2::HBox->new( FALSE, 8 );
my $hbox7      = Gtk2::HBox->new( FALSE, 8 );
my $hbox8      = Gtk2::HBox->new( FALSE, 8 );

my $hbox_row1 = Gtk2::HBox->new( TRUE, 8 );
my $hbox_row2 = Gtk2::HBox->new( TRUE, 8 );

my $vbox_param = Gtk2::VBox->new( FALSE,  8 );
my $vbox_left = Gtk2::VBox->new( FALSE, 8 );
my $vbox_right = Gtk2::VBox->new( FALSE, 8 );

my $hbox_btn  = Gtk2::HBox->new( TRUE,  8 );
my $vbox_btn  = Gtk2::VBox->new( FALSE, 8 );
my $vbox_main = Gtk2::VBox->new( FALSE, 8 );

#packing
$hbox1->pack_start( $pan_label, FALSE, TRUE, 5 );
$hbox1->pack_start( $pan_sbutton, TRUE,  TRUE, 5 );

$hbox2->pack_start( $tilt_label, FALSE, TRUE, 5 );
$hbox2->pack_start( $tilt_sbutton, TRUE,  TRUE, 5 );

$hbox3->pack_start( $roll_label, FALSE, TRUE, 5 );
$hbox3->pack_start( $roll_sbutton, TRUE,  TRUE, 5 );

$hbox4->pack_start( $pef_label, FALSE, TRUE, 5 );
$hbox4->pack_start( $pef_sbutton, TRUE,  TRUE, 5 );

$hbox5->pack_start( $auto_label, FALSE, TRUE, 5 );
$hbox5->pack_start( $auto_combo, TRUE,  TRUE, 5 );

$hbox6->pack_start( $zoom_label, FALSE, TRUE, 5 );
$hbox6->pack_start( $zoom_sbutton, TRUE,  TRUE, 5 );

$hbox7->pack_start( $back_color_label, FALSE, TRUE, 5 );
$hbox7->pack_start( $back_color, TRUE,  TRUE, 5 );

$hbox8->pack_start( $sky_color_label, FALSE, TRUE, 5 );
$hbox8->pack_start( $sky_color, TRUE,  TRUE, 5 );

#row 1
$hbox_row1->pack_start_defaults( $hbox7 );
$hbox_row1->pack_start_defaults( $hbox8 );

#controls on the left side
$vbox_left->pack_start_defaults( $hbox1 );
$vbox_left->pack_start_defaults( $hbox2 );
$vbox_left->pack_start_defaults( $hbox3 );
$vbox_left->pack_start_defaults( $hbox4 );
$vbox_left->pack_start_defaults( $hbox5 );
$vbox_left->pack_start_defaults( $hbox6 );

#preview
$vbox_right->pack_start_defaults( $preview );
$vbox_right->pack_start_defaults( $preview_throb );

#row 2
$hbox_row2->pack_start_defaults( $vbox_left );
$hbox_row2->pack_start_defaults( $vbox_right );

$vbox_param->pack_start( $hbox_row1, TRUE, TRUE, 5 );
$vbox_param->pack_start( $hbox_row2, TRUE, TRUE, 5 );

$vbox_main->pack_start( $vbox_param, FALSE, TRUE, 5 );
$vbox_main->pack_start( $refresh_btn, TRUE, TRUE, 5 );

$hbox_btn->pack_start( $cancel_btn, TRUE, TRUE, 5 );
$hbox_btn->pack_start( $save_btn,   TRUE, TRUE, 5 );

$vbox_main->pack_start( $hbox_btn,  TRUE, TRUE, 5 );

$plug->add( $vbox_main );

$plug->show_all;

#hide the preview widget at startup 
$preview->hide_all;

#create tempfile
my ( $tmpfh, $tmpfilename ) = tempfile();
#png format
$tmpfilename .= ".png";

#we fork a child process to do the work
my $process = Proc::Simple->new;
#create tempfiles for subprocess outputs
my ( $tmpfh_stdout, $tmpfilename_stdout ) = tempfile(UNLINK => 1);
my ( $tmpfh_stderr, $tmpfilename_sterr ) = tempfile(UNLINK => 1);
$process->redirect_output ($tmpfilename_stdout, $tmpfilename_sterr);

#generate first preview at startup
Glib::Idle->add (
	sub{
		&fct_imagemagick_3Drotate( undef, 'refresh' );
		return FALSE;	
	}, 
undef);

#lets'start
Gtk2->main;


sub fct_imagemagick_3Drotate {
	my ( $widget, $data ) = @_;

	$save_btn->set_sensitive(FALSE);

	if ( $data eq 'save' ) {

		my $image = Image::Magick->new;
		$image->ReadImage( $tmpfilename );
		$image->WriteImage( filename => $filename );

		#delete temp files
		unlink $tmpfilename;
		
		Gtk2->main_quit;
		return TRUE;
	} else {

		$preview->hide_all;
		$preview_throb->show_all;

		$process->start(
			sub {
				&apply_effect();
				POSIX::_exit(0);
			}
		);

		#so we can update the gui
		while ( $process->poll ) {
			&fct_update_gui;
			usleep 100000;
		}
		
		eval{
			$preview->set_from_pixbuf( Gtk2::Gdk::Pixbuf->new_from_file_at_scale(
										$tmpfilename, 300, 300, TRUE)
								 	 );
		};
		#error -> read output from tempfiles
		if($@){		
			#store error here
			my $error_string = "Plugin error:\n";
			
			#reading stdout from file
			while (<$tmpfh_stdout>){
				$error_string .= $_;	
			}
			#reading stderr from file
			while (<$tmpfh_stderr>){
				$error_string .= $_;	
			}

			#get the parent window of the plug
			require X11::Protocol;
			my $x11 = X11::Protocol->new( $ENV{ 'DISPLAY' } );
			
			my $plugp = Gtk2::Gdk::Window->foreign_new( &find_wm_window( $x11, $plug->get_id ));
			
			#show error message
			my $shutter_dialog = Shutter::App::SimpleDialogs->new($plug, $plugp);
			$shutter_dialog->dlg_error_message( 
				sprintf ( $d->get(  "Error while executing plugin %s." ), "'" . $plugin_info{'name'} . "'" ) ,
				$d->get( "There was an error executing the plugin." ),
				undef, undef, undef,
				undef, undef, undef,
				$error_string
			);
			
			#delete temp files
			unlink $tmpfilename;

			Gtk2->main_quit;
			exit 1;	
		}

		$save_btn->set_sensitive(TRUE);
		$preview->show_all;
		$preview_throb->hide_all;

		return TRUE;
	}
}

sub apply_effect {	

	my $bcolor = $back_color->get_color;
	my $scolor = $sky_color->get_color;

	#auto value
	my $auto = "";
	$auto = " auto=".$auto_combo->get_active_text if $auto_combo->get_active_text ne "off";

	#check if script is executable
	unless ( -x $Bin."/3Drotate"){
		my $changed = chmod(0755, $Bin."/3Drotate");
		unless($changed){
			print "\nERROR: $Bin./3Drotate is not executable\n";
			Gtk2->main_quit;
		}
	}
	
	#execute imagemagick command		
	system(   $Bin."/3Drotate"
			. " pan="
			. $pan_sbutton->get_value
			. " tilt="
			. $tilt_sbutton->get_value
			. " roll="
			. $roll_sbutton->get_value
			. " pef="
			. $pef_sbutton->get_value										
			. " zoom="
			. $zoom_sbutton->get_value
			. $auto
			. " bgcolor='"
			. sprintf( "#%04x%04x%04x%04x", $bcolor->red, $bcolor->green, $bcolor->blue, $back_color->get_alpha )
			. "' skycolor='"
			. sprintf( "#%04x%04x%04x%04x", $scolor->red, $scolor->green, $scolor->blue, $sky_color->get_alpha )													
			. "' '$filename'"
			. " $tmpfilename" );
	
}

sub fct_update_gui {

	while ( Gtk2->events_pending ) {
		Gtk2->main_iteration;
	}
	Gtk2::Gdk->flush;

	return TRUE;
}

sub find_wm_window {
	my $x11  = shift;
	my $xid  = shift;

	do {
		my ( $qroot, $qparent, @qkids ) = $x11->QueryTree($xid);
		return undef unless ( $qroot || $qparent );
		return $xid if ( $qroot == $qparent );
		$xid = $qparent;
	} while (TRUE);
}

1;

