# Arch Perl library, Copyright (C) 2004 Mikhael Goikhman, Enno Cramer
#
# 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

use 5.006;
use strict;
use warnings;

package ArchWay::MainWindow::Tree;

use base 'ArchWay::MainWindow::Base';

use Glib qw(TRUE FALSE);
use Gtk2;

use Arch::Inventory qw(:category :type :id_type);
use Arch::Changes qw(:type);
use Arch::Util qw(load_file save_file);

use Arch::FileHighlighter;

use ArchWay::Widget::Inventory::Tree;
use ArchWay::Widget::Inventory::Directory;
use ArchWay::Widget::Inventory::Entry;

use ArchWay::Widget::FrameScroll;
use ArchWay::Widget::ChangeList;
use ArchWay::Widget::Text;
use ArchWay::Widget::Diff;
use ArchWay::Widget::Log;

use ArchWay::MainWindow::Revisions;

sub init ($) {
	my $self = shift;

	$self->SUPER::init;

	# define action groups
	my $tree_actions = Gtk2::ActionGroup->new("TreeActions");
	$tree_actions->add_actions([
		[ "TreeMenu", undef, "_Tree" ],
		[ "FileMenu", undef, "_File" ],
	], undef);

	$tree_actions->add_actions([
		[
			"Refresh", "gtk-refresh", "_Refresh",
			undef, "Refresh inventory",
			sub { $self->refresh },
		],
		[
			"Commit", "gtk-apply", "_Commit",
			undef, "Commit",
			sub { $self->commit }
		],
		[
			"EditLog", "gtk-paste", "_Edit Log",
			undef, "Edit commit log message",
			sub { $self->edit_log }
		],
		[
			"Changes", "gtk-find", "_Changes",
			undef, "View uncommited changes in tree",
			sub { $self->show_changes }
		],
		[
			"Ancestry", "gtk-paste", "_Ancestry",
			undef, "View project tree ancestry",
			sub { $self->show_ancestry }
		],
	], undef);

	my $file_actions = Gtk2::ActionGroup->new("FileActions");
	$file_actions->add_actions([
		[
			"View", "gtk-paste", "_View",
			undef, "View and annotate selected file",
			sub { $self->show_file }
		],
		[
			"Remove", "gtk-delete", "_Remove",
			undef, "Remove the selected file",
			sub { $self->remove_file }
		]
	], undef);

	my $source_actions = Gtk2::ActionGroup->new("SourceActions");
	$source_actions->add_actions([
		[
			"Diff", "gtk-find", "_View Diff",
			undef, "View file diff",
			sub { $self->show_file_diff }
		],
		[
			"History", "gtk-paste", "_History",
			undef, "View File History",
			sub { $self->show_file_history }
		],
	], undef);

	my $non_source_actions = Gtk2::ActionGroup->new("NonSourceActions");
	$non_source_actions->add_actions([
		[
			"Add", "gtk-add", "_Add",
			undef, "Add an explicit inventory id to the selected file",
			sub { $self->add_file }
		],
	], undef);

	# add actions
	$self->ui->insert_action_group($tree_actions, 0);
	$self->ui->insert_action_group($file_actions, 0);
	$self->ui->insert_action_group($source_actions, 0);
	$self->ui->insert_action_group($non_source_actions, 0);

	# menu/toolbar layout
	my $layout = <<_EOF_;
<ui>
	<menubar name="MenuBar">
		<menu action="FileMenu">
			<placeholder name="FileMenuItems">
				<menuitem action="Add" />
				<menuitem action="Remove" />
				<separator />
				<menuitem action="View" />
				<menuitem action="Diff" />
				<menuitem action="History" />
			</placeholder>
		</menu>

		<placeholder name="ApplicationMenus">
			<menu action="TreeMenu">
				<menuitem action="Ancestry" />
				<menuitem action="Changes" />
				<menuitem action="EditLog" />
				<menuitem action="Commit" />
				<separator />
				<menuitem action="Refresh" />
			</menu>
		</placeholder>
	</menubar>

	<toolbar name="ToolBar">
		<toolitem action="Add" />
		<toolitem action="Remove" />
		<separator />
		<toolitem action="View" />
		<toolitem action="Diff" />
		<toolitem action="History" />
		<separator />
		<toolitem action="Ancestry" />
		<toolitem action="Changes" />
		<toolitem action="EditLog" />
		<toolitem action="Commit" />
		<separator />
		<toolitem action="Refresh" />
	</toolbar>

	<popup name="Popup">
		<menuitem action="Add" />
		<menuitem action="Remove" />
		<separator />
		<menuitem action="View" />
		<menuitem action="Diff" />
		<menuitem action="History" />
	</popup>
</ui>
_EOF_

	$self->ui->add_ui_from_string($layout);

	# directory view popup
	$self->file_view->signal_connect('button-press-event', sub {
		my $ev = $_[1];

		$self->ui->get_widget('/Popup')->popup(
			undef, undef, undef, undef,
			$ev->button, $ev->time
		) if $ev->button == 3;
	});

	$self->set_default_size(800, 600);
	$self->refresh;
}

