/*
 * 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.rel.type.DynamicRecordType;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.util.SqlVisitor;
import org.apache.calcite.sql.validate.SqlMonotonicity;
import org.apache.calcite.sql.validate.SqlQualified;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.util.Litmus;
import org.apache.calcite.util.Util;

import com.google.common.collect.ImmutableList;

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

import java.util.ArrayList;
import java.util.List;

/**
 * A <code>SqlIdentifier</code> is an identifier, possibly compound.
 */
public class SqlIdentifier extends SqlNode {
  /** An identifier for star, "*".
   *
   * @see SqlNodeList#SINGLETON_STAR */
  public static final SqlIdentifier STAR = star(SqlParserPos.ZERO);

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

  /**
   * Array of the components of this compound identifier.
   *
   * <p>The empty string represents the wildcard "*",
   * to distinguish it from a real "*" (presumably specified using quotes).
   *
   * <p>It's convenient to have this member public, and it's convenient to
   * have this member not-final, but it's a shame it's public and not-final.
   * If you assign to this member, please use
   * {@link #setNames(java.util.List, java.util.List)}.
   * And yes, we'd like to make identifiers immutable one day.
   */
  public ImmutableList<String> names;

  /**
   * This identifier's collation (if any).
   */
  final @Nullable SqlCollation collation;

  /**
   * A list of the positions of the components of compound identifiers.
   */
  protected @Nullable ImmutableList<SqlParserPos> componentPositions;

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

  /**
   * Creates a compound identifier, for example <code>foo.bar</code>.
   *
   * @param names Parts of the identifier, length &ge; 1
   */
  public SqlIdentifier(
      List<String> names,
      @Nullable SqlCollation collation,
      SqlParserPos pos,
      @Nullable List<SqlParserPos> componentPositions) {
    super(pos);
    this.names = ImmutableList.copyOf(names);
    this.collation = collation;
    this.componentPositions = componentPositions == null ? null
        : ImmutableList.copyOf(componentPositions);
    for (String name : names) {
      assert name != null;
    }
  }

  public SqlIdentifier(List<String> names, SqlParserPos pos) {
    this(names, null, pos, null);
  }

  /**
   * Creates a simple identifier, for example <code>foo</code>, with a
   * collation.
   */
  public SqlIdentifier(
      String name,
      @Nullable SqlCollation collation,
      SqlParserPos pos) {
    this(ImmutableList.of(name), collation, pos, null);
  }

  /**
   * Creates a simple identifier, for example <code>foo</code>.
   */
  public SqlIdentifier(
      String name,
      SqlParserPos pos) {
    this(ImmutableList.of(name), null, pos, null);
  }

  /** Creates an identifier that is a singleton wildcard star. */
  public static SqlIdentifier star(SqlParserPos pos) {
    return star(ImmutableList.of(""), pos, ImmutableList.of(pos));
  }

  /** Creates an identifier that ends in a wildcard star. */
  public static SqlIdentifier star(List<String> names, SqlParserPos pos,
      List<SqlParserPos> componentPositions) {
    return new SqlIdentifier(
        Util.transform(names, s -> s.equals("*") ? "" : s), null, pos,
        componentPositions);
  }

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

  @Override public SqlKind getKind() {
    return SqlKind.IDENTIFIER;
  }

  @Override public SqlNode clone(SqlParserPos pos) {
    return new SqlIdentifier(names, collation, pos, componentPositions);
  }

  @Override public String toString() {
    return getString(names);
  }

  /** Converts a list of strings to a qualified identifier. */
  public static String getString(List<String> names) {
    return Util.sepList(toStar(names), ".");
  }

  /** Converts empty strings in a list of names to stars. */
  public static List<String> toStar(List<String> names) {
    return Util.transform(names, s -> s.equals("") ? "*" : s);
  }

