/*
 * Copyright 2010 JBoss Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.drools.base.evaluators;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.drools.RuntimeDroolsException;
import org.drools.base.BaseEvaluator;
import org.drools.base.ValueType;
import org.drools.common.EventFactHandle;
import org.drools.common.InternalFactHandle;
import org.drools.common.InternalWorkingMemory;
import org.drools.rule.VariableRestriction.LongVariableContextEntry;
import org.drools.rule.VariableRestriction.ObjectVariableContextEntry;
import org.drools.rule.VariableRestriction.VariableContextEntry;
import org.drools.spi.Evaluator;
import org.drools.spi.FieldValue;
import org.drools.spi.InternalReadAccessor;
import org.drools.time.Interval;

/**
 * <p>The implementation of the 'before' evaluator definition.</p>
 * 
 * <p>The <b><code>before</code></b> evaluator correlates two events and matches when the temporal 
 * distance from the event being correlated to the current correlated belongs to the distance range declared 
 * for the operator.</p> 
 * 
 * <p>Lets look at an example:</p>
 * 
 * <pre>$eventA : EventA( this before[ 3m30s, 4m ] $eventB )</pre>
 *
 * <p>The previous pattern will match if and only if the temporal distance between the 
 * time when $eventA finished and the time when $eventB started is between ( 3 minutes 
 * and 30 seconds ) and ( 4 minutes ). In other words:</p>
 * 
 * <pre> 3m30s <= $eventB.startTimestamp - $eventA.endTimeStamp <= 4m </pre>
 * 
 * <p>The temporal distance interval for the <b><code>before</code></b> operator is optional:</p>
 * 
 * <ul><li>If two values are defined (like in the example below), the interval starts on the
 * first value and finishes on the second.</li>
 * <li>If only one value is defined, then the interval starts on the value and finishes on 
 * the positive infinity.</li>
 * <li>If no value is defined, it is assumed that the initial value is 1ms and the final value
 * is the positive infinity.</li></ul>
 * 
 * <p><b>NOTE:</b> it is allowed to define negative distances for this operator. Example:</p>
 * 
 * <pre>$eventA : EventA( this before[ -3m30s, -2m ] $eventB )</pre>
 *
 * <p><b>NOTE:</b> if the initial value is greater than the finish value, the engine automatically
 * reverse them, as there is no reason to have the initial value greater than the finish value. Example: 
 * the following two patterns are considered to have the same semantics:</p>
 * 
 * <pre>
 * $eventA : EventA( this before[ -3m30s, -2m ] $eventB )
 * $eventA : EventA( this before[ -2m, -3m30s ] $eventB )
 * </pre>
 */
