# Module of TWiki Enterprise Collaboration Platform, http://TWiki.org/
#
# Copyright (C) 2005-2006 TWiki Contributors. All Rights Reserved.
# TWiki Contributors
# are listed in the AUTHORS file in the root of this distribution.
# NOTE: Please extend that file, not this notice.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version. For
# more details read LICENSE in the root of this distribution.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# As per the GPL, removal of this notice is prohibited.

use strict;
use Assert;

=pod

---+ package TWiki::If

Support for the conditions in %IF{} statements. Basically a simple
stack-based parser for infix expressions that generates a parse
tree that can subsequently be evaluated.

=cut

package TWiki::IfNode;

=pod

---++ ClassMethod new( $l, $o, $r ) -> \$if
Construct a new parse node,

=cut

sub new {
    my( $class, $left, $op, $right ) = @_;
    my $this = bless( {}, $class );
    $this->{right} = $right;
    $this->{left} = $left;
    $this->{op} = $op;
    return $this;
}

=pod

---++ ObjectMethod evaluate($twiki) -> $result
Execute the parse node.

=cut

sub evaluate {
    my( $this, $session ) = @_;

    return $this->{op} unless ref( $this->{op} );

    my $fn = $this->{op}->{exec};
    return &$fn( $session, $this->{left}, $this->{right} );
}

sub stringify {
    my $this = shift;

    return $this->{op} unless ref( $this->{op} );

    my $res = $this->{op}->{name};
    if( $this->{left} ) {
        $res = $this->{left}->stringify() . $res;
    }
    return '('.$res . $this->{right}->stringify().')';
}

package TWiki::If;

# Operator precedences
my %defOps;

$defOps{context} =
  { name => 'context',
    prec => 5,
    type => 0, # unary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        return $twiki->inContext($b->evaluate($twiki)) || 0;
    }
   };
$defOps{'$'} =
  { name => '$',
    prec => 5,
    type => 0, # unary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        my $text = $b->evaluate($twiki) || '';
        if( $text && defined( $twiki->{cgiQuery}->param( $text ))) {
            return $twiki->{cgiQuery}->param( $text );
        }
        $text = "%$text%";
        $twiki->_expandAllTags(\$text,
                               $twiki->{topicName},
                               $twiki->{webName});
        return $text || '';
    }
   };
$defOps{defined} =
  { name => 'defined',
    prec => 5,
    type => 0, # unary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        my $eval =  $b->evaluate($twiki);
        return 0 unless $eval;
        return 1 if( defined( $twiki->{cgiQuery}->param( $eval )));
        return 1 if( defined( $twiki->{prefs}->getPreferencesValue( $eval )));
        return 0;
    }
   };
$defOps{'='} =
  { name => '=',
    prec => 4,
    type => 1, # binary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        return  $a->evaluate($twiki ) eq $b->evaluate($twiki);
    }
   };
$defOps{'!='} =
  { name => '!=',
    prec => 4,
    type => 1, # binary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        return $a->evaluate($twiki) ne $b->evaluate($twiki);
    }
   };
$defOps{'>='} =
  { name => '>=',
    prec => 4,
    type => 1, # binary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        my $ea = $a->evaluate($twiki) || 0;
        my $eb = $b->evaluate($twiki) || 0;
        return $ea >= $eb;
    }
   };
$defOps{'<='} =
  { name => '<=',
    prec => 4,
    type => 1, # binary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        my $ea = $a->evaluate($twiki) || 0;
        my $eb = $b->evaluate($twiki) || 0;
        return $ea <= $eb;
    }
   };
$defOps{'>'} =
  { name => '>',
    prec => 4,
    type => 1, # binary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        my $ea = $a->evaluate($twiki) || 0;
        my $eb = $b->evaluate($twiki) || 0;
        return $ea > $eb;
    }
   };
$defOps{'<'} =
  { name => '<',
    prec => 4,
    type => 1, # binary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        my $ea = $a->evaluate($twiki) || 0;
        my $eb = $b->evaluate($twiki) || 0;
        return $ea < $eb;
    }
   };
$defOps{not} =
  { name => 'not',
    prec => 3,
    type => 0, # unary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        return !$b->evaluate($twiki);
    }
   };
