#!/usr/bin/perl

use strict;

use POSIX qw(strftime);
use Storable qw/nstore retrieve/;
use File::Spec;
use File::BaseDir;
use Gtk2 '-init';
use Gtk2::Gdk::Keysyms;  # key values
use Gtk2::Pango;         # pango constants
use Gtk2::HyperTextView; # custom widget
use Zim;

sub version { # Version and Copyright notice
	return << "EOT";
zim $Zim::VERSION

Copyright (c) 2005 Jaap G Karssenberg. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl.

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.

Please report bugs to pardus\@cpan.org
EOT
}

my %CONFIG = (  # Default config values
	pane_pos	=> 120,
	pane_vis	=> 0,
	icon_size	=> 'large-toolbar',
	x		=> 500,
	y		=> 350,
	default_root	=> undef,
	browser		=> undef,
	home		=> 'Home',
	date_page	=> ':Date:%Y_%m_%d',
	untitled_page	=> ':Untitled:%Y_%m_%d_%H.%M.%S',
	hist_max	=> 20,
	undo_max	=> 50,
	follow_new_link => 1,
	use_xdg_cache	=> 0,
);

# ############ #
# Program Init #
# ############ #

my ($ROOT, $PAGE, @BACK, @FORW, $UNDO, $REDO);
my (%tree_iter, $tree_loaded, $pane_visible);
my (@undo_mini, $undo_lock, $save_lock, $overwrite_toggle);
my $DATA_DIR  = find_data_dir();
my $ICON_FILE = File::Spec->catfile($DATA_DIR, qw/images zim64.png/);

my %opts = ( 'read_only' => 0 );
for (@ARGV) {
	next unless /^-/;
	if (/^(--version|-v)$/) {
		print version();
		exit;
	}
	elsif (/^--read-?only$/) { $opts{read_only}++ }
	elsif (/^--doc$/) {
		$ROOT = File::Spec->catdir($DATA_DIR, 'doc');
		$opts{doc}++;
		$opts{read_only}++;
	}
	elsif (/^--?\w/) { exit_usage() } # include --help etc.
	$_ = undef;
	last if /^--?$/;
}
@ARGV = grep {defined $_} @ARGV;

my $CONF_FILE = load_config();

$ROOT = shift @ARGV unless defined $ROOT;
$ROOT = $CONFIG{default_root} unless defined $ROOT;
exit_usage() unless defined $ROOT;

$PAGE = shift @ARGV;
exit_usage() if @ARGV;

my $CACHE_FILE = find_cache_file();
my $HOME = $CONFIG{home};

{ # clean up old history
	use File::MimeInfo qw/mimetype/;
	my $hist = File::Spec->catfile($ROOT, '.zim.history');
	if (-f $hist and mimetype($hist) =~ /^text/) {
		unlink $hist;
		print "Removed $hist\n";
	}
}

# ######## #
# GUI init #
# ######## #

my $window = Gtk2::Window->new('toplevel');
$window->set_default_size(@CONFIG{'x', 'y'});
$window->signal_connect(delete_event => \&_quit);
$window->signal_connect(destroy => sub { Gtk2->main_quit });
$window->set_icon(
	Gtk2::Gdk::Pixbuf->new_from_file($ICON_FILE) ); # FIXME pass list for better quality
$window->set_title('Zim');

my $accels = Gtk2::AccelGroup->new;
$window->add_accel_group($accels);

my $vbox = Gtk2::VBox->new(0, 0); # homogeneous, spacing
$window->add($vbox);

# Divide window

my $toolbar = Gtk2::Toolbar->new();
$toolbar->set_style('icons');
$vbox->pack_start($toolbar, 0, 0, 0); # ($child, $expand, $fill, $padding)

my $hpaned = Gtk2::HPaned->new();
$hpaned->set_position($CONFIG{pane_pos});
$hpaned->set_border_width(5);
$vbox->add($hpaned);

my $l_scroll_window = Gtk2::ScrolledWindow->new();
$l_scroll_window->set_policy('automatic', 'automatic');
$l_scroll_window->set_shadow_type('in');
$hpaned->add1($l_scroll_window);

my $r_vbox = Gtk2::VBox->new(0, 0);
$hpaned->add2($r_vbox);

my $r_scroll_window = Gtk2::ScrolledWindow->new();
$r_scroll_window->set_policy('automatic', 'automatic');
$r_scroll_window->set_shadow_type('in');
$r_vbox->add($r_scroll_window);

my $status = Gtk2::Statusbar->new;
$vbox->pack_start($status, 0, 0, 0);

# ############### #
# Create TreeView #
# ############### #

my $tree_store = Gtk2::TreeStore->new('Glib::String', 'Glib::String');
#$tree_store->set_sort_column_id(0, 'ascending');
my $tree_view = Gtk2::TreeView->new($tree_store);
my $cellrenderer = Gtk2::CellRendererText->new();
my $page_column = Gtk2::TreeViewColumn->new_with_attributes(
        'Pages', $cellrenderer, 'text', 0);
$tree_view->append_column($page_column);
$tree_view->set_headers_visible(0);
$tree_view->set_reorderable(1);
$tree_view->signal_connect(row_activated => \&page_activated);
$l_scroll_window->add($tree_view);

# some extra keybindings for the treeview
$tree_view->signal_connect(key_press_event => sub {
	my ($view, $event) = @_;
	my $val = $event->keyval();
	my $key = chr($val);
	#print "got key $key ($val)\n";
	if    ($key eq '*' ) { $view->expand_all   } # for thunderbird compat
	elsif ($key eq '\\') { $view->collapse_all } #  -idem-
	elsif ($val == $Gtk2::Gdk::Keysyms{'Left'}) {
		my ($path) = $tree_view->get_selection->get_selected_rows;
		$tree_view->collapse_row($path);
	}
	elsif ($val == $Gtk2::Gdk::Keysyms{'Right'}) {
		my ($path) = $tree_view->get_selection->get_selected_rows;
		$tree_view->expand_row($path, 0) if defined $path;
	}
	else { return 0 }
	return 1;
} );

# ############### #
# Create TextView #
# ############### #

my $htext = Gtk2::HyperTextView->new();
$htext->set_left_margin(10);
$htext->set_right_margin(5);
$htext->set_editable(0) if $opts{read_only};
$htext->signal_connect(link_clicked => sub { go($_[1]) });
$htext->signal_connect(link_enter => sub { push_status("Go to $_[1]", 'link') });
$htext->signal_connect(link_leave => sub { pop_status('link') });
$htext->signal_connect(toggle_overwrite => sub {
		$overwrite_toggle = $overwrite_toggle ? 0 : 1;
		set_status();
	} );
#$htext->signal_connect(move_cursor => sub {print "move cursor @_\n"});
$htext->set_tabs( Gtk2::Pango::TabArray->new_with_positions(
	# initial_size, pos_in_pixels, ... allign => position
	1, 0, 'left' => 40 * PANGO_SCALE ) );