public class BeforeEvaluatorDefinition
    implements
    EvaluatorDefinition {

    public static final Operator         BEFORE        = Operator.addOperatorToRegistry( "before",
                                                                                         false );
    public static final Operator         NOT_BEFORE    = Operator.addOperatorToRegistry( "before",
                                                                                         true );

    private static final String[]        SUPPORTED_IDS = {BEFORE.getOperatorString()};

    private Map<String, BeforeEvaluator> cache         = Collections.emptyMap();
    private volatile TimeIntervalParser  parser        = new TimeIntervalParser();

    @SuppressWarnings("unchecked")
    public void readExternal(ObjectInput in) throws IOException,
                                            ClassNotFoundException {
        cache = (Map<String, BeforeEvaluator>) in.readObject();
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject( cache );
    }

    /**
     * @inheridDoc
     */
    public Evaluator getEvaluator(ValueType type,
                                  Operator operator) {
        return this.getEvaluator( type,
                                  operator.getOperatorString(),
                                  operator.isNegated(),
                                  null );
    }

    /**
     * @inheridDoc
     */
    public Evaluator getEvaluator(ValueType type,
                                  Operator operator,
                                  String parameterText) {
        return this.getEvaluator( type,
                                  operator.getOperatorString(),
                                  operator.isNegated(),
                                  parameterText );
    }

    /**
     * @inheritDoc
     */
    public Evaluator getEvaluator(final ValueType type,
                                  final String operatorId,
                                  final boolean isNegated,
                                  final String parameterText) {
        return this.getEvaluator( type,
                                  operatorId,
                                  isNegated,
                                  parameterText,
                                  Target.HANDLE,
                                  Target.HANDLE );

    }

    /**
     * @inheritDoc
     */
    public Evaluator getEvaluator(final ValueType type,
                                  final String operatorId,
                                  final boolean isNegated,
                                  final String parameterText,
                                  final Target left,
                                  final Target right) {
        if ( this.cache == Collections.EMPTY_MAP ) {
            this.cache = new HashMap<String, BeforeEvaluator>();
        }
        String key = left+":"+right+":"+isNegated + ":" + parameterText;
        BeforeEvaluator eval = this.cache.get( key );
        if ( eval == null ) {
            Long[] params = parser.parse( parameterText );
            eval = new BeforeEvaluator( type,
                                        isNegated,
                                        params,
                                        parameterText,
                                        left == Target.FACT,
                                        right == Target.FACT );
            this.cache.put( key,
                            eval );
        }
        return eval;
    }

    /**
     * @inheritDoc
     */
    public String[] getEvaluatorIds() {
        return SUPPORTED_IDS;
    }

    /**
     * @inheritDoc
     */
    public boolean isNegatable() {
        return true;
    }

    /**
     * @inheritDoc
     */
    public Target getTarget() {
        return Target.BOTH;
    }

    /**
     * @inheritDoc
     */
    public boolean supportsType(ValueType type) {
        // supports all types, since it operates over fact handles
        // Note: should we change this interface to allow checking of event classes only?
        return true;
    }

    /**
     * Implements the 'before' evaluator itself
     */
    public static class BeforeEvaluator extends BaseEvaluator {
        private static final long serialVersionUID = 510l;

        private long              initRange;
        private long              finalRange;
        private String            paramText;
        private boolean           unwrapLeft;
        private boolean           unwrapRight;

        public BeforeEvaluator() {
        }

        public BeforeEvaluator(final ValueType type,
                               final boolean isNegated,
                               final Long[] parameters,
                               final String paramText,
                               final boolean unwrapLeft,
                               final boolean unwrapRight) {
            super( type,
                   isNegated ? NOT_BEFORE : BEFORE );
            this.paramText = paramText;
            this.unwrapLeft = unwrapLeft;
            this.unwrapRight = unwrapRight;
            this.setParameters( parameters );
        }

        public void readExternal(ObjectInput in) throws IOException,
                                                ClassNotFoundException {
            super.readExternal( in );
            initRange = in.readLong();
            finalRange = in.readLong();
            paramText = (String) in.readObject();
            unwrapLeft = in.readBoolean();
            unwrapRight = in.readBoolean();
        }

        public void writeExternal(ObjectOutput out) throws IOException {
            super.writeExternal( out );
            out.writeLong( initRange );
            out.writeLong( finalRange );
            out.writeObject( paramText );
            out.writeBoolean( unwrapLeft );
            out.writeBoolean( unwrapRight );
        }

        @Override
        public Object prepareLeftObject(InternalFactHandle handle) {
            return unwrapLeft ? handle.getObject() : handle;
        }

        @Override
        public Object prepareRightObject(InternalFactHandle handle) {
            return unwrapRight ? handle.getObject() : handle;
        }

        @Override
        public boolean isTemporal() {
            return true;
        }

        @Override
        public Interval getInterval() {
            long init = (this.finalRange == Interval.MAX) ? Interval.MIN : -this.finalRange;
            long end = (this.initRange == Interval.MIN) ? Interval.MAX : -this.initRange;
            if ( this.getOperator().isNegated() ) {
                if ( init == Interval.MIN && end != Interval.MAX ) {
                    init = finalRange + 1;
                    end = Interval.MAX;
                } else if ( init != Interval.MIN && end == Interval.MAX ) {
                    init = Interval.MIN;
                    end = initRange - 1;
                } else if ( init == Interval.MIN && end == Interval.MAX ) {
                    init = 0;
                    end = -1;
                } else {
                    init = Interval.MIN;
                    end = Interval.MAX;
                }
            }
            return new Interval( init,
                                 end );
        }

        public boolean evaluate(InternalWorkingMemory workingMemory,
                                final InternalReadAccessor extractor,
                                final Object object1,
                                final FieldValue object2) {
            throw new RuntimeDroolsException( "The 'before' operator can only be used to compare one event to another, and never to compare to literal constraints." );
        }

        public boolean evaluateCachedRight(InternalWorkingMemory workingMemory,
                                           final VariableContextEntry context,
                                           final Object left) {
            if ( context.rightNull ) {
                return false;
            }
            long rightTS;
            if ( this.unwrapRight ) {
                if ( context instanceof ObjectVariableContextEntry ) {
                    if ( ((ObjectVariableContextEntry) context).right instanceof Date ) {
                        rightTS = ((Date) ((ObjectVariableContextEntry) context).right).getTime();
                    } else {
                        rightTS = ((Number) ((ObjectVariableContextEntry) context).right).longValue();
                    }
                } else {
                    rightTS = ((LongVariableContextEntry) context).right;
                }
            } else {
                rightTS = ((EventFactHandle) ((ObjectVariableContextEntry) context).right).getEndTimestamp();
            }
            long leftTS = this.unwrapLeft ? context.declaration.getExtractor().getLongValue( workingMemory,
                                                                                             left ) : ((EventFactHandle) left).getStartTimestamp();

            long dist = leftTS - rightTS;
            return this.getOperator().isNegated() ^ (dist >= this.initRange && dist <= this.finalRange);
        }

        public boolean evaluateCachedLeft(InternalWorkingMemory workingMemory,
                                          final VariableContextEntry context,
                                          final Object right) {
            if ( context.extractor.isNullValue( workingMemory,
                                                right ) ) {
                return false;
            }
            long rightTS = this.unwrapRight ? context.extractor.getLongValue( workingMemory,
                                                                              right ) : ((EventFactHandle) right).getEndTimestamp();

            long leftTS;
            if ( this.unwrapLeft ) {
                if ( context instanceof ObjectVariableContextEntry ) {
                    if ( ((ObjectVariableContextEntry) context).left instanceof Date ) {
                        leftTS = ((Date) ((ObjectVariableContextEntry) context).left).getTime();
                    } else {
                        leftTS = ((Number) ((ObjectVariableContextEntry) context).left).longValue();
                    }
                } else {
                    leftTS = ((LongVariableContextEntry) context).left;
                }
            } else {
                leftTS = ((EventFactHandle) ((ObjectVariableContextEntry) context).left).getStartTimestamp();
            }
            long dist = leftTS - rightTS;

            return this.getOperator().isNegated() ^ (dist >= this.initRange && dist <= this.finalRange);
        }

        public boolean evaluate(InternalWorkingMemory workingMemory,
                                final InternalReadAccessor extractor1,
                                final Object object1,
                                final InternalReadAccessor extractor2,
                                final Object object2) {
            if ( extractor1.isNullValue( workingMemory,
                                         object1 ) ) {
                return false;
            }
            long rightTS = this.unwrapRight ? extractor1.getLongValue( workingMemory,
                                                                       object1 ) : ((EventFactHandle) object1).getEndTimestamp();

            long leftTS = this.unwrapLeft ? extractor2.getLongValue( workingMemory,
                                                                     object2 ) : ((EventFactHandle) object2).getStartTimestamp();

            long dist = leftTS - rightTS;

            return this.getOperator().isNegated() ^ (dist >= this.initRange && dist <= this.finalRange);
        }

        public String toString() {
            return this.getOperator().toString() + "[" + paramText + "]";
        }

        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            final int PRIME = 31;
            int result = super.hashCode();
            result = PRIME * result + (int) (finalRange ^ (finalRange >>> 32));
            result = PRIME * result + (int) (initRange ^ (initRange >>> 32));
            result = PRIME * result + ((paramText == null) ? 0 : paramText.hashCode());
            result = PRIME * result + (unwrapLeft ? 1231 : 1237);
            result = PRIME * result + (unwrapRight ? 1231 : 1237);
            return result;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            if ( this == obj ) return true;
            if ( !super.equals( obj ) ) return false;
            if ( getClass() != obj.getClass() ) return false;
            BeforeEvaluator other = (BeforeEvaluator) obj;
            if ( finalRange != other.finalRange ) return false;
            if ( initRange != other.initRange ) return false;
            if ( paramText == null ) {
                if ( other.paramText != null ) return false;
            } else if ( !paramText.equals( other.paramText ) ) return false;
            if ( unwrapLeft != other.unwrapLeft ) return false;
            if ( unwrapRight != other.unwrapRight ) return false;
            return true;
        }

        /**
         * This methods sets the parameters appropriately.
         *
         * @param parameters
         */
        private void setParameters(Long[] parameters) {
            if ( parameters == null || parameters.length == 0 ) {
                // open bounded range
                this.initRange = 1;
                this.finalRange = Long.MAX_VALUE;
                return;
            } else if ( parameters.length == 1 ) {
                this.initRange = parameters[0].longValue();
                this.finalRange = Long.MAX_VALUE;
            } else if ( parameters.length == 2 ) {
                if ( parameters[0].longValue() <= parameters[1].longValue() ) {
                    this.initRange = parameters[0].longValue();
                    this.finalRange = parameters[1].longValue();
                } else {
                    this.initRange = parameters[1].longValue();
                    this.finalRange = parameters[0].longValue();
                }
            } else {
                throw new RuntimeDroolsException( "[Before Evaluator]: Not possible to have more than 2 parameters: '" + paramText + "'" );
            }
        }

    }

}