sub main_widget ($) {
	my $self = shift;

	if (! exists $self->{main_widget}) {
		my $scwin_tv = Gtk2::ScrolledWindow->new;
		$scwin_tv->set_policy('automatic', 'automatic');
		$scwin_tv->add($self->tree_view);

		my $scwin_fv = Gtk2::ScrolledWindow->new;
		$scwin_fv->set_policy('automatic', 'automatic');
		$scwin_fv->add($self->file_view);

		my $hpaned = Gtk2::HPaned->new;
		$hpaned->pack1($scwin_tv, TRUE, TRUE);
		$hpaned->pack2($scwin_fv, TRUE, TRUE);
		$hpaned->set_position(200);

		my $vbox = Gtk2::VBox->new(FALSE, 0);
		$vbox->pack_start($self->version_label, FALSE, FALSE, 3);
		$vbox->pack_start($hpaned, TRUE, TRUE, 0);
		$vbox->pack_start($self->entry_view, FALSE, FALSE, 0);

		$vbox->set_focus_child($hpaned);
		$self->set_deep_widget_focus_child($hpaned, 1);
		
		$self->{main_widget} = $vbox;
	}

	return $self->{main_widget};
}

sub version_label ($) {
	my $self = shift;

	if (! exists $self->{version_label}) {
		my $lbl = Gtk2::Label->new;
		$lbl->set_alignment(0.0, 0.0);
		$lbl->set_selectable(TRUE);

		$self->{version_label} = $lbl;
	}

	return $self->{version_label};
}

sub tree_view ($) {
	my $self = shift;

	if (! exists $self->{tree_view}) {
		my $tv = ArchWay::Widget::Inventory::Tree->new;

		$tv->get_selection->signal_connect(
			changed => sub { $self->update_file_view }
		);

		$self->{tree_view} = $tv;
	}

	return $self->{tree_view};
}

sub file_view ($) {
	my $self = shift;

	if (! exists $self->{file_view}) {
		my $fv = ArchWay::Widget::Inventory::Directory->new;

		$fv->get_selection->signal_connect(
			changed => sub { $self->update_entry_view }
		);
		$fv->signal_connect(
			'row-activated' => sub { $self->activate_item }
		);

		$self->{file_view} = $fv;
	}

	return $self->{file_view};
}

sub entry_view ($) {
	my $self = shift;

	if (! exists $self->{entry_view}) {
		my $ev = ArchWay::Widget::Inventory::Entry->new;

		$self->{entry_view} = $ev;
	}

	return $self->{entry_view};
}