$htext->{link_properties} = [foreground => 'blue']; # TextTag properties
$r_scroll_window->add($htext);

#for (qw/cut_clipboard copy_clipboard paste_clipboard/) {
#	$htext->signal_connect($_ => eval "sub { print \"$_\\n\" }");
#}

# extra key handler for our HyperTextView
$htext->signal_connect(key_press_event => sub {
	my ($self, $event) = @_;
	my $val = $event->keyval;
	if (
		$val == $Gtk2::Gdk::Keysyms{KP_Enter} or
		$val == $Gtk2::Gdk::Keysyms{Return}
	) {
		my $buffer = $self->get_buffer;
		my $iter = $buffer->get_iter_at_mark($buffer->get_insert());
		return 1 if defined $self->click_if_link_at_iter($iter);

		return parse_line($iter);
	}
	#else { printf "key %x pressed\n", $val } # perldoc -m Gtk2::Gdk::Keysyms

	return 0;
} )
	unless $opts{read_only};

Glib::Timeout->add(5000, \&save_page)
	unless $opts{read_only}; # TODO also use this timer for maintaining an undo stack

# ################# #
# Create Search bar #
# ################# #

my $find_hbox = Gtk2::HBox->new(0, 0);
$r_vbox->pack_start($find_hbox, 0, 1, 0);
#$find_hbox->set_events('focus-change-mask');
#$find_hbox->signal_connect(focus_out_event => sub {print "focus out\n"; 0 });

my $find_entry = Gtk2::Entry->new();
$find_entry->signal_connect(key_press_event => sub {
		my $val = $_[1]->keyval;
		if ($val == $Gtk2::Gdk::Keysyms{Escape}) {
			$find_hbox->hide;
			$htext->grab_focus;
		}
		else { find_string($find_entry->get_text().chr($val)) }
	} );
$find_entry->signal_connect(activate => sub {
		$find_hbox->hide;
		find_string( $find_entry->get_text );
		$htext->grab_focus;
	} );
$find_hbox->pack_start($find_entry, 0, 1, 0);

my $find_button = Gtk2::Button->new_from_stock('gtk-find');
$find_button->signal_connect(clicked =>
	sub { find_string($find_entry->get_text, 'NEXT') } );
$find_hbox->pack_start($find_button, 0, 1, 0);


# ############### #
# Toolbar Buttons #
# ############### #

my $toggle_pane_button = add_button(
	'Toggle Index', 'Alt I', 'gtk-index', sub {
		set_pane($CONFIG{pane_vis} ? 0 : 1);
		$tree_view->grab_focus if $CONFIG{pane_vis};
       });
my $expand_button = add_button(
	'Expand All', 'Ctrl +', 'gtk-zoom-in', sub { $tree_view->expand_all });
$expand_button->add_accelerator(
	'clicked', $accels, $Gtk2::Gdk::Keysyms{'KP_Add'}, 'control-mask', 'visible');
$expand_button->add_accelerator(
	'clicked', $accels, ord('='), 'control-mask', 'visible');
my $collapse_button = add_button(
	'Collapse All', 'Ctrl -', 'gtk-zoom-out', sub { $tree_view->collapse_all });
$collapse_button->add_accelerator(
	'clicked', $accels, $Gtk2::Gdk::Keysyms{'KP_Subtract'}, 'control-mask', 'visible');

$toolbar->append_space;
my $home_button = add_button(
	'Home', 'Alt Home', 'gtk-home', sub { go(':'.$HOME) });
my $back_button = add_button(
	'Go Back', 'Alt Left', 'gtk-go-back', \&go_back);
$back_button->set_sensitive(0);
my $forw_button = add_button(
	'Go Forward', 'Alt Right', 'gtk-go-forward', \&go_forw);
$forw_button->set_sensitive(0);

$toolbar->append_space;

unless ($opts{read_only}) { # Style menu  # FIXME better menu layout
	my $style_menu = Gtk2::Menu->new;
	for (
		[ 'Head 1   (Ctrl 1)' => sub { apply_tag('head1')     } ],
		[ 'Head 2   (Ctrl 2)' => sub { apply_tag('head2')     } ],
		[ 'Head 3   (Ctrl 3)' => sub { apply_tag('head3')     } ],
		[ 'Head 4   (Ctrl 4)' => sub { apply_tag('head4')     } ],
		[ 'Head 5   (Ctrl 5)' => sub { apply_tag('head5')     } ],
		[ 'Normal   (Ctrl 6)' => sub { apply_tag('normal')    } ],
		[ 'Mark     (Ctrl U)' => sub { apply_tag('underline') } ],
	) {
		my $item = Gtk2::MenuItem->new($$_[0]);
		$item->signal_connect(activate => $$_[1]);
		$style_menu->append($item);
	}
	$style_menu->show_all;

	my $icon = Gtk2::Image->new_from_stock('gtk-select-font', $CONFIG{icon_size});
	my $style_button = $toolbar->append_item(
		#  text, tooltip_text, tooltip_private_text, icon, callback
		'Styles', 'Styles (Alt T)', 'Foo Bar', $icon, sub {
			$style_menu->popup(undef, undef, undef, undef, 1, 0);
		});
	$style_button->add_accelerator(
			'clicked', $accels, ord('T'), 'mod1-mask', 'visible');
	$style_button->signal_connect( button_press_event => sub {
			my (undef, $event) = @_;
			return 0 unless $event->button == 1;
			
			# parent_menu_shell, parent_menu_item, menu_pos_func, data, button, activate_time
			$style_menu->popup(undef, undef, undef, undef,
				$event->button, $event->time); # FIXME set pos_func
			return 1;
		});
	$style_menu->attach_to_widget($style_button, 0);

}

my $link_button = add_button(
	'Link', 'Ctrl L', 'gtk-connect', \&apply_link);
my $edit_link_button = add_button(
	'Edit Link', 'Ctrl E', 'gtk-properties', \&edit_link_dialog)
		unless $opts{read_only};

$toolbar->append_space;
#my $actions_button = add_button(
#	'Actions', '', 'gtk-execute', sub {});
my $about_button = add_button(
	'About', '', 'gtk-dialog-info', \&about_dialog);

# ################# #
# Other Keybindings #
# ################# #

$accels->connect(  # ^O => open page
	# accel_key, accel_mods, accel_flags, func
	ord('O'), 'control-mask', 'visible', \&open_page_dialog);
$accels->connect(  # ^R => reload window
	ord('R'), 'control-mask', 'visible', sub { load_page($PAGE->name) });
$accels->connect(  # ^W => close window
	ord('W'), 'control-mask', 'visible', \&quit);
$accels->connect(  # alt-up => namespace up
	$Gtk2::Gdk::Keysyms{'Up'}, 'mod1-mask', 'visible', sub { 
		my $namespace = $PAGE->namespace;
		go(':'.( ($namespace eq ':') ? $HOME : $namespace) );
	} );
$accels->connect(  # alt-D => todays page
	ord('D'), 'mod1-mask', 'visible', sub {
		go( strftime($CONFIG{date_page}, localtime) );
	} );
