# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: Search.pm 3236 2008-08-21 14:30:44Z josh $

package BigMed::Search;
use strict;
use warnings;
use utf8;
use Carp;
$Carp::Verbose = 1;
use BigMed::DiskUtil qw(bm_file_path bm_load_file);
use BigMed::Prefs;

sub new {
    my ( $class, %param ) = @_;

    my $subclass = $param{engine};
    if ($subclass) {
        ( my $file = $class . qq{::$subclass.pm} ) =~ s{::}{/}msg;
        local $SIG{'__DIE__'} = undef;
        eval { require $file };
        croak "Bad search driver: $subclass. $@" if $@;
    }
    else {
        $subclass = $class;
    }

    my $locale = lc( $param{locale} ) || 'en-us';
    $locale =~ s/_/-/msg;
    $locale = 'en-us' if $locale eq 'en';

    my $self = bless { locale => $locale }, $subclass;
    return $self;
}

# INDEXER AND RETRIEVER OBJECTS -------------------------------------
#most subclasses will want to override these methods to include their
#own indexer/retriever subclasses

sub locale {
    return $_[0]->{locale};
}

sub indexer {
    my $self = shift;
    if ( !$self->{indexer} ) {
        require BigMed::Search::Indexer;
        $self->{indexer} = BigMed::Search::Indexer->new( $self->{locale} );
    }
    return $self->{indexer};
}

sub retriever {
    my $self = shift;
    if ( !$self->{retriever} ) {
        require BigMed::Search::Retriever;
        $self->{retriever} =
          BigMed::Search::Retriever->new( $self->{locale} );
    }
    return $self->{retriever};
}

# WRAPPER ROUTINES TO THE INDEXER/RETRIEVERS ------------------------

#for external search engines (e.g. google, yahoo), override
#index_page and remove_page to, e.g., `sub { return 1 }` since
#they don't need indexing at all

sub index_page {    # $_[0]:self, $_[1]: object or selection
    my ( $self, $data ) = @_;
    return $self->indexer->index_page($data);
}

sub remove_page {    # $_[0]:self, $_[1]: object or selection
    my ( $self, $data ) = @_;
    return $self->indexer->remove_page($data);
}

sub search {         #self, site, query, \%param
    my $self = shift;
    return $self->retriever->search(@_);
}

# HTML-BUILDING ROUTINES --------------------------------------------

#for external search engines (e.g. google, yahoo), override
#form_html and result_html

my %PREFS = (
    'html_search_button' => {
        edit_type   => 'simple_text',
        default     => 'Search',
        sitewide    => 1,
        priority    => 95,
        edit_params => { required => 1, },
    },
    'html_search_title' => {
        edit_type   => 'simple_text',
        default     => 'Search Results',
        sitewide    => 1,
        priority    => 90,
        edit_params => { required => 1, },
    },
    'html_search_intro' => {
        edit_type => 'rich_text_inline',
        default => 'Results &lt;%start%&gt;-&lt;%end%&gt; of &lt;%total%&gt; '
          . 'for <strong>&lt;%query%&gt;</strong>:',
        sitewide    => 1,
        priority    => 85,
        edit_params => {
            required    => 1,
            description => 'PREFS_DESC_html_search_intro',
        },
    },
    'html_search_noresults' => {
        edit_type   => 'rich_text_inline',
        default     => 'Your search returned no documents.',
        sitewide    => 1,
        priority    => 80,
        edit_params => {
            description => 'BM_rich_text_inline_notice',
            required    => 1,
        }
    },
    'html_search_previous' => {
        edit_type => 'simple_text',
        default   => 'Previous',
        sitewide  => 1,
        priority  => 75,
    },
    'html_search_next' => {
        edit_type => 'simple_text',
        default   => 'Next',
        sitewide  => 1,
        priority  => 70,
    },

);

sub search_prefs {
    return %PREFS;
}

sub register_search_prefs {
    return if BigMed::Prefs->pref_exists('html_search_searchtext');
    foreach my $pref_name ( keys %PREFS ) {
        BigMed::Prefs->register_pref( $pref_name, $PREFS{$pref_name} );
    }
    if ( !BigMed::Prefs->pref_exists('html_links_window') ) {    #ugly
        BigMed::Prefs->register_pref( 'html_htmlhead_lang',
            { default => 'en', edit_type => 'raw_text' } );
        BigMed::Prefs->register_pref(
            'html_links_window',
            {   edit_type => 'boolean',
                default   => 0,
                sitewide  => 1,
            }
        );
        BigMed::Prefs->register_pref(
            'html_links_window_intdomains',
            {   edit_type => 'value_freeform',
                default   => [],
                sitewide  => 1,
            }
        );
    }
    return;
}

