The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Padre::Wx::Dialog::Patch;

use 5.008;
use strict;
use warnings;
use Padre::Util           ();
use Padre::Wx             ();
use Padre::Wx::FBP::Patch ();
use Padre::Logger;

our $VERSION = '0.96';
our @ISA     = qw{
	Padre::Wx::FBP::Patch
};


#######
# new
#######
sub new {
	my $class = shift;
	my $self  = $class->SUPER::new(@_);

	$self->CenterOnParent;

	$self->{action_request} = 'Patch';
	$self->{selection}      = 0;

	return $self;
}

#######
# Method run
#######
sub run {
	my $self = shift;

	# auto-fill dialogue
	$self->set_up;

	# TODO but I want nonModal, ie $self->Show;
	# Show the dialog
	my $result = $self->ShowModal;
	if ( $result == Wx::ID_CANCEL ) {

		# As we leave the Find dialog, return the user to the current editor
		# window so they don't need to click it.
		$self->main->editor_focus;
		$self->Destroy;
	}

	return;
}

#######
# Method set_up
#######
sub set_up {
	my $self = shift;

	# test for local svn_local
	$self->test_svn();

	# generate open file bucket
	$self->current_files();

	# display default saved file lists
	$self->file_lists_saved();

	# display correct file-2 list
	$self->file2_list_type();

	$self->against->SetSelection(0);

	return;
}

#######
# Event Handler process_clicked
#######
sub process_clicked {
	my $self = shift;

	my $file1 = @{ $self->{file1_list_ref} }[ $self->file1->GetSelection() ];
	my $file2 = @{ $self->{file2_list_ref} }[ $self->file2->GetCurrentSelection() ];

	TRACE( '$self->file1->GetSelection(): ' . $self->file1->GetSelection() )               if DEBUG;
	TRACE( '$file1: ' . $file1 )                                                           if DEBUG;
	TRACE( '$self->file2->GetCurrentSelection(): ' . $self->file2->GetCurrentSelection() ) if DEBUG;
	TRACE( '$file2: ' . $file2 )                                                           if DEBUG;
	TRACE( $self->action->GetStringSelection() )                                           if DEBUG;

	if ( $self->action->GetStringSelection() eq 'Patch' ) {
		$self->apply_patch( $file1, $file2 );
	}

	if ( $self->action->GetStringSelection() eq 'Diff' ) {
		if ( $self->against->GetStringSelection() eq 'File-2' ) {
			$self->make_patch_diff( $file1, $file2 );
		} elsif ( $self->against->GetStringSelection() eq 'SVN' ) {
			$self->make_patch_svn($file1);
		}
	}

	# reset dialogue's display information
	$self->set_up;

	return;
}

#######
# Event Handler on_action
#######
sub on_action {
	my $self = shift;

	# re-generate open file bucket
	$self->current_files();

	if ( $self->action->GetStringSelection() eq 'Patch' ) {

		$self->{action_request} = 'Patch';
		$self->set_up;
		$self->against->Enable(0);
		$self->file2->Enable(1);
	} else {

		$self->{action_request} = 'Diff';
		$self->set_up;
		$self->against->Enable(1);
		$self->file2->Enable(1);

		# as we can not added items to a radio-box,
		# we can only enable & disable when radio-box enabled
		unless ( $self->{svn_local} ) {
			$self->against->EnableItem( 1, 0 );
		}
		$self->against->SetSelection(0);

	}
	return;
}

#######
# Event Handler on_against
#######
sub on_against {
	my $self = shift;

	if ( $self->against->GetStringSelection() eq 'File-2' ) {

		# show saved files only
		$self->file2->Enable(1);
		$self->file_lists_saved();

	} elsif ( $self->against->GetStringSelection() eq 'SVN' ) {

		# SVN only display files that are part of a SVN
		$self->file2->Enable(0);
		$self->file1_list_svn();
	}

	return;
}

