/*
 * 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.rel.core;

import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelCollations;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.sql.SqlAggFunction;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.util.Optionality;
import org.apache.calcite.util.mapping.Mapping;
import org.apache.calcite.util.mapping.Mappings;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;

import java.util.List;
import java.util.Objects;

/**
 * Call to an aggregate function within an
 * {@link org.apache.calcite.rel.core.Aggregate}.
 */
public class AggregateCall {
  //~ Instance fields --------------------------------------------------------

  private final SqlAggFunction aggFunction;

  private final boolean distinct;
  private final boolean approximate;
  private final boolean ignoreNulls;
  public final RelDataType type;
  public final String name;

  // We considered using ImmutableIntList but we would not save much memory:
  // since all values are small, ImmutableList uses cached Integer values.
  private final ImmutableList<Integer> argList;
  public final int filterArg;
  public final RelCollation collation;

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

  /**
   * Creates an AggregateCall.
   *
   * @param aggFunction Aggregate function
   * @param distinct    Whether distinct
   * @param argList     List of ordinals of arguments
   * @param type        Result type
   * @param name        Name (may be null)
   */
  @Deprecated // to be removed before 2.0
  public AggregateCall(
      SqlAggFunction aggFunction,
      boolean distinct,
      List<Integer> argList,
      RelDataType type,
      String name) {
    this(aggFunction, distinct, false, false,
        argList, -1, RelCollations.EMPTY, type, name);
  }

  /**
   * Creates an AggregateCall.
   *
   * @param aggFunction Aggregate function
   * @param distinct    Whether distinct
   * @param approximate Whether approximate
   * @param argList     List of ordinals of arguments
   * @param filterArg   Ordinal of filter argument (the
   *                    {@code FILTER (WHERE ...)} clause in SQL), or -1
   * @param collation   How to sort values before aggregation (the
   *                    {@code WITHIN GROUP} clause in SQL)
   * @param type        Result type
   * @param name        Name (may be null)
   */
  private AggregateCall(SqlAggFunction aggFunction, boolean distinct,
      boolean approximate, boolean ignoreNulls, List<Integer> argList,
      int filterArg, RelCollation collation, RelDataType type, String name) {
    this.type = Objects.requireNonNull(type);
    this.name = name;
    this.aggFunction = Objects.requireNonNull(aggFunction);
    this.argList = ImmutableList.copyOf(argList);
    this.filterArg = filterArg;
    this.collation = Objects.requireNonNull(collation);
    this.distinct = distinct;
    this.approximate = approximate;
    this.ignoreNulls = ignoreNulls;
    Preconditions.checkArgument(
        aggFunction.getDistinctOptionality() != Optionality.IGNORED || !distinct,
        "DISTINCT has no effect for this aggregate function, so must be false");
    Preconditions.checkArgument(filterArg < 0 || aggFunction.allowsFilter());
  }

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

  @Deprecated // to be removed before 2.0
  public static AggregateCall create(SqlAggFunction aggFunction,
      boolean distinct, List<Integer> argList, int groupCount, RelNode input,
      RelDataType type, String name) {
    return create(aggFunction, distinct, false, false, argList, -1,
        RelCollations.EMPTY, groupCount, input, type, name);
  }

  @Deprecated // to be removed before 2.0
  public static AggregateCall create(SqlAggFunction aggFunction,
      boolean distinct, List<Integer> argList, int filterArg, int groupCount,
      RelNode input, RelDataType type, String name) {
    return create(aggFunction, distinct, false, false, argList, filterArg,
        RelCollations.EMPTY, groupCount, input, type, name);
  }

  @Deprecated // to be removed before 2.0
  public static AggregateCall create(SqlAggFunction aggFunction,
      boolean distinct, boolean approximate, List<Integer> argList,
      int filterArg, int groupCount,
      RelNode input, RelDataType type, String name) {
    return create(aggFunction, distinct, approximate, false, argList,
        filterArg, RelCollations.EMPTY, groupCount, input, type, name);
  }

  @Deprecated // to be removed before 2.0
  public static AggregateCall create(SqlAggFunction aggFunction,
      boolean distinct, boolean approximate, List<Integer> argList,
      int filterArg, RelCollation collation, int groupCount,
      RelNode input, RelDataType type, String name) {
    return create(aggFunction, distinct, approximate, false, argList, filterArg,
        collation, groupCount, input, type, name);
  }

