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

use 5.008;
use strict;
use warnings;
use Padre::Current ();
use Padre::Wx      ();

our $VERSION = '0.45';
our @ISA     = 'Wx::SearchCtrl';

# Create a new Search object and show a search text field above the tree
sub new {
	my $class = shift;
	my $panel = shift;
	my $self  = $class->SUPER::new(
		$panel, -1, '',
		Wx::wxDefaultPosition,
		Wx::wxDefaultSize,
		Wx::wxTE_PROCESS_ENTER
	);

	# Caches each project search WORD and result
	$self->{CACHED} = {};

	# Text that is showed when the search field is empty
	$self->SetDescriptiveText( Wx::gettext('Search') );

	# Setups the search box menu
	$self->SetMenu( $self->create_menu );

	# Setups events related with the search field
	Wx::Event::EVT_TEXT(
		$self, $self,
		\&_on_text
	);

	Wx::Event::EVT_SEARCHCTRL_CANCEL_BTN(
		$self, $self,
		sub {
			$self->SetValue('');
		}
	);

	Wx::Event::EVT_SET_FOCUS(
		$self,
		sub {
			$_[0]->parent->refresh;
		},
	);

	return $self;
}

# Returns the Directory Panel object reference
sub parent {
	$_[0]->GetParent;
}

# Returns the main object reference
sub main {
	$_[0]->GetParent->main;
}

# Traverse to the sibling tree widget
sub tree {
	$_[0]->GetParent->tree;
}

sub current {
	Padre::Current->new( main => $_[0]->main );
}

# Called by Directory.pm
sub refresh {
	my $self   = shift;
	my $parent = $self->parent;

	# Gets the last and current actived projects
	my $project_dir  = $parent->project_dir;
	my $previous_dir = $parent->previous_dir;

	# Compares if they are not the same, if not updates search field
	# content
	if (    defined($project_dir)
		and defined($previous_dir)
		and $previous_dir ne $project_dir )
	{
		$self->{use_cache} = 1;
		my $value = $self->{CACHED}->{$project_dir}->{value};
		$self->SetValue( defined $value ? $value : '' );

		# Checks the currently mode view
		my $mode = "sub_" . $parent->mode;
		$self->{$mode}->Check(1);

		# (Un)Checks current project Searcher Menu Skips options
		my $skips_hidden = $self->{_skip_hidden}->{$project_dir};
		my $skips_vcs    = $self->{_skip_vcs}->{$project_dir};

		$self->{skip_hidden}->Check( defined $skips_hidden ? $skips_hidden : 1 );
		$self->{skip_vcs}->Check( defined $skips_vcs       ? $skips_vcs    : 1 );
	}
}

# Searchs recursively per items that matchs the REGEX typed in search field,
# showing all items matched below the ROOT project directory will all the
# folders that paths to them expanded.
sub _search {
	my ( $self, $node ) = @_;
	my $parent      = $self->parent;
	my $project_dir = $parent->project_dir;

	# Fetch the ignore criteria
	my $project = $self->current->project;
	my $rule = $project ? $project->ignore_rule : undef;

	# Check if it is to use the Cached search (in case of a project
	# switching)
	if ( $self->{use_cache} ) {
		delete $self->{use_cache};
		if ( defined $self->{CACHED}->{$project_dir}->{Data} ) {
			return $self->_display_cached_search(
				$node,
				$self->{CACHED}->{$project_dir}->{Data},
			);
		}
	}

	# If there is a Cached Word (in case that the user is still typing)
	if ( my $last_word = $self->{CACHED}->{$project_dir}->{value} ) {

		# Quotes meta characters
		$last_word = quotemeta($last_word);

		# If the typed word contains the cached word, use Cached result to do
		# the new search and returns the result
		if ( $self->GetValue =~ /$last_word/i ) {
			return $self->_search_in_cache(
				$node,
				$self->{CACHED}->{$project_dir}->{Data},
			);
		}
	}

	# Quotes meta characters
	my $word = quotemeta( $self->GetValue );

	# Gets the node's data and generates its path
	my $tree      = $self->tree;
	my $node_data = $tree->GetPlData($node);
	my $path      = File::Spec->catfile( $node_data->{dir}, $node_data->{name} );

	# Opens the current directory and sort its items by type and name
	my ( $dirs, $files ) = $tree->readdir($path);

	# Accept some regex like characters
	#   ^ = begin with
	#   $ = end with
	#   * = any string
	#   ? = any character
	$word =~ s/^\\\^/^/g;
	$word =~ s/\\\$$/\$/g;
	$word =~ s/\\\*/.*?/g;
	$word =~ s/\\\?/./g;

	# Filter the file list by the search criteria (but not the dir list)
	@$files = grep { $_ =~ /$word/i } @$files;

	# Search recursively inside each folder of the current folder
	my $found  = scalar @$files;
	my @result = ();
	foreach (@$dirs) {
		my %temp = (
			name => $_,
			dir  => $path,
			type => 'folder',
		);

		# Are we ignoring this directory
		if ( $self->{skip_hidden}->IsChecked ) {
			if ($rule) {
				local $_ = \%temp;
				unless ( $rule->() ) {
					next;
				}
			} elsif ( $temp{name} =~ /^\./ ) {
				next;
			}
		}

		# Skips VCS folders if selected to
		if ( $self->{skip_vcs}->IsChecked ) {
			if ( $temp{name} =~ /^(cvs|blib|\.(svn|git))$/i ) {
				next;
			}
		}

		# Creates each folder node
		my $new_folder = $tree->AppendItem(
			$node, $_, -1, -1,
			Wx::TreeItemData->new(
				{   dir  => $path,
					name => $_,
					type => 'folder',
				}
			)
		);
		$tree->SetItemImage(
			$new_folder,
			$tree->{file_types}->{folder},
			Wx::wxTreeItemIcon_Normal,
		);

		# Deletes the folder node if any file below it was found
		if ( @{ $temp{data} } = $self->_search($new_folder) ) {
			$found = 1;
			push @result, \%temp;
		} else {
			$tree->Delete($new_folder);
		}
	}

	# Adds each matched file
	foreach (@$files) {
		my $new_elem = $tree->AppendItem(
			$node, $_, -1, -1,
			Wx::TreeItemData->new(
				{   name => $_,
					dir  => $path,
					type => 'package',
				}
			)
		);
		$tree->SetItemImage(
			$new_elem,
			$tree->{file_types}->{package},
			Wx::wxTreeItemIcon_Normal,
		);
		push @result,
			{
			name => $_,
			dir  => $path,
			type => 'package',
			};
	}

	# Returns 1 if any file above this path node was found or 0 and
	# deletes parent node if none
	return @result;
}

