/*
 * 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.drill.exec.planner.logical;

import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptPredicateList;
import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.rel.RelCollations;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.SingleRel;
import org.apache.calcite.rel.core.Calc;
import org.apache.calcite.rel.core.Filter;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.logical.LogicalSort;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.rules.ReduceExpressionsRule;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexExecutor;
import org.apache.calcite.rex.RexInputRef;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexLocalRef;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexProgram;
import org.apache.calcite.rex.RexProgramBuilder;
import org.apache.calcite.rex.RexShuttle;
import org.apache.calcite.rex.RexSimplify;
import org.apache.calcite.rex.RexUnknownAs;
import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.util.Util;
import org.apache.drill.shaded.guava.com.google.common.collect.Lists;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public abstract class DrillReduceExpressionsRule
  extends ReduceExpressionsRule<ReduceExpressionsRule.Config> {

  public static final DrillReduceFilterRule FILTER_INSTANCE_DRILL =
      new DrillReduceFilterRule();

  public static final DrillReduceCalcRule CALC_INSTANCE_DRILL =
      new DrillReduceCalcRule();

  public static final DrillReduceProjectRule PROJECT_INSTANCE_DRILL =
      new DrillReduceProjectRule();

  protected DrillReduceExpressionsRule(Config config) {
    super(config);
  }

  private static class DrillReduceFilterRule extends FilterReduceExpressionsRule {

    DrillReduceFilterRule() {
      super(FilterReduceExpressionsRuleConfig.DEFAULT
        .withOperandFor(Filter.class)
        .withMatchNullability(false)
        .withRelBuilderFactory(DrillRelFactories.LOGICAL_BUILDER)
        .as(FilterReduceExpressionsRuleConfig.class));
    }

    /**
     * Drills schema flexibility requires us to override the default behavior of calcite
     * to produce an EmptyRel in the case of a constant false filter. We need to propagate
     * schema at runtime, so we cannot just produce a simple operator at planning time to
     * expose the planning time known schema. Instead we have to insert a limit 0.
     */
    @Override
    protected RelNode createEmptyRelOrEquivalent(RelOptRuleCall call, Filter filter) {
      return createEmptyEmptyRelHelper(filter);
    }

    @Override
    public void onMatch(RelOptRuleCall call) {
      final Filter filter = call.rel(0);
      final List<RexNode> expList =
        Lists.newArrayList(filter.getCondition());
      RexNode newConditionExp;
      boolean reduced;
      final RelMetadataQuery mq = call.getMetadataQuery();
      final RelOptPredicateList predicates =
        mq.getPulledUpPredicates(filter.getInput());
      if (reduceExpressionsNoSimplify(filter, expList, predicates, true,
        config.treatDynamicCallsAsConstant())) {
        assert expList.size() == 1;
        newConditionExp = expList.get(0);
        reduced = true;
      } else {
        // No reduction, but let's still test the original
        // predicate to see if it was already a constant,
        // in which case we don't need any runtime decision
        // about filtering.
        newConditionExp = filter.getCondition();
        reduced = false;
      }

      // Even if no reduction, let's still test the original
      // predicate to see if it was already a constant,
      // in which case we don't need any runtime decision
      // about filtering.
      if (newConditionExp.isAlwaysTrue()) {
        call.transformTo(
          filter.getInput());
      } else if (newConditionExp instanceof RexLiteral
        || RexUtil.isNullLiteral(newConditionExp, true)) {
        call.transformTo(createEmptyRelOrEquivalent(call, filter));
      } else if (reduced) {
        call.transformTo(call.builder()
          .push(filter.getInput())
          .filter(newConditionExp).build());
      } else {
        if (newConditionExp instanceof RexCall) {
          boolean reverse = newConditionExp.getKind() == SqlKind.NOT;
          if (reverse) {
            newConditionExp = ((RexCall) newConditionExp).getOperands().get(0);
          }
          reduceNotNullableFilter(call, filter, newConditionExp, reverse);
        }
        return;
      }

      // New plan is absolutely better than old plan.
      call.getPlanner().prune(filter);
    }

    private void reduceNotNullableFilter(
      RelOptRuleCall call,
      Filter filter,
      RexNode rexNode,
      boolean reverse) {
      // If the expression is a IS [NOT] NULL on a non-nullable
      // column, then we can either remove the filter or replace
      // it with an Empty.
      boolean alwaysTrue;
      switch (rexNode.getKind()) {
        case IS_NULL:
        case IS_UNKNOWN:
          alwaysTrue = false;
          break;
        case IS_NOT_NULL:
          alwaysTrue = true;
          break;
        default:
          return;
      }
      if (reverse) {
        alwaysTrue = !alwaysTrue;
      }
      RexNode operand = ((RexCall) rexNode).getOperands().get(0);
      if (operand instanceof RexInputRef) {
        RexInputRef inputRef = (RexInputRef) operand;
        if (!inputRef.getType().isNullable()) {
          if (alwaysTrue) {
            call.transformTo(filter.getInput());
          } else {
            call.transformTo(createEmptyRelOrEquivalent(call, filter));
          }
          // New plan is absolutely better than old plan.
          call.getPlanner().prune(filter);
        }
      }
    }
  }

  private static class DrillReduceCalcRule extends CalcReduceExpressionsRule {

    DrillReduceCalcRule() {
      super(CalcReduceExpressionsRuleConfig.DEFAULT
        .withOperandFor(Calc.class)
        .withMatchNullability(true)
        .withRelBuilderFactory(DrillRelFactories.LOGICAL_BUILDER)
        .as(CalcReduceExpressionsRuleConfig.class));
    }

    /**
     * Drills schema flexibility requires us to override the default behavior of calcite
     * to produce an EmptyRel in the case of a constant false filter. We need to propagate
     * schema at runtime, so we cannot just produce a simple operator at planning time to
     * expose the planning time known schema. Instead we have to insert a limit 0.
     */
    @Override
    protected RelNode createEmptyRelOrEquivalent(RelOptRuleCall call, Calc input) {
      return createEmptyEmptyRelHelper(input);
    }

    @Override
    public void onMatch(RelOptRuleCall call) {
      Calc calc = call.rel(0);
      RexProgram program = calc.getProgram();
      final List<RexNode> exprList = program.getExprList();

      // Form a list of expressions with sub-expressions fully expanded.
      final List<RexNode> expandedExprList = new ArrayList<>();
      final RexShuttle shuttle =
        new RexShuttle() {
          @Override
          public RexNode visitLocalRef(RexLocalRef localRef) {
            return expandedExprList.get(localRef.getIndex());
          }
        };
      for (RexNode expr : exprList) {
        expandedExprList.add(expr.accept(shuttle));
      }
      final RelOptPredicateList predicates = RelOptPredicateList.EMPTY;
      if (reduceExpressionsNoSimplify(calc, expandedExprList, predicates, false,
        config.treatDynamicCallsAsConstant())) {
        final RexProgramBuilder builder =
          new RexProgramBuilder(
            calc.getInput().getRowType(),
            calc.getCluster().getRexBuilder());
        final List<RexLocalRef> list = new ArrayList<>();
        for (RexNode expr : expandedExprList) {
          list.add(builder.registerInput(expr));
        }
        if (program.getCondition() != null) {
          final int conditionIndex =
            program.getCondition().getIndex();
          final RexNode newConditionExp =
            expandedExprList.get(conditionIndex);
          if (newConditionExp.isAlwaysTrue()) {
            // condition is always TRUE - drop it.
          } else if (newConditionExp instanceof RexLiteral
            || RexUtil.isNullLiteral(newConditionExp, true)) {
            // condition is always NULL or FALSE - replace calc
            // with empty.
            call.transformTo(createEmptyRelOrEquivalent(call, calc));
            return;
          } else {
            builder.addCondition(list.get(conditionIndex));
          }
        }
        int k = 0;
        for (RexLocalRef projectExpr : program.getProjectList()) {
          final int index = projectExpr.getIndex();
          builder.addProject(
            list.get(index).getIndex(),
            program.getOutputRowType().getFieldNames().get(k++));
        }
        call.transformTo(
          calc.copy(calc.getTraitSet(), calc.getInput(), builder.getProgram()));

        // New plan is absolutely better than old plan.
        call.getPlanner().prune(calc);
      }
    }
  }

  private static class DrillReduceProjectRule extends ProjectReduceExpressionsRule {

    DrillReduceProjectRule() {
      super(ProjectReduceExpressionsRuleConfig.DEFAULT
        .withOperandFor(Project.class)
        .withMatchNullability(true)
        .withRelBuilderFactory(DrillRelFactories.LOGICAL_BUILDER)
        .as(ProjectReduceExpressionsRuleConfig.class));
    }

    @Override
    public void onMatch(RelOptRuleCall call) {
      final Project project = call.rel(0);
      final RelMetadataQuery mq = call.getMetadataQuery();
      final RelOptPredicateList predicates =
        mq.getPulledUpPredicates(project.getInput());
      final List<RexNode> expList =
        Lists.newArrayList(project.getProjects());
      if (reduceExpressionsNoSimplify(project, expList, predicates, false,
        config.treatDynamicCallsAsConstant())) {
        assert !project.getProjects().equals(expList)
          : "Reduced expressions should be different from original expressions";
        call.transformTo(
          call.builder()
            .push(project.getInput())
            .project(expList, project.getRowType().getFieldNames())
            .build());

        // New plan is absolutely better than old plan.
        call.getPlanner().prune(project);
      }
    }
  }

  protected static boolean reduceExpressionsNoSimplify(RelNode rel, List<RexNode> expList,
    RelOptPredicateList predicates, boolean unknownAsFalse, boolean treatDynamicCallsAsConstant) {
    RelOptCluster cluster = rel.getCluster();
    RexBuilder rexBuilder = cluster.getRexBuilder();
    RexExecutor executor =
      Util.first(cluster.getPlanner().getExecutor(), RexUtil.EXECUTOR);
    RexSimplify simplify =
      new RexSimplify(rexBuilder, predicates, executor);

    // Simplify predicates in place
    RexUnknownAs unknownAs = RexUnknownAs.falseIf(unknownAsFalse);

    return ReduceExpressionsRule.reduceExpressionsInternal(rel, simplify, unknownAs,
      expList, predicates, treatDynamicCallsAsConstant);
  }

  private static RelNode createEmptyEmptyRelHelper(SingleRel input) {
    return LogicalSort.create(input.getInput(), RelCollations.EMPTY,
        input.getCluster().getRexBuilder().makeExactLiteral(BigDecimal.valueOf(0)),
        input.getCluster().getRexBuilder().makeExactLiteral(BigDecimal.valueOf(0)));
  }
}
