/*
 * 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.physical.impl.project;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.apache.commons.collections.map.CaseInsensitiveMap;
import org.apache.drill.common.expression.ConvertExpression;
import org.apache.drill.common.expression.ErrorCollector;
import org.apache.drill.common.expression.ErrorCollectorImpl;
import org.apache.drill.common.expression.ExpressionPosition;
import org.apache.drill.common.expression.FieldReference;
import org.apache.drill.common.expression.FunctionCall;
import org.apache.drill.common.expression.FunctionCallFactory;
import org.apache.drill.common.expression.LogicalExpression;
import org.apache.drill.common.expression.PathSegment.NameSegment;
import org.apache.drill.common.expression.SchemaPath;
import org.apache.drill.common.expression.ValueExpressions;
import org.apache.drill.common.expression.fn.FunctionReplacementUtils;
import org.apache.drill.common.logical.data.NamedExpression;
import org.apache.drill.common.types.TypeProtos.MinorType;
import org.apache.drill.common.types.Types;
import org.apache.drill.exec.expr.ClassGenerator;
import org.apache.drill.exec.expr.CodeGenerator;
import org.apache.drill.exec.expr.DrillFuncHolderExpr;
import org.apache.drill.exec.expr.ExpressionTreeMaterializer;
import org.apache.drill.exec.expr.ValueVectorReadExpression;
import org.apache.drill.exec.expr.ValueVectorWriteExpression;
import org.apache.drill.exec.expr.fn.FunctionLookupContext;
import org.apache.drill.exec.ops.FragmentContext;
import org.apache.drill.exec.physical.resultSet.impl.VectorState;
import org.apache.drill.exec.planner.StarColumnHelper;
import org.apache.drill.exec.record.BatchSchema;
import org.apache.drill.exec.record.BatchSchema.SelectionVectorMode;
import org.apache.drill.exec.record.MaterializedField;
import org.apache.drill.exec.record.VectorAccessible;
import org.apache.drill.exec.record.VectorWrapper;
import org.apache.drill.exec.server.options.OptionManager;
import org.apache.drill.exec.store.ColumnExplorer;
import org.apache.drill.exec.vector.ValueVector;
import org.apache.drill.shaded.guava.com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.carrotsearch.hppc.IntHashSet;

/**
 * Plans the projection given the incoming and requested outgoing schemas. Works
 * with the {@link VectorState} to create required vectors, writers and so on.
 * Populates the code generator with the "projector" expressions.
 */
class ProjectionMaterializer {
  private static final Logger logger = LoggerFactory.getLogger(ProjectionMaterializer.class);
  private static final String EMPTY_STRING = "";

  /**
   * Abstracts the physical vector setup operations to separate
   * the physical setup, in <code>ProjectRecordBatch</code>, from the
   * logical setup in the materializer class.
   */
  public interface BatchBuilder {
    void addTransferField(String name, ValueVector vvIn);
    ValueVectorWriteExpression addOutputVector(String name, LogicalExpression expr);
    int addDirectTransfer(FieldReference ref, ValueVectorReadExpression vectorRead);
    void addComplexField(FieldReference ref);
    ValueVectorWriteExpression addEvalVector(String outputName,
        LogicalExpression expr);
  }

  private static class ClassifierResult {
    private boolean isStar;
    private List<String> outputNames;
    private String prefix = "";
    private final HashMap<String, Integer> prefixMap = Maps.newHashMap();
    private final CaseInsensitiveMap outputMap = new CaseInsensitiveMap();
    private final CaseInsensitiveMap sequenceMap = new CaseInsensitiveMap();

    private void clear() {
      isStar = false;
      prefix = "";
      if (outputNames != null) {
        outputNames.clear();
      }

      // note: don't clear the internal maps since they have cumulative data..
    }
  }

  private final ClassGenerator<Projector> cg;
  private final VectorAccessible incomingBatch;
  private final BatchSchema incomingSchema;
  private final List<NamedExpression> exprSpec;
  private final FunctionLookupContext functionLookupContext;
  private final BatchBuilder batchBuilder;
  private final boolean unionTypeEnabled;
  private final ErrorCollector collector = new ErrorCollectorImpl();
  private final ColumnExplorer columnExplorer;
  private final IntHashSet transferFieldIds = new IntHashSet();
  private final ProjectionMaterializer.ClassifierResult result = new ClassifierResult();
  private boolean isAnyWildcard;
  private boolean classify;

