/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.pig.newplan.optimizer;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.apache.pig.impl.logicalLayer.FrontendException;
import org.apache.pig.newplan.OperatorPlan;

/**
 * The core class of the optimizer.  The basic design of this class is that it
 * is provided a list of RuleSets.  RuleSets represent all of the optimizer
 * rules that can be run together.  The rules in the RuleSet will be run
 * repeatedly until either no rule in the RuleSet passes check and calls
 * transform or until maxIter iterations (default 500) has been made over
 * the RuleSet.  Then the next RuleSet will be moved to.  Once finished,
 * a given RuleSet is never returned to.
 *
 * Each rule is has two parts:  a pattern and and associated transformer.
 * Transformers have two important functions:   check(), and transform().
 * The pattern describes a pattern of node types that the optimizer will
 * look to match.  If that match is found anywhere in the plan, then check()
 * will be called.  check() allows the rule to look more in depth at the
 * matched pattern and decide whether the rule should be run or not.  For
 * example, one might design a rule to push filters above join that would
 * look for the pattern filter(join) (meaning a filter followed by a join).
 * But only certain types of filters can be pushed.  The check() function
 * would need to decide whether the filter that it found was pushable or not.
 * If check() returns true, the rule is said to have matched, and transform()
 * is then called.  This function is responsible for making changes in the
 * logical plan.  Once transform is complete PlanPatcher.patchUp will be
 * called to do any necessary cleanup in the plan, such as resetting
 * schemas, etc.
 */
public abstract class PlanOptimizer {

    protected List<Set<Rule>> ruleSets;
    protected OperatorPlan plan;
    protected List<PlanTransformListener> listeners;
    protected int maxIter;

    static final int defaultIterations = 500;

    /**
     * @param p Plan to optimize
     * @param rs List of RuleSets to use to optimize
     * @param iterations maximum number of optimization iterations,
     * set to -1 for default
     */
    protected PlanOptimizer(OperatorPlan p,
                            List<Set<Rule>> rs,
                            int iterations) {
        plan = p;
        ruleSets = rs;
        listeners = new ArrayList<PlanTransformListener>();
        maxIter = (iterations < 1 ? defaultIterations : iterations);
    }

    /**
     * Adds a listener to the optimization.  This listener will be fired
     * after each rule transforms a plan.  Listeners are guaranteed to
     * be fired in the order they are added.
     * @param listener
     */
    protected void addPlanTransformListener(PlanTransformListener listener) {
        listeners.add(listener);
    }

    /**
     * Run the optimizer.  This method attempts to match each of the Rules
     * against the plan.  If a Rule matches, it then calls the check
     * method of the associated Transformer to give the it a chance to
     * check whether it really wants to do the optimization.  If that
     * returns true as well, then Transformer.transform is called.
     * @throws FrontendException
     */
    public void optimize() throws FrontendException {

        for (Set<Rule> rs : ruleSets) {
            boolean sawMatch = false;
            int numIterations = 0;
            do {
                sawMatch = false;
                for (Rule rule : rs) {
                    List<OperatorPlan> matches = rule.match(plan);
                    if (matches != null) {
                        Transformer transformer = rule.getNewTransformer();
                        for (OperatorPlan m : matches) {
                            try {
                                if (transformer.check(m)) {
                                    sawMatch = true;
                                    transformer.transform(m);
                                    OperatorPlan change = transformer.reportChanges();
                                    if (change == null) {
                                        sawMatch = false;
                                    } else if (!rule.isSkipListener()) {
                                        for(PlanTransformListener l: listeners) {
                                            l.transformed(plan, change);
                                        }
                                    }
                                }
                            } catch (Exception e) {
                                StringBuffer message = new StringBuffer("Error processing rule " + rule.name);
                                if (!rule.isMandatory()) {
                                    message.append(". Try -t " + rule.name);
                                }
                                throw new FrontendException(message.toString(), 2000, e);
                            }
                        }
                    }
                }
            } while(sawMatch && ++numIterations < maxIter);
        }
    }
}
