The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package dtRdr::GUI::Wx::SearchPane;
$VERSION = eval{require version}?version::qv($_):$_ for(0.10.1);

use warnings;
use strict;
use Carp;


use Wx qw(
  wxVERTICAL
  wxHORIZONTAL
  wxEXPAND
  wxRIGHT
  wxADJUST_MINSIZE
  wxTE_PROCESS_ENTER
  wxSUNKEN_BORDER
);
use base 'Wx::Panel';

use MultiTask::Minion;

use Wx::Event;

use dtRdr::Logger;
use dtRdr::Search::Book;
use dtRdr::GUI::Wx::SearchTree;
use dtRdr::GUI::Wx::Utils qw(_accel);
use dtRdr::Annotation::Range;

sub WxPerl::make_style (@) {
  my (@styles) = @_;
  my $style = 0;
  foreach my $item (@styles) {
    my $method = 'wx' . $item;
    Wx->can($method) or croak("no style $method");
    $style |= Wx->$method;
  }
  return($style);
}

use Class::Accessor::Classy;
ro qw(
  main_frame
  bv_manager
  minion
  text_ctrl
  go_button
  stop_button
  message
  tree_ctrl
  sizer
  hsizer_main
  opt_sizer
  opt_sub_sizer
  type_chooser
  search_chooser
  check_case
  lib_button
  sticky_button
  stucky_button
  hide_opts_button
  show_opts_button
);
rw 'timer';
rw 'options_sticky';
no  Class::Accessor::Classy;

=head1 NAME

dtRdr::GUI::Wx::SearchPane - the search pane

=head1 SYNOPSIS

=cut

=head1 Constructor

=head2 new

  my $search = dtRdr::GUI::Wx::SearchPane->new($parent, blah blah);

=cut

sub new {
  my $class = shift;
  my ($parent, @args) = @_;

  my $self = $class->SUPER::new($parent, @args);

  $self->__create_children;
  $self->__do_properties;
  $self->__do_layout;

  return($self);
} # end subroutine new definition
########################################################################

=head1 Setup

=head2 __create_children

  $self->__create_children;

=cut

sub __create_children {
  my $self = shift;
  $self->{sizer} = Wx::BoxSizer->new(wxVERTICAL);
	$self->{hsizer_main} = Wx::BoxSizer->new(wxHORIZONTAL);
	$self->{opt_sizer} = Wx::FlexGridSizer->new(2,2,0,0);
  $self->{opt_sub_sizer} = Wx::BoxSizer->new(wxHORIZONTAL);

  my @PS = (Wx::wxDefaultPosition(), Wx::wxDefaultSize());
	$self->{text_ctrl} = Wx::TextCtrl->new($self, -1, "", @PS,
    wxTE_PROCESS_ENTER
  );

  # some bitmaps
  my %bmp = qw(
    go    std_button_search
    stop  std_button_stop
    stick std_button_sticky
    stuck std_button_stuck
    hide  ctrl_arrow_5_up
    show  ctrl_arrow_5_down
  );
  $_ = dtRdr::GUI::Wx::Utils->Bitmap($_) for(values(%bmp));

  {
    my @choices = (
      ['titles', 'T.O.C.'   ],
      ['text',   'Full Text'],
    );
    $self->{_type_choices} = [map({$_->[0]} @choices)];
    my $chooser =
      $self->{type_chooser} =
        Wx::Choice->new($self, -1, @PS, [map({$_->[1]} @choices)]);
    $chooser->SetSelection(0); # OSX nit
    # XXX tooltips on choicers might be too cluttered
    1 and $chooser->SetToolTipString(
      "* Titles and Table of Contents\n" .
      "* Entire text of book"
    );
  }
  {
    my @choices = (
      ['phrase', 'Phrase'],
      ['word', 'Word(s)'],
      ['regexp', 'RegExp']
    );
    $self->{_search_choices} = [map({$_->[0]} @choices)];
    my $chooser =
      $self->{search_chooser} =
        Wx::Choice->new($self, -1, @PS, [map({$_->[1]} @choices)]);
    $chooser->SetSelection(0); # OSX nit
    1 and $chooser->SetToolTipString(
      "* Free-form search\n" .
      "* Whole words only\n" .
      "* Regular expression"
    );
  }
  ($self->{go_button} = Wx::BitmapButton->new($self, -1, $bmp{go})
    )->SetToolTipString('Go');
  ($self->{stop_button} = Wx::BitmapButton->new($self, -1, $bmp{stop})
    )->SetToolTipString('Stop');
  ($self->{lib_button} = Wx::Button->new($self, -1, 'Library')
    )->SetToolTipString('Select extra books to search');
  ($self->{sticky_button} = Wx::BitmapButton->new($self, -1, $bmp{stick})
    )->SetToolTipString('Keep options open');
  $self->{stucky_button} = Wx::BitmapButton->new($self, -1, $bmp{stuck});
  $self->{check_case} = Wx::CheckBox->new($self, -1, "Case");
  ($self->{hide_opts_button} = Wx::BitmapButton->new($self, -1, $bmp{hide})
    )->SetToolTipString('Hide search options');
  ($self->{show_opts_button} = Wx::BitmapButton->new($self, -1, $bmp{show})
    )->SetToolTipString('Options');
	$self->{message} = Wx::StaticText->new($self, -1, 'no message here', @PS);
	$self->{tree_ctrl} = dtRdr::GUI::Wx::SearchTree->new($self, -1, @PS,
    WxPerl::make_style qw(
      TR_HAS_BUTTONS
      TR_NO_LINES
      TR_LINES_AT_ROOT
      TR_DEFAULT_STYLE
      SUNKEN_BORDER
      ),
      # 'TR_HIDE_ROOT'
  );
} # end subroutine __create_children definition
########################################################################

