/*
 * 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.phoenix.parse;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.phoenix.compile.ColumnResolver;
import org.apache.phoenix.expression.function.CountAggregateFunction;
import org.apache.phoenix.jdbc.PhoenixStatement.Operation;
import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunction;
import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunctionInfo;

/**
 * Top level node representing a SQL statement
 * @since 0.1
 */
public class SelectStatement implements FilterableStatement {
  public static final SelectStatement SELECT_STAR = new SelectStatement(null, null, false,
    Arrays.asList(), null, Collections.<ParseNode> emptyList(), null,
    Collections.<OrderByNode> emptyList(), null, null, 0, false, false,
    Collections.<SelectStatement> emptyList(), new HashMap<String, UDFParseNode>(1));
  public static final SelectStatement SELECT_ONE = new SelectStatement(null, null, false,
    Collections.<AliasedNode> singletonList(new AliasedNode(null, LiteralParseNode.ONE)), null,
    Collections.<ParseNode> emptyList(), null, Collections.<OrderByNode> emptyList(), null, null, 0,
    false, false, Collections.<SelectStatement> emptyList(), new HashMap<String, UDFParseNode>(1));
  public static final SelectStatement COUNT_ONE = new SelectStatement(null, null, false,
    Collections.<AliasedNode> singletonList(new AliasedNode(null,
      new AggregateFunctionParseNode(CountAggregateFunction.NORMALIZED_NAME, LiteralParseNode.STAR,
        new BuiltInFunctionInfo(CountAggregateFunction.class,
          CountAggregateFunction.class.getAnnotation(BuiltInFunction.class))))),
    null, Collections.<ParseNode> emptyList(), null, Collections.<OrderByNode> emptyList(), null,
    null, 0, true, false, Collections.<SelectStatement> emptyList(),
    new HashMap<String, UDFParseNode>(1));

  public static SelectStatement create(SelectStatement select, HintNode hint) {
    if (select.getHint() == hint || hint.isEmpty()) {
      return select;
    }
    return new SelectStatement(select.getFrom(), hint, select.isDistinct(), select.getSelect(),
      select.getWhere(), select.getGroupBy(), select.getHaving(), select.getOrderBy(),
      select.getLimit(), select.getOffset(), select.getBindCount(), select.isAggregate(),
      select.hasSequence(), select.getSelects(), select.getUdfParseNodes());
  }

  public static SelectStatement create(SelectStatement select, TableNode tableNode,
    List<AliasedNode> selects) {
    return new SelectStatement(tableNode, select.getHint(), select.isDistinct(), selects,
      select.getWhere(), select.getGroupBy(), select.getHaving(), select.getOrderBy(),
      select.getLimit(), select.getOffset(), select.getBindCount(), select.isAggregate(),
      select.hasSequence(), select.getSelects(), select.getUdfParseNodes());
  }

  public SelectStatement combine(ParseNode where) {
    if (where == null) {
      return this;
    }
    if (this.getWhere() != null) {
      where = new AndParseNode(Arrays.asList(this.getWhere(), where));
    }
    return new SelectStatement(this.getFrom(), this.getHint(), this.isDistinct(), this.getSelect(),
      where, this.getGroupBy(), this.getHaving(), this.getOrderBy(), this.getLimit(),
      this.getOffset(), this.getBindCount(), this.isAggregate(), this.hasSequence(), this.selects,
      this.udfParseNodes);
  }

  public static SelectStatement create(SelectStatement select, List<AliasedNode> selects) {
    return new SelectStatement(select.getFrom(), select.getHint(), select.isDistinct(), selects,
      select.getWhere(), select.getGroupBy(), select.getHaving(), select.getOrderBy(),
      select.getLimit(), select.getOffset(), select.getBindCount(), select.isAggregate(),
      select.hasSequence(), select.getSelects(), select.getUdfParseNodes());
  }

  // Copy constructor for sub select statements in a union
  public static SelectStatement create(SelectStatement select, List<OrderByNode> orderBy,
    LimitNode limit, OffsetNode offset, boolean isAggregate) {
    return new SelectStatement(select.getFrom(), select.getHint(), select.isDistinct(),
      select.getSelect(), select.getWhere(), select.getGroupBy(), select.getHaving(), orderBy,
      limit, offset, select.getBindCount(), isAggregate, select.hasSequence(), select.getSelects(),
      select.getUdfParseNodes());
  }