  /**
   * Modifies the components of this identifier and their positions.
   *
   * @param names Names of components
   * @param poses Positions of components
   */
  public void setNames(List<String> names, @Nullable List<SqlParserPos> poses) {
    this.names = ImmutableList.copyOf(names);
    this.componentPositions = poses == null ? null
        : ImmutableList.copyOf(poses);
  }

  /** Returns an identifier that is the same as this except one modified name.
   * Does not modify this identifier. */
  public SqlIdentifier setName(int i, String name) {
    if (!names.get(i).equals(name)) {
      String[] nameArray = names.toArray(new String[0]);
      nameArray[i] = name;
      return new SqlIdentifier(ImmutableList.copyOf(nameArray), collation, pos,
          componentPositions);
    } else {
      return this;
    }
  }

  /** Returns an identifier that is the same as this except with a component
   * added at a given position. Does not modify this identifier. */
  public SqlIdentifier add(int i, String name, SqlParserPos pos) {
    final List<String> names2 = new ArrayList<>(names);
    names2.add(i, name);
    final List<SqlParserPos> pos2;
    if (componentPositions == null) {
      pos2 = null;
    } else {
      pos2 = new ArrayList<>(componentPositions);
      pos2.add(i, pos);
    }
    return new SqlIdentifier(names2, collation, pos, pos2);
  }

  /**
   * Returns the position of the <code>i</code>th component of a compound
   * identifier, or the position of the whole identifier if that information
   * is not present.
   *
   * @param i Ordinal of component.
   * @return Position of i'th component
   */
  public SqlParserPos getComponentParserPosition(int i) {
    assert (i >= 0) && (i < names.size());
    return (componentPositions == null) ? getParserPosition()
        : componentPositions.get(i);
  }

  /**
   * Copies names and components from another identifier. Does not modify the
   * cross-component parser position.
   *
   * @param other identifier from which to copy
   */
  public void assignNamesFrom(SqlIdentifier other) {
    setNames(other.names, other.componentPositions);
  }

  /**
   * Creates an identifier which contains only the <code>ordinal</code>th
   * component of this compound identifier. It will have the correct
   * {@link SqlParserPos}, provided that detailed position information is
   * available.
   */
  public SqlIdentifier getComponent(int ordinal) {
    return getComponent(ordinal, ordinal + 1);
  }

  public SqlIdentifier getComponent(int from, int to) {
    final SqlParserPos pos;
    final ImmutableList<SqlParserPos> pos2;
    if (componentPositions == null) {
      pos2 = null;
      pos = this.pos;
    } else {
      pos2 = componentPositions.subList(from, to);
      pos = SqlParserPos.sum(pos2);
    }
    return new SqlIdentifier(names.subList(from, to), collation, pos, pos2);
  }

  /**
   * Creates an identifier that consists of this identifier plus a name segment.
   * Does not modify this identifier.
   */
  public SqlIdentifier plus(String name, SqlParserPos pos) {
    final ImmutableList<String> names =
        ImmutableList.<String>builder().addAll(this.names).add(name).build();
    final ImmutableList<SqlParserPos> componentPositions;
    final SqlParserPos pos2;
    ImmutableList<SqlParserPos> thisComponentPositions = this.componentPositions;
    if (thisComponentPositions != null) {
      final ImmutableList.Builder<SqlParserPos> builder =
          ImmutableList.builder();
      componentPositions =
          builder.addAll(thisComponentPositions).add(pos).build();
      pos2 = SqlParserPos.sum(builder.add(this.pos).build());
    } else {
      componentPositions = null;
      pos2 = pos;
    }
    return new SqlIdentifier(names, collation, pos2, componentPositions);
  }

  /**
   * Creates an identifier that consists of this identifier plus a wildcard star.
   * Does not modify this identifier.
   */
  public SqlIdentifier plusStar() {
    final SqlIdentifier id = this.plus("*", SqlParserPos.ZERO);
    return new SqlIdentifier(
        id.names.stream().map(s -> s.equals("*") ? "" : s)
            .collect(Util.toImmutableList()),
        null, id.pos, id.componentPositions);
  }