$accels->connect(  # ^SPACE => toggle index auto
	ord(' '), 'control-mask', 'visible', sub {
		if ($CONFIG{pane_vis}) {
			if ($tree_view->has_focus) { $htext->grab_focus }
			else { $tree_view->grab_focus }
		}
		else { set_pane($pane_visible ? 0 : -1) }
	} );
$accels->connect(  # ^F => search dialog
	ord('F'), 'control-mask', 'visible', \&search_dialog );
$accels->connect(  # alt-/ => inline search
	ord('/'), 'mod1-mask', 'visible',
	sub { $find_hbox->show; $find_entry->grab_focus } );

if ($opts{read_only}) {
	$htext->signal_connect(key_press_event => sub {
			return 0 unless $_[1]->keyval == ord('/');
			$find_hbox->show;
			$find_entry->grab_focus;
			return 1;
		} );
}
else { # Keybindings for Editing
	my %tag_keys = (
		B => 'bold',       # ^B => bold
		I => 'italic',     # ^I => italic
		U => 'underline',  # ^U => underline
		6 => 'normal',     # ^6 => normal
		(map {($_ => "head$_")} 1 .. 5)
		                   # ^1 .. ^5 => head1 .. head5
	);

	while (my ($k, $t) = each %tag_keys) {
		$accels->connect( 
			ord($k), 'control-mask', 'visible',
			eval "sub { apply_tag('$t') }"     );
	}
		
	$accels->connect(  # ^S => save
		ord('S'), 'control-mask', 'visible',
		sub { save_page('FORCE') });
	$accels->connect(  # ^Z => undo
		ord('Z'), 'control-mask', 'visible', \&undo);
	$accels->connect(  # ^Y => redo
		ord('Y'), 'control-mask', 'visible', \&redo);
	$accels->connect(  # ^D => insert stamp  # FIXME make format configable
		ord('D'), 'control-mask', 'visible', sub {
			$htext->get_buffer->insert_at_cursor(
				strftime('%A %d/%m/%Y', localtime) )
		} );
	$accels->connect(  # ^N => new page
		ord('N'), 'control-mask', 'visible', sub {
			go( strftime($CONFIG{untitled_page}, localtime) );
		} );
}

# ## #

$window->show_all;
$find_hbox->hide;


# ################### #
# Zim repository init #
# ################### #

my $zim = Zim->new($ROOT);

{
	if (defined $opts{doc}) {
		$PAGE = 'Help' unless defined $PAGE;
		$HOME = 'Help';
	}
	my $page = load_history();
	
	unless (length $PAGE) { # no commandline arg
		if ($page) { load_page(undef, $page) }
		else { load_page($HOME) }
	}
	elsif ($page->{name} eq $PAGE) { load_page(undef, $page) }
	else {
		if ($page) {
			push @BACK, $page;
			shift @BACK if @BACK > $CONFIG{max_hist};
		}
		load_page($PAGE);
	}
}

set_pane($CONFIG{pane_vis}); # calls load_tree
$htext->grab_focus;

# #### #
# MAIN #
# #### #

Gtk2->main;

exit;

# #### #

sub add_button {
	my ($text, $key, $stock, $code) = @_;
	my $icon = Gtk2::Image->new_from_stock($stock,  $CONFIG{icon_size});
	my $button = $toolbar->append_item(
		#  text, tooltip_text, tooltip_private_text, icon, callback
		$text, $text.($key?" ($key)":''), 'Foo Bar', $icon, $code);
	if ($key) {
		my ($mod, $key) = split ' ', $key;
		if    ($mod eq 'Alt')  { $mod = 'mod1-mask'    }
		elsif ($mod eq 'Ctrl') { $mod = 'control-mask' }
		$key = $Gtk2::Gdk::Keysyms{$key} || ord($key);
		$button->add_accelerator(
			'clicked', $accels, $key, $mod, 'visible');
	}	
	return $button;
}

sub new_buffer { # always start with a clean start
	# It seems to me that 10 * PANGO_SCALE is the normal font size
	my $buffer = Gtk2::TextBuffer->new();

	# FIXME make tag styles also configable
	$buffer->create_tag(
		'bold',
		weight => PANGO_WEIGHT_BOLD );
	$buffer->create_tag(
		'italic',
		style => 'italic' );
	$buffer->create_tag(
		'underline',
		background => 'yellow' );
	$buffer->create_tag(
		'head1',
		weight => PANGO_WEIGHT_BOLD,
		size   => 10*1.2**3 * PANGO_SCALE );
	$buffer->create_tag(
		'head2',
		weight => PANGO_WEIGHT_BOLD,
		size   => 10*1.2**2 * PANGO_SCALE );
	$buffer->create_tag(
		'head3',
		weight => PANGO_WEIGHT_BOLD,
		size   => 12 * PANGO_SCALE );
	$buffer->create_tag(
		'head4',
		weight => PANGO_WEIGHT_BOLD,
		size   => 10 * PANGO_SCALE );
	$buffer->create_tag(
		'head5',
		style  => 'italic',
		weight => PANGO_WEIGHT_BOLD,
		size   => 10 * PANGO_SCALE );
	$buffer->create_tag(
		'pre',
		family      => 'monospace',
		wrap_mode   => 'none'       );

	return $buffer;
}

# ############################# #
# Load and save config Routines #
# ############################# #

sub find_data_dir {
	my ($data_dir) =
		grep { -d $_ && -r $_ }
		map  { File::Spec->catdir($_, 'zim') }
		( File::BaseDir->xdg_data_home, File::BaseDir->xdg_data_dirs );

	unless ($data_dir) {
		my @dirs = File::Spec->splitdir($0);
		pop @dirs; pop @dirs;
		my $dir = File::Spec->catdir(@dirs, 'share', 'zim');
		$data_dir = $dir if -d $dir and -r $dir;
	}

	return $data_dir if defined $data_dir;
	
	print << 'EOT';
Could not find data files.
Typically these should be found in a dir like 
    /usr/share/zim/         or
    /usr/local/share/zim/

If you have found the directory try adding the base
part of the directory ('/usr/share/' for '/usr/share/zim') 
to the XDG_DATA_DIRS variable.
EOT
	exit 1;
}

sub load_config {
	my $name = $opts{read_only} ? 'conf.ro' : 'conf.rw' ;
	my ($file) =
		grep { -f $_ && -r $_ }
		map  { File::Spec->catdir($_, 'zim', $name) }
		( File::BaseDir->xdg_config_home, File::BaseDir->xdg_config_dirs );

	return unless defined $file;
	open CONF, $file or exit_error("could not read\n$file");
	while (<CONF>) {
		/^(.+?)=(.*)$/ or next;
		$CONFIG{$1} = $2 if length $2;
	}
	close CONF;
	print "loaded config from $file\n";
	return $file;
}

