#!/bin/perl
use strict;
use warnings;
use Gtk2 "-init";

my %PixCache;

my $path=$ARGV[0]||$ENV{PWD};

my $window=Gtk2::Window->new;
my $vscroll=Gtk2::VScrollbar->new;
my $mosaic=Mosaic->new(\&select,\&get_data,\&makepixbuf,\&activate,\&menupopup,$vscroll);
my $hbox=Gtk2::HBox->new;
$hbox->pack_start($mosaic,1,1,2);
$hbox->pack_start($vscroll,0,0,2);
$window->add($hbox);
$window->show_all;


Gtk2->main;

sub get_data
{	opendir my($dh),$path;
	my @l=grep m/\.jpg$/i, readdir $dh;
	closedir $dh;
	return \@l;
}

sub select
{	warn "select @_\n";
}
sub activate
{	warn "activate @_\n";
}

sub menupopup
{	warn "menu @_\n"
}

sub makepixbuf
{	my ($key,$width,$height)=@_;
	my $pixbuf=$PixCache{$key};
	my $purge;
	unless ($pixbuf)
	{	if (defined $width)
		{	$pixbuf=$PixCache{$key}= Gtk2::Gdk::Pixbuf->new_from_file_at_scale($path.'/'.$key, $width, $height,1);
			$purge=1 if keys %PixCache >120;
		}
		else {return undef}
	}
	$pixbuf->{lastuse}=time;
	if ($purge) #should be done in an idle
	{	my $nb= keys(%PixCache)-100;
		delete $PixCache{$_} for (sort {$PixCache{$a}{lastuse} <=> $PixCache{$b}{lastuse}} keys %PixCache)[0..$nb];
	}
	return $pixbuf;
}

package Mosaic;
use Gtk2;
use base 'Gtk2::DrawingArea';

use constant
{	XPAD => 2,	YPAD => 2,
	TRUE => 1,	FALSE =>0,
};

sub new
{	my ($class,$selectsub,$getdatasub,$makepixbufsub,$activatesub,$menupopupsub,$vscroll)=@_;
	my $self = bless Gtk2::DrawingArea->new, $class;
	$self->can_focus(TRUE);
	$self->add_events([qw/button_press_mask button_release_mask pointer-motion-mask leave-notify-mask/]);
	$self->{vscroll}=$vscroll;
	$vscroll->get_adjustment->signal_connect(value_changed => sub {$self->queue_draw});
	$self->signal_connect(scroll_event	=> \&scroll_event_cb);
	$self->signal_connect(expose_event	=> \&expose_cb);
	$self->signal_connect(focus_out_event	=> \&focus_change);
	$self->signal_connect(focus_in_event	=> \&focus_change);
	$self->signal_connect(configure_event	=> \&configure_cb);
	$self->signal_connect(drag_begin	=> \&drag_begin_cb);
	$self->signal_connect(button_press_event=> \&button_press_cb);
	$self->signal_connect(button_release_event=> \&button_release_cb);
	$self->signal_connect(key_press_event	=> \&key_press_cb);
	$self->signal_connect(realize	=> sub { $_[0]->Fill });
	#$self->signal_connect(motion_notify_event=> \&start_tooltip);
	#$self->signal_connect(leave_notify_event=> \&abort_tooltip);
	$self->{selectsub}=$selectsub;
	$self->{get_fill_data_sub}=$getdatasub;
	$self->{activatesub}=$activatesub;
	$self->{menupopupsub}=$menupopupsub;
	$self->{makepixbuf}=$makepixbufsub;
	return $self;
}

