/*
 * 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.calcite.sql;

import org.apache.calcite.avatica.util.TimeUnit;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.TimeFrame;
import org.apache.calcite.rel.type.TimeFrameSet;
import org.apache.calcite.runtime.CalciteException;
import org.apache.calcite.runtime.Resources;
import org.apache.calcite.sql.fun.SqlLiteralChainOperator;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.SqlOperandMetadata;
import org.apache.calcite.sql.type.SqlOperandTypeChecker;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.sql.validate.SelectScope;
import org.apache.calcite.sql.validate.SqlMonotonicity;
import org.apache.calcite.sql.validate.SqlNameMatcher;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorException;
import org.apache.calcite.sql.validate.SqlValidatorNamespace;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.util.ImmutableNullableList;
import org.apache.calcite.util.NlsString;
import org.apache.calcite.util.Pair;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import org.checkerframework.checker.nullness.qual.Nullable;

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

import static org.apache.calcite.util.Static.RESOURCE;

import static java.util.Objects.requireNonNull;

/**
 * <code>SqlCallBinding</code> implements {@link SqlOperatorBinding} by
 * analyzing to the operands of a {@link SqlCall} with a {@link SqlValidator}.
 */
public class SqlCallBinding extends SqlOperatorBinding {

  /** Static nested class required due to
   * <a href="https://issues.apache.org/jira/browse/CALCITE-4393">[CALCITE-4393]
   * ExceptionInInitializerError due to NPE in SqlCallBinding caused by circular dependency</a>.
   * The static field inside it cannot be part of the outer class: it must be defined
   * within a nested class in order to break the cycle during class loading. */
  private static class DefaultCallHolder {
    private static final SqlCall DEFAULT_CALL =
        SqlStdOperatorTable.DEFAULT.createCall(SqlParserPos.ZERO);
  }

  //~ Instance fields --------------------------------------------------------

  private final SqlValidator validator;
  private final SqlValidatorScope scope;
  private final SqlCall call;

  //~ Constructors -----------------------------------------------------------

  /**
   * Creates a call binding.
   *
   * @param validator Validator
   * @param scope     Scope of call
   * @param call      Call node
   */
  public SqlCallBinding(SqlValidator validator, SqlValidatorScope scope,
      SqlCall call) {
    super(validator.getTypeFactory(), call.getOperator());
    this.validator = validator;
    this.scope = requireNonNull(scope, "scope");
    this.call = call;
  }

  //~ Methods ----------------------------------------------------------------

  @Override public int getGroupCount() {
    final SelectScope selectScope =
        SqlValidatorUtil.getEnclosingSelectScope(scope);
    if (selectScope == null) {
      // Probably "VALUES expr". Treat same as "SELECT expr GROUP BY ()"
      return 0;
    }
    final SqlSelect select = selectScope.getNode();
    final SqlNodeList group = select.getGroup();
    if (group != null) {
      int n = 0;
      for (SqlNode groupItem : group) {
        if (!(groupItem instanceof SqlNodeList)
            || ((SqlNodeList) groupItem).size() != 0) {
          ++n;
        }
      }
      return n;
    }
    return validator.isAggregate(select) ? 0 : -1;
  }

  /**
   * Returns the validator.
   */
  public SqlValidator getValidator() {
    return validator;
  }

  /**
   * Returns the scope of the call.
   */
  public SqlValidatorScope getScope() {
    return scope;
  }

  /**
   * Returns the call node.
   */
  public SqlCall getCall() {
    return call;
  }

  /** Returns the operands to a call permuted into the same order as the
   * formal parameters of the function. */
  public List<SqlNode> operands() {
    if (hasAssignment()
        && !(call.getOperator() instanceof SqlUnresolvedFunction)) {
      return permutedOperands(call);
    } else {
      final List<SqlNode> operandList = call.getOperandList();
      final SqlOperandTypeChecker checker =
          call.getOperator().getOperandTypeChecker();
      if (checker == null) {
        return operandList;
      }
      final SqlOperandCountRange range = checker.getOperandCountRange();
      final List<SqlNode> list = Lists.newArrayList(operandList);
      while (list.size() < range.getMax()
          && checker.isOptional(list.size())
          && checker.isFixedParameters()) {
        list.add(DefaultCallHolder.DEFAULT_CALL);
      }
      return list;
    }
  }

  /** Returns whether arguments have name assignment. */
  private boolean hasAssignment() {
    for (SqlNode operand : call.getOperandList()) {
      if (operand != null
          && operand.getKind() == SqlKind.ARGUMENT_ASSIGNMENT) {
        return true;
      }
    }
    return false;
  }

