/*
 * 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.store.mapr.db;

import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.plan.RelOptRuleOperand;
import org.apache.calcite.plan.RelTrait;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.rules.ProjectRemoveRule;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rex.RexNode;
import org.apache.drill.common.exceptions.DrillRuntimeException;
import org.apache.drill.exec.planner.common.DrillRelOptUtil;
import org.apache.drill.exec.planner.logical.RelOptHelper;
import org.apache.drill.exec.planner.physical.Prel;
import org.apache.drill.exec.planner.physical.ProjectPrel;
import org.apache.drill.exec.planner.physical.ScanPrel;
import org.apache.drill.exec.store.StoragePluginOptimizerRule;
import org.apache.drill.exec.store.mapr.db.json.JsonTableGroupScan;
import org.apache.drill.exec.util.Utilities;

import java.util.List;
import java.util.stream.Collectors;

/**
 * Push a physical Project into Scan. Currently, this rule is only doing projection pushdown for MapRDB-JSON tables
 * since it was needed for the secondary index feature which only applies to Json tables.
 * For binary tables, note that the DrillPushProjectIntoScanRule is still applicable during the logical
 * planning phase.
 */
public abstract class MapRDBPushProjectIntoScan extends StoragePluginOptimizerRule {

  private MapRDBPushProjectIntoScan(RelOptRuleOperand operand, String description) {
    super(operand, description);
  }

  public static final StoragePluginOptimizerRule PROJECT_ON_SCAN = new MapRDBPushProjectIntoScan(
      RelOptHelper.some(ProjectPrel.class, RelOptHelper.any(ScanPrel.class)), "MapRDBPushProjIntoScan:Proj_On_Scan") {
    @Override
    public void onMatch(RelOptRuleCall call) {
      final ProjectPrel project = call.rel(0);
      final ScanPrel scan = call.rel(1);

      if (scan.getGroupScan() instanceof JsonTableGroupScan) {
        JsonTableGroupScan groupScan = (JsonTableGroupScan) scan.getGroupScan();

        doPushProjectIntoGroupScan(call, project, scan, groupScan);
      }
    }

    @Override
    public boolean matches(RelOptRuleCall call) {
      final ScanPrel scan = call.rel(1);

      // See class level comments above for why only JsonGroupScan is considered
      if (scan.getGroupScan() instanceof JsonTableGroupScan) {
        return super.matches(call);
      }
      return false;
    }
  };

  protected void doPushProjectIntoGroupScan(RelOptRuleCall call,
      ProjectPrel project, ScanPrel scan, JsonTableGroupScan groupScan) {
    try {

      DrillRelOptUtil.ProjectPushInfo columnInfo =
          DrillRelOptUtil.getFieldsInformation(scan.getRowType(), project.getProjects());
      if (columnInfo == null || Utilities.isStarQuery(columnInfo.getFields())
          || !groupScan.canPushdownProjects(columnInfo.getFields())) {
        return;
      }
      RelTraitSet newTraits = call.getPlanner().emptyTraitSet();
      // Clear out collation trait
      for (RelTrait trait : scan.getTraitSet()) {
        if (!(trait instanceof RelCollation)) {
          newTraits.plus(trait);
        }
      }
      final ScanPrel newScan = new ScanPrel(scan.getCluster(), newTraits.plus(Prel.DRILL_PHYSICAL),
          groupScan.clone(columnInfo.getFields()),
          columnInfo.createNewRowType(project.getInput().getCluster().getTypeFactory()), scan.getTable());

      List<RexNode> newProjects = project.getProjects().stream()
        .map(n -> n.accept(columnInfo.getInputReWriter()))
        .collect(Collectors.toList());

      final ProjectPrel newProj =
          new ProjectPrel(project.getCluster(),
              project.getTraitSet().plus(Prel.DRILL_PHYSICAL),
              newScan,
              newProjects,
              project.getRowType());

      if (ProjectRemoveRule.isTrivial(newProj) &&
          // the old project did not involve any column renaming
          sameRowTypeProjectionsFields(project.getRowType(), newScan.getRowType())) {
        call.transformTo(newScan);
      } else {
        call.transformTo(newProj);
      }
    } catch (Exception e) {
      throw new DrillRuntimeException(e);
    }
  }

  private boolean sameRowTypeProjectionsFields(RelDataType oldRowType, RelDataType newRowType) {
    for (RelDataTypeField oldField : oldRowType.getFieldList()) {
      String oldProjName = oldField.getName();
      boolean match = false;
      for (RelDataTypeField newField : newRowType.getFieldList()) {
        if (oldProjName.equals(newField.getName())) {
          match = true;
          break;
        }
      }
      if (!match) {
        return false;
      }
    }
    return true;
  }
}