=head2 __do_layout

  $self->__do_layout;

=cut

sub __do_layout {
  my $self = shift;

  $self->SetAutoLayout(1);
  my $sizer = $self->sizer;
  my $hsizer_main = $self->hsizer_main;
  my $opt_sizer = $self->opt_sizer;
  $self->SetSizer($sizer);
	#$sizer->Fit($self); # Is glade just wrong?
	$sizer->SetSizeHints($self);

 	$hsizer_main->Add($self->text_ctrl, 1, wxEXPAND, 0);
 	$hsizer_main->Add($self->go_button, 0, 0, 0);
 	$hsizer_main->Add($self->stop_button, 0, 0, 0);
  $hsizer_main->Show($self->stop_button, 0);
 	$sizer->Add($hsizer_main, 0, wxEXPAND|wxADJUST_MINSIZE, 0);

  $sizer->Add($self->show_opts_button, 0, wxEXPAND, 0);
  $sizer->Show($self->show_opts_button, 0);

  $opt_sizer->AddGrowableCol(0);
  $opt_sizer->Add($self->type_chooser, 0, wxEXPAND, 0);
  # TODO library select dialog
  $opt_sizer->Add($self->lib_button, 1, 0, 0);
  $opt_sizer->Add($self->search_chooser, 0, wxEXPAND, 0);
  {
    my $sub_sizer = $self->opt_sub_sizer;
    $sub_sizer->Add($self->check_case, 1, wxEXPAND, 0);
    $sub_sizer->Add($self->sticky_button, 0, wxEXPAND, 0);
    $sub_sizer->Add($self->stucky_button, 0, wxEXPAND, 0);
    $sub_sizer->Show($self->stucky_button, 0);

    $opt_sizer->Add($sub_sizer, 0, wxEXPAND, 0);
  }
  1 or $opt_sizer->Show($self->lib_button, 0);
  $sizer->Add($opt_sizer, 0, wxEXPAND, 0);
  1 or $sizer->Show($opt_sizer, 0);

  $sizer->Add($self->hide_opts_button, 0, wxEXPAND, 0);

  $sizer->Add($self->message, 0, 0, 0);
  $sizer->Show($self->message, 0);
  1 or $sizer->Show(1, 0); # hides it

 	$sizer->Add($self->tree_ctrl, 1, wxEXPAND, 0);

  $self->set_options_sticky(0);
  $self->_show_options(1);

} # end subroutine __do_layout definition
########################################################################