  /** Creates an AggregateCall, inferring its type if {@code type} is null. */
  public static AggregateCall create(SqlAggFunction aggFunction,
      boolean distinct, boolean approximate, boolean ignoreNulls,
      List<Integer> argList, int filterArg, RelCollation collation,
      int groupCount,
      RelNode input, RelDataType type, String name) {
    if (type == null) {
      final RelDataTypeFactory typeFactory =
          input.getCluster().getTypeFactory();
      final List<RelDataType> types =
          SqlTypeUtil.projectTypes(input.getRowType(), argList);
      final Aggregate.AggCallBinding callBinding =
          new Aggregate.AggCallBinding(typeFactory, aggFunction, types,
              groupCount, filterArg >= 0);
      type = aggFunction.inferReturnType(callBinding);
    }
    return create(aggFunction, distinct, approximate, ignoreNulls, argList,
        filterArg, collation, type, name);
  }

  @Deprecated // to be removed before 2.0
  public static AggregateCall create(SqlAggFunction aggFunction,
      boolean distinct, List<Integer> argList, int filterArg, RelDataType type,
      String name) {
    return create(aggFunction, distinct, false, false, argList, filterArg,
        RelCollations.EMPTY, type, name);
  }

  @Deprecated // to be removed before 2.0
  public static AggregateCall create(SqlAggFunction aggFunction,
      boolean distinct, boolean approximate, List<Integer> argList,
      int filterArg, RelDataType type, String name) {
    return create(aggFunction, distinct, approximate, false, argList, filterArg,
        RelCollations.EMPTY, type, name);
  }

  @Deprecated // to be removed before 2.0
  public static AggregateCall create(SqlAggFunction aggFunction,
      boolean distinct, boolean approximate, List<Integer> argList,
      int filterArg, RelCollation collation, RelDataType type, String name) {
    return create(aggFunction, distinct, approximate, false, argList, filterArg,
        collation, type, name);
  }

  /** Creates an AggregateCall. */
  public static AggregateCall create(SqlAggFunction aggFunction,
      boolean distinct, boolean approximate, boolean ignoreNulls,
      List<Integer> argList, int filterArg, RelCollation collation,
      RelDataType type, String name) {
    final boolean distinct2 = distinct
        && (aggFunction.getDistinctOptionality() != Optionality.IGNORED);
    return new AggregateCall(aggFunction, distinct2, approximate, ignoreNulls,
        argList, filterArg, collation, type, name);
  }

  /**
   * Returns whether this AggregateCall is distinct, as in <code>
   * COUNT(DISTINCT empno)</code>.
   *
   * @return whether distinct
   */
  public final boolean isDistinct() {
    return distinct;
  }

  /**
   * Returns whether this AggregateCall is approximate, as in <code>
   * APPROX_COUNT_DISTINCT(empno)</code>.
   *
   * @return whether approximate
   */
  public final boolean isApproximate() {
    return approximate;
  }

  /**
   * Returns whether this AggregateCall ignores nulls.
   *
   * @return whether ignore nulls
   */
  public final boolean ignoreNulls() {
    return ignoreNulls;
  }

  /**
   * Returns the aggregate function.
   *
   * @return aggregate function
   */
  public final SqlAggFunction getAggregation() {
    return aggFunction;
  }

  /**
   * Returns the aggregate ordering definition (the {@code WITHIN GROUP} clause
   * in SQL), or the empty list if not specified.
   *
   * @return ordering definition
   */
  public RelCollation getCollation() {
    return collation;
  }

  /**
   * Returns the ordinals of the arguments to this call.
   *
   * <p>The list is immutable.
   *
   * @return list of argument ordinals
   */
  public final List<Integer> getArgList() {
    return argList;
  }

  /**
   * Returns the result type.
   *
   * @return result type
   */
  public final RelDataType getType() {
    return type;
  }

  /**
   * Returns the name.
   *
   * @return name
   */
  public String getName() {
    return name;
  }

  /**
   * Creates an equivalent AggregateCall that has a new name.
   *
   * @param name New name (may be null)
   */
  public AggregateCall rename(String name) {
    if (Objects.equals(this.name, name)) {
      return this;
    }
    return new AggregateCall(aggFunction, distinct, approximate, ignoreNulls,
        argList,
        filterArg, RelCollations.EMPTY, type,
        name);
  }