  public ProjectionMaterializer(OptionManager options,
      VectorAccessible incomingBatch, List<NamedExpression> exprSpec,
      FunctionLookupContext functionLookupContext, BatchBuilder batchBuilder,
      boolean unionTypeEnabled) {
    this.incomingBatch = incomingBatch;
    this.incomingSchema = incomingBatch.getSchema();
    this.exprSpec = exprSpec;
    this.functionLookupContext = functionLookupContext;
    this.batchBuilder = batchBuilder;
    this.unionTypeEnabled = unionTypeEnabled;
    columnExplorer = new ColumnExplorer(options);
    cg = CodeGenerator.getRoot(Projector.TEMPLATE_DEFINITION, options);
  }

  public Projector generateProjector(FragmentContext context, boolean saveCode) {
    long setupNewSchemaStartTime = System.currentTimeMillis();
    setup();
    CodeGenerator<Projector> codeGen = cg.getCodeGenerator();
    codeGen.plainJavaCapable(true);
    codeGen.saveCodeForDebugging(saveCode);
    Projector projector = context.getImplementationClass(codeGen);

    long setupNewSchemaEndTime = System.currentTimeMillis();
    logger.trace("generateProjector: time {}  ms, Project {}, incoming {}",
             (setupNewSchemaEndTime - setupNewSchemaStartTime), exprSpec, incomingSchema);
    return projector;
  }

  private void setup() {
    List<NamedExpression> exprs = exprSpec != null ? exprSpec
        : inferExpressions();
    isAnyWildcard = isAnyWildcard(exprs);
    classify = isClassificationNeeded(exprs);

    for (NamedExpression namedExpression : exprs) {
      setupExpression(namedExpression);
    }
  }

  private List<NamedExpression> inferExpressions() {
    List<NamedExpression> exprs = new ArrayList<>();
    for (MaterializedField field : incomingSchema) {
      String fieldName = field.getName();
      if (Types.isComplex(field.getType())
          || Types.isRepeated(field.getType())) {
        LogicalExpression convertToJson = FunctionCallFactory.createConvert(
            ConvertExpression.CONVERT_TO, "JSON",
            SchemaPath.getSimplePath(fieldName), ExpressionPosition.UNKNOWN);
        String castFuncName = FunctionReplacementUtils
            .getCastFunc(MinorType.VARCHAR);
        List<LogicalExpression> castArgs = new ArrayList<>();
        castArgs.add(convertToJson); // input_expr
        // Implicitly casting to varchar, since we don't know actual source
        // length, cast to undefined length, which will preserve source length
        castArgs.add(new ValueExpressions.LongExpression(
            Types.MAX_VARCHAR_LENGTH, null));
        FunctionCall castCall = new FunctionCall(castFuncName, castArgs,
            ExpressionPosition.UNKNOWN);
        exprs.add(new NamedExpression(castCall, new FieldReference(fieldName)));
      } else {
        exprs.add(new NamedExpression(SchemaPath.getSimplePath(fieldName),
            new FieldReference(fieldName)));
      }
    }
    return exprs;
  }

  private boolean isAnyWildcard(List<NamedExpression> exprs) {
    for (NamedExpression e : exprs) {
      if (isWildcard(e)) {
        return true;
      }
    }
    return false;
  }

  private boolean isWildcard(NamedExpression ex) {
    if (!(ex.getExpr() instanceof SchemaPath)) {
      return false;
    }
    NameSegment expr = ((SchemaPath) ex.getExpr()).getRootSegment();
    return expr.getPath().contains(SchemaPath.DYNAMIC_STAR);
  }

  private boolean isClassificationNeeded(List<NamedExpression> exprs) {
    boolean needed = false;
    for (NamedExpression ex : exprs) {
      if (!(ex.getExpr() instanceof SchemaPath)) {
        continue;
      }
      NameSegment expr = ((SchemaPath) ex.getExpr()).getRootSegment();
      NameSegment ref = ex.getRef().getRootSegment();
      boolean refHasPrefix = ref.getPath()
          .contains(StarColumnHelper.PREFIX_DELIMITER);
      boolean exprContainsStar = expr.getPath()
          .contains(SchemaPath.DYNAMIC_STAR);

      if (refHasPrefix || exprContainsStar) {
        needed = true;
        break;
      }
    }
    return needed;
  }