sub refresh ($) {
	my $self = shift;

	# save currently selected file
	my $tpath = $self->tree_view->get_selected_path;
	my $dpath = $self->file_view->get_selected_path;

	# rebuild inventory
	$self->{inventory} = $self->tree->get_inventory;
	$self->{inventory}->annotate_fs;

	# annotate inventory tree with changes
	my $changes = $self->tree->get_changes;
	if (! defined $changes) {
		$self->alert(
			"You may be using <b>untagged-source unrecognized</b> "
				. "and have untagged source files in your tree. Please add "
				. "file ids or remove the offending files, and press the "
				. "<b>Refresh</b> button.",
			"Could not get local tree changes"
		);
	} else {
	 	foreach my $change ($changes->get) {
	 		my $path = $change->{type} eq RENAME ?
	 			$change->{arguments}->[1] : $change->{arguments}->[0];
	 
	 		# skip arch internal files
	 		next if $path =~ m!^{arch}(/|$)!;
	 		next if $path =~ m!(^|/)\.arch-ids(/|$)!;
	 		next if $path =~ m!(^|/)\.arch-inventory$!;
	 
	 		my $entry = $self->inventory->get_entry($path);
	 		if ($entry) {
	 			$entry->{changes}->{$change->{type}} = $change;
	 		} else {
	 			warn "cannot annotate changes for $path\n";
	 		}
		}
	}

	$self->version_label->set_markup(
		'<b>Tree Revision:</b> '
			. $self->tree->get_log_revisions->[-1]
	);

	# display inventory
	$self->tree_view->show($self->inventory->get_root_entry);

	# restore selection if possible
	$self->tree_view->select_by_path($tpath)
		if defined $tpath;
	$self->file_view->select_by_path($dpath)
		if defined $dpath;
}

sub tree ($) {
	my $self = shift;

	return $self->{tree};
}

sub inventory ($) {
	my $self = shift;

	return $self->{inventory};
}

sub update_sensitivity ($) {
	my $self = shift;

	my $path  = $self->file_view->get_selected_path;
	my $entry = defined $path ? $self->inventory->get_entry($path) : undef;

	foreach my $ag ($self->ui->get_action_groups) {
		$ag->set_sensitive(defined $entry && $entry->{path})
			if $ag->get_name eq 'FileActions';

		$ag->set_sensitive(defined $entry && ($entry->{category} eq SOURCE))
			if $ag->get_name eq 'SourceActions';

		$ag->set_sensitive(defined $entry && $entry->{untagged})
			if $ag->get_name eq 'NonSourceActions';
	}
}

sub update_file_view ($) {
	my $self = shift;

	my $path  = $self->tree_view->get_selected_path;
	my $entry  = defined $path  ? $self->inventory->get_entry($path) : undef;

	my $ppath = $self->tree_view->get_selected_parent_path;
	my $parent = defined $ppath ? $self->inventory->get_entry($ppath) : undef;

	$self->file_view->show($entry, $parent);
	$self->update_sensitivity;
}

sub update_entry_view ($) {
	my $self = shift;

	my $path  = $self->file_view->get_selected_path;
	my $entry = defined $path ? $self->inventory->get_entry($path) : undef;

	$self->entry_view->show($entry);
	$self->update_sensitivity;
}

sub activate_item ($) {
	my $self = shift;

	my $path = $self->file_view->get_selected_path;
	my $entry = defined $path ? $self->inventory->get_entry($path) : undef;

	return unless $entry;

	if ($entry->{type} eq DIRECTORY) {
		$self->tree_view->select_by_path($entry->{path});
	}
}

