#------------------------------------------------------------------------------
#$Author: andrius $
#$Date: 2020-06-10 03:42:10 -0400 (Wed, 10 Jun 2020) $ 
#$Revision: 6931 $
#$URL: svn://saulius-grazulis.lt/restful/tags/v0.15.2/lib/Database/Filter.pm $
#------------------------------------------------------------------------------
#*
#  An object to encapsulate a filter query.
#**

package Database::Filter;

use strict;
use warnings;

use List::Util qw(any);
use Scalar::Util qw(blessed);
use URI::Encode qw( uri_encode );

use Database;
use OPTIMADE::Filter;
use OPTIMADE::Filter::Parser;
use OPTIMADE::Filter::AndOr;
use OPTIMADE::Filter::Comparison;
use OPTIMADE::Filter::Negation;
use OPTIMADE::Filter::Property;
use RestfulDB::Exception;

$OPTIMADE::Filter::Parser::allow_LIKE_operator = 1;

#=======================================================================
# Constructors

## $params = {
#    'filter' => 'filter expression from URL',
#    'select_column' => 'from URL',
#    'select_operator' => 'from URL',
#    'search_value' => 'from URL',
#    'select_not_operator' => 'from URL',
#    'select_combining' => 'from URL'
#    }
sub new
{
    my ( $class, $params ) = @_;

    my $filter;
    if ( defined $params->{filter} && length $params->{filter} ) {
        my $parser = new OPTIMADE::Filter::Parser;
        eval {
            $filter = $parser->parse_string( $params->{filter} );
        };
        InputException->throw(
            "WARNING, incorrect filter expression '$$params{filter}': " . $@ ) if $@;
    }           
    # Transfer all new 'select' parameters from the filtering form to the
    # 'filter' expression, possibly merging with the previous filter
    # expression:
    
    if( defined $params->{select_column} &&
        defined $params->{select_operator} ) {

        $params->{search_value} = '' if !defined $params->{search_value};

        my $property = new OPTIMADE::Filter::Property;
        push @$property, lc $params->{select_column};

        my $selected_filter;
        if( $params->{select_operator} =~ /^(un)?known$/ ) {
            $selected_filter = [ $property, 'IS ' . uc $params->{select_operator} ];
        } else {
            my $operator =
                uc $Database::filter_operators->{$params->{select_operator}};

            $selected_filter = new OPTIMADE::Filter::Comparison;
            $selected_filter->operator( $operator );
            $selected_filter->push_operand( $property );
            $selected_filter->push_operand( $params->{search_value} );
        }
        
        if( defined $params->{select_not_operator} &&
            $params->{select_not_operator} eq 'not' ) {
            $selected_filter = [ 'NOT', [ $selected_filter ] ];
        }
        if( !defined $filter ) {
            $filter = $selected_filter;
        } else {
            if( !$params->{select_combining} ||
                 $params->{select_combining} eq 'new' ) {
                $filter = $selected_filter;
            } elsif( $params->{select_combining} eq 'within' ) {
                $filter = [ $filter, 'AND', $selected_filter ];
            } elsif( $params->{select_combining} eq 'append' ) {
                $filter = [ $filter, 'OR', $selected_filter ];
            } else {
                die "unrecognised filter combination request " .
                    "'$params->{select_combining}'";
            }
        }
    }

    return bless { filter => $filter }, $class;
}

sub new_from_tree
{
    my( $class, $tree ) = @_;
    return bless { filter => $tree }, $class;
}

sub new_for_conjunction
{
    my( $class, $columns, $values ) = @_;

    my @columns = @$columns;
    my @values = @$values;

    my $root;
    while( @columns ) {
        my $clause = OPTIMADE::Filter::Comparison->new( '=' );
        $clause->left( OPTIMADE::Filter::Property->new( @{shift @columns} ) );
        $clause->right( shift @values );
        if( $root ) {
            $root = OPTIMADE::Filter::AndOr->new( $root,
                                                  'AND',
                                                  $clause );
        } else {
            $root = $clause;
        }
    }

    return $class->new_from_tree( $root );
}

#=======================================================================
# Methods

sub query_string
{
    &get_filter_representation;
}

sub query_string_uri
{
    my( $self ) = @_;
    return '' if !$self->filter;
    return 'filter=' . uri_encode( $self->query_string,
                                   { encode_reserved => 1 } );
}

sub where_clause
{
    my( $self, $delim ) = @_;
    return '' if !$self->filter;

    my( $sql, $values ) = $self->filter->to_SQL( { delim => $delim,
                                                   flatten => 1,
                                                   placeholder => '?' } );
    return ( "WHERE $sql", $values );
}

sub filter
{
    my( $self ) = @_;
    return $self->{filter};
}

sub tables
{
    my( $self ) = @_;
    my %tables;
    _get_tables( $self->filter, \%tables );
    return sort keys %tables;
}

sub rightmost
{
    my( $self ) = @_;
    return if !$self->filter;

    if( $self->filter->isa( OPTIMADE::Filter::Comparison:: ) ||
        $self->filter->isa( OPTIMADE::Filter::Known:: ) ||
        $self->filter->isa( OPTIMADE::Filter::Negation:: ) ) {
        return $self->filter;
    }
    if( $self->filter->isa( OPTIMADE::Filter::AndOr:: ) ) {
        return $self->filter->right;
    }
}

sub topmost_operation
{
    my( $self ) = @_;
    return if  !$self->filter || !$self->filter->isa( OPTIMADE::Filter::AndOr:: );
    return $self->filter->operator;
}

sub canonicalize_table_names
{
    my( $self, @tables ) = @_;
    return unless defined $self->{filter};

    my $lookup = { map { lc $_ => $_ } @tables };
    $self->{filter} =
        $self->{filter}->modify( sub {
                                    my( $node ) = @_;
                                    if( blessed $node &&
                                        $node->isa( OPTIMADE::Filter::Property:: ) &&
                                        @$node == 2 ) {
                                        $node = OPTIMADE::Filter::Property->new(
                                            ($lookup->{$node->[0]}
                                                ? $lookup->{$node->[0]}
                                                : $node->[0]), $node->[1] );
                                    }
                                    return $node;
                                  } );
}

# Private methods

sub get_filter_representation
{
    my $self = shift;

    return _get_filter_representation( $self->filter );
}

sub _get_filter_representation
{
    my( $node ) = @_;

    if( blessed $node && $node->can( 'to_filter' ) ) {
        return $node->to_filter;
    } elsif( blessed $node ) {
        die 'cannot convert filter to its string representation';
    } elsif( ref $node && ref $node eq 'ARRAY' ) {
        return '(' . join( ' ', map { _get_filter_representation( $_ ) }
                                    @{$node} ) . ')';
    } else {
        return $node;
    }
}

sub _get_tables
{
    my( $node, $tables ) = @_;

    if( blessed $node && $node->isa( OPTIMADE::Filter::Comparison:: ) ) {
        foreach (@{$node->{operands}}) {
            _get_tables( $_, $tables );
        }
    } elsif( blessed $node && $node->isa( OPTIMADE::Filter::Property:: ) ) {
        $tables->{$node->{name}[0]} = 1 if @{$node->{name}} > 1;
    } elsif( ref $node eq 'ARRAY' ) {
        foreach (@$node) {
            _get_tables( $_, $tables );
        }
    }
}

1;