sub save_config {
	$CONFIG{pane_pos} = $hpaned->get_position;
	
	my $dir  = File::Spec->catdir(
		File::BaseDir->xdg_config_home, 'zim');
	_mkdir($dir) unless -e $dir;
	my $name = $opts{read_only} ? 'conf.ro' : 'conf.rw' ;
	my $file = File::Spec->catfile($dir, $name);
	open CONF, ">$file" or exit_error("could not write config\n$file");
	print CONF "# zim version:$Zim::VERSION\n";
	print CONF "$_=$CONFIG{$_}\n"
		for sort keys %CONFIG;
	close CONF;
	print "saved config to $file\n";

}

sub _mkdir {
	my ($vol, $dirs) = (@_ == 1)
		 ? (File::Spec->splitpath(shift(@_), 'NO_FILE')) : (@_) ;
	my @dirs = File::Spec->splitdir($dirs);
	my $path = File::Spec->catpath($vol, shift @dirs);
	mkdir $path or die "Could not create dir $path\n"
		if length $path and ! -d $path;
	while (@dirs) {
		$path = File::Spec->catdir($path, shift @dirs);
		mkdir $path or die "Could not create dir $path\n"
			unless -d $path;
	}
}

sub find_cache_file {
	if ($CONFIG{use_xdg_cache}) {
		my $root = File::Spec->rel2abs($ROOT);
		my ($vol, $dirs) = File::Spec->splitpath($root, 'NO_FILE');
		my @dirs = File::Spec->splitdir($dirs);
		return File::Spec->catfile( 
			File::BaseDir->xdg_cache_home, 'zim', join ':', $vol, @dirs );
	}
	else { return File::Spec->catfile($ROOT, '.zim.history') }
}

sub load_history {
	print "hist file: $CACHE_FILE\n";
	return unless -f $CACHE_FILE;
	my ($back, $page, $forw) = @{ retrieve($CACHE_FILE) };
	@BACK = @$back;
	$back_button->set_sensitive(1) if @BACK;
	@FORW = @$forw;
	$forw_button->set_sensitive(1) if @FORW;
	return $page;
}

sub save_history {
	my ($vol, $dirs, $file) = File::Spec->splitpath($CACHE_FILE);
	_mkdir($vol, $dirs);
	nstore([\@BACK, hist_rec(), \@FORW], $CACHE_FILE);
}

sub hist_rec { # list name and cursor -- FIXME hist format _will_ change
	my $buffer = $htext->get_buffer;
	my $iter = $buffer->get_iter_at_mark( $buffer->get_insert );
	#print "cursor: ".$iter->get_offset."\n";
	return {
		name     => $PAGE->name,
		realname => $PAGE->realname,
		cursor   => $iter->get_offset,
		undo     => $UNDO,
		redo     => $REDO,
	}
}

# ################### #
# Navigation routines #
# ################### #

# go(NAME)
#
# Loads a new page or starts the browser.
# NAME is considered to be either an url or a page name.
# Page names are resolved as relative links first.
#
# If NAME is undef or empty string a new page will
# be created without a name.

sub go {
	my $link = shift;
	return warn "Warning: You tried to folow an empty link.\n"
		unless length $link;

	if ($link =~ /^\w+:\/\//) { # link is an url
		my $browser = $CONFIG{browser} || $ENV{BROWSER};
		return error_dialog("You have no browser configured.\nTry setting the \$BROWSER environment variable") unless $browser;
		$browser =~ s/\%s/$link/ or $browser .= ' '.$link;
		unless (fork) { exec $browser } # exec in child process
		return;
	}

	$link = $PAGE->resolve_link($link);
	
	return error_dialog("Page does not exist:\n$link")
		if $opts{read_only} and ! $zim->page_exists($link);

	my $rec = hist_rec();
	load_page($link);

	unless ($rec->{realname} eq $PAGE->realname) {
		push @BACK, $rec;
		shift @BACK if @BACK > $CONFIG{hist_max};
		$back_button->set_sensitive(1);
	}
	@FORW = ();
	$forw_button->set_sensitive(0);
}

# go_back()
#
# go one step back in the history stack.

sub go_back {
	return unless @BACK;
	my $rec = hist_rec();
	load_page(undef, pop @BACK);

	unless ($rec->{realname} eq $PAGE->realname) {
		unshift @FORW, $rec;
		pop @FORW if @FORW > $CONFIG{hist_max};
		$forw_button->set_sensitive(1);
	}
	$back_button->set_sensitive(0) unless @BACK;
}

# go_forw()
#
# Go one step forward in the history stack

sub go_forw {
	return unless @FORW;
	my $rec = hist_rec();
	load_page(undef, shift @FORW);
	
	unless ($rec->{realname} eq $PAGE->realname) {
		push @BACK, $rec;
		shift @BACK if @BACK > $CONFIG{hist_max};
		$back_button->set_sensitive(1);
	}
	$forw_button->set_sensitive(0) unless @FORW;
}

# ########################### #
# Load and save page routines #
# ########################### #

# load_page(NAME)
#
# Wrapper around Zim->load_page
# Calls save_page first
# NAME is considered to be an absolute page name

sub load_page {
	save_page();
	my ($page, $rec) = @_;
	$page = $rec->{name} unless length $page;
	
	if (ref $page) { $PAGE = $page }
	else {
		$page =~ s/^:+|:+$//g;
		$PAGE = $zim->load_page($page);
	}

	# create a new TextBuffer
	my $buffer = new_buffer();
	$htext->set_buffer($buffer);
	load_text();
	unless ($opts{read_only}) { # connect signals _after_ load_text()
		$buffer->signal_connect(delete_range => \&on_delete_range);
		$buffer->signal_connect_after(insert_text => \&on_insert_text);
		$buffer->signal_connect(modified_changed => \&set_status);
	}
	$buffer->set_modified(0);

	# set some GUI elements to reflect the current page
	my $realname = $PAGE->realname;
	$window->set_title($PAGE->name.' - Zim');
	select_page($realname);
	set_status();
	
	# look for previous cursor position and undo stack
	($rec) = grep {$_->{realname} eq $realname} @FORW, reverse(@BACK)
		unless defined $rec;
	if (defined $rec) {
		$buffer->place_cursor(
			$buffer->get_iter_at_offset($rec->{cursor}) );
		$htext->scroll_mark_onscreen( $buffer->get_insert );

		$UNDO = $rec->{undo};
		$REDO = $rec->{redo};
	}
	else {
		$UNDO = [];
		$REDO = [];
	}	
		
	$htext->grab_focus;
}

# load_text()
#
# This method parses the data from the PAGE object and
# puts it into the text buffer