# Searchs recursively per items that matchs the REGEX typed in search field,
# using the cached result. Only when the new word contains the lastest
# searched word
sub _search_in_cache {
	my $self = shift;
	my $node = shift;
	my $data = shift;
	my $tree = $self->tree;

	# Quotes meta characters
	my $word = quotemeta( $self->GetValue );

	# Accept some regex like characters
	#   ^ = begin with
	#   $ = end with
	#   * = any string
	#   ? = any character
	$word =~ s/^\\\^/^/g;
	$word =~ s/\\\$$/\$/g;
	$word =~ s/\\\*/.*?/g;
	$word =~ s/\\\?/./g;

	# Goes thought each item from $data, if is a folder , searchs
	# recursively inside it, if is a file tries to match its name
	my @result = ();
	foreach (@$data) {

		# If it is a folder, searchs recursively below it
		if ( defined $_->{data} ) {
			my %temp = (
				dir  => $_->{dir},
				name => $_->{name},
				type => $_->{type}
			);

			# Creates each folder node
			my $new_folder = $tree->AppendItem(
				$node,
				$_->{name},
				$tree->{file_types}->{folder},
				-1,
				Wx::TreeItemData->new(
					{   dir  => $_->{dir},
						name => $_->{name},
						type => $_->{type},
					}
				)
			);

			# Deletes the folder node if any file below it was found
			if ( @{ $temp{data} } = $self->_search_in_cache( $new_folder, $_->{data} ) ) {
				push @result, \%temp;
			} else {
				$tree->Delete($new_folder);
			}
		} else {

			# Adds each matched file
			if ( $_->{name} =~ /$word/i ) {
				my $new_elem = $tree->AppendItem(
					$node,
					$_->{name},
					$tree->{file_types}->{package},
					-1,
					Wx::TreeItemData->new(
						{   name => $_->{name},
							dir  => $_->{dir},
							type => 'package',
						}
					)
				);
				push @result,
					{
					name => $_->{name},
					dir  => $_->{dir},
					type => 'package',
					};
			}
		}
	}

	# Returns 1 if any file above this path node was found or 0 and
	# deletes parent node if none
	return @result;
}

# If was switched between projects, and the search is actived, use the cached
# result set instead of doing the search again
sub _display_cached_search {
	my ( $self, $node, $data ) = @_;
	my $tree      = $self->tree;
	my $node_data = $tree->GetPlData($node);
	my $path      = File::Spec->catfile( $node_data->{dir}, $node_data->{name} );

	# Files that matchs and Dirs arrays
	my @dirs  = grep { $_->{type} eq 'folder' } @{$data};
	my @files = grep { $_->{type} eq 'package' } @{$data};

	# Search recursively inside each folder of the current folder
	for (@dirs) {

		# Creates each folder node
		my $new_folder = $tree->AppendItem(
			$node,
			$_->{name},
			$tree->{file_types}->{folder},
			-1,
			Wx::TreeItemData->new(
				{   dir  => $path,
					name => $_->{name},
					type => 'folder',
				}
			)
		);

		# Deletes the folder node if any file below it was found
		$self->_search( $new_folder, $_->{Data} );
	}

	# Adds each matched file
	foreach (@files) {
		my $new_elem = $tree->AppendItem(
			$node,
			$_->{name},
			$tree->{file_types}->{package},
			-1,
			Wx::TreeItemData->new(
				{   dir  => $path,
					name => $_->{name},
					type => 'package',
				}
			)
		);
	}

	return @{$data};
}

