package Padre::Search;

=pod

=head1 NAME

Padre::Search - The Padre Search API

=head2 SYNOPSIS

  # Create the search object
  my $search = Padre::Search->new(
      find_term => 'foo',
  );  
  
  # Execute the search on the current editor
  $search->search_next(Padre::Current->editor);

=head2 DESCRIPTION

This is the Padre Search API. It allows the creation of abstract objects
object that can independantly search and/or replace in an editor object.

=head2 METHODS

=cut

use 5.008;
use strict;
use warnings;
use Encode     ();
use List::Util ();
use Params::Util '_INSTANCE';

our $VERSION = '0.47';

use Class::XSAccessor getters => {
	find_term    => 'find_term',
	find_case    => 'find_case',
	find_regex   => 'find_regex',
	find_reverse => 'find_reverse',
	replace_term => 'replace_term',
	search_regex => 'search_regex',
};

sub new {
	my $class = shift;
	my $self = bless {@_}, $class;
	unless ( defined $self->find_term ) {
		die("Did not provide 'find_term' search term");
	}
	unless ( length $self->find_term ) {

		# Pointless zero-length search
		return;
	}
	unless ( defined $self->find_case ) {
		$self->{find_case} = $self->config->find_case;
	}
	unless ( defined $self->find_regex ) {
		$self->{find_regex} = $self->config->find_regex;
	}
	unless ( defined $self->find_reverse ) {
		$self->{find_reverse} = $self->config->find_reverse;
	}

	# Escape the raw search term
	my $term = $self->find_term;
	if ( $self->find_regex ) {

		# Escape non-trailing $ so they won't interpolate
		$term =~ s/\$(?!\z)/\\\$/g;
	} else {

		# Escape everything
		$term = quotemeta $term;
	}

	# Compile the regex
	$self->{search_regex} = eval { $self->find_case ? qr/$term/m : qr/$term/mi };
	return undef if $@;

	return $self;
}

sub config {
	my $self = shift;
	unless ( defined $self->{config} ) {
		$self->{config} = Padre::Current->config;
	}
	return $self->{config};
}





#####################################################################
# Command Abstraction

sub search_next {
	my $self = shift;
	if ( $self->find_reverse ) {
		return $self->search_up(@_);
	} else {
		return $self->search_down(@_);
	}
}

sub search_previous {
	my $self = shift;
	if ( $self->find_reverse ) {
		return $self->search_down(@_);
	} else {
		return $self->search_up(@_);
	}
}

sub replace_next {
	my $self = shift;

	# Replace the currently selected match
	$self->replace(@_);

	# Select and move to the next match
	if ( $self->find_reverse ) {
		return $self->search_down(@_);
	} else {
		return $self->search_up(@_);
	}
}

sub replace_previous {
	my $self = shift;

	# Replace the currently selected match
	$self->replace(@_);

	# Select and move to the next match
	if ( $self->find_reverse ) {
		return $self->search_up(@_);
	} else {
		return $self->search_down(@_);
	}
}





#####################################################################
# Content Abstraction

sub search_down {
	my $self = shift;
	if ( _INSTANCE( $_[0], 'Padre::Wx::Editor' ) ) {
		return $self->editor_search_down(@_);
	}
	die("Missing or invalid content object to search in");
}

sub search_up {
	my $self = shift;
	if ( _INSTANCE( $_[0], 'Padre::Wx::Editor' ) ) {
		return $self->editor_search_up(@_);
	}
	die("Missing or invalid content object to search in");
}

sub replace {
	my $self = shift;
	if ( _INSTANCE( $_[0], 'Padre::Wx::Editor' ) ) {
		return $self->editor_replace(@_);
	}
	die("Missing or invalid content object to search in");
}

sub replace_all {
	my $self = shift;
	if ( _INSTANCE( $_[0], 'Padre::Wx::Editor' ) ) {
		return $self->editor_replace_all(@_);
	}
	die("Missing or invalid content object to search in");
}

sub count_all {
	my $self = shift;
	if ( _INSTANCE( $_[0], 'Padre::Wx::Editor' ) ) {
		return $self->editor_count_all(@_);
	}
	die("Missing or invalid ccontent object to search in");
}





#####################################################################
# Editor Interaction

sub editor_search_down {
	my $self   = shift;
	my $editor = shift;
	unless ( _INSTANCE( $editor, 'Padre::Wx::Editor' ) ) {
		die("Failed to provide editor object to search in");
	}

	# Execute the search and move to the resulting location
	my ( $start, $end, @matches ) = $self->matches(
		$editor->GetTextRange( 0, $editor->GetLength ),
		$self->search_regex,
		$editor->GetSelection,
	);
	return unless defined $start;

	# Highlight the found item
	$editor->SetSelection( $start, $end );
	return 1;
}