sub form_html {
    my ( $self, $context, $obj, $rparam ) = @_;
    my $site   = $context->site;
    my $button = $site->get_pref_value('html_search_button');
    my $bm     = BigMed->bigmed;

    #special handling for path_info replacement
    my $query_mode  = $bm->env('USE_BMQUERY');
    my $pdiv        = $query_mode ? '?' : '/';
    my $form_method = $query_mode ? 'post' : 'get';

    #build form url; force a runmode (even though there's just one) to
    #help compatibility with the USE_BMQUERY plugin.
    my $sid = $site->id;
    my $form_url = $bm->env('MOXIEBIN') . "/bm-search.cgi${pdiv}q/$sid";

    return $context->build_markup(
        'wi_search.tmpl',
        site_id       => $sid,
        form_url      => $form_url,
        search_text   => q{},
        search_button => $button,
        form_method   => $form_method,
        query_mode    => $query_mode,
        'close'       => BigMed::Format::HTML::tag_closer($context),
    );
}

sub result_page_html {
    my ( $self, $context ) = @_;

    my $site  = $context->site;
    my $title = $site->get_pref_value('html_search_title');
    my $bc = BigMed::Format::HTML::top_level_breadcrumbs( $context, $title );
    my $close = BigMed::Format::HTML::tag_closer($context);

    #htmlhead replacement, with base href
    my $homesec  = $site->homepage_obj;
    my $home     = $homesec->section_page_obj;
    my $base     = $site->homepage_url . '/';
    my $htmlhead = BigMed::Format::HTML::wi_htmlhead( $context, $home );
    $htmlhead =~ s{(<head>)(\s+)}{$1$2<base href="$base"$close>$2}ms;

    #load replacement for <%content%> widget; it's HTML::Template code
    #stored in the template directory.
    my @paths =
      ref $context->widget_template_path
      ? @{ $context->widget_template_path }
      : ( $context->widget_template_path );
    my $tpath;
    foreach my $d (@paths) {
        last if -e ( $tpath = bm_file_path( $d, 'wi_search_result.tmpl' ) );
    }
    my $content = join( "\n", bm_load_file($tpath) );

    my $dot = BigMed->bigmed->env('DOT');
    return {
        filename      => "bm${dot}search",
        htmlhead      => $htmlhead,
        content       => $content,
        title         => $title,
        breadcrumbs   => $bc,
        headline      => qq{<h2 class="bmw_headline">$title</h2>},
        tips          => q{},
        announcements => q{},
    };
}

1;
__END__


=head1 NAME

BigMed::Search

=head1 DESCRIPTION

BigMed::Search provides an interface for searching Big Medium sites and
can be subclassed to create custom search engines.

The default search uses BigMed::Search::Indexer and BigMed::Search::Retriever
to handle indexing and searching, respectively.

=head1 SUMMARY

    use BigMed::Search;
    
    #USE THE BUILT-IN SEARCH ENGINE
    my $search = BigMed::Search->new( locale => 'en-us' );
    
    #add or reindex pages
    $search->index_page( $page_obj )
      or BigMed::Error->error_stop;
    $search->index_page( $selection_of_pages )
      or BigMed::Error->error_stop;
    
    #remove pages from the index
    $search->remove_page( $page_obj )
      or BigMed::Error->error_stop;
    $search->remove_page( $selection_of_pages )
      or BigMed::Error->error_stop;

    #search
    $search->search( $site_obj_or_id, $query_string )
      or BigMed::Error->error_stop;
    
    
    #USE AN ALTERNATE SEARCH ENGINE
    #this uses BigMed::Search::Yahoo engine
    $search = BigMed::Search->new( engine => 'Yahoo' );
    $search->search( $site_obj_or_ir, $query_string );

=head1 METHODS

=head2 C<new>

    my $search = BigMed::Search->new( locale => $lang, engine => $engine );

Returns a new BigMed::Search (or subclass) object. Accepts a hash of the
following optional parameters:

=over 4

=item * locale => $lang

The ISO code for the language to use. If no value provided, default is
C<en-us>.

=item * engine => $engine_name

