# ==================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
#   GT::SQL::Search::NONINDEXED::Search
#   Author  : Alex Krohn
#   CVS Info :                          
#   $Id: Search.pm,v 1.28 2004/08/28 03:53:50 jagerman Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
# ==================================================================
#
# Description:
#   Nonindex search system
#

package GT::SQL::Search::NONINDEXED::Search;
# ==================================================================
    use strict;
    use vars qw/@ISA $ATTRIBS $VERSION $DEBUG/;
    use GT::SQL::Search::Base::Search;
    use GT::SQL::Condition;
    @ISA = qw( GT::SQL::Search::Base::Search );

    $DEBUG      = 0;
    $VERSION    = sprintf "%d.%03d", q$Revision: 1.28 $ =~ /(\d+)\.(\d+)/;  
    $ATTRIBS    = {
# parse based on latin characters
        latin_query_parse => 0
    };

sub load {
    shift;
    return GT::SQL::Search::NONINDEXED::Search->new(@_)
}

sub query {
#--------------------------------------------------------------------------------
# Returns a sth based on a query
#
# Options:
#        - paging
#           mh            : max hits
#           nh            : number hit (or page of hits)
#
#        - searching
#           ww            : whole word
#           ma            : 1 => OR match, 0 => AND match, undefined => QUERY
#           substring     : search for substrings of words
#           bool          : 'and' => and search, 'or' => or search, '' => regular query
#           query         : the string of things to ask for
#
#        - filtering
#           field_name    : value       # Find all rows with field_name = value
#           field_name    : ">value"    # Find all rows with field_name > value.
#           field_name    : "<value"    # Find all rows with field_name < value.
#           field_name-gt : value       # Find all rows with field_name > value.
#           field_name-lt : value       # Find all rows with field_name < value.
#
# Parameters:
#        ( $CGI ) : a single cgi object
#        ( $HASH ) : a hash of the parameters
#
    my $self = shift;

# find out what sort of a parameter we're dealing with
    my $input = $self->common_param(@_);

# add additional parameters if required
    foreach my $parameter ( keys %{$ATTRIBS} ) {
        if ( not exists $input->{$parameter} ) {
            $input->{$parameter} = $self->{$parameter};
        }
    }

# parse query
    $self->debug( "Search Query: $$input{query}", 1 ) if ($self->{_debug});
    my ( $query, $rejected ) = $self->_parse_query_string( $input->{'query'} );

    $self->{rejected_keywords} = $rejected;

# setup the additional input parameters
    $query = $self->_preset_options( $query, $input );

    $self->debug( "Set the pre-options: ", $query ) if ($self->{_debug});

# now sort into distinct buckets
    my $buckets = GT::SQL::Search::Base::Search::_create_buckets( $query );
    $self->debug_dumper( "Created Buckets for querying: ", $buckets ) if ($self->{_debug});


    require GT::SQL::Condition;
    my $query_condition = new GT::SQL::Condition;

# now handle the separate possibilities
# the union
    my $union_cond     = $self->_get_condition( $buckets->{keywords},        $buckets->{phrases} );
    $query_condition->add(GT::SQL::Condition->new(@$union_cond, 'OR')) if $union_cond;
# the intersect
    my $intersect_cond = $self->_get_condition( $buckets->{keywords_must},   $buckets->{phrases_must} );
    $query_condition->add(GT::SQL::Condition->new(@$intersect_cond)) if $intersect_cond;

# the disjoin
    my $disjoin_cond   = $self->_get_condition( $buckets->{keywords_cannot}, $buckets->{phrases_cannot} );
    $query_condition->add(GT::SQL::Condition->new(@$disjoin_cond, 'OR')->not) if $disjoin_cond;

# now handle filters
    my $cols    = $self->{'table'}->cols();
    my %filters = map {
        (my $column = $_) =~ s/-[lg]t$//;
        exists $cols->{$column}
            ? ($_ => $input->{$_})
            : ()
    } keys %{$input};

# if there was no query nor filter return nothing.
    keys %$query or keys %filters or return $self->sth({});

    if (keys %filters) {
        $self->debug( "Creating Filters: ", \%filters ) if ($self->{_debug});
        $self->_add_filters( \%filters );
        $query_condition = GT::SQL::Condition->new( keys %$query ? $query_condition : (), $self->{filter} );
    }
    elsif ($self->{filter} and keys %{$self->{filter}} ) {
        $self->debug( "Filtering results", $self->{filter} ) if ($self->{_debug});
        $query_condition = GT::SQL::Condition->new( keys %$query ? $query_condition : (), $self->{filter} );
    }
    else {
        $self->debug( "No filters being used.") if ($self->{_debug});
    }

# now this query should probably clear the filters once it's been used, so i'll do that here
    $self->{filter} = undef;

    my $tbl  = $self->{table};
    my ($pk) = $tbl->pk;

# now run through a callback function if needed.
    if ($self->{callback}) {

# Warning: this slows things a heck of a lot.
        unless (ref $self->{callback} and ref $self->{callback} eq 'CODE') {
            return $self->error ('BADARGS', 'FATAL', "callback '$self->{callback}' must be a code ref!");
        }

        my $sth     = $tbl->select( [ $pk ], $query_condition );
        my $results = {};
        while (my $result = $sth->fetchrow) {
            $results->{$result} = undef;
        }
        $self->debug_dumper("Running results through callback. Had: " . scalar (keys %$results) . " results.", $results) if ($self->{_debug});
        $results = $self->{callback}->($self, $results);
        $self->debug_dumper("New result set: " . scalar (keys %$results) . " results.", $results) if ($self->{_debug});
        $self->{rows} = scalar($results ? keys %{$results} : ());

        return $self->sth( $results );
    }

# and now create a search sth object to handle all this
    $input->{nh} = (defined $input->{nh} and $input->{nh} =~ /^(\d+)$/) ? $1 : 1;
    $input->{mh} = (defined $input->{mh} and $input->{mh} =~ /^(\d+)$/) ? $1 : 25;
    $input->{so} = (defined $input->{so} and $input->{so} =~ /^(asc(?:end)?|desc(?:end)?)$/i) ? $1 : '';

# check that sb is not dangerous
    my $sb = $self->clean_sb($input->{sb}, $input->{so});

    my $offset = ( $input->{nh} - 1 ) * $input->{mh};
    $tbl->select_options($sb) if ($sb);
    $tbl->select_options("LIMIT $offset, $input->{mh}");
    my $sth = $tbl->select( $query_condition ) or return;

# so how many hits did we get?
    $self->{rows} = $sth->rows();
    if (($input->{nh} > 1) or ($self->{rows} == $input->{mh})) {
        $self->{rows} = $tbl->count($query_condition);
    }
    return $sth;
}