  private void setupExpression(NamedExpression namedExpression) {
    result.clear();
    if (classify && namedExpression.getExpr() instanceof SchemaPath) {
      classifyExpr(namedExpression, result);

      if (result.isStar) {
        setupImplicitColumnRef(namedExpression);
        return;
      }
    } else {
      // For the columns which do not needed to be classified,
      // it is still necessary to ensure the output column name is unique
      result.outputNames = new ArrayList<>();
      String outputName = getRef(namedExpression).getRootSegment().getPath(); // moved
                                                                              // to
                                                                              // before
                                                                              // the
                                                                              // if
      addToResultMaps(outputName, result, true);
    }
    String outputName = getRef(namedExpression).getRootSegment().getPath();
    if (result != null && result.outputNames != null
        && result.outputNames.size() > 0) {
      boolean isMatched = false;
      for (int j = 0; j < result.outputNames.size(); j++) {
        if (!result.outputNames.get(j).isEmpty()) {
          outputName = result.outputNames.get(j);
          isMatched = true;
          break;
        }
      }

      if (!isMatched) {
        return;
      }
    }

    LogicalExpression expr = ExpressionTreeMaterializer.materialize(
        namedExpression.getExpr(), incomingBatch, collector,
        functionLookupContext, true, unionTypeEnabled);
    collector.reportErrors(logger);

    // Add value vector to transfer if direct reference and this is allowed,
    // otherwise, add to evaluation stack.
    if (expr instanceof ValueVectorReadExpression
        && incomingSchema.getSelectionVectorMode() == SelectionVectorMode.NONE
        && !((ValueVectorReadExpression) expr).hasReadPath() && !isAnyWildcard
        && !transferFieldIds.contains(
            ((ValueVectorReadExpression) expr).getFieldId().getFieldIds()[0])) {
      setupDirectTransfer(namedExpression, expr);
    } else if (expr instanceof DrillFuncHolderExpr
        && ((DrillFuncHolderExpr) expr).getHolder()
            .isComplexWriterFuncHolder()) {
      setupFnCall(namedExpression, expr);
    } else {
      // need to do evaluation.
      setupExprEval(namedExpression, expr, outputName);
    }
  }

  private void setupImplicitColumnRef(NamedExpression namedExpression) {
    // The value indicates which wildcard we are processing now
    Integer value = result.prefixMap.get(result.prefix);
    if (value != null && value == 1) {
      int k = 0;
      for (VectorWrapper<?> wrapper : incomingBatch) {
        ValueVector vvIn = wrapper.getValueVector();
        if (k > result.outputNames.size() - 1) {
          assert false;
        }
        String name = result.outputNames.get(k++); // get the renamed column
                                                   // names
        if (name.isEmpty()) {
          continue;
        }

        if (isImplicitFileColumn(vvIn.getField())) {
          continue;
        }

        batchBuilder.addTransferField(name, vvIn);
      }
    } else if (value != null && value > 1) { // subsequent wildcards should do a
                                             // copy of incoming value vectors
      int k = 0;
      for (MaterializedField field : incomingSchema) {
        SchemaPath originalPath = SchemaPath
            .getSimplePath(field.getName());
        if (k > result.outputNames.size() - 1) {
          assert false;
        }
        String name = result.outputNames.get(k++); // get the renamed column
                                                   // names
        if (name.isEmpty()) {
          continue;
        }

        if (isImplicitFileColumn(field)) {
          continue;
        }

        LogicalExpression expr = ExpressionTreeMaterializer.materialize(
            originalPath, incomingBatch, collector, functionLookupContext);
        collector.reportErrors(logger);

        ValueVectorWriteExpression write = batchBuilder.addOutputVector(name,
            expr);
        cg.addExpr(write, ClassGenerator.BlkCreateMode.TRUE_IF_BOUND);
      }
    }
  }

  private void setupDirectTransfer(NamedExpression namedExpression,
      LogicalExpression expr) {
    FieldReference ref = getRef(namedExpression);
    int fid = batchBuilder.addDirectTransfer(ref,
        (ValueVectorReadExpression) expr);
    transferFieldIds.add(fid);
  }

  private void setupFnCall(NamedExpression namedExpression, LogicalExpression expr) {

    // Need to process ComplexWriter function evaluation.
    // The reference name will be passed to ComplexWriter, used as the name of
    // the output vector from the writer.
    ((DrillFuncHolderExpr) expr).setFieldReference(namedExpression.getRef());
    cg.addExpr(expr, ClassGenerator.BlkCreateMode.TRUE_IF_BOUND);
    batchBuilder.addComplexField(namedExpression.getRef());
  }