sub get_selected
{	sort keys %{$_[0]{selected}};
}
sub reset_selection
{	$_[0]{selected}={};
	$_[0]{lastclick}=undef;
	$_[0]{startgrow}=undef;
}
sub drag_begin_cb
{	$_[0]->{pressed}=undef;
}
sub button_press_cb
{	my ($self,$event)=@_;
	$self->grab_focus;
	my $but=$event->button;
	my $selected=$self->{selected};
	if ($but==1 && $event->type eq '2button-press')
	{	$self->{activatesub}->(keys %$selected);
		return 1;
	}
	if ($but==1)
	{	my ($i,$j,$key)=$self->coord_to_index($event->get_coords);
		return 0 unless defined $j;
		if ( $event->get_state * ['shift-mask', 'control-mask'] || !exists $selected->{$key} )
			{ $self->key_selected($event,$i,$j);}
		else	{ $self->{pressed}=1; }
		return 0;
	}
	if ($but==3)
	{	my ($i,$j,$key)=$self->coord_to_index($event->get_coords);
		if (defined $key && !exists $selected->{$key})
		{	$self->key_selected($event,$i,$j);
		}
		$self->{menupopupsub}->($event,[keys %$selected]);
		return 1;
	}
	1;
}
sub button_release_cb
{	my ($self,$event)=@_;
	return 0 unless $event->button==1 && $self->{pressed};
	$self->{pressed}=undef;
	my ($i,$j)=$self->coord_to_index($event->get_coords);
	return 0 unless defined $j;
	$self->key_selected($event,$i,$j);
	return 1;
}

sub Fill
{	my ($self,$samelist)=@_;
	my $list=$self->{list};
	$self->{list}=$list= $self->{get_fill_data_sub}->() unless $samelist;
	my $window=$self->window;

	#unless ($window) { $self->{delayed}||=Glib::Timeout->add(200,\&Fill,$self); warn 2; return 1 }
	#Glib::Source->remove( $self->{delayed} ) if $self->{delayed};warn 3;
	#delete $self->{delayed};

	my ($width,$height)=$window->get_size;
	$self->{width}=$width;#warn "$self : filling with width=$width\n";

	$self->{hsize}=64;
	$self->{vsize}=128;

	my $nw= int($width / ($self->{hsize}+2*XPAD)) || 1;
	my $nh= int(@$list/$nw);
	my $nwlast= @$list % $nw;
	$nh++ if $nwlast;
	$nwlast=$nw unless $nwlast;
	$self->{dim}=[$nw,$nh,$nwlast];
	#$self->set_size_request(-1,$nh*($self->{vsize}+2*YPAD));
	$self->{maxy}= $nh*($self->{vsize}+2*YPAD);
	$self->{viewwindowsize}=[$self->window->get_size];
	$self->update_scrollbar;
	$self->queue_draw;
	#$self->start_tooltip;
}
sub update_scrollbar
{	my $self=$_[0];
	my $scroll= $self->{vscroll};
	my $pagesize=$self->{viewwindowsize}[1]||0;
	my $upper=$self->{maxy}||0;
	my $adj=$scroll->get_adjustment;
	my $oldpos= $adj->upper ? ($adj->page_size/2+$adj->value) / $adj->upper : 0;
	$adj->page_size($pagesize);
	if ($upper>$pagesize)	{$scroll->show; $adj->upper($upper); $scroll->queue_draw; }
	else			{$scroll->hide; $adj->upper(0);}
	$adj->step_increment($pagesize*.125);
	$adj->page_increment($pagesize*.75);
	my $newval= $oldpos*$adj->upper - $adj->page_size/2;
	$newval=$adj->upper-$pagesize if $newval > $adj->upper-$pagesize;
	$adj->set_value($newval);
}
sub scroll_event_cb
{	my ($self,$event,$pageinc)=@_;
	my $dir= ref $event ? $event->direction : $event;
	$dir= $dir eq 'up' ? -1 : $dir eq 'down' ? 1 : 0;
	return unless $dir;
	my $adj=$self->{vscroll}->get_adjustment;
	my $max= $adj->upper - $adj->page_size;
	my $value= $adj->value + $dir* ($pageinc? $adj->page_increment : $adj->step_increment);
	$value=$max if $value>$max;
	$value=0 if $value<0;
	#FIXME copy part that is still visible and queue_draw only what has changed
	$adj->set_value($value);
	1;
}