  private final TableNode fromTable;
  private final HintNode hint;
  private final boolean isDistinct;
  private final List<AliasedNode> select;
  private final ParseNode where;
  private final List<ParseNode> groupBy;
  private final ParseNode having;
  private final List<OrderByNode> orderBy;
  private final LimitNode limit;
  private final int bindCount;
  private final boolean isAggregate;
  private final boolean hasSequence;
  private final boolean hasWildcard;
  private final List<SelectStatement> selects = new ArrayList<SelectStatement>();
  private final Map<String, UDFParseNode> udfParseNodes;
  private final OffsetNode offset;

  @Override
  public final String toString() {
    StringBuilder buf = new StringBuilder();
    toSQL(null, buf);
    return buf.toString();
  }

  public void toSQL(ColumnResolver resolver, StringBuilder buf) {
    buf.append("SELECT ");
    if (hint != null) buf.append(hint);
    if (isDistinct) buf.append("DISTINCT ");
    for (AliasedNode selectNode : select) {
      selectNode.toSQL(resolver, buf);
      buf.append(',');
    }
    buf.setLength(buf.length() - 1);
    if (fromTable != null) {
      buf.append(" FROM ");
      fromTable.toSQL(resolver, buf);
    }
    if (where != null) {
      buf.append(" WHERE ");
      where.toSQL(resolver, buf);
    }
    if (!groupBy.isEmpty()) {
      buf.append(" GROUP BY ");
      for (ParseNode node : groupBy) {
        node.toSQL(resolver, buf);
        buf.append(',');
      }
      buf.setLength(buf.length() - 1);
    }
    if (having != null) {
      buf.append(" HAVING ");
      having.toSQL(resolver, buf);
    }
    if (!orderBy.isEmpty()) {
      buf.append(" ORDER BY ");
      for (OrderByNode node : orderBy) {
        node.toSQL(resolver, buf);
        buf.append(',');
      }
      buf.setLength(buf.length() - 1);
    }
    if (limit != null) {
      buf.append(" LIMIT " + limit.toString());
    }
    if (offset != null) {
      buf.append(" OFFSET " + offset.toString());
    }
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((fromTable == null) ? 0 : fromTable.hashCode());
    result = prime * result + ((groupBy == null) ? 0 : groupBy.hashCode());
    result = prime * result + ((having == null) ? 0 : having.hashCode());
    result = prime * result + ((hint == null) ? 0 : hint.hashCode());
    result = prime * result + (isDistinct ? 1231 : 1237);
    result = prime * result + ((limit == null) ? 0 : limit.hashCode());
    result = prime * result + ((orderBy == null) ? 0 : orderBy.hashCode());
    result = prime * result + ((select == null) ? 0 : select.hashCode());
    result = prime * result + ((where == null) ? 0 : where.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    SelectStatement other = (SelectStatement) obj;
    if (fromTable == null) {
      if (other.fromTable != null) return false;
    } else if (!fromTable.equals(other.fromTable)) return false;
    if (groupBy == null) {
      if (other.groupBy != null) return false;
    } else if (!groupBy.equals(other.groupBy)) return false;
    if (having == null) {
      if (other.having != null) return false;
    } else if (!having.equals(other.having)) return false;
    if (hint == null) {
      if (other.hint != null) return false;
    } else if (!hint.equals(other.hint)) return false;
    if (isDistinct != other.isDistinct) return false;
    if (limit == null) {
      if (other.limit != null) return false;
    } else if (!limit.equals(other.limit)) return false;
    if (orderBy == null) {
      if (other.orderBy != null) return false;
    } else if (!orderBy.equals(other.orderBy)) return false;
    if (select == null) {
      if (other.select != null) return false;
    } else if (!select.equals(other.select)) return false;
    if (where == null) {
      if (other.where != null) return false;
    } else if (!where.equals(other.where)) return false;
    return true;
  }

  // Count constant expressions
  private static int countConstants(List<ParseNode> nodes) {
    int count = 0;
    for (int i = 0; i < nodes.size(); i++) {
      if (nodes.get(i).isStateless()) {
        count++;
      }
    }
    return count;
  }

  protected SelectStatement(TableNode from, HintNode hint, boolean isDistinct,
    List<AliasedNode> select, ParseNode where, List<ParseNode> groupBy, ParseNode having,
    List<OrderByNode> orderBy, LimitNode limit, OffsetNode offset, int bindCount,
    boolean isAggregate, boolean hasSequence, List<SelectStatement> selects,
    Map<String, UDFParseNode> udfParseNodes) {
    this.fromTable = from;
    this.hint = hint == null ? HintNode.EMPTY_HINT_NODE : hint;
    this.isDistinct = isDistinct;
    this.select = Collections.unmodifiableList(select);
    this.where = where;
    this.groupBy = Collections.unmodifiableList(groupBy);
    this.having = having;
    this.orderBy = Collections.unmodifiableList(orderBy);
    this.limit = limit;
    this.offset = offset;
    this.bindCount = bindCount;
    this.isAggregate =
      isAggregate || groupBy.size() != countConstants(groupBy) || this.having != null;
    this.hasSequence = hasSequence;
    boolean hasWildcard = false;
    for (AliasedNode aliasedNode : select) {
      ParseNode node = aliasedNode.getNode();
      if (
        node instanceof WildcardParseNode || node instanceof TableWildcardParseNode
          || node instanceof FamilyWildcardParseNode
      ) {
        hasWildcard = true;
        break;
      }
    }
    this.hasWildcard = hasWildcard;
    if (!selects.isEmpty()) {
      this.selects.addAll(selects);
    }
    this.udfParseNodes = udfParseNodes;
  }

  @Override
  public boolean isDistinct() {
    return isDistinct;
  }

  @Override
  public LimitNode getLimit() {
    return limit;
  }

  /**
   * This method should not be called during the early stage of the plan preparation phase since
   * fromTable might not be ConcreteTableNode at that time(e.g., JoinTableNode). By the time
   * getTableSamplingRate method is called, each select statements should have exactly one
   * ConcreteTableNode, with its corresponding sampling rate associate with it.
   */
  @Override
  public Double getTableSamplingRate() {
    if (fromTable == null || !(fromTable instanceof ConcreteTableNode)) return null;
    return ((ConcreteTableNode) fromTable).getTableSamplingRate();
  }

  @Override
  public int getBindCount() {
    return bindCount;
  }

  public TableNode getFrom() {
    return fromTable;
  }

  @Override
  public HintNode getHint() {
    return hint;
  }

  public List<AliasedNode> getSelect() {
    return select;
  }

  /**
   * Gets the where condition, or null if none.
   */
  @Override
  public ParseNode getWhere() {
    return where;
  }

  /**
   * Gets the group-by, containing at least 1 element, or empty list, if none.
   */
  public List<ParseNode> getGroupBy() {
    return groupBy;
  }

  public ParseNode getHaving() {
    return having;
  }

  /**
   * Gets the order-by, containing at least 1 element, or null, if none.
   */
  @Override
  public List<OrderByNode> getOrderBy() {
    return orderBy;
  }

  @Override
  public boolean isAggregate() {
    return isAggregate;
  }

  public boolean hasSequence() {
    return hasSequence;
  }

  @Override
  public Operation getOperation() {
    return Operation.QUERY;
  }

  public boolean isJoin() {
    return fromTable != null && fromTable instanceof JoinTableNode;
  }

  public SelectStatement getInnerSelectStatement() {
    if (fromTable == null || !(fromTable instanceof DerivedTableNode)) return null;

    return ((DerivedTableNode) fromTable).getSelect();
  }

  public boolean isUnion() {
    return !getSelects().isEmpty();
  }

  public List<SelectStatement> getSelects() {
    return selects;
  }

  public boolean hasWildcard() {
    return hasWildcard;
  }

  public Map<String, UDFParseNode> getUdfParseNodes() {
    return udfParseNodes;
  }

  @Override
  public OffsetNode getOffset() {
    return offset;
  }

  public boolean haveGroupBy() {
    return this.getGroupBy() != null && this.getGroupBy().size() > 0 || !this.isAggregate()
      && this.isDistinct() && this.getSelect() != null && this.getSelect().size() > 0;
  }

  public boolean haveOrderBy() {
    return this.getOrderBy() != null && this.getOrderBy().size() > 0;
  }

}