=head2 __do_properties

  $self->__do_properties;

=cut

sub __do_properties {
  my $self = shift;
  $self->tree_ctrl->SetBackgroundColour(Wx::Colour->new(244, 245, 255));
} # end subroutine __do_properties definition
########################################################################

=head1 Methods

=head2 init

  $pane->init($frame);

=cut

sub init {
  my $self = shift;
  my ($frame) = @_;

  $self->{main_frame} = $frame;
  my @attributes = qw(
    bv_manager
  );
  foreach my $attrib (@attributes) {
    my $l_attrib = $attrib;
    $self->{$l_attrib} = $frame->$attrib;
  }

  my $starter = sub {$_[1]->Skip; $self->search};
  my $stopper = sub {$_[1]->Skip; $self->stop_search};
  # keys
  Wx::Event::EVT_TEXT_ENTER($self, $self->text_ctrl, $starter);
  $self->SetAcceleratorTable( Wx::AcceleratorTable->new(
    $self->_accel('ESCAPE', $stopper))
  );

  # buttons
  Wx::Event::EVT_BUTTON($self->go_button, -1, $starter);
  Wx::Event::EVT_BUTTON($self->stop_button, -1, $stopper);

  { # expando
    my $show = sub {$_[1]->Skip; $self->_show_options(1)};
    my $hide = sub {$_[1]->Skip; $self->_show_options(0)};
    Wx::Event::EVT_BUTTON($self->show_opts_button, -1, $show);
    Wx::Event::EVT_BUTTON($self->hide_opts_button, -1, $hide);
  }
  { # sticky
    my $stick = sub {$_[1]->Skip; $self->set_options_sticky(1)};
    my $stuck = sub {$_[1]->Skip; $self->set_options_sticky(0)};
    Wx::Event::EVT_BUTTON($self->sticky_button, -1, $stick);
    Wx::Event::EVT_BUTTON($self->stucky_button, -1, $stuck);
  }

  # pretend we're the main_frame
  $self->tree_ctrl->init($self);

  # disable this for now
  $self->lib_button->Enable(0);
} # end subroutine init definition
########################################################################

=head2 search

  $self->search;

=cut

sub search {
  my $self = shift;
  WARN("search");

  if($self->minion) {
    delete($self->{minion})->quit;
    die "search in bad state";
  }

  my $bvm = $self->bv_manager;

  my $bv = $bvm->book_view or return;

  my $book = $bv->book;
  $book or die "no book?";
  $book->drop_selections;


  my $find = $self->text_ctrl->GetValue;
  length($find) or return;
  L->debug("search for $find");

  my $type = $self->get_type_choice;

  { # setup the regexp
    my $search = $self->get_search_choice;
    my $mod = $self->check_case->IsChecked ? '#' : 'i';
    if($search eq 'regexp') {
      $find = qr/(?$mod)$find/
    }
    elsif($search eq 'word') {
      $find = qr/(?$mod)\b\Q$find\E\b/;
    }
    elsif($search eq 'phrase') {
      $find = qr/(?$mod)\Q$find\E/;
    }
  }
  L->debug("regexp compiled to $find");

  $self->hide_message;
  $self->hide_options;
  $self->enable_search(0);

  my $tree = $self->tree_ctrl;
  $tree->DeleteAllItems;
  $self->{_hits} = {};

  if($type eq 'text') { # TODO other types
    $self->search_book($find, $book);
  }
  elsif($type eq 'titles') {
    $self->search_toc($find, $book);
  }
  else {
    die "'$type' type searches are not supported";
  }


} # end subroutine search definition
########################################################################

=head2 search_toc

  $self->search_toc($regexp, $book);

=cut