#sub show_tooltip
#{	my $self=$_[0];
#	Glib::Source->remove(delete $self->{tooltip_t}) if $self->{tooltip_t};
#	$self->{tooltip_t}=Glib::Timeout->add(5000, \&abort_tooltip,$self);
#
#	my ($window,$px,$py)=Gtk2::Gdk::Display->get_default->get_window_at_pointer;
#	return 0 unless $window && $window==$self->window;
#	my ($i,$j,$key)=$self->coord_to_index($px,$py);
#	return 0 unless defined $key;
#	my $win=$self->{tooltip_w}=Gtk2::Window->new('popup');
#	#$win->{key}=$key;
#	#$win->set_border_width(3);
#	my $label=Gtk2::Label->new;
#	$label->set_markup(::ReplaceAAFields($key,"<b>%a</b>%Y\n<small>%s <small>%l</small></small>",$self->{col},1));
#	my $request=$label->size_request;
#	my ($x,$y,$w,$h)=$self->index_to_rect($i,$j);
#	my ($rx,$ry)=$self->window->get_origin;
#	$x+= $rx + $w/2 - $request->width/2;
#	$y+= $ry + $h+YPAD+1;
#
#	my $screen=$self->get_screen;
#	my $monitor=$screen->get_monitor_at_window($self->window);
#	my (undef,undef,$xmax,$ymax)=$screen->get_monitor_geometry($monitor)->values;
#	$xmax-=$request->width;
#	$ymax-=$request->height;
#	$x=$xmax if $x>$xmax;
#	$y-=$h+$request->height if $y>$ymax;
#
#	my $frame=Gtk2::Frame->new;
#	$frame->add($label);
#	$win->add($frame);
#	$win->move($x,$y);
#	$win->show_all;
#	return 0;
#}

#sub start_tooltip
#{	my ($self,$event)=@_;
#	my $timeout= $self->{tooltip_browsemode} ? 100 : 1000;
#	$self->abort_tooltip;
#	$self->{tooltip_t}=Glib::Timeout->add($timeout, \&show_tooltip,$self);
#	return 0;
#}
#sub abort_tooltip
#{	my $self=$_[0];
#	Glib::Source->remove(delete $self->{tooltip_t}) if $self->{tooltip_t};
#	if ($self->{tooltip_w})
#	{	$self->{tooltip_browsemode}=1;
#		Glib::Source->remove($self->{tooltip_t2}) if $self->{tooltip_t2};
#		$self->{tooltip_t2}=Glib::Timeout->add(500, sub{$_[0]{tooltip_browsemode}=$_[0]{tooltip_t2}=0;} ,$self);
#		$self->{tooltip_w}->destroy;
#	}
#	$self->{tooltip_w}=undef;
#	0;
#}

sub configure_cb
{	my ($self,$event)=@_;
	return unless $self->{width};
	$self->{viewwindowsize}=[$event->width,$event->height];
	my $iw= $self->{hsize}+2*XPAD;
	if ( int($self->{width}/$iw) == int($event->width/$iw))
	{	$self->update_scrollbar;
		return;
	}
	$self->reset;
	$self->Fill(1);
}