  /** Creates an identifier that consists of all but the last {@code n}
   * name segments of this one. */
  public SqlIdentifier skipLast(int n) {
    return getComponent(0, names.size() - n);
  }

  @Override public void unparse(
      SqlWriter writer,
      int leftPrec,
      int rightPrec) {
    SqlUtil.unparseSqlIdentifierSyntax(writer, this, false);
  }

  @Override public void validate(SqlValidator validator, SqlValidatorScope scope) {
    validator.validateIdentifier(this, scope);
  }

  @Override public void validateExpr(SqlValidator validator, SqlValidatorScope scope) {
    // First check for builtin functions which don't have parentheses,
    // like "LOCALTIME".
    final SqlCall call = validator.makeNullaryCall(this);
    if (call != null) {
      validator.validateCall(call, scope);
      return;
    }

    validator.validateIdentifier(this, scope);
  }

  @Override public boolean equalsDeep(@Nullable SqlNode node, Litmus litmus) {
    if (!(node instanceof SqlIdentifier)) {
      return litmus.fail("{} != {}", this, node);
    }
    SqlIdentifier that = (SqlIdentifier) node;
    if (this.names.size() != that.names.size()) {
      return litmus.fail("{} != {}", this, node);
    }
    for (int i = 0; i < names.size(); i++) {
      if (!this.names.get(i).equals(that.names.get(i))) {
        return litmus.fail("{} != {}", this, node);
      }
    }
    return litmus.succeed();
  }

  @Override public <R> R accept(SqlVisitor<R> visitor) {
    return visitor.visit(this);
  }

  @Pure
  public @Nullable SqlCollation getCollation() {
    return collation;
  }

  public String getSimple() {
    assert names.size() == 1;
    return names.get(0);
  }

  /** Returns the simple names in a list of identifiers.
   * Assumes that the list consists of are not-null, simple identifiers. */
  public static List<String> simpleNames(List<? extends SqlNode> list) {
    return Util.transform(list, n -> ((SqlIdentifier) n).getSimple());
  }

  /** Returns the simple names in a iterable of identifiers.
   * Assumes that the iterable consists of not-null, simple identifiers. */
  public static Iterable<String> simpleNames(Iterable<? extends SqlNode> list) {
    return Util.transform(list, n -> ((SqlIdentifier) n).getSimple());
  }

  /**
   * Returns whether this identifier is a star, such as "*" or "foo.bar.*".
   */
  public boolean isStar() {
    return Util.last(names).equals("");
  }

  /**
   * Returns whether this is a simple identifier. "FOO" is simple; "*",
   * "FOO.*" and "FOO.BAR" are not.
   */
  public boolean isSimple() {
    return names.size() == 1 && !isStar();
  }

  /**
   * Returns whether the {@code i}th component of a compound identifier is
   * quoted.
   *
   * @param i Ordinal of component
   * @return Whether i'th component is quoted
   */
  public boolean isComponentQuoted(int i) {
    return componentPositions != null
        && componentPositions.get(i).isQuoted();
  }

  @Override public SqlMonotonicity getMonotonicity(SqlValidatorScope scope) {
    // for "star" column, whether it's static or dynamic return not_monotonic directly.
    if (Util.last(names).equals("") || DynamicRecordType.isDynamicStarColName(Util.last(names))) {
      return SqlMonotonicity.NOT_MONOTONIC;
    }

    // First check for builtin functions which don't have parentheses,
    // like "LOCALTIME".
    final SqlValidator validator = scope.getValidator();
    final SqlCall call = validator.makeNullaryCall(this);
    if (call != null) {
      return call.getMonotonicity(scope);
    }
    final SqlQualified qualified = scope.fullyQualify(this);
    assert qualified.namespace != null : "namespace must not be null in " + qualified;
    final SqlIdentifier fqId = qualified.identifier;
    return qualified.namespace.resolve().getMonotonicity(Util.last(fqId.names));
  }
}
