/*
 * Copyright 2007 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.
 *
 * Created on Dec 6, 2007
 */
package org.drools.base.evaluators;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.HashMap;
import java.util.Map;

import org.drools.RuntimeDroolsException;
import org.drools.base.ValueType;
import org.drools.common.DroolsObjectInput;
import org.drools.spi.Evaluator;

/**
 * A registry class for all available evaluators
 *
 * @author etirelli
 */
public class EvaluatorRegistry
    implements
    Externalizable {

    private static final long                serialVersionUID = -3047718531857258033L;

    private Map<String, EvaluatorDefinition> evaluators;
    private ClassLoader                      classloader;

    /**
     * Default constructor. The registry will use the context classloader (if available)
     * to load the evaluator definition classes or this class classloader if it is
     * not available.
     */
    public EvaluatorRegistry() {
        this( null );
    }

    /**
     * Creates a new EvaluatorRegistry using the given classloader to load
     * the evaluator definition classes.
     *
     * @param classloader the classloader to use to load evaluator definition
     *                    classes. If it is null, try to obtain the context
     *                    classloader. If it is also null, uses the same classloader
     *                    that loaded this class.
     *
     */
    public EvaluatorRegistry(ClassLoader classloader) {
        this.evaluators = new HashMap<String, EvaluatorDefinition>();
        if ( classloader != null ) {
            this.classloader = classloader;
        } else {
            this.classloader = getDefaultClassLoader();
        }

        // loading default built in evaluators
        this.addEvaluatorDefinition( new EqualityEvaluatorsDefinition() );
        this.addEvaluatorDefinition( new ComparableEvaluatorsDefinition() );
        this.addEvaluatorDefinition( new SetEvaluatorsDefinition() );
        this.addEvaluatorDefinition( new MatchesEvaluatorsDefinition() );
        this.addEvaluatorDefinition( new SoundslikeEvaluatorsDefinition() );
        this.addEvaluatorDefinition( new BeforeEvaluatorDefinition() );
        this.addEvaluatorDefinition( new AfterEvaluatorDefinition() );
        this.addEvaluatorDefinition( new MeetsEvaluatorDefinition() );
        this.addEvaluatorDefinition( new MetByEvaluatorDefinition() );
        this.addEvaluatorDefinition( new OverlapsEvaluatorDefinition() );
        this.addEvaluatorDefinition( new OverlappedByEvaluatorDefinition() );
        this.addEvaluatorDefinition( new IncludesEvaluatorDefinition() );
        this.addEvaluatorDefinition( new DuringEvaluatorDefinition() );
        this.addEvaluatorDefinition( new FinishesEvaluatorDefinition() );
        this.addEvaluatorDefinition( new FinishedByEvaluatorDefinition() );
        this.addEvaluatorDefinition( new StartsEvaluatorDefinition() );
        this.addEvaluatorDefinition( new StartedByEvaluatorDefinition() );
        this.addEvaluatorDefinition( new CoincidesEvaluatorDefinition() );
    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        evaluators  = (Map<String, EvaluatorDefinition>)in.readObject();
        if (in instanceof DroolsObjectInput) {
            classloader = ((DroolsObjectInput)in).getClassLoader();
        } else {
            classloader = getDefaultClassLoader();
        }
    }

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

    private static ClassLoader getDefaultClassLoader() {
        if (Thread.currentThread().getContextClassLoader() != null)
            return Thread.currentThread().getContextClassLoader();
        return EvaluatorRegistry.class.getClassLoader();
    }

    /**
     * Adds an evaluator definition class to the registry using the
     * evaluator class name. The class will be loaded and the corresponting
     * evaluator ID will be added to the registry. In case there exists
     * an implementation for that ID already, the new implementation will
     * replace the previous one.
     *
     * @param className the name of the class for the implementation definition.
     *                  The class must implement the EvaluatorDefinition interface.
     *
     * @return true if the new class implementation is replacing an old
     *         implementation for the same evaluator ID. False otherwise.
     */
    public void addEvaluatorDefinition(String className) {
        try {
            Class<EvaluatorDefinition> defClass = (Class<EvaluatorDefinition>) this.classloader.loadClass( className );
            EvaluatorDefinition def = defClass.newInstance();
            addEvaluatorDefinition( def );
        } catch ( ClassNotFoundException e ) {
            throw new RuntimeDroolsException( "Class not found for evaluator definition: " + className,
                                              e );
        } catch ( InstantiationException e ) {
            throw new RuntimeDroolsException( "Error instantiating class for evaluator definition: " + className,
                                              e );
        } catch ( IllegalAccessException e ) {
            throw new RuntimeDroolsException( "Illegal access instantiating class for evaluator definition: " + className,
                                              e );
        }
    }

    /**
     * Adds an evaluator definition class to the registry. In case there exists
     * an implementation for that evaluator ID already, the new implementation will
     * replace the previous one.
     *
     * @param def the evaluator definition to be added.
     */
    public void addEvaluatorDefinition(EvaluatorDefinition def) {
        for ( String id : def.getEvaluatorIds() ) {
            this.evaluators.put( id,
                                 def );
        }
    }

    /**
     * Returns the evaluator definition for the given evaluator ID
     * or null if no one was found
     *
     * @param evaluatorId
     * @return
     */
    public EvaluatorDefinition getEvaluatorDefinition(String evaluatorId) {
        return this.evaluators.get( evaluatorId );
    }

    /**
     * Returns the evaluator definition for the given operator
     * or null if no one was found
     *
     * @param operator the operator implemented by the evaluator definition
     * @return
     */
    public EvaluatorDefinition getEvaluatorDefinition(Operator operator) {
        return this.evaluators.get( operator.getOperatorString() );
    }

    /**
     * Returns the evaluator instance for the given type and the
     * defined parameterText
     *
     * @param type the type of the attributes this evaluator will
     *             operate on. This is important because the evaluator
     *             may do optimizations and type coercion based on the
     *             types it is evaluating. It is also possible that
     *             this evaluator does not support a given type.
     *
     * @param operatorId the string identifier of the evaluator
     *
     * @param isNegated true if the evaluator instance to be returned is
     *                  the negated version of the evaluator.
     *
     * @param parameterText some evaluators support parameters and these
     *                      parameters are defined as a String that is
     *                      parsed by the evaluator itself.
     *
     * @return an Evaluator instance capable of evaluating expressions
     *         between values of the given type, or null in case the type
     *         is not supported.
     */
    public Evaluator getEvaluator(ValueType type,
                                  String operatorId,
                                  boolean isNegated,
                                  String parameterText) {
        return this.getEvaluatorDefinition( operatorId ).getEvaluator( type,
                                                                       operatorId,
                                                                       isNegated,
                                                                       parameterText );
    }

    /**
     * Returns the evaluator instance for the given type and the
     * defined parameterText
     *
     * @param type the type of the attributes this evaluator will
     *             operate on. This is important because the evaluator
     *             may do optimizations and type coercion based on the
     *             types it is evaluating. It is also possible that
     *             this evaluator does not support a given type.
     *
     * @param operator the operator that evaluator implements
     *
     * @param parameterText some evaluators support parameters and these
     *                      parameters are defined as a String that is
     *                      parsed by the evaluator itself.
     *
     * @return an Evaluator instance capable of evaluating expressions
     *         between values of the given type, or null in case the type
     *         is not supported.
     */
    public Evaluator getEvaluator(ValueType type,
                                  Operator operator,
                                  String parameterText) {
        return this.getEvaluatorDefinition( operator ).getEvaluator( type,
                                                                     operator,
                                                                     parameterText );
    }

    /**
     * Returns the evaluator instance for the given type and the
     * defined parameterText
     *
     * @param type the type of the attributes this evaluator will
     *             operate on. This is important because the evaluator
     *             may do optimizations and type coercion based on the
     *             types it is evaluating. It is also possible that
     *             this evaluator does not support a given type.
     *
     * @param operator the operator that evaluator implements
     *
     * @return an Evaluator instance capable of evaluating expressions
     *         between values of the given type, or null in case the type
     *         is not supported.
     */
    public Evaluator getEvaluator(ValueType type,
                                  Operator operator) {
        return this.getEvaluatorDefinition( operator ).getEvaluator( type,
                                                                     operator );
    }
}