sub editor_search_up {
	my $self   = shift;
	my $editor = shift;
	unless ( _INSTANCE( $editor, 'Padre::Wx::Editor' ) ) {
		die("Failed to provide editor object to search in");
	}

	# Execute the search and move to the resulting location
	my ( $start, $end, @matches ) = $self->matches(
		$editor->GetTextRange( 0, $editor->GetLength ),
		$self->search_regex,
		$editor->GetSelection,
		'backwards'
	);
	return unless defined $start;

	# Highlight the found item
	$editor->SetSelection( $start, $end );
	return 1;
}

sub editor_replace {
	my $self   = shift;
	my $editor = shift;
	unless ( _INSTANCE( $editor, 'Padre::Wx::Editor' ) ) {
		die("Failed to provide editor object to replace in");
	}

	# Execute the search
	my ( $start, $end, @matches ) = $self->matches(
		$editor->GetTextRange( 0, $editor->GetLength ),
		$self->search_regex,
		$editor->GetSelection,
	);

	# Are they perfectly selecting a match already?
	my $selection = [ $editor->GetSelection ];
	if ( $selection->[0] != $selection->[1] ) {
		if ( grep { $selection->[0] == $_->[0] and $selection->[1] == $_->[1] } @matches ) {

			# Yes, replace it
			$editor->ReplaceSelection( $self->replace_term );

			# Move our selection to a point just before/after the replace,
			# so that it doesn't double-match
			if ( $self->find_reverse ) {
				$editor->SetSelection( $start, $start );
			} else {

				# TODO: There might be unicode bugs in this.
				# TODO: Someone that understands needs to check.
				$start = $start + length( $self->replace_term );
				$editor->SetSelection( $start, $start );
			}
		}
	}

	# Move to the next match
	$self->search_next($editor);
}

sub editor_replace_all {
	my $self   = shift;
	my $editor = shift;
	unless ( _INSTANCE( $editor, 'Padre::Wx::Editor' ) ) {
		die("Failed to provide editor object to replace in");
	}

	# Execute the search for all matches
	my ( undef, undef, @matches ) = $self->matches(
		$editor->GetTextRange( 0, $editor->GetLength ),
		$self->search_regex,
		$editor->GetSelection
	);

	# Replace all matches as a single undo
	if (@matches) {
		my $replace = $self->replace_text;
		$editor->BeginUndoAction;
		foreach my $match ( reverse @matches ) {
			$editor->SetTargetStart( $match->[0] );
			$editor->SetTargetEnd( $match->[1] );
			$editor->ReplaceTarget($replace);
		}
		$editor->EndUndoAction;
	}

	# Return the number of matches we replaced
	return scalar @matches;
}

sub editor_count_all {
	my $self   = shift;
	my $editor = shift;
	unless ( _INSTANCE( $editor, 'Padre::Wx::Editor' ) ) {
		die("Failed to provide editor object to replace in");
	}

	# Execute the search for all matches
	my ( undef, undef, @matches ) = $self->matches(
		$editor->GetTextRange( 0, $editor->GetLength ),
		$self->search_regex,
		$editor->GetSelection,
	);

	return scalar @matches;
}





#####################################################################
# Core Search

=pod

=head2 matches

Parameters:

* The text in which we need to search

* The regular expression

* The offset within the text where we the last match started so the next
  forward match must start after this.

* The offset within the text where we the last match ended so the next
  backward match must end before this.

* backward bit (1 = search backward, 0 = search forward)

=cut

sub matches {
	my $self = shift;
	die "missing parameters" if @_ < 4;

	# Searches run in unicode
	my $text = Encode::encode( 'utf-8', shift );

	# Find all matches for the regex
	my $regex   = shift;
	my @matches = ();
	while ( $text =~ /$regex/g ) {
		push @matches, [ $-[0], $+[0] ];
	}
	unless (@matches) {
		return ( undef, undef );
	}

	my $pair = [];
	my $from = shift || 0;
	my $to   = shift || 0;
	if ( $_[0] ) {

		# Search backwards
		$pair = List::Util::first { $to > $_->[1] } reverse @matches;
		$pair = $matches[-1] unless $pair;
	} else {

		# Search forwards
		$pair = List::Util::first { $from < $_->[0] } @matches;
		$pair = $matches[0] unless $pair;
	}

	return ( @$pair, @matches );
}

1;

=pod

=head1 COPYRIGHT & LICENSE

Copyright 2008-2009 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.

=cut

# Copyright 2008-2009 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.