sub expose_cb
{	my ($self,$event)=@_;
	my ($exp_x1,$exp_y1,$exp_x2,$exp_y2)=$event->area->values;
	$exp_x2+=$exp_x1; $exp_y2+=$exp_y1;
	my $dy=$self->{vscroll}->get_adjustment->value;
	#$self->start_tooltip if exists $self->{lastdy} && $self->{lastdy}!=$dy;
	$self->{lastdy}=$dy;
	my $window=$self->window;
	my $style=$self->get_style;
	#my ($width,$height)=$window->get_size;
	#warn "expose_cb : $width,$height\n";
	my $state= $self->state eq 'insensitive' ? 'insensitive' : 'normal';
	my $sstate=$self->has_focus ? 'selected' : 'active';
	#my $gc= $style->text_gc($state);
	my $bgc= $style->base_gc($state);
	#my $sgc= $style->text_gc($sstate);
	my $sbgc= $style->base_gc($sstate);
	$window->draw_rectangle($bgc,TRUE,$event->area->values); #clear the area with the base bg color
	#$style->paint_flat_box( $window,$state,'none',$event->area,$self,'',$event->area->values);

	return unless $self->{list};
	my ($nw,$nh,$nwlast)=@{$self->{dim}};
	my $list=$self->{list};
	my $vsize=$self->{vsize};
	my $hsize=$self->{hsize};
	my $i1=int($exp_x1/($hsize+2*XPAD));
	my $i2=int($exp_x2/($hsize+2*XPAD));
	my $j1=int(($dy+$exp_y1)/($vsize+2*YPAD));
	my $j2=int(($dy+$exp_y2)/($vsize+2*YPAD));
	$i2=$nw-1 if $i2>=$nw;
	$j2=$nh-1 if $j2>=$nh;
	for my $j ($j1..$j2)
	{	my $y=$j*($vsize+2*YPAD)+YPAD - $dy;  #warn "j=$j y=$y\n";
		$i2=$nwlast-1 if $j==$nh-1;
		for my $i ($i1..$i2)
		{	my $pos=$i+$j*$nw;
			#last if $pos>$#$list;
			my $key=$list->[$pos];
			my $x=$i*($hsize+2*XPAD)+XPAD;
			my $state=$state;
			if (exists $self->{selected}{$key})
			{	$window->draw_rectangle($sbgc,1,$x-XPAD(),$y-YPAD(),$hsize+XPAD*2,$vsize+YPAD*2);
				#$state=$sstate;
				#$style->paint_flat_box( $window,$state,'none',$event->area,$self,'',
				#			$x-XPAD(),$y-YPAD(),$hsize+XPAD*2,$vsize+YPAD*2 );
			}
			#$window->draw_rectangle($style->text_gc($state),1,$x+20,$y+20,24,24); #DEBUG
			my $pixbuf= $self->{makepixbuf}->($key);
			if ($pixbuf) { $self->drawpic($x,$y,$pixbuf) }
			#elsif (defined $pixbuf)
			else
			{	#warn "add idle\n" unless $self->{idle};
				$self->{idle}||=Glib::Idle->add(\&idle,$self);
				$self->{window}||=$window;
				$self->{queue}{$i+$j*$nw}=[$x,$y+$dy,$key];
			}
#			else
#			{	my $layout=Gtk2::Pango::Layout->new( $self->create_pango_context );
#				#$layout->set_text($key);
#				$layout->set_markup('<small>'.Glib::Markup::escape_text($key).'</small>');
#				$layout->set_wrap('word-char');
#				$layout->set_width($hsize * Gtk2::Pango->scale);
#				$layout->set_height($vsize * Gtk2::Pango->scale);
#				$layout->set_ellipsize('end');
#				$style->paint_layout($window, $state, 1,
#					Gtk2::Gdk::Rectangle->new($x,$y,$hsize,$vsize), $self, undef, $x, $y, $layout);
#			}
		}
	}
	1;
}

sub focus_change
{	my $self=$_[0];
	my $sel=$self->{selected};
	return unless keys %$sel;
	#FIXME could redraw only selected keys
	$self->queue_draw;
}

sub coord_to_index
{	my ($self,$x,$y)=@_;
	$y+=$self->{vscroll}->get_adjustment->value;
	my ($nw,$nh,$nwlast)=@{$self->{dim}};
	my $i=int($x/($self->{hsize}+2*XPAD));
	return undef if $i>=$nw;
	my $j=int($y/($self->{vsize}+2*YPAD));
	return undef if $j>=$nh;
	return undef if $j==$nh-1 && $i>=$nwlast;
	my $key=$self->{list}[$i+$j*$nw];
	return $i,$j,$key;
}
sub index_to_rect
{	my ($self,$i,$j)=@_;
	my $x=$i*($self->{hsize}+2*XPAD)+XPAD;
	my $y=$j*($self->{vsize}+2*YPAD)+YPAD;
	$y-=$self->{vscroll}->get_adjustment->value;
	return $x,$y,$self->{hsize},$self->{vsize};
}

