#!/usr/bin/perl -w -I.

=head1 NAME

bloksi - Sliding block puzzle.

=head1 SYNOPSIS

bloksi [puzzle file]

=head1 DESCRIPTION

bloksi is a sliding block puzzle : push the blocks around, using the
mouse. The goal is to put some of the blocks in a certain
position. The goal position is shown by pressing the 'goal' button on
the menubar.

Puzzles can be loaded  using the 'file' menu. 

Puzzles can be saved in their current state with the history of moves,
using the 'file-E<gt>save' and 'file-E<gt>save_as' menus. The default
place for saving puzzles is in a C<$HOME/.bloksi> directory that is
created the first time bloksi is run.

=head1 Bloksi ressembles Glotski? 

Right! bloksi is almost a clone of the game 'glotski' by Martin Hock
(http://gfpoken.bigw.org/glotski/). As far as I know, the glotski
puzzles can be loaded by bloksi. glotski loads puzzles saved by
bloksi, but without the history of moves.

=head1 CREATING PUZZLE

Puzzle creation is documented in the L<bloksi_format> man page.

=head1 AUTHOR

Etienne Grossmann E<lt>etienne@isr.ist.utl.ptE<gt>

=head1 HOMEPAGE 

http://anonimo.isr.ist.utl.pt/~etienne/bloksi

=cut

$appname = "bloksi" ;
#  unlink("$ENV{HOME}/.gnome/$appname") if 
#      -w "$ENV{HOME}/.gnome/$appname" ;

use strict 'vars';
use vars qw/ $app $fs $appname $initfile $bar $about $already_opening /;
## use strict;
use Bloksi ;
use Gtk;
use Gtk::Gdk::ImlibImage ;
use Gnome;
use Bloksi::CanvasBloksi ;
use BloksiPath ;
## print "datapath = '$Bloksi::datapath'\n" ;

init Gnome $appname, "$Bloksi::VERSION";
## init Gtk ;
## init Gtk::Gdk::ImlibImage ;

## Global variables :
##
## $app   : Gnome::App with a "canvas" field returned by bloksi_root
##
## $app->{canvas}->{blok} is a Bloksi object that represents the
##          puzzle itself
##
## $appname  : string        : Name of the gnome app (="bloksi")
## $initfile : string        : Name of initial file
## $bar      : Gnome::AppBar
## $about    : Gnome::About
##
##
## $fs    : Gtk::FileSelection created by callbacks for load/save
## 
## $already_opening : int   : Set while a file selection widget is
##                            open. No new file selection widget will
##                            get opened while this variable is true.

my $app = new Gnome::App $appname, $appname;

$app->signal_connect("destroy", sub {
    destroy $app ;
    bloksi_quit ();
} );
############################# Add menus ##############################
$app->create_menus(		
		   {type => 'subtree',
		    label => '_Game',
		    subtree => [{type        => 'item',
				 label       => '_Restart',
				 pixmap_type => 'stock',
				 pixmap_info => 'Menu_Refresh',
				 callback    =>  \&restart_callback
				 },
				{type        => 'item',
				 label       => '_Undo',
				 pixmap_type => 'stock',
				 pixmap_info => 'Menu_Undo',
				 callback    =>  \&undo_callback
				 },
				{type        => 'item',
				 label       => '_Redo',
				 pixmap_type => 'stock',
				 pixmap_info => 'Menu_Redo',
				 callback    =>  \&redo_callback
				 }
				]},
		   {type => 'subtree',
		    label => '_File',
		    subtree => [{type        => 'item',
				 label       => '_Open Stock',
				 pixmap_type => 'stock',
				 pixmap_info => 'Menu_New',
				 callback    => \&open_stock_callback
				 },
				{type        => 'item',
				 label       => '_Open Mine',
				 pixmap_type => 'stock',
				 pixmap_info => 'Menu_New',
				 callback    => \&open_mine_callback
				 },
				{type        => 'item',
				 label       => '_Save',
				 pixmap_type => 'stock',
				 pixmap_info => 'Menu_Save',
				 callback    => \&save_callback
				 },
				{type        => 'item',
				 label       => '_Save as',
				 pixmap_type => 'stock',
				 pixmap_info => 'Menu_Save As',
				 callback    => \&save_as_callback
				 },
				{type        => 'item',
				 label       => 'E_xit',
				 pixmap_type => 'stock',
				 pixmap_info => 'Menu_Quit',
				 callback    => \&bloksi_quit}]},
		   {type => 'subtree',
		    label => '_Help',
		    subtree => [{type        => 'item', 
				 label       => '_About',
				 pixmap_type => 'stock',
				 pixmap_info => 'Menu_About',
				 callback    => \&about_callback
				 },
#                               {type        => 'item', 
#  				 label       => '_Info',
#  				 pixmap_type => 'stock',
#  				 pixmap_info => 'Help',
#  				 callback    => \&info_callback
#				 }
				]}
		   );

############################# Add toolbar ############################
$app->create_toolbar({type        => 'item', 
		      label       => 'Restart',
		      hint        => "Start puzzle over again",
		      pixmap_type => 'stock', 
		      pixmap_info => 'Refresh',
		      callback    =>  \&restart_callback
		      },
		     {type        => 'separator' 
		      },
		     {type        => 'item', 
		      label       => 'Undo',
		      pixmap_type => 'stock', 
		      pixmap_info => 'Undo',
		      callback    =>  \&undo_callback
		      },
		     {type        => 'item', 
		      label       => 'Redo',
		      pixmap_type => 'stock', 
		      pixmap_info => 'Redo',
		      callback    =>  \&redo_callback
		      },
		     {type        => 'item', 
		      label       => 'Target',
		      pixmap_type => 'filename', 
		      # Boy is it hard to install that pixmap!
		      pixmap_info => "bloksi_target.xpm",
		      hint        => "Show the target position",
		      callback    =>  \&target_callback
		      },
		     );

############################# Add canvas #############################

if (@ARGV && -r $ARGV[0]) {	# Read file from command line
  $initfile = shift @ARGV;
  ## print "Loading $initfile\n";
  conf_set_string ("lastfile", $initfile);
  my ($p,$f) = path_file ($initfile);
  conf_set_string ("lastloadpath", $p);
} else {			# Read last loaded file
  ## print "Getting last file\n";
  $initfile = full_last_file ();
  $initfile = "" unless  defined ($initfile) && -r $initfile;
}
if ($initfile) {
  $app->{canvas} = bloksi_root ($initfile, $app);
  $app->set_contents( $app->{canvas} );

  my ($p,$f) = path_file ($initfile);

  $app->set_title ($f);
} else {
  open_stock_callback ($app);
}

$bar = new Gnome::AppBar 0,1,"user" ;
$bar->set_status("   Welcome   ");

## print join ", ",keys( %Gnome::AppBar:: ),"\n";
$app->set_statusbar( $bar );
$app->{bar} = $bar ;

show $app;

main Gtk; ####################### Last point reached #################

########################### Menu Stuff ###############################

## open_stock opens in $Bloksi::datapath,
## open_mine looks for 'lastloadpath' in config and opens there. Saves
## lastloadpath after, too.

				# wrapper for open_both. Open stock
				# game
sub open_stock_callback {
  open_both (@_,"$Bloksi::datapath/",0);
}
				# wrapper for open_both. Open game
				# from personal dir
sub  open_mine_callback {		
  my $path = conf_get_string ("lastsavepath");
  $path =  conf_get_string ("lastloadpath") unless defined $path;
  $path = "$Bloksi::personalpath" unless defined $path;
  open_both (@_,$path,1);
}

## Pop up the open file menu. The actual loading is done by select_file.
##
## open_both ($app, $path, $save_yes_no)
BEGIN {$already_opening = 0;}
sub open_both 
{
    my ($app,$path,$save) = @_;

    return if $already_opening;

    $already_opening = 1;

    $path .= "/" unless $path =~ m|/$|;
    $fs = new Gtk::FileSelection ("Select .lev file");
    $fs->set_filename($path);
    $fs->complete("*.lev");

    map {$_->[0]->signal_connect("clicked",$_->[1],$fs)}
    [$fs->ok_button,
     sub {
       my $fp = select_file ($_[1]->get_filename());
       if ($fp) {
	 $fs->destroy();
	 $already_opening = 0;
	 conf_set_string ("lastfile", $fp); # Always save file

	 if ($save) {		# Only save path if asked (e.g. not
                                # for stock)
	   my ($p) = path_file ($fp);
	   conf_set_string ("lastloadpath",$p);
	 }
       }
     }],
    [$fs->cancel_button, 
    sub {
      $fs->destroy();
      $already_opening = 0;
    }] ;
 
    $fs->signal_connect (destroy => sub {
			   $fs->destroy();
			   $already_opening = 0;
			 });
    $fs->show();
}

# select_file : After a filename has been selected, try to load
# it. Return the filename if successful, "" otherwise
sub select_file			
{
  
  my $f = shift ;
  my $fs = shift if @_ ;
  
  # print "You've selected '$f'\n";
  my $errmsg = ($f eq "" || -r $f) ? "" : "not a readable file" ;
  unless( $errmsg ){
    # print "That's a nice file.\n";
    my $canvas = bloksi_root ($f, $app );
    if ( defined ($canvas->{blok}) ) {
      $app->{canvas} = $canvas ;
      $app->set_contents ($canvas) ;

      my ($fp,$fn) = path_file ($f);
      $app->set_title ($fn);

      show_status();
    } else {
      $errmsg = "not a valid puzzle" ;
    }
  }
  if ( $errmsg ) {
    Gnome::MessageBox->new
      ("Can't open '$f' : $errmsg. Try again.",
       # types are : "info","error","warning","generic","question"
       "warning", 
       "Okey dokey, I'll try again.")->show ;
    return "";
  }
  return $f;
}

## Pops up a menu for saving the current game. Looks in config for
## lastsavepath. Sets lastsavepath and lastfile
sub save_as_callback {
  ## my $app = shift ;
  ## print join ",",@_,"\n" ;
  return if $already_opening;
  $already_opening = 1;

  my $path;
  $path = conf_get_string ("lastsavepath");
  $path = "$Bloksi::personalpath" unless defined $path;
  $path .= "/" unless $path =~ m|/$|;

				# Propose a filename #################
  my $proposed_name = conf_get_string ("lastfile");
				# Remove path from last file
  $proposed_name =~ s{.*/([^/]+)}{$1};
  $proposed_name = $path . $proposed_name;

				# If it already exists, find a derived
				# name
  if (-e $proposed_name) {

      my ($base,$vnum,$ext);

				# Looks like there's a version number
      (($base,$vnum,$ext) = $proposed_name =~ m!^(.*)\.(\d+)(\..*|)$!)

				# No version number, but extension
	|| (($base,$ext)   = $proposed_name =~ m|^(.*)(\.[^\.]*)$|)

				# No nuthin
	  ||  (($base)     = $proposed_name =~ m|^(.*)$|) ;

      $vnum = 1  unless defined $vnum;
      $ext  = "" unless defined $ext;

      print "Trying $base . $vnum  $ext\n";

				# Find next available version number
      while (-e "$base.$vnum$ext") { $vnum++}
      
      $proposed_name = "$base.$vnum$ext";
  }
  print "proposing $proposed_name\n";
  $fs = new Gtk::FileSelection("Select .lev file");
  $fs->complete("*.lev");
  $fs->set_filename ($proposed_name);
  ## $fs->hide_fileop_buttons;
  ## $fs->set_filename ($path);

  my $file = $app->{canvas}->{blok}->{file} ;
  
  ## $fs->complete( "$file" );
  
  map {$_->[0]->signal_connect("clicked",$_->[1],$fs)}
    [$fs->ok_button,
     sub {
       my $pf = $app->{canvas}->{blok}->{file} = $_[1]->get_filename();
       # print "Gonna save to '$f'\n";
       if( $app->{canvas}->{blok}->save() ){
	 
	 my ($p,$f)  = path_file ($pf);
	 conf_set_string ("lastsavepath", $p);
	 conf_set_string ("lastfile", $pf);
	 $app->set_title ($f);
	 
	 $fs->destroy() ;	# I'm done
	 $already_opening = 0;

       } else {		# Ask again
	 Gnome::MessageBox->new
	   ("Sorry, I can't write to '$file'. Try again.",
	    # types are : "info","error","warning","generic","question"
	    "warning", 
	    "Let me try once more.")->show ;
	 return 0 ;
       }
     }
    ],
       [$fs->cancel_button, 
	sub {
	  $fs->destroy();
	  $already_opening = 0;
	}] ;
  $fs->signal_connect (destroy => sub {
			 $fs->destroy();
			 $already_opening = 0;
		       });
  $fs->show();
}

## HERE 
## if not writeable, save_as
## 
## set lastpath and lastfile

sub save_callback {
  my $fullname = conf_get_string ("lastfile");
  my ($p,$f) = path_file ($fullname);
  unless (defined ($fullname) && -w $fullname) {
    save_as_callback (@_);
    return;
  }
  
  my $success = $app->{canvas}->{blok}->save ($fullname) ;
  if( !$success ){
    Gnome::MessageBox->new
      ("Sorry, I can't write to '$app->{canvas}->{blok}->{file}'. Try Save-As.",
       # types are : "info","error","warning","generic","question"
       "warning", 
       "Let me try once more.")->show ;
  }
  ## print "Saved '$app->{canvas}->{blok}->{file}'\n";
}

sub about_callback {		# Pop up an "about" window.
    $about = new Gnome::About 
	"bloksi", 
	"$Bloksi::VERSION", 
	"(c) 2000 Etienne Grossmann", 
	["Etienne Grossmann <etienne\@isr.ist.utl.pt>"], 
	join("\n","'bloksi' : a clone of 'blotski', by Martin Hock",
	     "Drag the blocks around to achieve the target.",
	     "The target is shown when you press the 'target' button.",
	     "This program is free software.")
    ;

$about->signal_connect(destroy => sub {$about->destroy()});
show $about;
}

sub info_callback {		# Pop up an "about" window.
  Gnome::MessageBox->new
      (q{
bloksi is a clone of the game 'blotski', by Martin Hock
(oxymoron@cmu.edu). 

Drag the blocks around to achieve the target.

The target is shown when you press the 'target' button.

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 2 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, write to the Free 
Software Foundation, Inc., 59 Temple Place, Suite 330, 
Boston, MA 02111-1307 USA
},
       "generic", "Glad to learn.")->show ; 
}

sub congratulations {
  Gnome::MessageBox->new
      ("Congratulations, you've solved the puzzle.", # Message
       "info",			# image
       "I'm so happy!")->show ;	# close button
}

sub show_status {
    my $txt = @_ ? shift : "" ;
    return unless 
	exists( $app->{canvas} ) && exists( $app->{canvas}->{blok} );
    my $msg = sprintf(" %-4d  Moves.    %s",
		      $app->{canvas}->{blok}->{nmoves},
		      $txt ) ;
    $app->{bar}->set_status( $msg );
}

sub target_callback {
    $app->{canvas}->{curtain}->show ;
    show_status("That's the goal");
    ## $app->{$shown}->update_now ;
    ## I can't just sleep, because nothing gets drawn while sleep.
    ## sleep 1 ;
    Gtk->timeout_add( 1500, 
		      sub {
			  $app->{canvas}->{curtain}->hide ;
			  show_status();
			  0; # Return false to remove timeout func
		      });
}

sub undo_callback { 
    canvas_unredo( $app->{canvas}->{blok}, "unmake" );
}
sub redo_callback { 
  canvas_unredo( $app->{canvas}->{blok}, "remake" );
}
sub restart_callback { 
    select_file ($app->{canvas}->{filename}) 
	if exists($app->{canvas}) && 
	    exists( $app->{canvas}->{filename}) ;
}
## HERE must do bloksi_quit and bloksi_save_and_quit
## the second does :
## if current file is not writeable,
##     save_as
## else
##     plain save
## 
sub bloksi_quit {
  ## printf "Bye bye\n";
  ## Gnome::Config->set_string("/bloksi/General/lastfile",
  ##                           "Foobar");
  ## Gnome::Config->sync;
  ## my $foo = Gnome::Config->get_string("/bloksi/General/lastfile");
  ## print "--- $foo --\n";
  Gtk->main_quit ;
  Gtk->exit(0) ;
}

sub conf_get_string { Gnome::Config->get_string ("/bloksi/General/" . shift) }
sub conf_set_string { 
  my ($key, $value) = @_;
  ## print "Setting $key to $value\n";
  Gnome::Config->set_string ("/bloksi/General/$key", $value);
  Gnome::Config->sync();
}

## full_last_file ("load" or "save")
sub full_last_file {  
  ## my ($lp,$lf) = map {conf_get_string($_)} ("last$_[0]path", "lastfile");
  
  my $lastfile = conf_get_string ("lastfile");
  ## if (defined ($lastfile)) {
  ## $lastfile = $lp . $lp;
  ## }
  
  ## print "full_last_file -> $lastfile\n";
  $lastfile;
}

sub path_file {			# Split full filename in path and file
  my $pf = shift;
  $pf =~ m|^(.*?)([^/]*)$|;
  ($1,$2);
}