  private void setupExprEval(NamedExpression namedExpression,
      LogicalExpression expr, String outputName) {
    ValueVectorWriteExpression write = batchBuilder.addEvalVector(
        outputName, expr);
    cg.addExpr(write, ClassGenerator.BlkCreateMode.TRUE_IF_BOUND);
  }

  private boolean isImplicitFileColumn(MaterializedField field) {
    return columnExplorer
        .isImplicitOrInternalFileColumn(field.getName());
  }

  private void classifyExpr(NamedExpression ex, ClassifierResult result) {
    NameSegment expr = ((SchemaPath) ex.getExpr()).getRootSegment();
    NameSegment ref = ex.getRef().getRootSegment();
    boolean exprHasPrefix = expr.getPath()
        .contains(StarColumnHelper.PREFIX_DELIMITER);
    boolean refHasPrefix = ref.getPath()
        .contains(StarColumnHelper.PREFIX_DELIMITER);
    boolean exprIsStar = expr.getPath().equals(SchemaPath.DYNAMIC_STAR);
    boolean refContainsStar = ref.getPath().contains(SchemaPath.DYNAMIC_STAR);
    boolean exprContainsStar = expr.getPath().contains(SchemaPath.DYNAMIC_STAR);
    boolean refEndsWithStar = ref.getPath().endsWith(SchemaPath.DYNAMIC_STAR);

    String exprPrefix = EMPTY_STRING;
    String exprSuffix = expr.getPath();

    if (exprHasPrefix) {
      // get the prefix of the expr
      String[] exprComponents = expr.getPath()
          .split(StarColumnHelper.PREFIX_DELIMITER, 2);
      assert (exprComponents.length == 2);
      exprPrefix = exprComponents[0];
      exprSuffix = exprComponents[1];
      result.prefix = exprPrefix;
    }

    boolean exprIsFirstWildcard = false;
    if (exprContainsStar) {
      result.isStar = true;
      Integer value = result.prefixMap.get(exprPrefix);
      if (value == null) {
        result.prefixMap.put(exprPrefix, 1);
        exprIsFirstWildcard = true;
      } else {
        result.prefixMap.put(exprPrefix, value + 1);
      }
    }

    int incomingSchemaSize = incomingSchema.getFieldCount();

    // input is '*' and output is 'prefix_*'
    if (exprIsStar && refHasPrefix && refEndsWithStar) {
      String[] components = ref.getPath()
          .split(StarColumnHelper.PREFIX_DELIMITER, 2);
      assert (components.length == 2);
      String prefix = components[0];
      result.outputNames = new ArrayList<>();
      for (MaterializedField field : incomingSchema) {
        String name = field.getName();

        // add the prefix to the incoming column name
        String newName = prefix + StarColumnHelper.PREFIX_DELIMITER + name;
        addToResultMaps(newName, result, false);
      }
    }
    // input and output are the same
    else if (expr.getPath().equalsIgnoreCase(ref.getPath())
        && (!exprContainsStar || exprIsFirstWildcard)) {
      if (exprContainsStar && exprHasPrefix) {
        assert exprPrefix != null;

        int k = 0;
        result.outputNames = new ArrayList<>(incomingSchemaSize);
        for (int j = 0; j < incomingSchemaSize; j++) {
          result.outputNames.add(EMPTY_STRING); // initialize
        }

        for (MaterializedField field : incomingSchema) {
          String incomingName = field.getName();
          // get the prefix of the name
          String[] nameComponents = incomingName
              .split(StarColumnHelper.PREFIX_DELIMITER, 2);
          // if incoming value vector does not have a prefix, ignore it since
          // this expression is not referencing it
          if (nameComponents.length <= 1) {
            k++;
            continue;
          }
          String namePrefix = nameComponents[0];
          if (exprPrefix.equalsIgnoreCase(namePrefix)) {
            if (!result.outputMap.containsKey(incomingName)) {
              result.outputNames.set(k, incomingName);
              result.outputMap.put(incomingName, incomingName);
            }
          }
          k++;
        }
      } else {
        result.outputNames = new ArrayList<>();
        if (exprContainsStar) {
          for (MaterializedField field : incomingSchema) {
            String incomingName = field.getName();
            if (refContainsStar) {
              addToResultMaps(incomingName, result, true); // allow dups since
                                                           // this is likely
                                                           // top-level project
            } else {
              addToResultMaps(incomingName, result, false);
            }
          }
        } else {
          String newName = expr.getPath();
          if (!refHasPrefix && !exprHasPrefix) {
            addToResultMaps(newName, result, true); // allow dups since this is
                                                    // likely top-level project
          } else {
            addToResultMaps(newName, result, false);
          }
        }
      }
    }

    // Input is wildcard and it is not the first wildcard
    else if (exprIsStar) {
      result.outputNames = new ArrayList<>();
      for (MaterializedField field : incomingSchema) {
        String incomingName = field.getName();
        addToResultMaps(incomingName, result, true); // allow dups since this is
                                                     // likely top-level project
      }
    }

    // Only the output has prefix
    else if (!exprHasPrefix && refHasPrefix) {
      result.outputNames = new ArrayList<>();
      String newName = ref.getPath();
      addToResultMaps(newName, result, false);
    }
    // Input has prefix but output does not
    else if (exprHasPrefix && !refHasPrefix) {
      int k = 0;
      result.outputNames = new ArrayList<>(incomingSchemaSize);
      for (int j = 0; j < incomingSchemaSize; j++) {
        result.outputNames.add(EMPTY_STRING); // initialize
      }

      for (MaterializedField field : incomingSchema) {
        String name = field.getName();
        String[] components = name.split(StarColumnHelper.PREFIX_DELIMITER, 2);
        if (components.length <= 1) {
          k++;
          continue;
        }
        String namePrefix = components[0];
        String nameSuffix = components[1];
        if (exprPrefix.equalsIgnoreCase(namePrefix)) { // // case insensitive
                                                       // matching of prefix.
          if (refContainsStar) {
            // remove the prefix from the incoming column names
            String newName = getUniqueName(nameSuffix, result); // for top level
                                                                // we need to
                                                                // make names
                                                                // unique
            result.outputNames.set(k, newName);
          } else if (exprSuffix.equalsIgnoreCase(nameSuffix)) { // case
                                                                // insensitive
                                                                // matching of
                                                                // field name.
            // example: ref: $f1, expr: T0<PREFIX><column_name>
            String newName = ref.getPath();
            result.outputNames.set(k, newName);
          }
        } else {
          result.outputNames.add(EMPTY_STRING);
        }
        k++;
      }
    }
    // input and output have prefixes although they could be different...
    else if (exprHasPrefix && refHasPrefix) {
      String[] input = expr.getPath().split(StarColumnHelper.PREFIX_DELIMITER,
          2);
      assert (input.length == 2);
      assert false : "Unexpected project expression or reference"; // not
                                                                   // handled
                                                                   // yet
    } else {
      // if the incoming schema's column name matches the expression name of the
      // Project,
      // then we just want to pick the ref name as the output column name

      result.outputNames = new ArrayList<>();
      for (MaterializedField field : incomingSchema) {
        String incomingName = field.getName();
        if (expr.getPath().equalsIgnoreCase(incomingName)) { // case insensitive
                                                             // matching of
                                                             // field name.
          String newName = ref.getPath();
          addToResultMaps(newName, result, true);
        }
      }
    }
  }