  /** Returns the operands to a call permuted into the same order as the
   * formal parameters of the function. */
  private List<SqlNode> permutedOperands(final SqlCall call) {
    final SqlOperator operator = call.getOperator();
    final SqlOperandMetadata operandMetadata =
        requireNonNull((SqlOperandMetadata) operator.getOperandTypeChecker(),
            () -> "operandTypeChecker is null for " + call
                + ", operator " + operator);
    final List<String> paramNames = operandMetadata.paramNames();
    final List<SqlNode> permuted = new ArrayList<>();
    final SqlNameMatcher nameMatcher =
        validator.getCatalogReader().nameMatcher();
    for (final String paramName : paramNames) {
      Pair<String, SqlIdentifier> args = null;
      for (int j = 0; j < call.getOperandList().size(); j++) {
        final SqlCall call2 = call.operand(j);
        assert call2.getKind() == SqlKind.ARGUMENT_ASSIGNMENT;
        final SqlIdentifier operandID = call2.operand(1);
        final String operandName = operandID.getSimple();
        if (nameMatcher.matches(operandName, paramName)) {
          permuted.add(call2.operand(0));
          break;
        } else if (args == null
            && nameMatcher.isCaseSensitive()
            && operandName.equalsIgnoreCase(paramName)) {
          args = Pair.of(paramName, operandID);
        }
        // the last operand, there is still no match.
        if (j == call.getOperandList().size() - 1) {
          if (args != null) {
            throw SqlUtil.newContextException(args.right.getParserPosition(),
                RESOURCE.paramNotFoundInFunctionDidYouMean(args.right.getSimple(),
                    operator.getName(), args.left));
          }
          if (operandMetadata.isFixedParameters()) {
            // Not like user defined functions, we do not patch up the operands
            // with DEFAULT and then convert to nulls during sql-to-rel conversion.
            // Thus, there is no need to show the optional operands in the plan and
            // decide if the optional operand is null when code generation.
            permuted.add(DefaultCallHolder.DEFAULT_CALL);
          }
        }
      }
    }
    return permuted;
  }

  /**
   * Returns a particular operand.
   */
  public SqlNode operand(int i) {
    return operands().get(i);
  }

  /** Returns a call that is equivalent except that arguments have been
   * permuted into the logical order. Any arguments whose default value is being
   * used are null. */
  public SqlCall permutedCall() {
    final List<SqlNode> operandList = operands();
    if (operandList.equals(call.getOperandList())) {
      return call;
    }
    return call.getOperator().createCall(call.pos, operandList);
  }

  @Override public SqlMonotonicity getOperandMonotonicity(int ordinal) {
    return call.getOperandList().get(ordinal).getMonotonicity(scope);
  }

  @SuppressWarnings("deprecation")
  @Override public @Nullable String getStringLiteralOperand(int ordinal) {
    SqlNode node = call.operand(ordinal);
    final Object o = SqlLiteral.value(node);
    return o instanceof NlsString ? ((NlsString) o).getValue() : null;
  }

  @SuppressWarnings("deprecation")
  @Override public int getIntLiteralOperand(int ordinal) {
    SqlNode node = call.operand(ordinal);
    final Object o = SqlLiteral.value(node);
    if (o instanceof BigDecimal) {
      BigDecimal bd = (BigDecimal) o;
      try {
        return bd.intValueExact();
      } catch (ArithmeticException e) {
        throw SqlUtil.newContextException(node.pos,
            RESOURCE.numberLiteralOutOfRange(bd.toString()));
      }
    }
    throw new AssertionError();
  }

  @Override public <T extends Object> @Nullable T getOperandLiteralValue(int ordinal,
      Class<T> clazz) {
    final SqlNode node = operand(ordinal);
    return valueAs(node, clazz);
  }