#######
# Method current_files
#######
sub current_files {
	my $self     = shift;
	my $main     = $self->main;
	my $current  = $main->current;
	my $notebook = $current->notebook;
	my @label    = $notebook->labels;

	# get last element # not size
	$self->{tab_cardinality} = $#label;

	# thanks Alias
	my @file_vcs = map { $_->project->vcs } $self->main->documents;

	# create a bucket for open file info, as only a current file bucket exist
	for ( 0 .. $self->{tab_cardinality} ) {
		$self->{open_file_info}->{$_} = (
			{   'index'    => $_,
				'URL'      => $label[$_][1],
				'filename' => $notebook->GetPageText($_),
				'changed'  => 0,
				'vcs'      => $file_vcs[$_],
			},
		);

		if ( $notebook->GetPageText($_) =~ /^\*/sxm ) {
			TRACE("Found an unsaved file, will ignore: $notebook->GetPageText($_)") if DEBUG;
			$self->{open_file_info}->{$_}->{'changed'} = 1;
		}
	}

	return;
}

#######
# Composed Method file2_list_type
#######
sub file2_list_type {
	my $self = shift;

	if ( $self->{action_request} eq 'Patch' ) {

		# update File-2 = *.patch
		$self->file2_list_patch();
	} else {

		# File-1 = File-2 = saved files
		$self->file_lists_saved();
	}

	return;
}

#######
# Composed Method file_lists_saved
#######
sub file_lists_saved {
	my $self = shift;
	my @file_lists_saved;
	for ( 0 .. $self->{tab_cardinality} ) {
		unless ( $self->{open_file_info}->{$_}->{'changed'}
			|| $self->{open_file_info}->{$_}->{'filename'} =~ /(patch|diff)$/sxm )
		{
			push @file_lists_saved, $self->{open_file_info}->{$_}->{'filename'};
		}
	}

	TRACE("file_lists_saved: @file_lists_saved") if DEBUG;

	$self->file1->Clear;
	$self->file1->Append( \@file_lists_saved );
	$self->{file1_list_ref} = \@file_lists_saved;
	$self->set_selection_file1();
	$self->file1->SetSelection( $self->{selection} );

	$self->file2->Clear;
	$self->file2->Append( \@file_lists_saved );
	$self->{file2_list_ref} = \@file_lists_saved;
	$self->set_selection_file2();
	$self->file2->SetSelection( $self->{selection} );

	return;
}

#######
# Composed Method file2_list_patch
#######
sub file2_list_patch {
	my $self = shift;

	my @file2_list_patch;
	for ( 0 .. $self->{tab_cardinality} ) {
		if ( $self->{open_file_info}->{$_}->{'filename'} =~ /(patch|diff)$/sxm ) {
			push @file2_list_patch, $self->{open_file_info}->{$_}->{'filename'};
		}
	}

	TRACE("file2_list_patch: @file2_list_patch") if DEBUG;

	$self->file2->Clear;
	$self->file2->Append( \@file2_list_patch );
	$self->{file2_list_ref} = \@file2_list_patch;
	$self->set_selection_file2();
	$self->file2->SetSelection( $self->{selection} );

	return;
}

#######
# Composed Method file1_list_svn
#######
sub file1_list_svn {
	my $self = shift;

	@{ $self->{file1_list_ref} } = ();
	for ( 0 .. $self->{tab_cardinality} ) {
		if (   ( $self->{open_file_info}->{$_}->{'vcs'} eq 'SVN' )
			&& !( $self->{open_file_info}->{$_}->{'changed'} )
			&& !( $self->{open_file_info}->{$_}->{'filename'} =~ /(patch|diff)$/sxm ) )
		{
			push @{ $self->{file1_list_ref} }, $self->{open_file_info}->{$_}->{'filename'};
		}
	}

	TRACE("file1_list_svn: @{ $self->{file1_list_ref} }") if DEBUG;

	$self->file1->Clear;
	$self->file1->Append( $self->{file1_list_ref} );
	$self->set_selection_file1();
	$self->file1->SetSelection( $self->{selection} );

	return;
}