sub search_toc {
  my $self = shift;
  my ($find, $book) = @_;

  # XXX bah -- lots of this is a copy of search_book, but I don't see a
  # good refactor right now.

  my $tree = $self->tree_ctrl;
  $tree->book_root($book);

  my @nodes = $book->visible_nodes; # that's about it for this

  # focus the tree on the first hit
  my $first_hit; $first_hit = sub {
    undef($first_hit);
    $tree->SetFocus;
    $tree->SelectItem($_[0]);
  };

  require Time::HiRes;
  my $start_time = Time::HiRes::time();
  my $hits = 0;
  my $minion = MultiTask::Minion->make(sub {
    my $m = shift;
    return(
    work => sub {
      my $node = shift(@nodes);
      unless($node) { # done
        $m->finish;
        L->debug('search done');
        return;
      }
      my $title = $node->title;
      #WARN("search ", $title);
      $title =~ m/$find/ or return;
      #WARN("hit");
      $hits++;
      my $item = $tree->want_item($node);
      $tree->SetItemBold($item);
      $first_hit and $first_hit->($item);
    },
    finish => sub {
      L->debug("running search finish");
      $m->quit;

      # TODO regen the current page if it is for this book and has hits?

      unless($hits) { # XXX per $book
        # XXX $self->nothing_found($book) ?
        $self->flash_message("Nothing found");

        $tree->DeleteAllItems;
        # XXX $tree->delete_book_items($book);

        $self->text_ctrl->SetFocus; # XXX iff the focus is on searchbox?
      }
      else {
        my $time = sprintf('%0.1f', Time::HiRes::time() - $start_time);
        $self->flash_message('Done (' . $hits . ' hits/' . $time . 's)');
      }
    },
    quit => sub {
      $m->SUPER_quit;
      delete($self->{minion});
      $self->enable_search(1);
    },
    );
  }); # end make minion
  $self->{minion} = $minion;
  $self->main_frame->taskmaster->add($minion);
  $self->flash_message('Searching...');
} # end subroutine search_toc definition
########################################################################

=head2 search_book

  $self->search_book($regexp, $book);

=cut

sub search_book {
  my $self = shift;
  my ($find, $book) = @_;

  my $tree = $self->tree_ctrl;
  $tree->book_root($book);

  my $searcher = dtRdr::Search::Book->new(
    find => $find,
    book => $book
  );

  # focus the tree on the first hit
  my $first_hit; $first_hit = sub {
    undef($first_hit);
    $tree->SetFocus;
    $tree->SelectItem($_[0]);
  };

  require Time::HiRes;
  my $start_time = Time::HiRes::time();
  my $hits = 0;
  my $minion = MultiTask::Minion->make(sub {
    my $m = shift;
    return(
    work => sub {
      my $result = $searcher->next;
      unless($result) { # searcher done
        $m->finish;
        L->debug('search done');
        return;
      }
      #WARN("search");
      $result->null and return;
      $hits++;
      #WARN("hit");
      my $node = $result->start_node;
      my $item = $tree->want_item($node);
      my $aselect = dtRdr::AnnoSelection->claim($result->selection);
      my $hitcount;
      {
        $self->{_hits}{$node} ||= [];
        $hitcount = push(@{$self->{_hits}{$node}}, $aselect);
        # TODO abstract that
        # plus display "+$childhits" ? (bit trickier)
      }
      $tree->SetItemText($item, "($hitcount) " . $node->title);
      $tree->SetItemBold($item);
      $first_hit and $first_hit->($item);
      $book->add_selection($aselect);
    },
    finish => sub {
      L->debug("running search finish");
      $m->quit;

      # TODO regen the current page if it is for this book and has hits?

      unless(%{$self->{_hits}}) { # XXX per $book
        $self->flash_message("Nothing found");

        $tree->DeleteAllItems;
        # XXX $tree->delete_book_items($book);

        $self->text_ctrl->SetFocus; # XXX iff the focus is on searchbox?
      }
      else {
        my $time = sprintf('%0.1f', Time::HiRes::time() - $start_time);
        $self->flash_message('Done (' . $hits . ' hits/' . $time . 's)');
      }
    },
    quit => sub {
      $m->SUPER_quit;
      delete($self->{minion});
      $self->enable_search(1);
    },
    );
  }); # end make minion
  $self->{minion} = $minion;
  $self->main_frame->taskmaster->add($minion);
  $self->flash_message('Searching...');
} # end subroutine search_book definition
########################################################################