sub load_text {
	my $buffer = $htext->get_buffer;
	#use Data::Dumper; print Dumper $PAGE;
	while (defined ($_ = $PAGE->read_block)) {
		unless (ref $_) {
				s/^(\s*)[\*\xB7](\s+)/$1\x{2022}$2/mg; # bullets
				# \xB7 is the latin1 "high dot" 
				# \x2022 is the utf8 bullet
				# FIXME lists should be identified by the backend
				$buffer->insert_at_cursor($_);
		}
		else {
			my ($tag, @node) = @$_;
			my $iter = $buffer->get_iter_at_mark(
					$buffer->get_insert() );
			if ($tag eq 'link') {
				$htext->insert_link_at_iter($iter, @node);
			}
			elsif ($tag eq 'image') { # experimental feature
				my $file = File::Spec->catfile($DATA_DIR, $node[0]);
				$file = Gtk2::Gdk::Pixbuf->new_from_file($file);
				$buffer->insert_pixbuf($iter, $file);
			}
			else {
				if ($tag eq 'pre' and $node[0] =~ /\n/) {
					$node[0] =~ s/^/\t/mg;
				}
				$buffer->insert_with_tags_by_name(
					$iter, $node[0], $tag );
			}
		}
	}
}

# save_page()
#
# Wrapper for Zim->save_page

sub save_page {
	return 0 if $opts{read_only};
	my $force = shift;
	
	return 1 unless $force
		or $htext->get_buffer->get_modified and ! $save_lock;
	$save_lock=1;
	
	eval {
		if (save_text()) { # returns FALSE if buffer is empty
			$zim->save_page($PAGE)
				and list_page($PAGE->realname)
				and select_page($PAGE->realname);
		}
		else { 
			$zim->delete_page($PAGE)
				and unlist_page($PAGE->realname);
		}
	};
	unless ($@) {
		$htext->get_buffer->set_modified(0);
		set_status();
	}
	else { # FIXME this dialog could be nicer
		my $page = prompt_page_dialog($PAGE->name,
			'Save As', 'Save Page As:', $@, 'gtk-dialog-error' );
		if ($page =~ /\S/) {
			# FIXME check page exists
			$page =~ s/^:+|:+$//g;
			$PAGE = $zim->load_page($page);
			save_page('FORCE'); # recurs
			select_page($PAGE->realname);
		}
	}
	$save_lock = 0;
	return 1; # else timeout object gets deleted
}

# save_text()
#
# This routine gets the text from the buffer and puts it back 
# into the PAGE object.

sub save_text {
	my $buffer = $htext->get_buffer();
	my ($start, $end) = $buffer->get_bounds;
	my $text = $buffer->get_text($start, $end, 0); # start, end, hidden_chars

	return 0 unless $text =~ /\S/;
	
	$PAGE->clear;
	my $last = length $text;
	for (reverse _find_tags()) {
		my ($start, $end, $name, $link_data) = @$_;
		next unless $start < $last; # just to be sure
		my $rest = substr($text, $end, ($last-$end), '');
		my $block = substr($text, $start, ($end-$start), '');
		#print "$name from $start till $end : $block\n";
		$rest =~ s/\x{2022}/\*/g;
		$block =~ s/^\t//mg if $name eq 'pre';
		$PAGE->unshift_block('normal', $rest);
		$PAGE->unshift_block($name, $block,
			((defined($link_data) and $link_data ne $block) ? ($link_data) : ()) );
		$last = $start;
	}
	$PAGE->unshift_block('normal', $text) if length $text;
	$PAGE->clean;
	
	return 1;
}

sub _find_tags { # Method to find all tags in the buffer with their ranges
	my $buffer = $htext->get_buffer;
	my $table  = $buffer->get_tag_table;
	my (@tags, @tag_objs);
#	print "table size: ", $table->get_size, "\n";
	$table->foreach(sub { push @tag_objs, @_ });
	for my $tag (@tag_objs) { # iterate over named and anonymous tags
		my $iter = $buffer->get_start_iter;
		while ( $iter->begins_tag($tag) or $iter->forward_to_tag_toggle($tag) ) {
			my @tag = ( $iter->get_offset );
			$iter->forward_to_tag_toggle($tag) or last;
			push @tag, $iter->get_offset;
			if ($tag->{is_link}) { push @tag, 'link', $tag->{link_data} }
			else { push @tag, $tag->get_property('name') }
			push @tags, \@tag;
			last if $iter->is_end; # prevent infinite loop
		}
	}
	return sort {$$a[0] <=> $$b[0]} @tags;
}

# ################### #
# Index tree routines #
# ################### #

# load_tree()
#
# Called the first time the pane is showed to fill the index tree.
#
# TODO this should be caching algo

sub load_tree {
	$tree_store->clear;
	%tree_iter = ();
	$tree_loaded = 1;
	eval { list_pages('') };
	select_page($PAGE->realname);
}

# list_pages(namespace)
#
# Wrapper around Zim->list_pages that fills the index tree.

