/*
 * 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.hadoop.hive.ql.plan;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.collections.Bag;
import org.apache.commons.collections.bag.TreeBag;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableSortedMultiset;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hive.conf.HiveConf.StrictChecks;
import org.apache.hadoop.hive.ql.exec.FunctionRegistry;
import org.apache.hadoop.hive.ql.exec.UDF;
import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.exec.Utilities;
import org.apache.hadoop.hive.ql.session.SessionState;
import org.apache.hadoop.hive.ql.session.SessionState.LogHelper;
import org.apache.hadoop.hive.ql.udf.UDFType;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDF;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDFBaseCompare;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDFBridge;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDFMacro;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorUtils;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoUtils;

/**
 * Describes a GenericFunc node.
 */
public class ExprNodeGenericFuncDesc extends ExprNodeDesc implements
    Serializable {

  private static final long serialVersionUID = 1L;

  private static final Logger LOG = LoggerFactory
      .getLogger(ExprNodeGenericFuncDesc.class.getName());

  /**
   * In case genericUDF is Serializable, we will serialize the object.
   *
   * In case genericUDF does not implement Serializable, Java will remember the
   * class of genericUDF and creates a new instance when deserialized. This is
   * exactly what we want.
   */
  private GenericUDF genericUDF;
  private List<ExprNodeDesc> chidren;
  private transient String funcText;
  /**
   * This class uses a writableObjectInspector rather than a TypeInfo to store
   * the canonical type information for this NodeDesc.
   */
  private transient ObjectInspector writableObjectInspector;
  //Is this an expression that should perform a comparison for sorted searches
  private boolean isSortedExpr;

  public ExprNodeGenericFuncDesc() {;
  }

  /* If the function has an explicit name like func(args) then call a
   * constructor that explicitly provides the function name in the
   * funcText argument.
   */
  public ExprNodeGenericFuncDesc(TypeInfo typeInfo, GenericUDF genericUDF,
      String funcText,
      List<ExprNodeDesc> children) {
    this(TypeInfoUtils.getStandardWritableObjectInspectorFromTypeInfo(typeInfo),
         genericUDF, funcText, children);
  }

  public ExprNodeGenericFuncDesc(ObjectInspector oi, GenericUDF genericUDF,
      String funcText,
      List<ExprNodeDesc> children) {
    super(TypeInfoUtils.getTypeInfoFromObjectInspector(oi));
    this.writableObjectInspector =
        ObjectInspectorUtils.getWritableObjectInspector(oi);
    assert (genericUDF != null);
    this.genericUDF = genericUDF;
    this.chidren = children;
    this.funcText = funcText;
  }

  // Backward-compatibility interfaces for functions without a user-visible name.
  public ExprNodeGenericFuncDesc(TypeInfo typeInfo, GenericUDF genericUDF,
      List<ExprNodeDesc> children) {
    this(typeInfo, genericUDF, null, children);
  }

  public ExprNodeGenericFuncDesc(ObjectInspector oi, GenericUDF genericUDF,
      List<ExprNodeDesc> children) {
    this(oi, genericUDF, null, children);
  }

  @Override
  public ObjectInspector getWritableObjectInspector() {
    return writableObjectInspector;
  }

  public GenericUDF getGenericUDF() {
    return genericUDF;
  }

  public void setGenericUDF(GenericUDF genericUDF) {
    this.genericUDF = genericUDF;
  }

  public void setChildren(List<ExprNodeDesc> children) {
    chidren = children;
  }

  @Override
  public List<ExprNodeDesc> getChildren() {
    return chidren;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append(genericUDF.getClass().getSimpleName());
    if (genericUDF instanceof GenericUDFBridge) {
      GenericUDFBridge genericUDFBridge = (GenericUDFBridge) genericUDF;
      sb.append(" ==> ");
      String udfName = genericUDFBridge.getUdfName();
      Class<? extends UDF> udfClass = genericUDFBridge.getUdfClass();
      sb.append(udfName != null ? udfName : (udfClass != null ? udfClass.getSimpleName() : "null"));
      sb.append(" ");
    }
    sb.append("(");
    if (chidren != null) {
      for (int i = 0; i < chidren.size(); i++) {
        if (i > 0) {
          sb.append(", ");
        }
        sb.append(chidren.get(i));
      }
    }
    sb.append(")");
    return sb.toString();
  }

  @Override
  public String getExprString() {
    // Get the children expr strings
    String[] childrenExprStrings = new String[chidren.size()];
    for (int i = 0; i < childrenExprStrings.length; i++) {
      childrenExprStrings[i] = chidren.get(i).getExprString();
    }

    return genericUDF.getDisplayString(childrenExprStrings);
  }

  @Override
  public String getExprString(boolean sortChildren) {
    if (sortChildren) {
      UDFType udfType = genericUDF.getClass().getAnnotation(UDFType.class);
      if (udfType.commutative()) {
        // Get the sorted children expr strings
        String[] childrenExprStrings = new String[chidren.size()];
        for (int i = 0; i < childrenExprStrings.length; i++) {
          childrenExprStrings[i] = chidren.get(i).getExprString();
        }
        return genericUDF.getDisplayString(
            ImmutableSortedMultiset.copyOf(childrenExprStrings).toArray(new String[childrenExprStrings.length]));
      }
    }
    return getExprString();
  }

  @Override
  public List<String> getCols() {
    List<String> colList = new ArrayList<String>();
    if (chidren != null) {
      int pos = 0;
      while (pos < chidren.size()) {
        List<String> colCh = chidren.get(pos).getCols();
        colList = Utilities.mergeUniqElems(colList, colCh);
        pos++;
      }
    }

    return colList;
  }

  @Override
  public ExprNodeDesc clone() {
    List<ExprNodeDesc> cloneCh = new ArrayList<ExprNodeDesc>(chidren.size());
    for (ExprNodeDesc ch : chidren) {
      cloneCh.add(ch.clone());
    }
    ExprNodeGenericFuncDesc clone = new ExprNodeGenericFuncDesc(typeInfo,
        FunctionRegistry.cloneGenericUDF(genericUDF), funcText, cloneCh);
    return clone;
  }

  /**
   * Create a ExprNodeGenericFuncDesc based on the genericUDFClass and the
   * children parameters. If the function has an explicit name, the
   * newInstance method should be passed the function name in the funcText
   * argument.
   *
   * @throws UDFArgumentException
   */
  public static ExprNodeGenericFuncDesc newInstance(GenericUDF genericUDF,
      String funcText,
      List<ExprNodeDesc> children) throws UDFArgumentException {
    ObjectInspector[] childrenOIs = new ObjectInspector[children.size()];
    for (int i = 0; i < childrenOIs.length; i++) {
      childrenOIs[i] = children.get(i).getWritableObjectInspector();
    }

    // Check if a bigint is implicitely cast to a double as part of a comparison
    // Perform the check here instead of in GenericUDFBaseCompare to guarantee it is only run once per operator
    if (genericUDF instanceof GenericUDFBaseCompare && children.size() == 2) {

      TypeInfo oiTypeInfo0 = children.get(0).getTypeInfo();
      TypeInfo oiTypeInfo1 = children.get(1).getTypeInfo();

      SessionState ss = SessionState.get();
      Configuration conf = (ss != null) ? ss.getConf() : new Configuration();

      LogHelper console = new LogHelper(LOG);

      // For now, if a bigint is going to be cast to a double throw an error or warning
      if ((oiTypeInfo0.equals(TypeInfoFactory.stringTypeInfo) && oiTypeInfo1.equals(TypeInfoFactory.longTypeInfo)) ||
          (oiTypeInfo0.equals(TypeInfoFactory.longTypeInfo) && oiTypeInfo1.equals(TypeInfoFactory.stringTypeInfo))) {
        String error = StrictChecks.checkTypeSafety(conf);
        if (error != null) throw new UDFArgumentException(error);
        console.printError("WARNING: Comparing a bigint and a string may result in a loss of precision.");
      } else if ((oiTypeInfo0.equals(TypeInfoFactory.doubleTypeInfo) && oiTypeInfo1.equals(TypeInfoFactory.longTypeInfo)) ||
          (oiTypeInfo0.equals(TypeInfoFactory.longTypeInfo) && oiTypeInfo1.equals(TypeInfoFactory.doubleTypeInfo))) {
        console.printError("WARNING: Comparing a bigint and a double may result in a loss of precision.");
      }
    }

    ObjectInspector oi = genericUDF.initializeAndFoldConstants(childrenOIs);

    String[] requiredJars = genericUDF.getRequiredJars();
    String[] requiredFiles = genericUDF.getRequiredFiles();
    SessionState ss = SessionState.get();

    if (requiredJars != null) {
      SessionState.ResourceType t = SessionState.find_resource_type("JAR");
      try {
        ss.add_resources(t, Arrays.asList(requiredJars));
      } catch (Exception e) {
        throw new UDFArgumentException(e);
      }
    }

    if (requiredFiles != null) {
      SessionState.ResourceType t = SessionState.find_resource_type("FILE");
      try {
        ss.add_resources(t, Arrays.asList(requiredFiles));
      } catch (Exception e) {
        throw new UDFArgumentException(e);
      }
    }

    return new ExprNodeGenericFuncDesc(oi, genericUDF, funcText, children);
  }

  /* Backward-compatibility interface for the case where there is no explicit
   * name for the function.
   */
  public static ExprNodeGenericFuncDesc newInstance(GenericUDF genericUDF,
    List<ExprNodeDesc> children) throws UDFArgumentException {
    return newInstance(genericUDF, null, children);
  }

  @Override
  public boolean isSame(Object o) {
    if (!(o instanceof ExprNodeGenericFuncDesc)) {
      return false;
    }
    ExprNodeGenericFuncDesc dest = (ExprNodeGenericFuncDesc) o;
    if (!typeInfo.equals(dest.getTypeInfo())
        || !genericUDF.getClass().equals(dest.getGenericUDF().getClass())) {
      return false;
    }

    if (genericUDF instanceof GenericUDFBridge) {
      GenericUDFBridge bridge = (GenericUDFBridge) genericUDF;
      GenericUDFBridge bridge2 = (GenericUDFBridge) dest.getGenericUDF();
      if (!bridge.getUdfClassName().equals(bridge2.getUdfClassName())
          || !bridge.getUdfName().equals(bridge2.getUdfName())
          || bridge.isOperator() != bridge2.isOperator()) {
        return false;
      }
    }

    if (genericUDF instanceof GenericUDFMacro) {
      // if getMacroName is null, we always treat it different from others.
      if (((GenericUDFMacro) genericUDF).getMacroName() == null
          || !(((GenericUDFMacro) genericUDF).getMacroName()
              .equals(((GenericUDFMacro) dest.genericUDF).getMacroName()))) {
        return false;
      }
    }

    if (chidren.size() != dest.getChildren().size()) {
      return false;
    }

    for (int pos = 0; pos < chidren.size(); pos++) {
      if (!chidren.get(pos).isSame(dest.getChildren().get(pos))) {
        return false;
      }
    }

    return true;
  }

  @Override
  public int hashCode() {
    int superHashCode = super.hashCode();
    HashCodeBuilder builder = new HashCodeBuilder();
    builder.appendSuper(superHashCode);
    builder.append(chidren);
    return builder.toHashCode();
  }

  public boolean isSortedExpr() {
    return isSortedExpr;
  }

  public void setSortedExpr(boolean isSortedExpr) {
    this.isSortedExpr = isSortedExpr;
  }

  public String getFuncText() {
    return this.funcText;
  }
}