=head2 hide_message

  $self->hide_message;

=cut

sub hide_message {
  my $self = shift;
  $self->sizer->Show($self->message, 0);
  $self->sizer->Layout;
} # end subroutine hide_message definition
########################################################################

=head2 flash_message

  $self->flash_message("Nothing Found", 3);

=cut

sub flash_message {
  my $self = shift;
  my ($message, $timeout) = @_;

  if(my $timer = $self->timer) {
    $timer->Stop;
  }

  $self->message->SetLabel('');
  $self->sizer->Show($self->message, 1);
  $self->sizer->Layout;
  $self->message->SetLabel($message);
  $self->set_timer( my $timer = Wx::Timer->new($self) );
  if(defined($timeout)) {
    Wx::Event::EVT_TIMER($self, -1, sub {
      $timer->Stop;
      $self->Disconnect($timer, -1, Wx::wxEVT_TIMER());
      $self->hide_message;
      $self->set_timer(undef);
    });
    $timer->Start($timeout * 1000);
  }
} # end subroutine flash_message definition
########################################################################

=head2 enable_search

  $self->enable_search($bool);

=cut

sub enable_search {
  my $self = shift;
  my ($bool) = @_;
  $self->text_ctrl->Enable($bool);
  # enable/disable/hide the buttons
  my $go = $self->go_button;
  my $stop = $self->stop_button;
  $go->Enable($bool);
  $stop->Enable(! $bool);
  my $hsizer_main = $self->hsizer_main;
  $hsizer_main->Show(($bool ? $stop : $go), 0);
  $hsizer_main->Show(($bool ? $go : $stop), 1);
  $hsizer_main->Layout;
} # end subroutine enable_search definition
########################################################################

=head2 stop_search

  $self->stop_search;

=cut

sub stop_search {
  my $self = shift;
  WARN "stop";
  if(my $minion = $self->minion) {
    $minion->quit;
  }
  $self->flash_message("Search cancelled", 1);
  $self->_show_options(1);
  $self->text_ctrl->SetFocus;
} # end subroutine stop_search definition
########################################################################

=head2 hide_options

Hides the options pane unless the sticky is set.

  $self->hide_options;

=cut

sub hide_options {
  my $self = shift;
  $self->options_sticky and return;
  $self->_show_options(0);
} # end subroutine hide_options definition
########################################################################

=head2 _show_options

  $self->_show_options(1|0);

=cut

sub _show_options {
  my $self = shift;
  my ($bool) = @_;

  my $show = $self->show_opts_button;
  my $hide = $self->hide_opts_button;
  my $sizer = $self->sizer;

  $sizer->Show(($bool ? $show : $hide), 0);
  $sizer->Show(($bool ? $hide : $show), 1);
  $sizer->Show($self->opt_sizer, $bool);
  # XXX sadly, that pokes our subsizer for some reason
  $self->set_options_sticky($self->options_sticky);
  $sizer->Layout;
  $self->opt_sizer->Layout;
  if($bool) {
    $self->type_chooser->SetFocus;
  }
  else {
    $self->text_ctrl->SetFocus;
  }
} # end subroutine _show_options definition
########################################################################

=head2 set_options_sticky

  $self->set_options_sticky(1|0);

=cut

sub set_options_sticky {
  my $self = shift;
  my ($bool) = @_;

  my $stick = $self->sticky_button;
  my $stuck = $self->stucky_button;
  my $sizer = $self->opt_sub_sizer;
  $sizer->Show(($bool ? $stick : $stuck), 0);
  $sizer->Show(($bool ? $stuck : $stick), 1);
  $sizer->Layout;

  $self->SUPER::set_options_sticky($bool);
} # end subroutine set_options_sticky definition
########################################################################