#######
# Composed Method set_selection_file1
#######
sub set_selection_file1 {
	my $self = shift;
	my $main = $self->main;

	$self->{selection} = 0;
	if ( $main->current->title =~ /(patch|diff)$/sxm ) {

		my @pathch_target = split( /\./, $main->current->title, 2 );

		# TODO this is a padre internal issue
		# remove obtuse leading space if exists
		$pathch_target[0] =~ s/^\p{Space}{1}//;
		TRACE("Looking for File-1 to apply a patch to: $pathch_target[0]") if DEBUG;

		# SetSelection should be Patch target file
		foreach ( 0 .. $#{ $self->{file1_list_ref} } ) {

			# add optional leading space \p{Space}?
			if ( @{ $self->{file1_list_ref} }[$_] =~ /^\p{Space}?$pathch_target[0]/ ) {
				$self->{selection} = $_;
				return;
			}
		}
	} else {

		# SetSelection should be current file
		foreach ( 0 .. $#{ $self->{file1_list_ref} } ) {

			if ( @{ $self->{file1_list_ref} }[$_] eq $main->current->title ) {
				$self->{selection} = $_;
				return;
			}
		}
	}

	return;
}

#######
# Composed Method set_selection_file2
#######
sub set_selection_file2 {
	my $self = shift;
	my $main = $self->main;

	$self->{selection} = 0;

	# SetSelection should be current file
	foreach ( 0 .. $#{ $self->{file2_list_ref} } ) {

		if ( @{ $self->{file2_list_ref} }[$_] eq $main->current->title ) {
			$self->{selection} = $_;
			return;
		}
	}

	return;
}

#######
# Composed Method filename_url
#######
sub filename_url {
	my $self     = shift;
	my $filename = shift;

	# given tab name get url of file
	for ( 0 .. $self->{tab_cardinality} ) {
		if ( $self->{open_file_info}->{$_}->{'filename'} eq $filename ) {
			return $self->{open_file_info}->{$_}->{'URL'};
		}
	}
	return;
}

########
# Method apply_patch
########
sub apply_patch {
	my $self       = shift;
	my $file1_name = shift;
	my $file2_name = shift;
	my $main       = $self->main;

	$main->show_output(1);
	my $output = $main->output;
	$output->clear;

	my ( $source, $diff );

	my $file1_url = $self->filename_url($file1_name);
	my $file2_url = $self->filename_url($file2_name);

	if ( -e $file1_url ) {
		TRACE("found file1 => $file1_name: $file1_url") if DEBUG;
		$source = Padre::Util::slurp($file1_url);
	}

	if ( -e $file2_url ) {
		TRACE("found file2 => $file2_name: $file2_url") if DEBUG;
		$diff = Padre::Util::slurp($file2_url);
		unless ( $file2_url =~ /(patch|diff)$/sxm ) {
			$main->info( Wx::gettext('Patch file should end in .patch or .diff, you should reselect & try again') );
			return;
		}
	}

	if ( -e $file1_url && -e $file2_url ) {

		require Text::Patch;
		my $our_patch;
		if ( eval { $our_patch = Text::Patch::patch( $source, $diff, { STYLE => 'Unified' } ) } ) {

			TRACE($our_patch) if DEBUG;

			# Open the patched file as a new file
			$main->new_document_from_string( $our_patch => 'application/x-perl', );
			$main->info( Wx::gettext('Patch successful, you should see a new tab in editor called Unsaved #') );
		} else {
			TRACE("error trying to patch: $@") if DEBUG;

			$output->AppendText("Patch Dialog failed to Complete.\n");
			$output->AppendText("Your requested Action Patch, with following parameters.\n");
			$output->AppendText("File-1: $file1_url \n");
			$output->AppendText("File-2: $file2_url \n");
			$output->AppendText("What follows is the error I received from Text::Patch::patch, if any: \n");
			$output->AppendText($@);

			$main->info(
				Wx::gettext('Sorry, patch failed, are you sure your choice of files was correct for this action') );
			return;
		}
	}

	return;
}