  private <T extends Object> @Nullable T valueAs(SqlNode node, Class<T> clazz) {
    final SqlLiteral literal;
    switch (node.getKind()) {
    case ARRAY_VALUE_CONSTRUCTOR:
      final List<@Nullable Object> list = new ArrayList<>();
      for (SqlNode o : ((SqlCall) node).getOperandList()) {
        list.add(valueAs(o, Object.class));
      }
      return clazz.cast(ImmutableNullableList.copyOf(list));

    case MAP_VALUE_CONSTRUCTOR:
      final ImmutableMap.Builder<Object, Object> builder2 =
          ImmutableMap.builder();
      final List<SqlNode> operands = ((SqlCall) node).getOperandList();
      for (int i = 0; i < operands.size(); i += 2) {
        final SqlNode key = operands.get(i);
        final SqlNode value = operands.get(i + 1);
        builder2.put(requireNonNull(valueAs(key, Object.class), "key"),
            requireNonNull(valueAs(value, Object.class), "value"));
      }
      return clazz.cast(builder2.build());

    case CAST:
      return valueAs(((SqlCall) node).operand(0), clazz);

    case LITERAL:
      literal = (SqlLiteral) node;
      if (literal.getTypeName() == SqlTypeName.NULL) {
        return null;
      }
      return literal.getValueAs(clazz);

    case LITERAL_CHAIN:
      literal = SqlLiteralChainOperator.concatenateOperands((SqlCall) node);
      return literal.getValueAs(clazz);

    case INTERVAL_QUALIFIER:
      final SqlIntervalQualifier q = (SqlIntervalQualifier) node;
      if (q.timeFrameName != null) {
        // Custom time frames can only be cast to String. You can do more with
        // them when validator has resolved to a TimeFrame.
        final TimeFrameSet timeFrameSet = validator.getTimeFrameSet();
        final TimeFrame timeFrame = timeFrameSet.getOpt(q.timeFrameName);
        if (clazz == String.class) {
          return clazz.cast(q.timeFrameName);
        }
        if (clazz == TimeUnit.class
            && timeFrame != null) {
          TimeUnit timeUnit = timeFrameSet.getUnit(timeFrame);
          return clazz.cast(timeUnit);
        }
        return null;
      }
      final SqlIntervalLiteral.IntervalValue intervalValue =
          new SqlIntervalLiteral.IntervalValue(q, 1, q.toString());
      literal = new SqlLiteral(intervalValue, q.typeName(), q.pos);
      return literal.getValueAs(clazz);

    case DEFAULT:
      return null; // currently NULL is the only default value

    default:
      if (SqlUtil.isNullLiteral(node, true)) {
        return null; // NULL literal
      }
      return null; // not a literal
    }
  }

  @Override public boolean isOperandNull(int ordinal, boolean allowCast) {
    return SqlUtil.isNullLiteral(operand(ordinal), allowCast);
  }

  @Override public boolean isOperandLiteral(int ordinal, boolean allowCast) {
    return SqlUtil.isLiteral(operand(ordinal), allowCast);
  }

  @Override public int getOperandCount() {
    return call.getOperandList().size();
  }

  @Override public RelDataType getOperandType(int ordinal) {
    final SqlNode operand = call.operand(ordinal);
    final RelDataType type = SqlTypeUtil.deriveType(this, operand);
    final SqlValidatorNamespace namespace = validator.getNamespace(operand);
    if (namespace != null) {
      return namespace.getType();
    }
    return type;
  }

  @Override public @Nullable RelDataType getCursorOperand(int ordinal) {
    final SqlNode operand = call.operand(ordinal);
    if (!SqlUtil.isCallTo(operand, SqlStdOperatorTable.CURSOR)) {
      return null;
    }
    final SqlCall cursorCall = (SqlCall) operand;
    final SqlNode query = cursorCall.operand(0);
    return SqlTypeUtil.deriveType(this, query);
  }

  @Override public @Nullable String getColumnListParamInfo(
      int ordinal,
      String paramName,
      List<String> columnList) {
    final SqlNode operand = call.operand(ordinal);
    if (!SqlUtil.isCallTo(operand, SqlStdOperatorTable.ROW)) {
      return null;
    }
    columnList.addAll(
        SqlIdentifier.simpleNames(((SqlCall) operand).getOperandList()));
    return validator.getParentCursor(paramName);
  }

  @Override public CalciteException newError(
      Resources.ExInst<SqlValidatorException> e) {
    return validator.newValidationError(call, e);
  }

  /**
   * Constructs a new validation signature error for the call.
   *
   * @return signature exception
   */
  public CalciteException newValidationSignatureError() {
    return validator.newValidationError(call,
        RESOURCE.canNotApplyOp2Type(getOperator().getName(),
            call.getCallSignature(validator, scope),
            getOperator().getAllowedSignatures()));
  }

  /**
   * Constructs a new validation error for the call. (Do not use this to
   * construct a validation error for other nodes such as an operands.)
   *
   * @param ex underlying exception
   * @return wrapped exception
   */
  public CalciteException newValidationError(
      Resources.ExInst<SqlValidatorException> ex) {
    return validator.newValidationError(call, ex);
  }

  /**
   * Returns whether to allow implicit type coercion when validation.
   * This is a short-cut method.
   */
  public boolean isTypeCoercionEnabled() {
    return validator.config().typeCoercionEnabled();
  }
}