  private String getUniqueName(String name,
      ProjectionMaterializer.ClassifierResult result) {
    Integer currentSeq = (Integer) result.sequenceMap.get(name);
    if (currentSeq == null) { // name is unique, so return the original name
      result.sequenceMap.put(name, -1);
      return name;
    }
    // create a new name
    int newSeq = currentSeq + 1;
    String newName = name + newSeq;
    result.sequenceMap.put(name, newSeq);
    result.sequenceMap.put(newName, -1);

    return newName;
  }

  /**
   * Helper method to ensure unique output column names. If allowDupsWithRename
   * is set to true, the original name will be appended with a suffix number to
   * ensure uniqueness. Otherwise, the original column would not be renamed even
   * even if it has been used
   *
   * @param origName
   *          the original input name of the column
   * @param result
   *          the data structure to keep track of the used names and decide what
   *          output name should be to ensure uniqueness
   * @param allowDupsWithRename
   *          if the original name has been used, is renaming allowed to ensure
   *          output name unique
   */
  private void addToResultMaps(String origName,
      ProjectionMaterializer.ClassifierResult result,
      boolean allowDupsWithRename) {
    String name = origName;
    if (allowDupsWithRename) {
      name = getUniqueName(origName, result);
    }
    if (!result.outputMap.containsKey(name)) {
      result.outputNames.add(name);
      result.outputMap.put(name, name);
    } else {
      result.outputNames.add(EMPTY_STRING);
    }
  }

  // Hack to make ref and full work together... need to figure out if this is
  // still necessary.
  private FieldReference getRef(NamedExpression e) {
    return e.getRef();
  }
}