#######
# Method make_patch_diff
#######
sub make_patch_diff {
	my $self       = shift;
	my $file1_name = shift;
	my $file2_name = shift;
	my $main       = $self->main;

	$main->show_output(1);
	my $output = $main->output;
	$output->clear;

	my $file1_url = $self->filename_url($file1_name);
	my $file2_url = $self->filename_url($file2_name);

	if ( -e $file1_url ) {
		TRACE("found file1 => $file1_name: $file1_url") if DEBUG;
	}

	if ( -e $file2_url ) {
		TRACE("found file2 => $file2_name: $file2_url") if DEBUG;
	}

	if ( -e $file1_url && -e $file2_url ) {
		require Text::Diff;
		my $our_diff;
		if ( eval { $our_diff = Text::Diff::diff( $file1_url, $file2_url, { STYLE => 'Unified' } ) } ) {
			TRACE($our_diff) if DEBUG;

			my $patch_file = $file1_url . '.patch';
			open( my $fh, '>', $patch_file ) or die "open: $!";
			print $fh $our_diff;
			close $fh;
			TRACE("writing file: $patch_file") if DEBUG;

			$main->setup_editor($patch_file);
			$main->info(
				sprintf( Wx::gettext('Diff successful, you should see a new tab in editor called %s'), $patch_file ) );
		} else {
			TRACE("error trying to patch: $@") if DEBUG;

			$output->AppendText("Patch Dialog failed to Complete.\n");
			$output->AppendText("Your requested Action Diff, with following parameters.\n");
			$output->AppendText("File-1: $file1_url \n");
			$output->AppendText("File-2: $file2_url \n");
			$output->AppendText("What follows is the error I received from Text::Diff::diff, if any: \n");
			$output->AppendText($@);

			$main->info(
				Wx::gettext('Sorry Diff Failed, are you sure your choice of files was correct for this action') );
			return;
		}
	}

	return;
}

#######
# Composed Method test_svn
#######
sub test_svn {
	my $self = shift;
	my $main = $self->main;

	$self->{svn_local} = 0;

	my $svn_client_version   = 0;
	my $required_svn_version = '1.6.2';

	if ( File::Which::which('svn') ) {

		# test svn version
		$svn_client_version = Padre::Util::run_in_directory_two('svn --version --quiet');
		if ($svn_client_version) {
			chomp $svn_client_version;

			require Sort::Versions;

			# This is so much better, now we are testing for version as well
			if ( Sort::Versions::versioncmp( $required_svn_version, $svn_client_version, ) == -1 ) {
				TRACE("Found local SVN v$svn_client_version, good to go.") if DEBUG;
				$self->{svn_local} = 1;
				return;
			} else {
				TRACE("Found SVN v$svn_client_version but require v$required_svn_version") if DEBUG;
				$main->info(
					sprintf(
						Wx::gettext(
							'Warning: found SVN v%s but we require SVN v%s and it is now called "Apache Subversion"'),
						$svn_client_version,
						$required_svn_version
					)
				);
			}
		}
	}
	return;
}