# Create the dropdown menu attached to the looking glass icon
sub create_menu {
	my $self        = shift;
	my $parent      = $self->parent;
	my $project_dir = $parent->project_dir;
	my $menu        = Wx::Menu->new;

	# Skip hidden files
	$self->{skip_hidden} = $menu->AppendCheckItem(
		-1,
		Wx::gettext('Skip hidden files')
	);
	$self->{skip_hidden}->Check(1);

	Wx::Event::EVT_MENU(
		$self,
		$self->{skip_hidden},
		sub {
			$self->{_skip_hidden}->{$project_dir} = $self->{skip_hidden}->IsChecked ? 1 : 0;
		},
	);

	# Skip CVS / .svn / blib and .git folders
	$self->{skip_vcs} = $menu->AppendCheckItem(
		-1,
		Wx::gettext('Skip CVS/.svn/.git/blib folders')
	);
	$self->{skip_vcs}->Check(1);

	Wx::Event::EVT_MENU(
		$self,
		$self->{skip_vcs},
		sub {
			$self->{_skip_vcs}->{$project_dir} = $self->{skip_vcs}->IsChecked ? 1 : 0;
		},
	);
	$menu->AppendSeparator();

	# Changes the project directory
	$self->{project_dir} = $menu->Append(
		-1,
		Wx::gettext('Change project directory')
	);

	Wx::Event::EVT_MENU(
		$self,
		$self->{project_dir},
		sub {
			$_[0]->parent->_change_project_dir;
		}
	);

	# Changes the Tree mode view
	my $submenu = Wx::Menu->new;
	$self->{sub_tree}     = $submenu->AppendRadioItem( 1, Wx::gettext('Tree listing') );
	$self->{sub_navigate} = $submenu->AppendRadioItem( 2, Wx::gettext('Navigate') );
	$self->{mode} = $menu->AppendSubMenu( $submenu, Wx::gettext('Change listing mode view') );
	$self->{sub_navigate}->Check(1);

	Wx::Event::EVT_MENU(
		$submenu,
		$self->{sub_tree},
		sub {
			$parent->{projects}->{ $parent->project_dir }->{mode} = 'tree';
			$parent->{mode_change} = 1;
			$parent->refresh;
		}
	);

	Wx::Event::EVT_MENU(
		$submenu,
		$self->{sub_navigate},
		sub {
			$parent->{projects}->{ $parent->project_dir }->{mode} = 'navigate';
			$parent->{mode_change} = 1;
			$parent->refresh;
		}
	);

	# Changes the panel side
	$self->{move_panel} = $menu->Append(
		-1,
		Wx::gettext('Move to other panel')
	);

	Wx::Event::EVT_MENU(
		$self,
		$self->{move_panel},
		sub {
			$_[0]->parent->move;
		}
	);

	return $menu;
}

# If it is a project, caches search field content while it is typed and
# searchs for files that matchs the type word
sub _on_text {
	my $self        = shift;
	my $parent      = $self->parent;
	my $tree        = $self->tree;
	my $value       = $self->GetValue;
	my $project_dir = $parent->project_dir or return;

	# If nothing is typed hides the Cancel button
	# and sets that the search is not in use
	unless ($value) {

		# Hides Cancel Button
		$self->ShowCancelButton(0);

		# Sets that the search for this project was just used
		# and is not in use anymore
		$self->{just_used}->{$project_dir} = 1;
		delete $self->{in_use}->{$project_dir};
		delete $self->{CACHED}->{$project_dir};

		# Updates the Directory Browser window
		$self->tree->refresh;

		return;
	}

	# Sets that the search is in use
	$self->{in_use}->{$project_dir} = 1;

	# Lock the gui here to make the updates look slicker
	# The locker holds the gui freeze until the update is done.
	my $locker = $self->current->main->freezer;

	# Cleans the Directory Browser window to show the result
	my $root = $tree->GetRootItem;
	$tree->DeleteChildren($root);

	# Searchs below the root path and caches it
	@{ $self->{CACHED}->{$project_dir}->{Data} } = $self->_search($root);

	# Caches the searched word to the project
	$self->{CACHED}->{$project_dir}->{value} = $value;

	# Expands all the folders to the files matched
	$tree->ExpandAll;

	# Shows the Cancel button
	$self->ShowCancelButton(1);

	return 1;
}

1;

# 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.