sub list_pages {
	my $namespace = shift;
	#print "> $namespace\n";
	for ($zim->list_pages($namespace)) {
		#print "\t$_\n";
		my $is_dir = ($_ =~ s/:$//);
		list_page($namespace.':'.$_);
		list_pages($namespace.':'.$_) if $is_dir;
	}
	Gtk2->main_iteration while (Gtk2->events_pending);	
}

# list_page(realname)
#
# Adds a page to the index tree.

sub list_page {
	my $name = shift;
	return unless defined $name;
	$name =~ s/^:+//;
	my ($page, @parts) = (undef, split /:+/, $name);
	my $iter;
	while (@parts) {
		$page = $page.':'.$parts[0];
		unless (exists $tree_iter{$page}) {
			$tree_iter{$page} = $tree_store->append($iter);
			$tree_store->set_value($tree_iter{$page},
				0 => $parts[0],
				1 => $page      );
		}
		$iter = $tree_iter{$page};
		shift @parts;
	}

	return 1;
}

# unlist_page(realname)
#
# Removes a page from the index unless it has children.

sub unlist_page {
	my $page = shift;
	return unless defined $page;
	$page =~ s/^:*/:/;
	return unless exists $tree_iter{$page};
	return if $tree_store->iter_children($tree_iter{$page}); # has children
	$tree_store->remove($tree_iter{$page});
	delete $tree_iter{$page};
}

# set_pane( BOOLEAN )
#
# Show or hide the side pane with the index tree.
# If BOOLEAN is "-1" the pane will only be toggled on
# untill a page is selected.

sub set_pane {
	$pane_visible = shift;
	if ($pane_visible) {
		$hpaned->set_position($CONFIG{pane_pos});
		$l_scroll_window->show_all;
		$expand_button->show;
		$collapse_button->show;
		load_tree() unless $tree_loaded;
		$tree_view->grab_focus;
	}
	else {
		$CONFIG{pane_pos} = $hpaned->get_position();
		$l_scroll_window->hide_all;
		$expand_button->hide;
		$collapse_button->hide;
		$htext->grab_focus;
	}
	$CONFIG{pane_vis} = $pane_visible
		unless $pane_visible == -1;
}

# page_activated( Gtk2::TreeView, Gtk2::TreePath, Gtk2::TreeViewColumn )
#
# Called by $tree_view when an index entry is double clicked.

sub page_activated {
	my ($tree_view, $path) = @_;
	my $model = $tree_view->get_model;
	my $iter = $model->get_iter($path);
	my (undef, $page) = $model->get($iter);
	set_pane(0) unless $CONFIG{pane_vis};
	go(':'.$page);
}

# select_page( page->realname )
#
# Update the tree_view to actually display and select a certain page.

sub select_page {
	my $page = shift;
	$page =~ s/^:*/:/;
	return unless exists $tree_iter{$page};
	
	my $path = $tree_view->get_model->get_path($tree_iter{$page});
	$tree_view->expand_to_path($path);
	$tree_view->get_selection->select_path($path);
	# FIXME this fails for certain cases with caps etc.

	$tree_view->scroll_to_cell($path, $page_column);
}

# ############### #
# Markup routines #
# ############### #

sub on_delete_range { # buffer, iter, iter
	my $string = $_[0]->get_text($_[1], $_[2], 0);
	add_undo('delete', $_[1]->get_offset, $string);
#	print "delete ", $_[1]->get_offset, ' ', $_[2]->get_offset, "\n";
# TODO end pre
# TODO remove \t _before_ bullet
}

sub on_insert_text { # buffer, iter, string
	add_undo('insert', $_[1]->get_offset - length($_[2]), $_[2]);
#	print "insert ", $_[1]->get_offset, " $_[3] ", length($_[2])," $_[2]\n";
	if ($_[2] eq ' ' or $_[2] eq "\t") { parse_word($_[1]) }
}

# parse_line( ITER )
#
# This method is called when the user is about to insert a linebreak.
# It checks the line left of the cursor of any markup that needs 
# updating. It also takes care of autoindenting.
# 
# When TRUE is returned the widget does not recieve the linebreak.

sub parse_line {
	my $iter = shift;
	my $buffer = $htext->get_buffer;
	my $lf = $buffer->get_iter_at_line( $iter->get_line );
	my $line = $buffer->get_text($lf, $iter, 0);
	#print ">>$line<<\n";
	if ($line =~ /^(\s*[\*\x{2022}]\s+)\S/) { # bullet
		$buffer->insert($iter, "\n$1");
		return 1;
	}
	elsif ($line =~ /^(\s*[\*\x{2022}]\s+)/) { # empty bullet
		$buffer->delete($lf, $iter);
	}
	elsif ($line =~ s/^(=+)\s+(\S)/$2/) {
		my $h = length($1); # no (7 - x) monkey bussiness here
		$h = 5 if $h > 5;
		$line =~ s/\s+=+\s*$//;
		$buffer->delete($lf, $iter);
		$buffer->insert_with_tags_by_name($lf, $line, "head$h");
	}
	elsif ($line =~ /^(\s*\W+\s+|\s+)/) { # normal indenting
		$buffer->insert($iter, "\n$1");
		$htext->scroll_mark_onscreen( $buffer->get_insert() );
		return 1;
	}
	return 0;
}

# parse_word( ITER )
#
# This method is called after the user ended typing a word.
# It checks the word left of the cursor for any markup that
# needs updating.

sub parse_word {
	my $iter = shift;
	my $buffer = $htext->get_buffer;
	my $lf = $buffer->get_iter_at_line( $iter->get_line );
	my $line = $buffer->get_text($lf, $iter, 0);
	#print ">>$line<<\n";
	if ($line =~ /^(\s*)[\*\x{2022}](\s+)$/) { # bullet
		# FIXME \s* => \t
		my ($pre, $post) = ($1, $2);
		$pre .= $1 if $post =~ s/(\t)+$//; # switch tabs
		$line = $pre."\x{2022}".$post;
		$buffer->delete($lf, $iter);
		$buffer->insert($lf, $line);
	}
#	elsif ($line =~ /^(\t|  )/) { # pre
#		# FIXME \s* => \t
#		$iter->forward_char unless $iter->is_end; # FIXME somthing at end
#		$buffer->apply_tag_by_name('pre', $lf, $iter);
#	}
	elsif ($line =~ /^(.*)\b(\w+:\/\/\S+[^\s\,\.\;])\s+$/) {
		# FIXME get the right iters to highlight link
	} # no wiki link markup supported here
	# TODO also on movement parse word if modified
}

# TODO when editing links you need to know if the link taget needs 
# updating. Ergo the link tags need a {link_locked} attribute for
# links that were manually edited to link to something else then the
# text.

sub apply_tag {
	my ($tag) = shift;
	my ($start, $end) = _get_selection();
	return unless defined $start;

	# TODO what if selection contains linebreaks ??
	
	my $buffer = $htext->get_buffer;
	$buffer->remove_all_tags($start, $end);
	$buffer->apply_tag_by_name($tag, $start, $end)
		unless $tag eq 'normal';
	$buffer->set_modified(1);

	$buffer->insert($end, "\n") if $tag =~ /^head/ and not $end->ends_line;
	# FIXME something similar for begin of line
}

# apply_link()
#
# This method is called by the "Link" button or by the ^L keybinding.
# It makes any selected text a link. This link is followed immediatly
# if the 'follow_new_link' config option is set.
#
# If no text is selected it calls the "New Link" dialog.
#
# In readonly modus the selected text is regarded as a link and
# followed immediatly, but no actual link is made

sub apply_link {
	my ($start, $end) = _get_selection();
	my $link = _apply_link($start, $end) if defined $start;

	unless (defined $link) {
		goto $opts{read_only}
			? \&open_page_dialog
			: \&edit_link_dialog ;
	}

	go($link) if $opts{read_only} or $CONFIG{follow_new_link};
}

sub _apply_link {
	my ($start, $end, $link) = @_;
	return unless defined $start;
	
	my $buffer = $htext->get_buffer;
	$link = $buffer->get_text($start, $end, 0) unless defined $link;

	return undef if $link =~ /\n/;
	
	unless ($link =~ /^\w+:\/\//) {
		$link =~ s/ /_/g;
		$link =~ s/[^\w:\.\-]//g;
		return undef unless $link =~ /\S/;
	}
	
	unless ($opts{read_only}) {
		$buffer->remove_all_tags($start, $end);
		$htext->apply_link($link, $start, $end);
		$buffer->set_modified(1);
	}

	return $link;
}

sub _insert_link {
	my ($start, $text, $link) = @_;

	return undef if $link =~ /\n/ or $text =~ /\n/;
	
	unless ($link =~ /^\w+:\/\//) {
		$link =~ s/ /_/g;
		$link =~ s/[^\w:\.\-]//g;
		return undef unless $link =~ /\S/;
	}
	
	$htext->insert_link_at_iter($start, $text, $link);
	$htext->get_buffer->set_modified(1);
}

sub _get_selection { # selects current word if no selections
	my $buffer = $htext->get_buffer;
	my ($start, $end) = $buffer->get_selection_bounds;
	if (!$end or $start == $end) {
		print "TODO select word\n";
		return undef;
	}
	return ($start, $end);
}

# ############### #
# Various dialogs #
# ############### #

# "Open Page" dialog
#
# Prompts the user for a page name. This page is then opened.

sub open_page_dialog {
	my $page = $PAGE->namespace;
	$page =~ s/^:+//;
	$page = prompt_page_dialog($page, 'Open Page', '_Open Page:');
	return unless defined $page;
	go(':'.$page) if $page =~ /\S/;
}

# promt_page_dialog(page, title, prompt, text, icon)
#
# Used by the "Open Page" dialog among others.

sub prompt_page_dialog {
	my ($page, $title, $prompt, $text, $icon) = @_;

	my $dialog = Gtk2::Dialog->new(
		# title, parent
		$title, $window,
		# flags
	       	[qw/modal destroy-with-parent no-separator/],
		# buttons
		'gtk-cancel' => 'cancel',
		'gtk-open'   => 'ok',
	);
	$dialog->set_resizable(0);
	$dialog->set_border_width(5);
	$dialog->set_icon($window->get_icon);
	$dialog->set_default_response('ok');

	if (defined $text) {
		my $text_hbox = Gtk2::HBox->new(0, 5);
		$text_hbox->set_border_width(5);
		$dialog->vbox->add($text_hbox);
		
		if (defined $icon) {
			my $image = Gtk2::Image->new_from_stock($icon, 'dialog');
			$text_hbox->add($image);
		}
		
		my $text_label = Gtk2::Label->new($text);
		$text_hbox->add($text_label);
	}
	
	my $hbox  = Gtk2::HBox->new(0, 5);
	$hbox->set_border_width(5);
	$dialog->vbox->add($hbox);
	
	my $label = Gtk2::Label->new_with_mnemonic($prompt);
	$hbox->add($label);
	
	my $entry = Gtk2::Entry->new();
	$entry->signal_connect(activate => sub { $dialog->response('ok') });
	$entry->set_text($page);
	$hbox->add($entry);
	
	$dialog->show_all;
	if ($dialog->run eq 'ok') { $page = $entry->get_text }
	else                      { $page = undef            }
	$dialog->destroy;

	return $page;
	
}

# "Edit link" dialog  (a.k.a. "New Link" dialog)
#
# This dialog allows the user to create a link for which
# the link target and the link text differ.
# It is also used to prompt for a new link when the user
# tries to link without selecting anything.

sub edit_link_dialog {
	my ($start, $end) = _get_selection();

	my ($text, $link);
	if (defined $start) {
		$link = $htext->get_link_at_iter($start);
		$text = $htext->get_buffer->get_text($start, $end, 0);
		$text = undef if $text =~ /\n/;
	}
	
	my $dialog = Gtk2::Dialog->new(
		# title, parent
		(defined($start) ? 'Edit Link' : 'New Link'), $window,
		# flags
	       	[qw/modal destroy-with-parent no-separator/],
		# buttons
		'gtk-cancel'  => 'cancel',
	);
	my $link_button = Gtk2::Button->new();
	$link_button->add(Gtk2::Alignment->new(0.5, 0.5, 0, 0));
	$link_button->child->add(Gtk2::HBox->new);
	$link_button->child->child->pack_start(
		Gtk2::Image->new_from_stock('gtk-connect', 'button'), 0, 0, 0);
	$link_button->child->child->pack_start(
		Gtk2::Label->new_with_mnemonic('_Link'), 1, 1, 0);
	$dialog->add_action_widget($link_button, 'ok');
	$dialog->set_resizable(0);
	$dialog->set_border_width(5);
	$dialog->set_icon($window->get_icon);
	#$dialog->set_default_response('ok');

	my $table = Gtk2::Table->new(2, 2);
	$table->set_border_width(5);
	$table->set_row_spacings(5);
	$table->set_col_spacings(5);
	$dialog->vbox->add($table);
	
	my $label1 = Gtk2::Label->new('Text:');
	my $label2 = Gtk2::Label->new('Links to:');
	my $text_entry = Gtk2::Entry->new();
	$text_entry->signal_connect(activate => sub { $dialog->response('ok') });
	my $link_entry = Gtk2::Entry->new();
	$link_entry->signal_connect(activate => sub { $dialog->response('ok') });
	$table->attach_defaults($label1, 0,1, 0,1);
	$table->attach_defaults($text_entry, 1,2, 0,1);
	$table->attach_defaults($label2, 0,1, 1,2);
	$table->attach_defaults($link_entry, 1,2, 1,2);
	
	$link_entry->set_text($link) if defined $link;
	if (defined $text) {
		$text_entry->set_text($text);
		$link_entry->grab_focus;
	}
	else { $text_entry->grab_focus }
	
	$dialog->show_all;
	if ($dialog->run eq 'ok') {
		$text = $text_entry->get_text;
		$link = $link_entry->get_text;
		return $dialog->destroy
			unless $text =~ /\S/ or $link =~ /\S/;
		# both entries default to the other
		$link = $text unless $link =~ /\S/;
		$text = $link unless $text =~ /\S/;
		if (defined $start) {
			$htext->get_buffer->delete($start, $end);
		}
		else {
			my $buffer = $htext->get_buffer;
			$start = $buffer->get_iter_at_mark(
						$buffer->get_insert());
		}
		_insert_link($start, $text, $link);
	}
	$dialog->destroy;
}

# "About" dialog
#
# This dialog tells you about the version of zim you are using,
# the copyright and the current config file. Most importantly 
# it has a button to open the manual.

sub about_dialog {
	my $dialog = Gtk2::Dialog->new(
		# title, parent
		'About Zim', $window,
		# flags
	       	[qw/modal destroy-with-parent/],
		# buttons
		'gtk-help' => 'help',
		'gtk-ok' => 'ok',
	);
	$dialog->set_resizable(0);
	$dialog->set_border_width(5);
	$dialog->set_icon($window->get_icon);
	$dialog->set_default_response('ok');
	
	$dialog->vbox->add( Gtk2::Image->new_from_file($ICON_FILE) );

	my $text = version();
	$text =~ s/^(.*)$/<b>$1<\/b>/m; # set first line bold
	$text .= "\n\nThe current config file is: $CONF_FILE\n";
	my $label = Gtk2::Label->new();
	$label->set_markup($text);
	$label->set_justify('center');
	$dialog->vbox->add($label);
	
	$dialog->show_all;
	if ($dialog->run eq 'help') {
		unless (fork) { # child process
			@CONFIG{'x', 'y'} = $window->get_size;
			save_config(); # maybe we are read_only too
			exec $0, '--doc';
		}
	}
	$dialog->destroy;
}

# "Search" dialog
#
# This dialog asks for a search query and then generates
# a page with search results.

sub search_dialog {
	my $dialog = Gtk2::Dialog->new(
		# title, parent
		'Search', $window,
		# flags
	       	[qw/modal destroy-with-parent no-separator/],
		# buttons
		'gtk-cancel' => 'cancel',
		'gtk-find'   => 'ok',
	);
	$dialog->set_resizable(0);
	$dialog->set_border_width(5);
	$dialog->set_icon($window->get_icon);
	#$dialog->set_default_response('ok');

	my $table = Gtk2::Table->new(2, 2);
	$table->set_border_width(5);
	$table->set_row_spacings(5);
	$table->set_col_spacings(5);
	$dialog->vbox->add($table);
	
	my $label1 = Gtk2::Label->new('Find what:');
	my $query_entry = Gtk2::Entry->new();
	$query_entry->signal_connect(activate => sub { $dialog->response('ok') });
	$table->attach_defaults($label1, 0,1, 0,1);
	$table->attach_defaults($query_entry, 1,2, 0,1);
	
	$dialog->show_all;
	if ($dialog->run eq 'ok') {
		print "TODO: search algorithm that doesn't depend on grep(1)\n";
		my $query = $query_entry->get_text;
		return unless $query =~ /\S/;
		my $page = Zim::Page->new($zim, 'Search Results');
		$page->push_blocks(
			['head1', 'Search Results'], "\n\n",
			['bold', '-- Search algorithm not yet stable --'],
			"\n\nSearching for \"$query\"\n\n",
		);
		$query =~ s/\s/\\W+/g;
		open GREP, "grep -ERi \"\\b$query\\b\" \"$ROOT\" |" or return;
		while (<GREP>) {
			/^(.+?):(.+)$/ or next;
			my ($file, $text) = ($1, $2);
			$file =~ s/^\Q$ROOT\E//;
			next if $file =~ /^\./;
			$page->push_blocks(
				['link', $zim->pagename($file)],
				"\n", ['pre', $text], "\n\n"
			);
		}
		close GREP;
		load_page($page);
	}
	$dialog->destroy;
}

# ############## #
# Other routines #
# ############## #

# find_string(STRING, NEXT)
#
# Finds next occurance of STRING in the buffer, scrolls the buffer
# and highlights the string. NEXT is a boolean to force finding the next
# occurance beyond the current cursor position.

sub find_string {
	my ($string, $next) = @_;
	my $buffer = $htext->get_buffer;
	my $iter = $buffer->get_iter_at_mark( $buffer->get_insert );
	$iter->forward_char if $next;
	my ($start, $end) = $iter->forward_search($string, 'visible-only');
	unless (defined $start) { # wrap around
		$iter = $buffer->get_start_iter;
		($start, $end) = $iter->forward_search($string, 'visible-only');
		return unless defined $start;
	}
	#print "found $string at offset ".$iter->get_offset."\n";
	$buffer->place_cursor($start);
	$buffer->move_mark($buffer->get_selection_bound, $end);
	$htext->scroll_mark_onscreen( $buffer->get_insert );
}

sub add_undo {
	return if $undo_lock;
#	flush_undo_mini();
	my ($action, $offset, @data) = @_;
#	print "do: $action \@$offset: >>@data<<\n";
	push @$UNDO, [$action, $offset, @data];
	shift @$UNDO if @$UNDO > $CONFIG{undo_max};
}

sub undo {
	return unless @$UNDO;
	my $step = pop @$UNDO;
	unshift @$REDO, [@$step]; # force copy;
	pop @$REDO if @$REDO > $CONFIG{undo_max};
	$$step[0] = ($$step[0] eq 'insert') ? 'delete' : 'insert';
	_do_step(@$step);
}

sub redo {
	return unless @$REDO;
	my $step = shift @$REDO;
	push @$UNDO, $step;
	shift @$UNDO if @$UNDO > $CONFIG{undo_max};
	_do_step(@$step);
}

sub _do_step {
	my ($action, $offset, @data) = @_;
	my $buffer = $htext->get_buffer;
	my $start = $buffer->get_iter_at_offset($offset);
	$undo_lock=1;
	if ($action eq 'insert') {
		$buffer->insert($start, $data[0]);
		$buffer->place_cursor(
			$buffer->get_iter_at_offset($offset + length($data[0])));
	}
	elsif ($action eq 'delete') {
		my $end = $buffer->get_iter_at_offset($offset + length($data[0]));
		$buffer->delete($start, $end);
		$buffer->place_cursor($start);
	}
	$htext->scroll_mark_onscreen( $buffer->get_insert );
	$undo_lock=0;
}

#sub flush_undo_mini {
#	return unless @undo_mini;
#	my (prev,
#	for (@undo_mini) {
#		my ($action, $offset, $char) = @$_;
#		
#}

# ########################## #
# Message and Error Routines #
# ########################## #

# set_status(STRING)
#
# Sets statusbar to page name and buffer status

sub set_status {
	my $stat = ' '.$PAGE->realname;
	if ($_ = $PAGE->status()) { $stat .= ' ('.uc($_).')' }
	else { $stat .= ' +' if $htext->get_buffer->get_modified }
	$stat .= ' -- OVERWRITE --' if $overwrite_toggle;
	push_status($stat, 'page');
}

# push_status(STRING, CONTEXT)
#
# Put STRING in the status bar
# CONTEXT can be 'link', 'page' or 'tree'

sub push_status {
	my ($str, $id) = @_;
	#print STDERR ucfirst($id).": $str\n";
	$id = $status->get_context_id($id);
	$status->pop($id);
	$status->push($id, $str);
	return 1;
}

# pop_status(CONTEXT)
#
# Removes a string from the status bar

sub pop_status {
	my $id = shift;
	$id = $status->get_context_id($id);
	$status->pop($id);
}


sub quit {
	_quit();
	Gtk2->main_quit;
}

sub _quit { # Called when the window is deleted
	@CONFIG{'x', 'y'} = $window->get_size;
	
	save_page();
	save_config();
	save_history() unless $opts{read_only};
	
	return 0;
}

sub exit_usage {
	print << "EOT";
Usage: $0 ROOT_DIR [PAGE]

  ROOT_DIR is the directory to store all docs, for example ~/zim/
  PAGE     is the page you want to open, this argument is optional

  To view the manual try "$0 --doc"
EOT
	exit;
}

sub error_dialog {
	my ($text1, $text2) = @_;
	if ($@) { $text1 ||= ($text2 ||= $@) }
	else    { $text2 ||= $text1          }
	my $dialog = Gtk2::MessageDialog->new(
		$window, 'modal', 'error', 'ok', $text1 );
		# parent, flags, type, buttons, format, ...
	print STDERR "Error: $text2\n";
	$dialog->run;
	$dialog->destroy;
}

sub exit_error {
	error_dialog(@_);
	exit 1;
}

__END__

=head1 NAME

zim - A desktop wiki

=head1 SYNOPSIS

zim [options] root_dir [page_name]

=head1 DESCRIPTION

Try to execute C<zim --doc> to view the user manual.

=head1 OPTIONS

...

=head1 AUTHOR

Jaap Karssenberg E<lt>pardus@cpan.orgE<gt>

Copyright (c) 2005 Jaap G Karssenberg and RL Zwart. All rights
reserved. This program is free software; you can redistribute it and/or
modify it under the same terms as Perl.

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

=head1 SEE ALSO

L<Zim>(3)

=cut