sub show_file ($) {
	my $self = shift;

	my $path = $self->file_view->get_selected_path;
	my $entry = $self->inventory->get_entry($path);

	if ($entry->{type} eq DIRECTORY) {
		$self->alert('Cannot view a directory.');
		return;
	}

	my $fpath = $self->tree->root . "/$path";

	if (-B $fpath) {
		$self->alert('Cannot view a binary file.');
		return;
	}

	my $text_widget = ArchWay::Widget::Text->new;
	$text_widget->show(
		${Arch::FileHighlighter->instance->highlight($fpath)}
	);

	my $dlg = $self->widget_dialog(
		$path, $text_widget,
		buttons => [ 'Annotate' => 1, 'gtk-close' => 'close' ]
	);

	my $annotated = 0; # gotta love closures
	$dlg->signal_connect('response' => sub {
		return if $annotated || ($_[1] ne 1);

		$annotated = 1;

		my ($lines, $rd_idx, $rd) =
			$self->tree->get_annotate_revision_descs($path, group => 1);

		my $buffer = $text_widget->get_buffer;
		my $len = (length(@$rd) || 1) + 6;
		
		my $iter = $buffer->get_iter_at_offset(0);
 		for (my $b = 0; $b < @$lines; ++$b) {
 			for (my $l = 0; $l < @{$lines->[$b]}; ++$l) {
				my $text;

				if ($l > 0) {
					$text = ' ' x $len;

				} else {
					my $label = 'rev-' . (
						defined $rd_idx->[$b]
							? ($rd_idx->[$b] + 1)
							: '?'
					);
					$text = sprintf('%-*s', $len, "$label:");
				}

 				$buffer->insert_with_tags_by_name(
 					$iter,
					$text,
 					"annotate_zebra" . $b % 2,
 				);
				$buffer->insert($iter, ' ');

 				$iter->forward_line;
 			}
 		}

		for (my $i = 0; $i < @$rd; ++$i) {
			$rd->[$i]->{name} = 'rev-' . ($i + 1) . " ($rd->[$i]->{name})";
		}

		# modify dialog
		my $text_scroll = ($dlg->vbox->get_children)[0];
		$dlg->vbox->remove($text_scroll);

		my $rev_widget = ArchWay::Widget::Revisions->new;
		$rev_widget->add_revision_descs($rd);

		my $rev_scroll =
			ArchWay::Widget::FrameScroll->new($rev_widget, undef, 1);

		my $vpaned = Gtk2::VPaned->new;
		$vpaned->pack1($text_scroll, TRUE, TRUE);
		$vpaned->pack2($rev_scroll, TRUE, TRUE);
		$vpaned->set_position(400);

		$dlg->vbox->pack_start($vpaned, TRUE, TRUE, 0);
		$dlg->show_all;
 	});

	$dlg->set_default_size(700, 600);
	$dlg->show_all;
}

sub show_file_diff ($) {
	my $self = shift;

	my $path = $self->file_view->get_selected_path;
	my $entry = $self->inventory->get_entry($path);

	if ($entry->{type} eq DIRECTORY) {
		$self->alert('Cannot run file-diff on a directory.');
		return;
	}

	my $diff_widget = ArchWay::Widget::Diff->new;
	$diff_widget->show($self->tree->get_file_diff($path));

	my $dlg = $self->widget_dialog("Diff for $path", $diff_widget);
	$dlg->set_default_size(500, 600);
	$dlg->show_all;
}

sub show_file_history ($) {
	my $self = shift;

	my $path = $self->file_view->get_selected_path;
	my $entry = $self->inventory->get_entry($path);

	if ($entry->{type} eq DIRECTORY) {
		$self->alert('Cannot run file history on a directory.');
		return;
	}

	my $descs = [ reverse @{$self->tree->get_ancestry_revision_descs($path)} ];

	my $dlg = $self->session->activate_gui('revs', "File history for $path");
	$dlg->add_revision_descs($descs);
	$dlg->show_all;
}

sub add_file ($) {
	my $self = shift;

	my $path = $self->file_view->get_selected_path;
	my $entry = $self->inventory->get_entry($path);

	if ($self->tree->add($path) == 0) {
		$entry->{category} = SOURCE;
		$entry->{untagged} = 0;
		$entry->{id}       = 'unknown';
		$entry->{id_type}  = EXPLICIT;

		$entry->{changes}  = {
			ADD() => {
				type      => ADD,
				is_dir    => $entry->{type} eq DIRECTORY,
				arguments => [ $path ],
			}
		};

		$self->update_file_view;
		$self->file_view->select_by_path($path);
	} else {
		$self->set_status('Error: add-id failed for ' . $path);
	}
}

