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

import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlWindow;

import com.google.common.collect.ImmutableList;

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

import java.util.Objects;

/**
 * Helpers for {@link RexWindowBound}.
 */
public final class RexWindowBounds {
  /** UNBOUNDED PRECEDING. */
  public static final RexWindowBound UNBOUNDED_PRECEDING =
      new RexUnboundedWindowBound(true);

  /** UNBOUNDED FOLLOWING. */
  public static final RexWindowBound UNBOUNDED_FOLLOWING =
      new RexUnboundedWindowBound(false);

  /** CURRENT ROW. */
  public static final RexWindowBound CURRENT_ROW =
      new RexCurrentRowWindowBound();

  private RexWindowBounds() {
  }

  /**
   * Creates a window bound from a {@link SqlNode}.
   *
   * @param node SqlNode of the bound
   * @param rexNode offset value when bound is not UNBOUNDED/CURRENT ROW
   * @return window bound
   */
  public static RexWindowBound create(SqlNode node, @Nullable RexNode rexNode) {
    if (SqlWindow.isUnboundedPreceding(node)) {
      return UNBOUNDED_PRECEDING;
    }
    if (SqlWindow.isUnboundedFollowing(node)) {
      return UNBOUNDED_FOLLOWING;
    }
    if (SqlWindow.isCurrentRow(node)) {
      return CURRENT_ROW;
    }
    assert rexNode != null : "offset value cannot be null for bounded window";
    return new RexBoundedWindowBound((RexCall) rexNode);
  }

  public static RexWindowBound following(RexNode offset) {
    return new RexBoundedWindowBound(
        new RexCall(offset.getType(),
            SqlWindow.FOLLOWING_OPERATOR, ImmutableList.of(offset)));
  }

  public static RexWindowBound preceding(RexNode offset) {
    return new RexBoundedWindowBound(
        new RexCall(offset.getType(),
            SqlWindow.PRECEDING_OPERATOR, ImmutableList.of(offset)));
  }

  /**
   * Implements UNBOUNDED PRECEDING/FOLLOWING bound.
   */
  private static class RexUnboundedWindowBound extends RexWindowBound {
    private final boolean preceding;

    RexUnboundedWindowBound(boolean preceding) {
      this.preceding = preceding;
    }

    @Override public boolean isUnbounded() {
      return true;
    }

    @Override public boolean isPreceding() {
      return preceding;
    }

    @Override public boolean isFollowing() {
      return !preceding;
    }

    @Override public String toString() {
      return preceding ? "UNBOUNDED PRECEDING" : "UNBOUNDED FOLLOWING";
    }

    @Override public int getOrderKey() {
      return preceding ? 0 : 2;
    }

    @Override public boolean equals(@Nullable Object o) {
      return this == o
          || o instanceof RexUnboundedWindowBound
          && preceding == ((RexUnboundedWindowBound) o).preceding;
    }

    @Override public int hashCode() {
      return preceding ? 1357 : 1358;
    }
  }

  /**
   * Implements CURRENT ROW bound.
   */
  private static class RexCurrentRowWindowBound extends RexWindowBound {
    @Override public boolean isCurrentRow() {
      return true;
    }

    @Override public String toString() {
      return "CURRENT ROW";
    }

    @Override public int getOrderKey() {
      return 1;
    }

    @Override public boolean equals(@Nullable Object o) {
      return this == o
          || o instanceof RexCurrentRowWindowBound;
    }

    @Override public int hashCode() {
      return 123;
    }
  }

  /**
   * Implements XX PRECEDING/FOLLOWING bound where XX is not UNBOUNDED.
   */
  private static class RexBoundedWindowBound extends RexWindowBound {
    private final SqlKind sqlKind;
    private final RexNode offset;

    RexBoundedWindowBound(RexCall node) {
      this.offset = Objects.requireNonNull(node.operands.get(0));
      this.sqlKind = Objects.requireNonNull(node.getKind());
    }

    private RexBoundedWindowBound(SqlKind sqlKind, RexNode offset) {
      this.sqlKind = sqlKind;
      this.offset = offset;
    }

    @Override public boolean isPreceding() {
      return sqlKind == SqlKind.PRECEDING;
    }

    @Override public boolean isFollowing() {
      return sqlKind == SqlKind.FOLLOWING;
    }

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

    @Override public int nodeCount() {
      return super.nodeCount() + offset.nodeCount();
    }

    @Override public <R> RexWindowBound accept(RexVisitor<R> visitor) {
      R r = offset.accept(visitor);
      if (r instanceof RexNode && r != offset) {
        return new RexBoundedWindowBound(sqlKind, (RexNode) r);
      }
      return this;
    }

    @Override public String toString() {
      return offset + " " + sqlKind;
    }

    @Override public boolean equals(@Nullable Object o) {
      return this == o
          || o instanceof RexBoundedWindowBound
          && offset.equals(((RexBoundedWindowBound) o).offset)
          && sqlKind == ((RexBoundedWindowBound) o).sqlKind;
    }

    @Override public int hashCode() {
      return Objects.hash(sqlKind, offset);
    }
  }
}