sub _get_condition {
#-------------------------------------------------------------------------------
    my ( $self, $keywords, $phrases ) = @_;

    my @list = ( keys %$keywords, keys %$phrases );

    my $tbl     = $self->{table} or return $self->error( 'NODRIVER', 'FATAL' );
    my @cond    = ();
    my %tmp     = $tbl->weight();
    my @weights = keys  %tmp or return;
    foreach my $element ( @list ) {
        my @where = ();
        foreach my $cols ( @weights ) {
            push @where, [$cols, 'LIKE', "%$element%"]; # Condition does quoting by default.
        }
        push @cond, GT::SQL::Condition->new(@where, 'OR');
    }
    @cond or return;

    return \@cond;
}

sub _parse_query_string {
#------------------------------------------------------------
# Parses a query string '+foo -"bar this" alpha' into a hash of
# words and modes.
#
    my ($self, $text) = @_;
    my %modes = (
        '+' => 'must',
        '-' => 'cannot',
        '<' => 'greater',
        '>' => 'less'
    );

# Latin will break up on actual words and punctuation.
    if ($self->{latin_query_parse}) {
        return $self->SUPER::_parse_query_string( $text );
    }
    else {
        my $words = {};
        my @terms;
        my $i = 0;
        foreach my $term (split /"/, $text) {
            push @terms, ($i++ % 2 ? $term : split ' ', $term);
        }
        for (my $i = 0; $i < @terms; $i++) {
            my $word = $terms[$i];
            $word =~ s/^\s*|\s*$//g;
            next if ($word eq '');
            ($word eq '-') and ($word = '-' . $terms[++$i]);
            ($word eq '+') and ($word = '+' . $terms[++$i]);
            $word         =~ s/^([<>+-])//;
            my $mode      = ($1 and $modes{$1} or 'can');
            my $substring = ($word =~ s/\*$//) || 0;
            if ($word =~ /\s/) {
                $words->{$word} = {
                    mode      => $mode,
                    phrase    => 1,
                    substring => $substring,
                    keyword   => 0,
                };
            }
            else {
                $words->{$word} = {
                    mode      => $mode,
                    phrase    => 0,
                    substring => $substring,
                    keyword   => 1,
                };
            }
        }
        return $words;
    }
}

1;