Alternate search engine to use. The value is used to find a subclass. If
the value is C<Yahoo>, for example, BigMed::Search::Yahoo will be loaded
and a BigMed::Search::Yahoo object returned. The method throws an exception
if no such module exists.

If no value is provided, Big Medium's internal search is used by default.

=back

=head2 C<index_page>

    $search->index_page( $page_obj ) or BigMed::Error->error_stop;
    $search->index_page( $selection ) or BigMed::Error->error_stop;

Accepts either a page object or selection of page objects as the argument
and adds or reindexes the content of the page(s) to the search index.
Returns a true value on success or false on error (also adding an error
to the BigMed::Error queue).

This is a wrapper method to the C<index_page> method of the object
returned by the C<indexer> method (BigMed::Search::Indexer by default).
The recommended method for subclasses to replace BigMed::Search's default
indexing methods is to provide a new C<indexer> method to return a custom
indexer object.

Or, if a subclass doesn't need to index at all (as with an external
engine like Yahoo), you can provide a custom C<index_page> object like
so:

    sub index_page { return 1; }

=head2 C<remove_page>

    $search->remove_page( $page_obj ) or BigMed::Error->error_stop;
    $search->remove_page( $selection ) or BigMed::Error->error_stop;
    $search->remove_page( { site => $site, pages => \@page_ids } )
      or BigMed::Error->error_stop;

Accepts one of three types of arguments:

=over 4

=item * A page object

=item * A selection of page objects

=item * A hash argument with two key/value pairs:

=over 4

=item * site => site id or site object

=item * pages => array reference of one or more page ids

=back

=back

The method removes the page(s) indicated by the argument from the search
index. Returns a true value on success or false on error (also adding an
error to the BigMed::Error queue).

As with C<index_page> above, this is a wrapper method to the C<remove_page>
method of the object returned by the C<indexer> method. Subclasses
can override this method in the same ways described in the entry for
C<index_page>.

=head2 C<search>

    $search->search( $site_obj_or_id, $query_string )
      or BigMed::Error->error_stop;

Returns a BigMed::Search::Result object. Accepts a BigMed::Site object or
id in the first argument and a query string in the second. Both arguments
are required.

Returns false if there's an error (also adding an error to the BigMed::Error
queue).

This is a wrapper method to the C<search> method of the object returned
by the C<retriever> method (BigMed::Search::Retriever by default).
The recommended method for subclasses to replace BigMed::Search's default
search method is to provide a new C<retriever> method to return a custom
retriever object.

=head2 C<indexer>

    my $indexer = $search->indexer;

By default, returns a BigMed::Search::Indexer object with the same
ISO locale as the search object itself. Subclasses can override this
method to return a custom indexer, which should provide their own
C<index_page> and C<remove_page> methods.

=head2 C<retriever>

    my $retriever = $search->retriever;

By default, returns a BigMed::Search::Retriever object with the same
ISO locale as the search object itself. Subclasses can override this
method to return a custom retriever, which should provide its own
C<search> method.

=head2 C<form_html>

    my $widget_html = $search->form_html($context, $obj, $rparam);

Called by BigMed::Format::HTML to build the search form html for
the C<< <%search%> >> widget. Subclasses should override this
method to provide custom html for the widget.

The arguments are the same as those received by all widget handler
methods: A BigMed::Context object, the page object, and a hash reference
of the widget attributes.

=head2 C<result_page_html>

    my $rhash = $search->result_page_html($context);

Called as a C<level_extras> routine for the top level when building
HTML pages. BigMed::Builder expects the method to return a hash
reference of values, corresponding to the widgets to replace in the
HTML utility template. If a false value is returned, no page will
be built by BigMed::Builder.

The sole argument is the same as that received by all level_extras
routines: A BigMed::Context object.

=head2 C<register_search_prefs>

    $search->register_search_prefs();
    BigMed::Search->register_search_prefs();

Registers the BigMed::Prefs HTML preferences for search-related functions.
Provides a shortcut so that BigMed::Search doesn't need to load the
entire BigMed::Format::HTML module and its dependencies.

=head2 C<search_prefs>

    BigMed::Search->search_prefs;

Returns an array of hash references describing the HTML preferences
for BigMed::Search. Used internally by BigMed::Format::HTML to load
the search-related preferences.

=head2 C<locale>

    my $locale = $search->locale;

Returns the search object's ISO locale.

=head1 AUTHOR & COPYRIGHTS

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content management system
are governed by Global Moxie's software licenses and may not be
used outside of the terms and conditions outlined there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut

