/*
 * 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.carbondata.core.scan.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.carbondata.common.logging.LogServiceFactory;
import org.apache.carbondata.core.index.IndexFilter;
import org.apache.carbondata.core.metadata.datatype.DataTypes;
import org.apache.carbondata.core.metadata.schema.table.CarbonTable;
import org.apache.carbondata.core.metadata.schema.table.column.CarbonDimension;
import org.apache.carbondata.core.metadata.schema.table.column.CarbonMeasure;
import org.apache.carbondata.core.util.DataTypeConverter;

import org.apache.log4j.Logger;

public class QueryModelBuilder {

  private CarbonTable table;
  private QueryProjection projection;
  private IndexFilter indexFilter;
  private DataTypeConverter dataTypeConverter;
  private boolean forcedDetailRawQuery;
  private boolean readPageByPage;
  private boolean convertToRangeFilter = true;
  /**
   * log information
   */
  private static final Logger LOGGER =
      LogServiceFactory.getLogService(QueryModelBuilder.class.getName());

  public QueryModelBuilder(CarbonTable table) {
    this.table = table;
  }

  public QueryModelBuilder projectColumns(String[] projectionColumns) {
    Objects.requireNonNull(projectionColumns);
    String factTableName = table.getTableName();
    QueryProjection projection = new QueryProjection();

    int i = 0;
    for (String projectionColumnName : projectionColumns) {
      CarbonDimension dimension = table.getDimensionByName(projectionColumnName);
      if (dimension != null) {
        CarbonDimension complexParentDimension = dimension.getComplexParentDimension();
        if (null != complexParentDimension && dimension.getDataType() == DataTypes.DATE) {
          if (!isAlreadyExists(complexParentDimension, projection.getDimensions())) {
            projection.addDimension(complexParentDimension, i);
            i++;
          }
        } else {
          projection.addDimension(dimension, i);
          i++;
        }
      } else {
        CarbonMeasure measure = table.getMeasureByName(projectionColumnName);
        if (measure == null) {
          throw new RuntimeException(
              projectionColumnName + " column not found in the table " + factTableName);
        }
        projection.addMeasure(measure, i);
        i++;
      }
    }
    projection = optimizeProjectionForComplexColumns(projection, projectionColumns, factTableName);
    List<String> projectionDimensionAndMeasures = new ArrayList<>();
    this.projection = projection;
    for (ProjectionDimension projectionDimension : projection.getDimensions()) {
      projectionDimensionAndMeasures.add(projectionDimension.getColumnName());
    }
    for (ProjectionMeasure projectionMeasure : projection.getMeasures()) {
      projectionDimensionAndMeasures.add(projectionMeasure.getColumnName());
    }
    LOGGER.info("Projection Columns: " + projectionDimensionAndMeasures);
    return this;
  }

  /**
   * For complex dimensions, check if the dimension already exists in the projection list or not
   *
   * @param dimension
   * @param projectionDimensions
   * @return
   */
  private boolean isAlreadyExists(CarbonDimension dimension,
      List<ProjectionDimension> projectionDimensions) {
    boolean exists = false;
    for (ProjectionDimension projectionDimension : projectionDimensions) {
      if (dimension.getColName().equals(projectionDimension.getColumnName())) {
        exists = true;
        break;
      }
    }
    return exists;
  }

  private QueryProjection optimizeProjectionForComplexColumns(QueryProjection projection,
      String[] projectionColumns, String factTableName) {
    // Get the List of Complex Column Projection.
    // The optimization techniques which can be applied are
    // A. Merging in Driver Side
    // B. Merging in the result Collector side.
    // Merging is driver side cases are
    // Driver merging will eliminate one of the CarbonDimension.
    // Executor merging will merge the column output in Result Collector.
    // In this routine we are going to do driver merging and leave executor merging.
    Map<Integer, List<Integer>> complexColumnMap = new HashMap<>();
    List<ProjectionDimension> carbonDimensions = projection.getDimensions();
    // Traverse and find out if the top most parent of projection column is already there
    List<CarbonDimension> projectionDimensionToBeMerged = new ArrayList<>();
    for (ProjectionDimension projectionDimension : carbonDimensions) {
      CarbonDimension complexParentDimension =
          projectionDimension.getDimension().getComplexParentDimension();
      if (null != complexParentDimension && isAlreadyExists(complexParentDimension,
          carbonDimensions)) {
        projectionDimensionToBeMerged.add(projectionDimension.getDimension());
      }
    }

    if (projectionDimensionToBeMerged.size() != 0) {
      projection =
          removeMergedDimensions(projectionDimensionToBeMerged, projectionColumns, factTableName);
      carbonDimensions = projection.getDimensions();
    }

    for (ProjectionDimension cols : carbonDimensions) {
      // get all the Projections with Parent Ordinal Set.
      if (null != cols.getDimension().getComplexParentDimension()) {
        if (complexColumnMap.get(cols.getDimension().getComplexParentDimension().getOrdinal())
            != null) {
          List<Integer> childColumns =
              complexColumnMap.get(cols.getDimension().getComplexParentDimension().getOrdinal());
          childColumns.add(cols.getDimension().getOrdinal());
          complexColumnMap
              .put(cols.getDimension().getComplexParentDimension().getOrdinal(), childColumns);
        } else {
          List<Integer> childColumns = new ArrayList<>();
          childColumns.add(cols.getDimension().getOrdinal());
          complexColumnMap
              .put(cols.getDimension().getComplexParentDimension().getOrdinal(), childColumns);
        }
      }
    }

    // Traverse the Map to Find any columns are parent.
    for (Map.Entry<Integer, List<Integer>> entry : complexColumnMap.entrySet()) {
      List<Integer> childOrdinals = entry.getValue();
      if (childOrdinals.size() > 1) {
        // In case of more that one child, have to check if the child columns are in the same path
        // and have a common parent.
        Collections.sort(childOrdinals);
        List<CarbonDimension> mergedDimensions = mergeChildColumns(childOrdinals);
        if (mergedDimensions.size() > 0) {
          projection = removeMergedDimensions(mergedDimensions, projectionColumns, factTableName);
        }
      }
    }
    return projection;
  }

  /**
   * Remove the dimensions from the projection list which are merged
   *
   * @param mergedDimensions
   * @param projectionColumns
   * @param factTableName
   * @return
   */
  private QueryProjection removeMergedDimensions(List<CarbonDimension> mergedDimensions,
      String[] projectionColumns, String factTableName) {
    QueryProjection queryProjection = new QueryProjection();
    int i = 0;
    for (String projectionColumnName : projectionColumns) {
      CarbonDimension dimension = table.getDimensionByName(projectionColumnName);
      if (dimension != null) {
        if (!mergedDimensions.contains(dimension)) {
          if (!isAlreadyExists(dimension, queryProjection.getDimensions())) {
            queryProjection.addDimension(dimension, i);
            i++;
          }
        }
      } else {
        CarbonMeasure measure = table.getMeasureByName(projectionColumnName);
        if (measure == null) {
          throw new RuntimeException(
              projectionColumnName + " column not found in the table " + factTableName);
        }
        queryProjection.addMeasure(measure, i);
        i++;
      }
    }
    return queryProjection;
  }

  private List<CarbonDimension> mergeChildColumns(List<Integer> childOrdinals) {
    // Check If children if they are in the path of not.
    List<CarbonDimension> mergedChild = new ArrayList<>();
    List<CarbonDimension> dimList = table.getVisibleDimensions();
    for (int i = 0; i < childOrdinals.size(); i++) {
      for (int j = i; j < childOrdinals.size(); j++) {
        CarbonDimension parentDimension = getDimensionBasedOnOrdinal(dimList, childOrdinals.get(i));
        CarbonDimension childDimension = getDimensionBasedOnOrdinal(dimList, childOrdinals.get(j));
        if (!mergedChild.contains(childOrdinals.get(j)) && checkChildrenInSamePath(parentDimension,
            childDimension)) {
          mergedChild.add(childDimension);
        }
      }
    }
    return mergedChild;
  }

  private boolean checkChildrenInSamePath(CarbonDimension parentDimension,
      CarbonDimension childDimension) {
    if (parentDimension.getColName().equals(childDimension.getColName())) {
      return false;
    }
    return checkForChildColumns(parentDimension, childDimension);
  }

  private boolean checkForChildColumns(CarbonDimension parentDimension,
      CarbonDimension childDimension) {
    boolean output = false;
    if (parentDimension.getOrdinal() == childDimension.getOrdinal()) {
      output = true;
    } else if (parentDimension.getNumberOfChild() > 0) {
      for (int i = 0; i < parentDimension.getNumberOfChild() && !output; i++) {
        output =
            checkForChildColumns(parentDimension.getListOfChildDimensions().get(i), childDimension);
      }
    } else {
      output = false;
    }
    return output;
  }

  private CarbonDimension getDimensionBasedOnOrdinal(List<CarbonDimension> dimList,
      Integer ordinal) {
    for (CarbonDimension dims : dimList) {
      if (dims.getOrdinal() == ordinal) {
        return dims;
      } else if (dims.getNumberOfChild() > 0) {
        CarbonDimension dimensionBasedOnOrdinal =
            getDimensionBasedOnOrdinal(dims.getListOfChildDimensions(), ordinal);
        if (null != dimensionBasedOnOrdinal) {
          return dimensionBasedOnOrdinal;
        }
      }
    }
    return null;
  }

  public QueryModelBuilder projectAllColumns() {
    QueryProjection projection = new QueryProjection();
    List<CarbonDimension> dimensions = table.getVisibleDimensions();
    for (int i = 0; i < dimensions.size(); i++) {
      projection.addDimension(dimensions.get(i), i);
    }
    List<CarbonMeasure> measures = table.getVisibleMeasures();
    for (int i = 0; i < measures.size(); i++) {
      projection.addMeasure(measures.get(i), i);
    }
    this.projection = projection;
    return this;
  }

  public QueryModelBuilder filterExpression(IndexFilter filterExpression) {
    this.indexFilter = filterExpression;
    return this;
  }

  public QueryModelBuilder dataConverter(DataTypeConverter dataTypeConverter) {
    this.dataTypeConverter = dataTypeConverter;
    return this;
  }

  public QueryModelBuilder enableForcedDetailRawQuery() {
    this.forcedDetailRawQuery = true;
    return this;
  }

  public QueryModelBuilder convertToRangeFilter(boolean convertToRangeFilter) {
    this.convertToRangeFilter = convertToRangeFilter;
    return this;
  }

  public boolean isConvertToRangeFilter() {
    return this.convertToRangeFilter;
  }

  public void enableReadPageByPage() {
    this.readPageByPage = true;
  }

  public QueryModel build() {
    QueryModel queryModel = QueryModel.newInstance(table);
    queryModel.setConverter(dataTypeConverter);
    queryModel.setForcedDetailRawQuery(forcedDetailRawQuery);
    queryModel.setReadPageByPage(readPageByPage);
    queryModel.setProjection(projection);

    if (table.isTransactionalTable() && !table.hasColumnDrift()) {
      // set the filter to the query model in order to filter blocklet before scan
      boolean[] isFilterDimensions = new boolean[table.getDimensionOrdinalMax()];
      boolean[] isFilterMeasures = new boolean[table.getAllMeasures().size()];
      queryModel.setIsFilterDimensions(isFilterDimensions);
      queryModel.setIsFilterMeasures(isFilterMeasures);
      // In case of Dictionary Include Range Column we do not optimize the range expression
      if (indexFilter != null) {
        if (isConvertToRangeFilter()) {
          indexFilter.processFilterExpression(isFilterDimensions, isFilterMeasures);
        } else {
          indexFilter.processFilterExpressionWithoutRange(isFilterDimensions, isFilterMeasures);
        }
      }
    }
    queryModel.setIndexFilter(indexFilter);
    return queryModel;
  }
}