$defOps{and} =
  { name => 'and',
    prec => 2,
    type => 1, # binary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        return 0 unless $a->evaluate($twiki);
        return $b->evaluate($twiki);
    }
   };
$defOps{or} =
  { name => 'or',
    prec => 1,
    type => 1, # binary
    exec => sub {
        my( $twiki, $a, $b ) = @_;
        return 1 if $a->evaluate($twiki);
        return $b->evaluate($twiki);
    }
   };

=pod

---++ ClassMethod new( \%operators ) -> \%factory
Construct a new if node factory.
   * =\%operators= reference to a hash of operators.
Each operator must have the following fields: prec (precedence) type (0 unary 1 binary) exec (ref to a function to execute). If not provided, the default set of boolean operations supported by %IF is used.

=cut

sub new {
    my( $class, $operators ) = @_;
    my $this = bless( {}, $class );

    $this->{operators} = $operators || \%defOps;

    # build up REs that match all the types
    foreach my $opn ( keys %{$this->{operators}} ) {
        my $re = $opn;
        $re =~ s/(\W)/\\$1/g;
        $this->{RE}[$this->{operators}->{$opn}->{type}] .= $re.'|';
    }
    $this->{RE}[0] =~ s/\|$//;
    $this->{RE}[1] =~ s/\|$//;

    return $this;
}

=pod

---++ ObjectMethod parse( $string ) -> \$if
   * =$string= - string containing an expression to parse
Construct a new search node by parsing the passed expression. Return
the new object.

=cut

sub parse {
    my( $this, $string ) = @_;
    if ( defined( $string )) {
        if ( $string =~ m/^\s*$/o ) {
            return new TWiki::IfNode( undef, '', undef );
        } else {
            my( $node, $rest ) = $this->_parse( $string );
            return $node;
        }
    }
    return undef;
}

# PRIVATE STATIC simple stack parser for grabbing boolean expressions
sub _parse {
    my( $this, $string ) = @_;
    $string .= " ";
    my @opands;
    my @opers;
    while( $string =~ m/\S/o ) {
        if ( $string =~ s/^\s*($this->{RE}[0])//i ||
               $string =~ s/^\s*($this->{RE}[1])//i ) {

            my $op = $this->{operators}->{$1};
            while( scalar( @opers ) > 0 &&
                     $op->{prec} < $opers[$#opers]->{prec} ) {
                $this->_apply( \@opers, \@opands );
            }
            die($this->{RE}[1]) unless $op;
            push( @opers, $op );
        }
        elsif( $string =~ s/^\s*'(.*?)'//o ) {
            push( @opands, new TWiki::IfNode( undef, $1, undef ));
        }
        elsif( $string =~ s/^\s*(\w+)//o ) {
            push( @opands, new TWiki::IfNode( undef, $1, undef ));
        }
        elsif( $string =~ s/\s*\(//o ) {
            my $oa;
            ( $oa, $string ) = $this->_parse( $string );
            push( @opands, $oa );
        }
        elsif( $string =~ s/^\s*\)//o ) {
            last;
        }
        else{
            # the parser is stuck; we have done as well as we can, so return
            $this->{error} = 'Bad expression at '.$string;
            return undef;
        }
    }
    while( scalar( @opers ) > 0 ) {
        return undef unless $this->_apply( \@opers, \@opands );
    }
    unless( scalar( @opands ) == 1 ) {
        $this->{error} = 'Missing operator?';
    }
    return ( pop( @opands ), $string );
}

# PRIVATE STATIC generate a Search by popping the top two operands
# and the top operator. Push the result back onto the operand stack.
sub _apply {
    my ( $this, $opers, $opands ) = @_;
    my $o = pop( @$opers );
    my $r = pop( @$opands );
    unless( defined( $r )) {
        $this->{error} = 'Missing operand after '.$o->{name};
        return undef;
    }
    my $l = undef;
    if( $o->{type} == 1 ) {
        # binary
        $l = pop( @$opands );
        unless( defined( $l )) {
            $this->{error} = 'Missing operand before '.$o->{name};
            return undef;
        }
    }
    my $n = new TWiki::IfNode( $l, $o, $r );
    push( @$opands, $n);
    return $n;
}

1;