  public String toString() {
    StringBuilder buf = new StringBuilder(aggFunction.toString());
    buf.append("(");
    if (approximate) {
      buf.append("APPROXIMATE ");
    }
    if (distinct) {
      buf.append((argList.size() == 0) ? "DISTINCT" : "DISTINCT ");
    }
    int i = -1;
    for (Integer arg : argList) {
      if (++i > 0) {
        buf.append(", ");
      }
      buf.append("$");
      buf.append(arg);
    }
    buf.append(")");
    if (!collation.equals(RelCollations.EMPTY)) {
      buf.append(" WITHIN GROUP (");
      buf.append(collation);
      buf.append(")");
    }
    if (hasFilter()) {
      buf.append(" FILTER $");
      buf.append(filterArg);
    }
    return buf.toString();
  }

  /** Returns whether this AggregateCall has a filter argument. */
  public boolean hasFilter() {
    return filterArg >= 0;
  }

  @Override public boolean equals(Object o) {
    if (!(o instanceof AggregateCall)) {
      return false;
    }
    AggregateCall other = (AggregateCall) o;
    return aggFunction.equals(other.aggFunction)
        && (distinct == other.distinct)
        && (approximate == other.approximate)
        && (ignoreNulls == other.ignoreNulls)
        && argList.equals(other.argList)
        && filterArg == other.filterArg
        && Objects.equals(collation, other.collation);
  }

  @Override public int hashCode() {
    return Objects.hash(aggFunction,
            distinct,
            approximate,
            ignoreNulls,
            argList,
            filterArg,
            collation);
  }

  /**
   * Creates a binding of this call in the context of an
   * {@link org.apache.calcite.rel.logical.LogicalAggregate},
   * which can then be used to infer the return type.
   */
  public Aggregate.AggCallBinding createBinding(
      Aggregate aggregateRelBase) {
    final RelDataType rowType = aggregateRelBase.getInput().getRowType();

    return new Aggregate.AggCallBinding(
        aggregateRelBase.getCluster().getTypeFactory(), aggFunction,
        SqlTypeUtil.projectTypes(rowType, argList),
        aggregateRelBase.getGroupCount(), hasFilter());
  }

  /**
   * Creates an equivalent AggregateCall with new argument ordinals.
   *
   * @see #transform(Mappings.TargetMapping)
   *
   * @param args Arguments
   * @return AggregateCall that suits new inputs and GROUP BY columns
   */
  public AggregateCall copy(List<Integer> args, int filterArg,
      RelCollation collation) {
    return new AggregateCall(aggFunction, distinct, approximate, ignoreNulls,
        args, filterArg, collation, type, name);
  }

  @Deprecated // to be removed before 2.0
  public AggregateCall copy(List<Integer> args, int filterArg) {
    // ignoring collation is error-prone
    return copy(args, filterArg, collation);
  }

  @Deprecated // to be removed before 2.0
  public AggregateCall copy(List<Integer> args) {
    // ignoring filterArg and collation is error-prone
    return copy(args, filterArg, collation);
  }

  /**
   * Creates an equivalent AggregateCall that is adapted to a new input types
   * and/or number of columns in GROUP BY.
   *
   * @param input            Relation that will be input of Aggregate
   * @param argList          Argument indices of the new call in the input
   * @param filterArg        Index of the filter, or -1
   * @param oldGroupKeyCount number of columns in GROUP BY of old aggregate
   * @param newGroupKeyCount number of columns in GROUP BY of new aggregate
   * @return AggregateCall that suits new inputs and GROUP BY columns
   */
  public AggregateCall adaptTo(RelNode input, List<Integer> argList,
      int filterArg, int oldGroupKeyCount, int newGroupKeyCount) {
    // The return type of aggregate call need to be recomputed.
    // Since it might depend on the number of columns in GROUP BY.
    final RelDataType newType =
        oldGroupKeyCount == newGroupKeyCount
            && argList.equals(this.argList)
            && filterArg == this.filterArg
            ? type
            : null;
    return create(aggFunction, distinct, approximate, ignoreNulls, argList,
        filterArg, collation, newGroupKeyCount, input, newType, getName());
  }

  /** Creates a copy of this aggregate call, applying a mapping to its
   * arguments. */
  public AggregateCall transform(Mappings.TargetMapping mapping) {
    return copy(Mappings.apply2((Mapping) mapping, argList),
        hasFilter() ? Mappings.apply(mapping, filterArg) : -1,
        RelCollations.permute(collation, mapping));
  }
}