sub key_selected
{	my ($self,$event,$i,$j)=@_;
	$self->scroll_to_row($j);
	my ($nw)=@{$self->{dim}};
	my $list=$self->{list};
	my $pos=$i+$j*$nw;
	my $key=$list->[$pos];
	unless ($event->get_state >= ['control-mask'])
	{	$self->{selected}={};
	}
	if ($event->get_state >= ['shift-mask'] && defined $self->{lastclick})
	{	$self->{startgrow}=$self->{lastclick} unless defined $self->{startgrow};
		my $i1=$self->{startgrow};
		my $i2=$pos;
		($i1,$i2)=($i2,$i1) if $i1>$i2;
		$self->{selected}{ $list->[$_] }=undef for $i1..$i2;
	}
	elsif (exists $self->{selected}{$key})
	{	delete $self->{selected}{$key};
		delete $self->{startgrow};
	}
	else
	{	$self->{selected}{$key}=undef;
		delete $self->{startgrow};
	}
	$self->{lastclick}=$pos;
	$self->queue_draw;
	$self->{selectsub}->([keys %{$self->{selected}}]);
}

sub scroll_to_row
{	my ($self,$j)=@_;
	my $y1=$j*($self->{vsize}+2*YPAD)+YPAD;
	my $y2=$y1+$self->{vsize};
	$self->{vscroll}->get_adjustment->clamp_page($y1,$y2);
}

sub key_press_cb
{	my ($self,$event)=@_;
	my $key=Gtk2::Gdk->keyval_name( $event->keyval );
	if ( $key eq 'space' || $key eq 'Return' )
	{	&{ $self->{activatesub} }($self);
		return 1;
	}
	my $pos=0;
	$pos=$self->{lastclick} if $self->{lastclick};
	my ($nw,$nh,$nwlast)=@{$self->{dim}};
	my $page= int($self->{vscroll}->get_adjustment->page_size / ($self->{vsize}+2*YPAD));
	my $i=$pos % $nw;
	my $j=int($pos/$nw);
	if	($key eq 'Left')	{$i--}
	elsif	($key eq 'Right')	{$i++}
	elsif	($key eq 'Up')		{$j--}
	elsif	($key eq 'Down')	{$j++}
	elsif	($key eq 'Home')	{$i=$j=0; }
	elsif	($key eq 'End')		{$i=$nwlast-1; $j=$nh-1;}
	elsif	($key eq 'Page_Up')	{ $j-=$page; }
	elsif	($key eq 'Page_Down')	{ $j+=$page; }
	else {return 0}
	if	($i<0)		{$j--; $i= $j<0 ? 0 : $nw-1}
	elsif	($i>=$nw)	{$j++; $i= $j>=$nh ? $nwlast-1 : 0 }
	if	($j<0)		{$j=0;$i=0}
	elsif	($j>=$nh-1)	{$j=$nh-1; $i=$nwlast-1 }
	$self->key_selected($event,$i,$j);
	return 1;
}

sub reset
{	my $self=$_[0];
	#delete $self->{list};
	delete $self->{queue};
	Glib::Source->remove( $self->{idle} ) if $self->{idle};
	delete $self->{idle};
}

sub idle
{	my $self=$_[0];#warn " ...idle...\n";
	{	last unless $self->{queue} && $self->mapped;
		my ($y,$ref)=each %{ $self->{queue} };
		last unless $ref;
		delete $self->{queue}{$y};
		$self->delayed_draw(@$ref);
		last unless scalar keys %{ $self->{queue} };
		return 1;
	}
	delete $self->{queue};
	delete $self->{window};#warn "...idle END\n";
	return $self->{idle}=undef;
}

sub delayed_draw
{	my ($self,$x,$y,$key)=@_;
	my $vadj=$self->{vscroll}->get_adjustment;
	my $dy=$vadj->get_value;
	my $page=$vadj->page_size;
	return if $dy > $y+$self->{vsize} || $dy+$page < $y; #no longer visible
	my $pixbuf= $self->{makepixbuf}->($key,$self->{hsize},$self->{vsize});
	$self->drawpic($x,$y-$dy,$pixbuf);
}

sub drawpic
{	my ($self,$x,$y,$pixbuf)=@_;
	my $offy=int(($self->{vsize}-$pixbuf->get_height)/2);#center pic
	my $offx=int(($self->{hsize}-$pixbuf->get_width )/2);
	my $window=$self->window;
	my $gc=Gtk2::Gdk::GC->new($window);
	$window->draw_pixbuf( $gc, $pixbuf,0,0,	$x+$offx, $y+$offy,-1,-1,'none',0,0);
}

