/*
 * 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.logical.rules;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.pig.backend.executionengine.ExecException;
import org.apache.pig.backend.hadoop.datastorage.ConfigurationUtil;
import org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.PigHadoopLogger;
import org.apache.pig.backend.hadoop.executionengine.physicalLayer.PhysicalOperator;
import org.apache.pig.backend.hadoop.executionengine.physicalLayer.plans.PhysicalPlan;
import org.apache.pig.impl.PigContext;
import org.apache.pig.impl.logicalLayer.FrontendException;
import org.apache.pig.impl.util.UDFContext;
import org.apache.pig.newplan.BaseOperatorPlan;
import org.apache.pig.newplan.DependencyOrderWalker;
import org.apache.pig.newplan.Operator;
import org.apache.pig.newplan.OperatorPlan;
import org.apache.pig.newplan.PlanWalker;
import org.apache.pig.newplan.ReverseDependencyOrderWalkerWOSeenChk;
import org.apache.pig.newplan.logical.expression.AllSameExpressionVisitor;
import org.apache.pig.newplan.logical.expression.ConstantExpression;
import org.apache.pig.newplan.logical.expression.ExpToPhyTranslationVisitor;
import org.apache.pig.newplan.logical.expression.LogicalExpression;
import org.apache.pig.newplan.logical.expression.LogicalExpressionPlan;
import org.apache.pig.newplan.logical.expression.LogicalExpressionVisitor;
import org.apache.pig.newplan.logical.expression.UserFuncExpression;
import org.apache.pig.newplan.logical.optimizer.AllExpressionVisitor;
import org.apache.pig.newplan.logical.relational.LogicalRelationalOperator;
import org.apache.pig.newplan.optimizer.Rule;
import org.apache.pig.newplan.optimizer.Transformer;
import org.joda.time.DateTimeZone;

public abstract class ConstantCalculator extends Rule {
    private List<LogicalRelationalOperator> processedOperators = new ArrayList<LogicalRelationalOperator>();
    private PigContext pc;

    public ConstantCalculator(String n, PigContext pc) {
        super(n, false);
        this.pc = pc;
    }

    @Override
    public Transformer getNewTransformer() {
        return new ConstantCalculatorTransformer(processedOperators, pc);
    }

    public static class ConstantCalculatorTransformer extends Transformer {
        private List<LogicalRelationalOperator> processedOperators = new ArrayList<LogicalRelationalOperator>();
        private OperatorPlan plan;
        private PigContext pc;

        public ConstantCalculatorTransformer(List<LogicalRelationalOperator> processedOperators, PigContext pc) {
            this.processedOperators = processedOperators;
            this.pc = pc;
        }

        @Override
        public boolean check(OperatorPlan matched) throws FrontendException {
            Iterator<Operator> operators = matched.getOperators();
            while (operators.hasNext()) {
                LogicalRelationalOperator operator = (LogicalRelationalOperator)operators.next();
    
                // If the operator is already processed, we quit.
                if (processedOperators.contains(operator)) {
                    continue;
                }

                processedOperators.add(operator);
                return true;
            }
            return false;
        }

        public static class ConstantCalculatorLogicalPlanVisitor extends AllExpressionVisitor {
            private PigContext pc;
            public ConstantCalculatorLogicalPlanVisitor(OperatorPlan plan, PigContext pc) throws FrontendException {
                super(plan, new DependencyOrderWalker(plan));
                this.pc = pc;
            }

            @Override
            protected LogicalExpressionVisitor getVisitor(
                    LogicalExpressionPlan expr) throws FrontendException {
                return new ConstantCalculatorExpressionVisitor(expr, currentOp, pc);
            }
        }

        public static class ConstantCalculatorExpressionVisitor extends AllSameExpressionVisitor {
            private LogicalRelationalOperator currentOp;
            private PigContext pc;
            private DateTimeZone currentDTZ = null;
            public ConstantCalculatorExpressionVisitor(OperatorPlan expPlan,
                    LogicalRelationalOperator currentOp, PigContext pc) throws FrontendException {
                super(expPlan, new ReverseDependencyOrderWalkerWOSeenChk(expPlan));
                this.currentOp = currentOp;
                this.pc = pc;
            }

            @Override
            protected void execute(LogicalExpression op) throws FrontendException {
                if (op instanceof UserFuncExpression) {
                    UserFuncExpression udf = (UserFuncExpression)op;
                    if (!udf.getEvalFunc().allowCompileTimeCalculation()) {
                        return;
                    }
                }
                boolean valSet = false;
                Object val = null;
                if (currentWalker.getPlan().getSuccessors(op) != null) {
                    // If has successors and all successors are constant, calculate the constant
                    for (Operator succ : currentWalker.getPlan().getSuccessors(op)) {
                        if (!(succ instanceof ConstantExpression)) {
                            return;
                        }
                    }
                    // All successors are constant, calculate the value
                    OperatorPlan expLogicalPlan = new LogicalExpressionPlan();
                    ((BaseOperatorPlan)currentWalker.getPlan()).moveTree(op, (BaseOperatorPlan)expLogicalPlan);
                    PhysicalPlan expPhysicalPlan = new PhysicalPlan();
                    Map<Operator, PhysicalOperator> logToPhyMap = new HashMap<Operator, PhysicalOperator>();
                    PlanWalker childWalker = new ReverseDependencyOrderWalkerWOSeenChk(expLogicalPlan);

                    // Save the old walker and use childWalker as current Walker
                    pushWalker(childWalker);
                    ExpToPhyTranslationVisitor expTranslationVisitor = new 
                            ExpToPhyTranslationVisitor(expLogicalPlan,
                                    childWalker, currentOp, expPhysicalPlan, logToPhyMap);
                    expTranslationVisitor.visit();
                    popWalker();
                    PhysicalOperator root = expPhysicalPlan.getLeaves().get(0);
                    try {
                        UDFContext.getUDFContext().addJobConf(ConfigurationUtil.toConfiguration(pc.getProperties(), true));
                        PigHadoopLogger pigHadoopLogger = PigHadoopLogger.getInstance();
                        PhysicalOperator.setPigLogger(pigHadoopLogger);
                        setDefaultTimeZone();
                        val = root.getNext(root.getResultType()).result;
                        restoreDefaultTimeZone();
                        UDFContext.getUDFContext().addJobConf(null);
                    } catch (ExecException e) {
                        throw new FrontendException(e);
                    }
                    valSet = true;
                } else if (op instanceof UserFuncExpression) {
                    // If solo UDF, calculate UDF
                    UserFuncExpression udf = (UserFuncExpression)op;
                    try {
                        UDFContext.getUDFContext().addJobConf(ConfigurationUtil.toConfiguration(pc.getProperties(), true));
                        setDefaultTimeZone();
                        val = udf.getEvalFunc().exec(null);
                        restoreDefaultTimeZone();
                        UDFContext.getUDFContext().addJobConf(null);
                    } catch (IOException e) {
                        throw new FrontendException(e);
                    }
                    valSet = true;
                }
                if (valSet) {
                    ConstantExpression constantExpr;
                    constantExpr = new ConstantExpression(currentWalker.getPlan(), val);
                    constantExpr.inheritSchema(op);
                    currentWalker.getPlan().replace(op, constantExpr);
                }
            }

            private void setDefaultTimeZone() {
                String dtzStr = pc.getProperties().getProperty("pig.datetime.default.tz");
                if (dtzStr != null && dtzStr.length() > 0) {
                    currentDTZ = DateTimeZone.getDefault();
                    DateTimeZone.setDefault(DateTimeZone.forID(dtzStr));
                }
            }

            private void restoreDefaultTimeZone() {
                if (currentDTZ != null) {
                    DateTimeZone.setDefault(currentDTZ);
                    currentDTZ = null;
                }
            }
        }

        @Override
        public void transform(OperatorPlan matched) throws FrontendException {
            AllExpressionVisitor expressionVisitor = new ConstantCalculatorLogicalPlanVisitor(matched, pc);
            expressionVisitor.visit();
        }

        @Override
        public OperatorPlan reportChanges() {
            return plan;
        }
    }
}