=head2 get_type_choice

Returns the key string corresponding to the type_chooser selection.

  $self->get_type_choice;

=cut

sub get_type_choice {
  my $self = shift;

  my $i = $self->type_chooser->GetSelection;
  return($self->{_type_choices}[$i]);
} # end subroutine get_type_choice definition
########################################################################

=head2 get_search_choice

  $self->get_search_choice;

=cut

sub get_search_choice {
  my $self = shift;

  my $i = $self->search_chooser->GetSelection;
  return($self->{_search_choices}[$i]);
} # end subroutine get_search_choice definition
########################################################################

=head2 _make_found_context

  $self->_make_found_context($book, $selection);

=cut

sub _make_found_context {
  my $self = shift;
  my ($book, $selection) = @_;
  # XXX this is too big when the selection is big
  my $snode = $selection->node;
  my $nc = $book->get_NC($snode);
  my ($s, $e) = ($selection->a, $selection->b);
  WARN("got context $s, $e");
  my $len = 10;
  $s -= $len;
  $e += $len;
  $s = 0 if($s < 0);
  my $span = $snode->word_end - $snode->word_start;
  $e = $span if($e > $span);
  WARN("got context $s, $e");
  my $context = substr($nc, $s, $e - $s);
  WARN("got context $context");
  return($context);
} # end subroutine _make_found_context definition
########################################################################

=head2 show_context_menu

  $pane->show_context_menu($node, $wxPoint);

=cut

sub show_context_menu {
  my $self = shift;
  my ($node, $point) = (@_);

  my @selections;
  my $book = $node->book;

  # TODO skip-out if this was a TOC search?

  foreach my $hnode ($node, $book->descendant_nodes($node)) {
    my $hits = $self->{_hits}{$hnode};
    next unless($hits and ref($hits));
    push(@selections, @$hits);
  }

  # node with no hits in visible children
  @selections or return;

  my $menu = Wx::Menu->new;
  foreach my $hit (@selections) {
    my $string = $self->_make_found_context($book, $hit);
    $string =~ s/\t/ /g; # just in case
    $string =~ s/&/&&/g;
    my $item = $menu->Append(Wx::NewId(), $string);
    Wx::Event::EVT_MENU($menu, $item, sub {
      $_[1]->Skip;
      $self->goto_item($node, $hit);
    });
  }

  $self->PopupMenu($menu, $point);
} # end subroutine show_context_menu definition
########################################################################

=head2 goto_item

  $pane->goto_item($node, $selection);

=cut

sub goto_item {
  my $self = shift;
  my ($node, $selection) = @_;

  # TODO insert selections in book
  # TODO set the current hit somewhere in self for prev/next buttons

  my $bv = $self->bv_manager->book_view;
  $bv->render_node_by_id($node->id);

  # scroll
  if($selection) {
    $bv->jump_to($selection);
  }
} # end subroutine goto_item definition
########################################################################

=head1 AUTHOR

Eric Wilhelm <ewilhelm at cpan dot org>

http://scratchcomputing.com/

=head1 COPYRIGHT

Copyright (C) 2006 Eric L. Wilhelm and OSoft, All Rights Reserved.

=head1 NO WARRANTY

Absolutely, positively NO WARRANTY, neither express or implied, is
offered with this software.  You use this software at your own risk.  In
case of loss, no person or entity owes you anything whatsoever.  You
have been warned.

=head1 LICENSE

The dotReader(TM) is OSI Certified Open Source Software licensed under
the GNU General Public License (GPL) Version 2, June 1991. Non-encrypted
and encrypted packages are usable in connection with the dotReader(TM).
The ability to create, edit, or otherwise modify content of such
encrypted packages is self-contained within the packages, and NOT
provided by the dotReader(TM), and is addressed in a separate commercial
license.

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.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

=cut

# vi:ts=2:sw=2:et:sta
1;