sub remove_file ($) {
	my $self = shift;

	my $path = $self->file_view->get_selected_path;
	my $entry = $self->inventory->get_entry($path);

	(my $name  = $path) =~ s!^.*/!!;
	(my $ppath = $path) =~ s!(^|/)[^/]*$!!;
	my $parent = $self->inventory->get_entry($ppath);

	if ($self->confirm("Really remove $path?")) {
		if ($entry->{type} eq DIRECTORY) {
			system('rm', '-r', $self->tree->root . "/$path");
		} else {
			my $id   = $self->tree->root . "/$ppath/.arch-ids/$name.id";
			my $file = $self->tree->root . "/$path";
			unlink($id)	if -f $id;
			unlink($file) && delete $parent->{children}->{$name};
		}

		$self->tree_view->show($self->inventory->get_root_entry);
		$self->tree_view->select_by_path($ppath);
	}
}

sub show_changes ($) {
	my $self = shift;

	$self->session->activate_gui('cset', $self->tree->root);
}

sub show_ancestry ($) {
	my $self = shift;

	my $descs = [ reverse @{$self->tree->get_ancestry_revision_descs} ];

	my $dlg = $self->session->activate_gui('revs', "Tree Ancestry");
	$dlg->add_revision_descs($descs);
	$dlg->show_all;
}

sub edit_log {
	my $self = shift;

	return if $self->{edit_log};

	my $log = $self->tree->make_log;

	my $log_widget = ArchWay::Widget::Log->new;
	$log_widget->show(load_file($log));

	$self->{edit_log} = my $dlg = $self->widget_dialog(
		'Commit Log',
		$log_widget,
		buttons => [ qw(
			gtk-save   accept
			gtk-cancel reject
		) ],
	);

	$dlg->signal_connect('response' => sub {
		save_file($log, $log_widget->get_text)
			if $_[1] eq 'accept';

		$dlg->destroy;
		$self->{edit_log} = undef;
	});

	$dlg->set_default_size(500, 300);
	$dlg->show_all;
}

sub commit {
	my $self = shift;

	return if $self->{commit};

	$self->{edit_log}->response('accept')
		if $self->{edit_log};

	my $tree = $self->tree;
	my $log  = $tree->make_log;

	my $msg = load_file($log);
	my $merge_log = $tree->get_merged_log_text;

	if ($merge_log) {
		$msg =~ s/\n*$/\n\n/;
		$msg .= $merge_log;
	}

	# changes
	my $chg_widget = ArchWay::Widget::ChangeList->new;
	$chg_widget->show($tree->get_changes);

	my $chg_frame = ArchWay::Widget::FrameScroll->new(
		$chg_widget, 'Changes', 1
	);

	# log
	my $log_widget = ArchWay::Widget::Log->new;
	$log_widget->show($msg);

	my $log_frame = ArchWay::Widget::FrameScroll->new(
		$log_widget, 'Commit Log', 1
	);

	my $vpaned = Gtk2::VPaned->new;
	$vpaned->pack1($chg_frame, TRUE, TRUE);
	$vpaned->pack2($log_frame, TRUE, TRUE);

	# dialog
	$self->{commit} = my $dlg = $self->create_dialog('Commit');
	$dlg->vbox->pack_start($vpaned, TRUE, TRUE, 0);

	$dlg->signal_connect('response' => sub {
		my $archlog = Arch::Log->new($log_widget->get_text);

		if (($_[1] eq 'ok') && ($archlog->summary !~ /\S/)) {
			return unless $self->confirm('Are you sure to commit without summary?');
		}

		if ($_[1] eq 'ok') {
			save_file($log, $log_widget->get_text);
			if ($tree->commit) {
				$self->alert(
					'Commit failed. See console for error messages.',
					'Commit failed'
				);
			} else {
				$self->refresh;
			}
		}

		$dlg->destroy;
		$self->{commit} = undef;
	});

	$dlg->set_default_size(500, 500);
	$dlg->show_all;
}

1;

__END__