#######
# Method make_patch_svn
# inspired by P-P-SVN
#######
sub make_patch_svn {
	my $self       = shift;
	my $file1_name = shift;
	my $main       = $self->main;

	$main->show_output(1);
	my $output = $main->output;
	$output->clear;

	my $file1_url = $self->filename_url($file1_name);

	TRACE("file1_url to svn: $file1_url") if DEBUG;

	# if (test_svn) {
	if ( $self->{svn_local} ) {
		TRACE('found local SVN, Good to go') if DEBUG;
		my $diff_str;
		if ( eval { $diff_str = qx{ svn diff $file1_url} } ) {

			TRACE($diff_str) if DEBUG;

			my $patch_file = $file1_url . '.patch';
			open( my $fh, '>', $patch_file ) or die "open: $!";
			print $fh $diff_str;
			close $fh;
			TRACE("writing file: $patch_file") if DEBUG;

			$main->setup_editor($patch_file);
			$main->info(
				sprintf(
					Wx::gettext('SVN Diff successful. You should see a new tab in editor called %s.'), $patch_file
				)
			);
		} else {
			TRACE("Error trying to get an SVN Diff: $@") if DEBUG;

			$output->AppendText("Patch Dialog failed to Complete.\n");
			$output->AppendText("Your requested Action Diff against SVN, with following parameters.\n");
			$output->AppendText("File-1: $file1_url \n");
			$output->AppendText("What follows is the error I received from SVN, if any: \n");
			if ($@) {
				$output->AppendText($@);
			} else {
				$output->AppendText("Sorry, Diff to SVN failed. There are any diffrences in this file: $file1_name");
			}

			$main->info(
				Wx::gettext('Sorry, Diff failed. Are you sure your have access to the repository for this action') );
			return;
		}
	}
	return;
}

1;

__END__

=head1 NAME

Padre::Wx::Dialog::Patch - The Padre Patch dialog

=head1 DESCRIPTION

You will find more infomation in our L<wiki|http://padre.perlide.org/trac/wiki/Features/EditPatch/> pages.

A very simplistic tool, only works on open saved files, in the Padre editor.

Patch a single file, in the editor with a patch/diff file that is also open.

Diff between two open files, the resulting patch file will be in Unified form.

Diff a single file to svn, only display files that are part of an SVN already, the resulting patch file will be in Unified form.

All results will be a new Tab.

=head1 METHODS

=head2 new

Constructor. Should be called with C<$main> by C<Patch::load_dialog_main()>.

=head2 run

C<run> configures the dialogue for your environment

=head2 set_up

C<set_up> configures the dialogue for your environment

=head2 on_action

Event handler for action, adjust dialogue accordingly

=head2 on_against

Event handler for against, adjust dialogue accordingly

=head2 process_clicked

Event handler for process_clicked, perform your chosen action, all results go into a new tab in editor.

=head2 current_files

extracts file info from Padre about all open files in editor

=head2 apply_patch

A convenience method to apply patch to chosen file.

uses Text::Patch

=head2 make_patch_diff

A convenience method to generate a patch/diff file from two selected files.

uses Text::Diff

=head2 test_svn

test for a local copy of svn in Path and version greater than 1.6.2.

=head2 make_patch_svn

A convenience method to generate a patch/diff file from a selected file and svn if applicable,
ie file has been checked out.

=head2 file2_list_type

composed method

=head2 filename_url

composed method

=head2 set_selection_file1

composed method

=head2 set_selection_file2

composed method

=head2 file1_list_svn

composed method

=head2 file2_list_patch

composed method

=head2 file_lists_saved

composed method

=head1 BUGS AND LIMITATIONS

List Order is that of load order, if you move your Tabs the List Order will not follow suite.

If you have multiple files open with same name but with different paths only the first will get matched.

=head1 AUTHORS

BOWTIE E<lt>kevin.dawson@btclick.comE<gt>

Adam Kennedy E<lt>adamk@cpan.orgE<gt>

=head1 LICENSE AND COPYRIGHT

Copyright 2008-2012 The Padre development team as listed in Padre.pm.

This program is free software; you can redistribute
it and/or modify it under the same terms as Perl 5 itself.

The full text of the license can be found in the
LICENSE file included with this module.

=cut

# Copyright 2008-2012 The Padre development team as listed in Padre.pm.
# LICENSE
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl 5 itself.