content: /*
   * 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.sql.parser.SqlParserPos;
  
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableMap;
  
  import org.checkerframework.checker.nullness.qual.Nullable;
  
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  import static com.google.common.collect.ImmutableList.toImmutableList;
  
  import static java.util.Objects.requireNonNull;
  
  /**
   * A <code>SqlHint</code> is a node of a parse tree which represents
   * a sql hint expression.
   *
   * <p>Basic hint grammar is: hint_name[(option1, option2 ...)].
   * The hint_name should be a simple identifier, the options part is optional.
   * Every option can be of four formats:
   *
   * <ul>
   *   <li>simple identifier</li>
   *   <li>literal</li>
   *   <li>key value pair whose key is a simple identifier and value is a string literal</li>
   *   <li>key value pair whose key and value are both string literal</li>
   * </ul>
   *
   * <p>The option format can not be mixed in, they should either be all simple identifiers
   * or all literals or all key value pairs.
   *
   * <p>We support 2 kinds of hints in the parser:
   * <ul>
   *   <li>Query hint, right after the select keyword, i.e.:
   *   <pre>
   *     select &#47;&#42;&#43; hint1, hint2, ... &#42;&#47; ...
   *   </pre>
   *   </li>
   *   <li>Table hint: right after the referenced table name, i.e.:
   *   <pre>
   *     select f0, f1, f2 from t1 &#47;&#42;&#43; hint1, hint2, ... &#42;&#47; ...
   *   </pre>
   *   </li>
   * </ul>
   */
  public class SqlHint extends SqlCall {
    //~ Instance fields --------------------------------------------------------
  
    private final SqlIdentifier name;
    private final SqlNodeList options;
    private final HintOptionFormat optionFormat;
  
    private static final SqlOperator OPERATOR =
        new SqlSpecialOperator("HINT", SqlKind.HINT) {
          @Override public SqlCall createCall(
              @Nullable SqlLiteral functionQualifier,
              SqlParserPos pos,
              @Nullable SqlNode... operands) {
            return new SqlHint(pos,
                (SqlIdentifier) requireNonNull(operands[0], "name"),
                (SqlNodeList) requireNonNull(operands[1], "options"),
                ((SqlLiteral) requireNonNull(operands[2], "optionFormat"))
                    .getValueAs(HintOptionFormat.class));
          }
        };
  
    //~ Constructors -----------------------------------------------------------
  
    public SqlHint(
        SqlParserPos pos,
        SqlIdentifier name,
        SqlNodeList options,
        HintOptionFormat optionFormat) {
      super(pos);
      this.name = name;
      this.optionFormat = optionFormat;
      this.options = options;
    }
  
    //~ Methods ----------------------------------------------------------------
  
    @Override public SqlOperator getOperator() {
      return OPERATOR;
    }
  
    @Override public List<SqlNode> getOperandList() {
      return ImmutableList.of(name, options, optionFormat.symbol(SqlParserPos.ZERO));
    }
  
    /**
     * Returns the sql hint name.
     */
    public String getName() {
      return name.getSimple();
    }
  
    /** Returns the hint option format. */
    public HintOptionFormat getOptionFormat() {
      return optionFormat;
    }
  
    /**
     * Returns a string list if the hint option is a list of
     * simple SQL identifier, or a list of literals,
     * else returns an empty list.
     */
    public List<String> getOptionList() {
      if (optionFormat == HintOptionFormat.ID_LIST) {
        return ImmutableList.copyOf(SqlIdentifier.simpleNames(options));
      } else if (optionFormat == HintOptionFormat.LITERAL_LIST) {
        return options.stream()
            .map(node -> {
              SqlLiteral literal = (SqlLiteral) node;
              return requireNonNull(literal.toValue(),
                  () -> "null hint literal in " + options);
            })
            .collect(toImmutableList());
      } else {
        return ImmutableList.of();
      }
    }
  
    /**
     * Returns a key value string map if the hint option is a list of
     * pair, each pair contains a simple SQL identifier and a string literal;
     * else returns an empty map.
     */
    public Map<String, String> getOptionKVPairs() {
      if (optionFormat == HintOptionFormat.KV_LIST) {
        final Map<String, String> attrs = new HashMap<>();
        for (int i = 0; i < options.size() - 1; i += 2) {
          final SqlNode k = options.get(i);
          final SqlNode v = options.get(i + 1);
          attrs.put(getOptionKeyAsString(k), ((SqlLiteral) v).getValueAs(String.class));
        }
        return ImmutableMap.copyOf(attrs);
      } else {
        return ImmutableMap.of();
      }
    }
  
    @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
      name.unparse(writer, leftPrec, rightPrec);
      if (!this.options.isEmpty()) {
        SqlWriter.Frame frame =
            writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, "(", ")");
        for (int i = 0; i < options.size(); i++) {
          SqlNode option = options.get(i);
          SqlNode nextOption = i < options.size() - 1 ? options.get(i + 1) : null;
          writer.sep(",", false);
          option.unparse(writer, leftPrec, rightPrec);
          if (optionFormat == HintOptionFormat.KV_LIST && nextOption != null) {
            writer.keyword("=");
            nextOption.unparse(writer, leftPrec, rightPrec);
            i += 1;
          }
        }
        writer.endList(frame);
      }
    }
  
    /** Enumeration that represents hint option format. */
    public enum HintOptionFormat implements Symbolizable {
      /**
       * The hint has no options.
       */
      EMPTY,
      /**
       * The hint options are as literal list.
       */
      LITERAL_LIST,
      /**
       * The hint options are as simple identifier list.
       */
      ID_LIST,
      /**
       * The hint options are list of key-value pairs.
       * For each pair,
       * the key is a simple identifier or string literal,
       * the value is a string literal.
       */
      KV_LIST
    }
  
    //~ Tools ------------------------------------------------------------------
  
    private static String getOptionKeyAsString(SqlNode node) {
      assert node instanceof SqlIdentifier || SqlUtil.isLiteral(node);
      if (node instanceof SqlIdentifier) {
        return ((SqlIdentifier) node).getSimple();
      }
      return ((SqlLiteral) node).getValueAs(String.class);
    }
  }
  