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.pretty;
  
  import org.apache.calcite.avatica.util.Spaces;
  import org.apache.calcite.sql.SqlBinaryOperator;
  import org.apache.calcite.sql.SqlDialect;
  import org.apache.calcite.sql.SqlNode;
  import org.apache.calcite.sql.SqlNodeList;
  import org.apache.calcite.sql.SqlWriter;
  import org.apache.calcite.sql.SqlWriterConfig;
  import org.apache.calcite.sql.dialect.AnsiSqlDialect;
  import org.apache.calcite.sql.dialect.CalciteSqlDialect;
  import org.apache.calcite.sql.util.SqlString;
  import org.apache.calcite.util.Util;
  import org.apache.calcite.util.trace.CalciteLogger;
  
  import com.google.common.collect.ImmutableList;
  
  import org.checkerframework.checker.nullness.qual.Nullable;
  import org.slf4j.LoggerFactory;
  
  import java.io.PrintWriter;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.util.ArrayDeque;
  import java.util.Deque;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Properties;
  import java.util.Set;
  import java.util.function.Consumer;
  
  import static com.google.common.base.Preconditions.checkArgument;
  
  import static java.util.Objects.requireNonNull;
  
  /**
   * Pretty printer for SQL statements.
   *
   * <p>There are several options to control the format.
   *
   * <table>
   * <caption>Formatting options</caption>
   * <tr>
   * <th>Option</th>
   * <th>Description</th>
   * <th>Default</th>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#clauseStartsLine()} ClauseStartsLine}</td>
   * <td>Whether a clause ({@code FROM}, {@code WHERE}, {@code GROUP BY},
   * {@code HAVING}, {@code WINDOW}, {@code ORDER BY}) starts a new line.
   * {@code SELECT} is always at the start of a line.</td>
   * <td>true</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#clauseEndsLine ClauseEndsLine}</td>
   * <td>Whether a clause ({@code SELECT}, {@code FROM}, {@code WHERE},
   * {@code GROUP BY}, {@code HAVING}, {@code WINDOW}, {@code ORDER BY}) is
   * followed by a new line.</td>
   * <td>false</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#caseClausesOnNewLines CaseClausesOnNewLines}</td>
   * <td>Whether the WHEN, THEN and ELSE clauses of a CASE expression appear at
   * the start of a new line.</td>
   * <td>false</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#indentation Indentation}</td>
   * <td>Number of spaces to indent</td>
   * <td>4</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#keywordsLowerCase KeywordsLowerCase}</td>
   * <td>Whether to print keywords (SELECT, AS, etc.) in lower-case.</td>
   * <td>false</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#alwaysUseParentheses AlwaysUseParentheses}</td>
   * <td>Whether to enclose all expressions in parentheses, even if the
   * operator has high enough precedence that the parentheses are not required.
   *
   * <p>For example, the parentheses are required in the expression
   * {@code (a + b) * c} because the '*' operator has higher precedence than the
   * '+' operator, and so without the parentheses, the expression would be
   * equivalent to {@code a + (b * c)}. The fully-parenthesized expression,
   * {@code ((a + b) * c)} is unambiguous even if you don't know the precedence
   * of every operator.</td>
   * <td>false</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#quoteAllIdentifiers QuoteAllIdentifiers}</td>
   * <td>Whether to quote all identifiers, even those which would be correct
   * according to the rules of the {@link SqlDialect} if quotation marks were
   * omitted.</td>
   * <td>true</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#subQueryStyle SubQueryStyle}</td>
   * <td>Style for formatting sub-queries. Values are:
   * {@link org.apache.calcite.sql.SqlWriter.SubQueryStyle#HYDE Hyde},
   * {@link org.apache.calcite.sql.SqlWriter.SubQueryStyle#BLACK Black}.</td>
   *
   * <td>{@link org.apache.calcite.sql.SqlWriter.SubQueryStyle#HYDE Hyde}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#lineLength LineLength}</td>
   * <td>The desired maximum length for lines (to look nice in editors,
   * printouts, etc.).</td>
   * <td>-1 (no maximum)</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#foldLength FoldLength}</td>
   * <td>The line length at which lines are folded or chopped down
   * (see {@code LineFolding}). Only has an effect if clauses are marked
   * {@link SqlWriterConfig.LineFolding#CHOP CHOP} or
   * {@link SqlWriterConfig.LineFolding#FOLD FOLD}.</td>
   * <td>80</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#lineFolding LineFolding}</td>
   * <td>How long lines are to be handled. Options are lines are
   * WIDE (do not wrap),
   * FOLD (wrap if long),
   * CHOP (chop down if long),
   * and TALL (wrap always).</td>
   * <td>WIDE</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#selectFolding() SelectFolding}</td>
   * <td>How the {@code SELECT} clause is to be folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#fromFolding FromFolding}</td>
   * <td>How the {@code FROM} clause and nested {@code JOIN} clauses are to be
   * folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#whereFolding WhereFolding}</td>
   * <td>How the {@code WHERE} clause is to be folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#groupByFolding GroupByFolding}</td>
   * <td>How the {@code GROUP BY} clause is to be folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#havingFolding HavingFolding}</td>
   * <td>How the {@code HAVING} clause is to be folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#orderByFolding OrderByFolding}</td>
   * <td>How the {@code ORDER BY} clause is to be folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#windowFolding WindowFolding}</td>
   * <td>How the {@code WINDOW} clause is to be folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#overFolding OverFolding}</td>
   * <td>How window declarations in the {@code WINDOW} clause
   * and in the {@code OVER} clause of aggregate functions are to be folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#valuesFolding ValuesFolding}</td>
   * <td>How lists of values in the {@code VALUES} clause are to be folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * <tr>
   * <td>{@link SqlWriterConfig#updateSetFolding UpdateSetFolding}</td>
   * <td>How assignments in the {@code SET} clause of an {@code UPDATE} statement
   * are to be folded.</td>
   * <td>{@code LineFolding}</td>
   * </tr>
   *
   * </table>
   *
   * <p>The following options exist for backwards compatibility. They are
   * used if {@link SqlWriterConfig#lineFolding LineFolding} and clause-specific
   * options such as {@link SqlWriterConfig#selectFolding SelectFolding} are not
   * specified:
   *
   * <ul>
   *
   * <li>{@link SqlWriterConfig#selectListItemsOnSeparateLines SelectListItemsOnSeparateLines}
   * replaced by {@link SqlWriterConfig#selectFolding SelectFolding},
   * {@link SqlWriterConfig#groupByFolding GroupByFolding}, and
   * {@link SqlWriterConfig#orderByFolding OrderByFolding};
   *
   * <li>{@link SqlWriterConfig#updateSetListNewline UpdateSetListNewline}
   * replaced by {@link SqlWriterConfig#updateSetFolding UpdateSetFolding};
   *
   * <li>{@link SqlWriterConfig#windowDeclListNewline WindowDeclListNewline}
   * replaced by {@link SqlWriterConfig#windowFolding WindowFolding};
   *
   * <li>{@link SqlWriterConfig#windowNewline WindowNewline}
   * replaced by {@link SqlWriterConfig#overFolding OverFolding};
   *
   * <li>{@link SqlWriterConfig#valuesListNewline ValuesListNewline}
   * replaced by {@link SqlWriterConfig#valuesFolding ValuesFolding}.
   *
   * </ul>
   */
  public class SqlPrettyWriter implements SqlWriter {
    //~ Static fields/initializers ---------------------------------------------
  
    protected static final CalciteLogger LOGGER =
        new CalciteLogger(
            LoggerFactory.getLogger("org.apache.calcite.sql.pretty.SqlPrettyWriter"));
  
    /**
     * Default SqlWriterConfig, reduce the overhead of "ImmutableBeans.create"
     */
    private static final SqlWriterConfig CONFIG =
        SqlWriterConfig.of()
            .withDialect(CalciteSqlDialect.DEFAULT);
  
    /**
     * Bean holding the default property values.
     */
    private static final Bean DEFAULT_BEAN =
        new SqlPrettyWriter(SqlPrettyWriter.config()
            .withDialect(AnsiSqlDialect.DEFAULT)).getBean();
    protected static final String NL = System.lineSeparator();
  
    //~ Instance fields --------------------------------------------------------
  
    private final SqlDialect dialect;
    private final StringBuilder buf;
    private final Deque<FrameImpl> listStack = new ArrayDeque<>();
    private ImmutableList.@Nullable Builder<Integer> dynamicParameters;
    protected @Nullable FrameImpl frame;
    private boolean needWhitespace;
    protected @Nullable String nextWhitespace;
    private SqlWriterConfig config;
    private @Nullable Bean bean;
    private int currentIndent;
  
    private int lineStart;
  
    //~ Constructors -----------------------------------------------------------
  
    @SuppressWarnings("method.invocation.invalid")
    private SqlPrettyWriter(SqlWriterConfig config,
        StringBuilder buf, @SuppressWarnings("unused") boolean ignore) {
      this.buf = requireNonNull(buf, "buf");
      this.dialect = requireNonNull(config.dialect());
      this.config = requireNonNull(config, "config");
      lineStart = 0;
      reset();
    }
  
    /** Creates a writer with the given configuration
     * and a given buffer to write to. */
    public SqlPrettyWriter(SqlWriterConfig config, StringBuilder buf) {
      this(config, buf, false);
    }
  
    /** Creates a writer with the given configuration and dialect,
     * and a given print writer (or a private print writer if it is null). */
    public SqlPrettyWriter(
        SqlDialect dialect,
        SqlWriterConfig config,
        StringBuilder buf) {
      this(config.withDialect(requireNonNull(dialect, "dialect")), buf);
    }
  
    /** Creates a writer with the given configuration
     * and a private print writer. */
    @Deprecated
    public SqlPrettyWriter(SqlDialect dialect, SqlWriterConfig config) {
      this(config.withDialect(requireNonNull(dialect, "dialect")));
    }
  
    @Deprecated
    public SqlPrettyWriter(
        SqlDialect dialect,
        boolean alwaysUseParentheses,
        PrintWriter pw) {
      // NOTE that 'pw' is ignored; there is no place for it in the new API
      this(config().withDialect(requireNonNull(dialect, "dialect"))
          .withAlwaysUseParentheses(alwaysUseParentheses));
    }
  
    @Deprecated
    public SqlPrettyWriter(
        SqlDialect dialect,
        boolean alwaysUseParentheses) {
      this(config().withDialect(requireNonNull(dialect, "dialect"))
          .withAlwaysUseParentheses(alwaysUseParentheses));
    }
  
    /** Creates a writer with a given dialect, the default configuration
     * and a private print writer. */
    @Deprecated
    public SqlPrettyWriter(SqlDialect dialect) {
      this(config().withDialect(requireNonNull(dialect, "dialect")));
    }
  
    /** Creates a writer with the given configuration,
     * and a private builder. */
    public SqlPrettyWriter(SqlWriterConfig config) {
      this(config, new StringBuilder(), true);
    }
  
    /** Creates a writer with the default configuration.
     *
     * @see #config() */
    public SqlPrettyWriter() {
      this(config());
    }
  
    /** Creates a {@link SqlWriterConfig} with Calcite's SQL dialect. */
    public static SqlWriterConfig config() {
      return CONFIG;
    }
  
    //~ Methods ----------------------------------------------------------------
  
    @Deprecated
    public void setCaseClausesOnNewLines(boolean caseClausesOnNewLines) {
      this.config = config.withCaseClausesOnNewLines(caseClausesOnNewLines);
    }
  
    @Deprecated
    public void setSubQueryStyle(SubQueryStyle subQueryStyle) {
      this.config = config.withSubQueryStyle(subQueryStyle);
    }
  
    @Deprecated
    public void setWindowNewline(boolean windowNewline) {
      this.config = config.withWindowNewline(windowNewline);
    }
  
    @Deprecated
    public void setWindowDeclListNewline(boolean windowDeclListNewline) {
      this.config = config.withWindowDeclListNewline(windowDeclListNewline);
    }
  
    @Deprecated
    @Override public int getIndentation() {
      return config.indentation();
    }
  
    @Deprecated
    @Override public boolean isAlwaysUseParentheses() {
      return config.alwaysUseParentheses();
    }
  
    @Override public boolean inQuery() {
      return (frame == null)
          || (frame.frameType == FrameTypeEnum.SELECT)
          || (frame.frameType == FrameTypeEnum.ORDER_BY)
          || (frame.frameType == FrameTypeEnum.WITH_BODY)
          || (frame.frameType == FrameTypeEnum.SETOP);
    }
  
    @Deprecated
    @Override public boolean isQuoteAllIdentifiers() {
      return config.quoteAllIdentifiers();
    }
  
    @Deprecated
    @Override public boolean isClauseStartsLine() {
      return config.clauseStartsLine();
    }
  
    @Deprecated
    @Override public boolean isSelectListItemsOnSeparateLines() {
      return config.selectListItemsOnSeparateLines();
    }
  
    @Deprecated
    public boolean isWhereListItemsOnSeparateLines() {
      return config.whereListItemsOnSeparateLines();
    }
  
    @Deprecated
    public boolean isSelectListExtraIndentFlag() {
      return config.selectListExtraIndentFlag();
    }
  
    @Deprecated
    @Override public boolean isKeywordsLowerCase() {
      return config.keywordsLowerCase();
    }
  
    @Deprecated
    public int getLineLength() {
      return config.lineLength();
    }
  
    @Override public void resetSettings() {
      reset();
      config = config();
    }
  
    @Override public void reset() {
      buf.setLength(0);
      lineStart = 0;
      dynamicParameters = null;
      setNeedWhitespace(false);
      nextWhitespace = " ";
    }
  
    /**
     * Returns an object which encapsulates each property as a get/set method.
     */
    private Bean getBean() {
      if (bean == null) {
        bean = new Bean(this);
      }
      return bean;
    }
  
    @Deprecated
    public void setIndentation(int indentation) {
      this.config = config.withIndentation(indentation);
    }
  
    /**
     * Prints the property settings of this pretty-writer to a writer.
     *
     * @param pw           Writer
     * @param omitDefaults Whether to omit properties whose value is the same as
     *                     the default
     */
    public void describe(PrintWriter pw, boolean omitDefaults) {
      final Bean properties = getBean();
      final String[] propertyNames = properties.getPropertyNames();
      int count = 0;
      for (String key : propertyNames) {
        final Object value = properties.get(key);
        final Object defaultValue = DEFAULT_BEAN.get(key);
        if (Objects.equals(value, defaultValue)) {
          continue;
        }
        if (count++ > 0) {
          pw.print(",");
        }
        pw.print(key + "=" + value);
      }
    }
  
    /**
     * Sets settings from a properties object.
     */
    public void setSettings(Properties properties) {
      resetSettings();
      final Bean bean = getBean();
      final String[] propertyNames = bean.getPropertyNames();
      for (String propertyName : propertyNames) {
        final String value = properties.getProperty(propertyName);
        if (value != null) {
          bean.set(propertyName, value);
        }
      }
    }
  
    @Deprecated
    public void setClauseStartsLine(boolean clauseStartsLine) {
      this.config = config.withClauseStartsLine(clauseStartsLine);
    }
  
    @Deprecated
    public void setSelectListItemsOnSeparateLines(boolean b) {
      this.config = config.withSelectListItemsOnSeparateLines(b);
    }
  
    @Deprecated
    public void setSelectListExtraIndentFlag(boolean selectListExtraIndentFlag) {
      this.config =
          config.withSelectListExtraIndentFlag(selectListExtraIndentFlag);
    }
  
    @Deprecated
    public void setKeywordsLowerCase(boolean keywordsLowerCase) {
      this.config = config.withKeywordsLowerCase(keywordsLowerCase);
    }
  
    @Deprecated
    public void setWhereListItemsOnSeparateLines(
        boolean whereListItemsOnSeparateLines) {
      this.config =
          config.withWhereListItemsOnSeparateLines(whereListItemsOnSeparateLines);
    }
  
    @Deprecated
    public void setAlwaysUseParentheses(boolean alwaysUseParentheses) {
      this.config = config.withAlwaysUseParentheses(alwaysUseParentheses);
    }
  
    @Override public void newlineAndIndent() {
      newlineAndIndent(currentIndent);
    }
  
    public void newlineAndIndent(int indent) {
      buf.append(NL);
      lineStart = buf.length();
      indent(indent);
      setNeedWhitespace(false); // no further whitespace necessary
    }
  
    void indent(int indent) {
      if (indent < 0) {
        throw new IllegalArgumentException("negative indent " + indent);
      }
      Spaces.append(buf, indent);
    }
  
    @Deprecated
    public void setQuoteAllIdentifiers(boolean quoteAllIdentifiers) {
      this.config = config.withQuoteAllIdentifiers(quoteAllIdentifiers);
    }
  
    /**
     * Creates a list frame.
     *
     * <p>Derived classes should override this method to specify the indentation
     * of the list.
     *
     * @param frameType What type of list
     * @param keyword   The keyword to be printed at the start of the list
     * @param open      The string to print at the start of the list
     * @param close     The string to print at the end of the list
     * @return A frame
     */
    protected FrameImpl createListFrame(
        FrameType frameType,
        @Nullable String keyword,
        String open,
        String close) {
      final FrameTypeEnum frameTypeEnum =
          frameType instanceof FrameTypeEnum ? (FrameTypeEnum) frameType
              : FrameTypeEnum.OTHER;
      final int indentation = config.indentation();
      boolean newlineAfterOpen = false;
      boolean newlineBeforeSep = false;
      boolean newlineAfterSep = false;
      boolean newlineBeforeClose = false;
      int left = column();
      int sepIndent = indentation;
      int extraIndent = 0;
      final SqlWriterConfig.LineFolding fold = fold(frameTypeEnum);
      final boolean newline = fold == SqlWriterConfig.LineFolding.TALL;
  
      switch (frameTypeEnum) {
      case SELECT:
        extraIndent = indentation;
        newlineAfterOpen = false;
        newlineBeforeSep = config.clauseStartsLine(); // newline before FROM, WHERE etc.
        newlineAfterSep = false;
        sepIndent = 0; // all clauses appear below SELECT
        break;
  
      case SETOP:
        extraIndent = 0;
        newlineAfterOpen = false;
        newlineBeforeSep = config.clauseStartsLine(); // newline before UNION, EXCEPT
        newlineAfterSep = config.clauseStartsLine(); // newline after UNION, EXCEPT
        sepIndent = 0; // all clauses appear below SELECT
        break;
  
      case SELECT_LIST:
      case FROM_LIST:
      case JOIN:
      case GROUP_BY_LIST:
      case ORDER_BY_LIST:
      case WINDOW_DECL_LIST:
      case VALUES:
        if (config.selectListExtraIndentFlag()) {
          extraIndent = indentation;
        }
        left = frame == null ? 0 : frame.left;
        newlineAfterOpen = config.clauseEndsLine()
            && (fold == SqlWriterConfig.LineFolding.TALL
                || fold == SqlWriterConfig.LineFolding.STEP);
        newlineBeforeSep = false;
        newlineAfterSep = newline;
        if (config.leadingComma() && newline) {
          newlineBeforeSep = true;
          newlineAfterSep = false;
          sepIndent = -", ".length();
        }
        break;
  
      case WHERE_LIST:
      case WINDOW:
        extraIndent = indentation;
        newlineAfterOpen = newline && config.clauseEndsLine();
        newlineBeforeSep = newline;
        sepIndent = 0;
        newlineAfterSep = false;
        break;
  
      case ORDER_BY:
      case OFFSET:
      case FETCH:
        newlineAfterOpen = false;
        newlineBeforeSep = true;
        sepIndent = 0;
        newlineAfterSep = false;
        break;
  
      case UPDATE_SET_LIST:
        extraIndent = indentation;
        newlineAfterOpen = newline;
        newlineBeforeSep = false;
        sepIndent = 0;
        newlineAfterSep = newline;
        break;
  
      case CASE:
        newlineAfterOpen = newline;
        newlineBeforeSep = newline;
        newlineBeforeClose = newline;
        sepIndent = 0;
        break;
  
      default:
        break;
      }
  
      final int chopColumn;
      SqlWriterConfig.LineFolding lineFolding = config.lineFolding();
      if (lineFolding == null) {
        lineFolding = SqlWriterConfig.LineFolding.WIDE;
        chopColumn = -1;
      } else {
        if (config.foldLength() > 0
            && (lineFolding == SqlWriterConfig.LineFolding.CHOP
                || lineFolding == SqlWriterConfig.LineFolding.FOLD
                || lineFolding == SqlWriterConfig.LineFolding.STEP)) {
          chopColumn = left + config.foldLength();
        } else {
          chopColumn = -1;
        }
      }
  
      switch (frameTypeEnum) {
      case UPDATE_SET_LIST:
      case WINDOW_DECL_LIST:
      case VALUES:
      case SELECT:
      case SETOP:
      case SELECT_LIST:
      case WHERE_LIST:
      case ORDER_BY_LIST:
      case GROUP_BY_LIST:
      case WINDOW:
      case ORDER_BY:
      case OFFSET:
      case FETCH:
        return new FrameImpl(frameType, keyword, open, close,
            left, extraIndent, chopColumn, lineFolding, newlineAfterOpen,
            newlineBeforeSep, sepIndent, newlineAfterSep, false, false);
  
      case SUB_QUERY:
        switch (config.subQueryStyle()) {
        case BLACK:
          // Generate, e.g.:
          //
          // WHERE foo = bar IN
          // (   SELECT ...
          open = Spaces.padRight("(", indentation - 1);
          return new FrameImpl(frameType, keyword, open, close,
              left, 0, chopColumn, lineFolding, false, true, indentation,
              false, false, false) {
            protected void _before() {
              newlineAndIndent();
            }
          };
  
        case HYDE:
          // Generate, e.g.:
          //
          // WHERE foo IN (
          //     SELECT ...
          return new FrameImpl(frameType, keyword, open, close, left, 0,
              chopColumn, lineFolding, false, true, 0, false, false, false) {
            protected void _before() {
              nextWhitespace = NL;
            }
          };
  
        default:
          throw Util.unexpected(config.subQueryStyle());
        }
  
      case FUN_CALL:
        setNeedWhitespace(false);
        return new FrameImpl(frameType, keyword, open, close,
            left, indentation, chopColumn, lineFolding, false, false,
            indentation, false, false, false);
  
      case PARENTHESES:
        open = "(";
        close = ")";
        // fall through
      case IDENTIFIER:
      case SIMPLE:
        return new FrameImpl(frameType, keyword, open, close,
            left, indentation, chopColumn, lineFolding, false, false,
            indentation, false, false, false);
  
      case FROM_LIST:
      case JOIN:
        newlineBeforeSep = newline;
        sepIndent = 0; // all clauses appear below SELECT
        newlineAfterSep = false;
        final boolean newlineBeforeComma = config.leadingComma() && newline;
        final boolean newlineAfterComma = !config.leadingComma() && newline;
        return new FrameImpl(frameType, keyword, open, close, left,
            indentation, chopColumn, lineFolding, newlineAfterOpen,
            newlineBeforeSep, sepIndent, newlineAfterSep, false, false) {
          @Override protected void sep(boolean printFirst, String sep) {
            final boolean newlineBeforeSep;
            final boolean newlineAfterSep;
            if (sep.equals(",")) {
              newlineBeforeSep = newlineBeforeComma;
              newlineAfterSep = newlineAfterComma;
            } else {
              newlineBeforeSep = this.newlineBeforeSep;
              newlineAfterSep = this.newlineAfterSep;
            }
            if (itemCount == 0) {
              if (newlineAfterOpen) {
                newlineAndIndent(currentIndent);
              }
            } else {
              if (newlineBeforeSep) {
                newlineAndIndent(currentIndent + sepIndent);
              }
            }
            if ((itemCount > 0) || printFirst) {
              keyword(sep);
              nextWhitespace = newlineAfterSep ? NL : " ";
            }
            ++itemCount;
          }
        };
  
      default:
      case OTHER:
        return new FrameImpl(frameType, keyword, open, close, left, indentation,
            chopColumn, lineFolding, newlineAfterOpen, newlineBeforeSep,
            sepIndent, newlineAfterSep, newlineBeforeClose, false);
      }
    }
  
    private SqlWriterConfig.LineFolding fold(FrameTypeEnum frameType) {
      switch (frameType) {
      case SELECT_LIST:
        return f3(config.selectFolding(), config.lineFolding(),
            config.selectListItemsOnSeparateLines());
      case GROUP_BY_LIST:
        return f3(config.groupByFolding(), config.lineFolding(),
            config.selectListItemsOnSeparateLines());
      case ORDER_BY_LIST:
        return f3(config.orderByFolding(), config.lineFolding(),
            config.selectListItemsOnSeparateLines());
      case UPDATE_SET_LIST:
        return f3(config.updateSetFolding(), config.lineFolding(),
            config.updateSetListNewline());
      case WHERE_LIST:
        return f3(config.whereFolding(), config.lineFolding(),
            config.whereListItemsOnSeparateLines());
      case WINDOW_DECL_LIST:
        return f3(config.windowFolding(), config.lineFolding(),
            config.clauseStartsLine() && config.windowDeclListNewline());
      case WINDOW:
        return f3(config.overFolding(), config.lineFolding(),
            config.windowNewline());
      case VALUES:
        return f3(config.valuesFolding(), config.lineFolding(),
            config.valuesListNewline());
      case FROM_LIST:
      case JOIN:
        return f3(config.fromFolding(), config.lineFolding(),
            config.caseClausesOnNewLines());
      case CASE:
        return f3(null, null, config.caseClausesOnNewLines());
      default:
        return SqlWriterConfig.LineFolding.WIDE;
      }
    }
  
    private static SqlWriterConfig.LineFolding f3(SqlWriterConfig.@Nullable LineFolding folding0,
        SqlWriterConfig.@Nullable LineFolding folding1, boolean opt) {
      return folding0 != null ? folding0
          : folding1 != null ? folding1
              : opt ? SqlWriterConfig.LineFolding.TALL
                  : SqlWriterConfig.LineFolding.WIDE;
    }
  
    /**
     * Starts a list.
     *
     * @param frameType Type of list. For example, a SELECT list will be
     *                  governed according to SELECT-list formatting preferences.
     * @param open      String to print at the start of the list; typically "(" or
     *                  the empty string.
     * @param close     String to print at the end of the list.
     */
    protected Frame startList(
        FrameType frameType,
        @Nullable String keyword,
        String open,
        String close) {
      requireNonNull(frameType, "frameType");
      FrameImpl frame = this.frame;
      if (frame != null) {
        if (frame.itemCount++ == 0 && frame.newlineAfterOpen) {
          newlineAndIndent();
        } else if (frameType == FrameTypeEnum.SUB_QUERY
            && config.subQueryStyle() == SubQueryStyle.BLACK) {
          newlineAndIndent(currentIndent - frame.extraIndent);
        }
  
        // REVIEW jvs 9-June-2006:  This is part of the fix for FRG-149
        // (extra frame for identifier was leading to extra indentation,
        // causing select list to come out raggedy with identifiers
        // deeper than literals); are there other frame types
        // for which extra indent should be suppressed?
        currentIndent += frame.extraIndent(frameType);
        assert !listStack.contains(frame);
        listStack.push(frame);
      }
      frame = createListFrame(frameType, keyword, open, close);
      this.frame = frame;
      frame.before();
      return frame;
    }
  
    @Override public void endList(@Nullable Frame frame) {
      FrameImpl endedFrame = (FrameImpl) frame;
      checkArgument(frame == this.frame,
          "Frame does not match current frame");
      if (endedFrame == null) {
        throw new RuntimeException("No list started");
      }
      if (endedFrame.open.equals("(")) {
        if (!endedFrame.close.equals(")")) {
          throw new RuntimeException("Expected ')'");
        }
      }
      if (endedFrame.newlineBeforeClose) {
        newlineAndIndent();
      }
      keyword(endedFrame.close);
      if (endedFrame.newlineAfterClose) {
        newlineAndIndent();
      }
  
      // Pop the frame, and move to the previous indentation level.
      if (listStack.isEmpty()) {
        this.frame = null;
        assert currentIndent == 0 : currentIndent;
      } else {
        this.frame = listStack.pop();
        currentIndent -= this.frame.extraIndent(endedFrame.frameType);
      }
    }
  
    public String format(SqlNode node) {
      assert frame == null;
      node.unparse(this, 0, 0);
      assert frame == null;
      return toString();
    }
  
    @Override public String toString() {
      return buf.toString();
    }
  
    @Override public SqlString toSqlString() {
      ImmutableList<Integer> dynamicParameters =
          this.dynamicParameters == null ? null : this.dynamicParameters.build();
      return new SqlString(dialect, toString(), dynamicParameters);
    }
  
    @Override public SqlDialect getDialect() {
      return dialect;
    }
  
    @Override public void literal(String s) {
      print(s);
      setNeedWhitespace(true);
    }
  
    @Override public void keyword(String s) {
      maybeWhitespace(s);
      buf.append(
          isKeywordsLowerCase()
              ? s.toLowerCase(Locale.ROOT)
              : s.toUpperCase(Locale.ROOT));
      if (!s.isEmpty()) {
        setNeedWhitespace(needWhitespaceAfter(s));
      }
    }
  
    private void maybeWhitespace(String s) {
      if (tooLong(s) || (needWhitespace && needWhitespaceBefore(s))) {
        whiteSpace();
      }
    }
  
    private static boolean needWhitespaceBefore(String s) {
      return !(s.equals(",")
          || s.equals(".")
          || s.equals(")")
          || s.equals("[")
          || s.equals("]")
          || s.isEmpty());
    }
  
    private static boolean needWhitespaceAfter(String s) {
      return !(s.equals("(")
          || s.equals("[")
          || s.equals("."));
    }
  
    protected void whiteSpace() {
      if (needWhitespace) {
        if (NL.equals(nextWhitespace)) {
          newlineAndIndent();
        } else {
          buf.append(nextWhitespace);
        }
        nextWhitespace = " ";
        setNeedWhitespace(false);
      }
    }
  
    /** Returns the number of characters appended since the last newline. */
    private int column() {
      return buf.length() - lineStart;
    }
  
    protected boolean tooLong(String s) {
      final int lineLength = config.lineLength();
      boolean result =
          lineLength > 0
              && (column() > currentIndent)
              && ((column() + s.length()) >= lineLength);
      if (result) {
        nextWhitespace = NL;
      }
      LOGGER.trace("Token is '{}'; result is {}", s, result);
      return result;
    }
  
    @Override public void print(String s) {
      maybeWhitespace(s);
      buf.append(s);
    }
  
    @Override public void print(int x) {
      maybeWhitespace("0");
      buf.append(x);
    }
  
    @Override public void identifier(String name, boolean quoted) {
      // If configured globally or the original identifier is quoted,
      // then quotes the identifier.
      maybeWhitespace(name);
      if (isQuoteAllIdentifiers() || quoted) {
        dialect.quoteIdentifier(buf, name);
      } else {
        buf.append(name);
      }
      setNeedWhitespace(true);
    }
  
    @Override public void dynamicParam(int index) {
      if (dynamicParameters == null) {
        dynamicParameters = ImmutableList.builder();
      }
      dynamicParameters.add(index);
      print("?");
      setNeedWhitespace(true);
    }
  
    @Override public void fetchOffset(@Nullable SqlNode fetch, @Nullable SqlNode offset) {
      if (fetch == null && offset == null) {
        return;
      }
      dialect.unparseOffsetFetch(this, offset, fetch);
    }
  
    @Override public void topN(@Nullable SqlNode fetch, @Nullable SqlNode offset) {
      if (fetch == null && offset == null) {
        return;
      }
      dialect.unparseTopN(this, offset, fetch);
    }
  
    @Override public Frame startFunCall(String funName) {
      keyword(funName);
      setNeedWhitespace(false);
      return startList(FrameTypeEnum.FUN_CALL, "(", ")");
    }
  
    @Override public void endFunCall(Frame frame) {
      endList(this.frame);
    }
  
    @Override public Frame startList(String open, String close) {
      return startList(FrameTypeEnum.SIMPLE, null, open, close);
    }
  
    @Override public Frame startList(FrameTypeEnum frameType) {
      return startList(requireNonNull(frameType, "frameType"), null, "", "");
    }
  
    @Override public Frame startList(FrameType frameType, String open,
        String close) {
      return startList(requireNonNull(frameType, "frameType"), null, open, close);
    }
  
    @Override public SqlWriter list(FrameTypeEnum frameType, Consumer<SqlWriter> action) {
      final SqlWriter.Frame selectListFrame =
          startList(SqlWriter.FrameTypeEnum.SELECT_LIST);
      final SqlWriter w = this;
      action.accept(w);
      endList(selectListFrame);
      return this;
    }
  
    @Override public SqlWriter list(FrameTypeEnum frameType, SqlBinaryOperator sepOp,
        SqlNodeList list) {
      final SqlWriter.Frame frame = startList(frameType);
      ((FrameImpl) frame).list(list, sepOp);
      endList(frame);
      return this;
    }
  
    @Override public void sep(String sep) {
      sep(sep, !(sep.equals(",") || sep.equals(".")));
    }
  
    @Override public void sep(String sep, boolean printFirst) {
      if (frame == null) {
        throw new RuntimeException("No list started");
      }
      if (sep.startsWith(" ") || sep.endsWith(" ")) {
        throw new RuntimeException("Separator must not contain whitespace");
      }
      frame.sep(printFirst, sep);
    }
  
    @Override public void setNeedWhitespace(boolean needWhitespace) {
      this.needWhitespace = needWhitespace;
    }
  
    @Deprecated
    public void setLineLength(int lineLength) {
      this.config = config.withLineLength(lineLength);
    }
  
    public void setFormatOptions(@Nullable SqlFormatOptions options) {
      if (options == null) {
        return;
      }
      setAlwaysUseParentheses(options.isAlwaysUseParentheses());
      setCaseClausesOnNewLines(options.isCaseClausesOnNewLines());
      setClauseStartsLine(options.isClauseStartsLine());
      setKeywordsLowerCase(options.isKeywordsLowercase());
      setQuoteAllIdentifiers(options.isQuoteAllIdentifiers());
      setSelectListItemsOnSeparateLines(
          options.isSelectListItemsOnSeparateLines());
      setWhereListItemsOnSeparateLines(
          options.isWhereListItemsOnSeparateLines());
      setWindowNewline(options.isWindowDeclarationStartsLine());
      setWindowDeclListNewline(options.isWindowListItemsOnSeparateLines());
      setIndentation(options.getIndentation());
      setLineLength(options.getLineLength());
    }
  
    //~ Inner Classes ----------------------------------------------------------
  
    /**
     * Implementation of {@link org.apache.calcite.sql.SqlWriter.Frame}.
     */
    protected class FrameImpl implements Frame {
      final FrameType frameType;
      final @Nullable String keyword;
      final String open;
      final String close;
  
      private final int left;
      /**
       * Indent of sub-frame with respect to this one.
       */
      final int extraIndent;
  
      /**
       * Indent of separators with respect to this frame's indent. Typically
       * zero.
       */
      final int sepIndent;
  
      /**
       * Number of items which have been printed in this list so far.
       */
      int itemCount;
  
      /**
       * Whether to print a newline before each separator.
       */
      public final boolean newlineBeforeSep;
  
      /**
       * Whether to print a newline after each separator.
       */
      public final boolean newlineAfterSep;
      protected final boolean newlineBeforeClose;
      protected final boolean newlineAfterClose;
      protected final boolean newlineAfterOpen;
  
      /** Character count after which we should move an item to the
       * next line. Or {@link Integer#MAX_VALUE} if we are not chopping. */
      private final int chopLimit;
  
      /** How lines are to be folded. */
      private final SqlWriterConfig.LineFolding lineFolding;
  
      FrameImpl(FrameType frameType, @Nullable String keyword, String open, String close,
          int left, int extraIndent, int chopLimit,
          SqlWriterConfig.LineFolding lineFolding, boolean newlineAfterOpen,
          boolean newlineBeforeSep, int sepIndent, boolean newlineAfterSep,
          boolean newlineBeforeClose, boolean newlineAfterClose) {
        this.frameType = frameType;
        this.keyword = keyword;
        this.open = requireNonNull(open, "open");
        this.close = requireNonNull(close, "close");
        this.left = left;
        this.extraIndent = extraIndent;
        this.chopLimit = chopLimit;
        this.lineFolding = requireNonNull(lineFolding, "lineFolding");
        this.newlineAfterOpen = newlineAfterOpen;
        this.newlineBeforeSep = newlineBeforeSep;
        this.newlineAfterSep = newlineAfterSep;
        this.newlineBeforeClose = newlineBeforeClose;
        this.newlineAfterClose = newlineAfterClose;
        this.sepIndent = sepIndent;
        assert chopLimit >= 0
            == (lineFolding == SqlWriterConfig.LineFolding.CHOP
            || lineFolding == SqlWriterConfig.LineFolding.FOLD
            || lineFolding == SqlWriterConfig.LineFolding.STEP);
      }
  
      protected void before() {
        if (!open.isEmpty()) {
          keyword(open);
        }
      }
  
      protected void after() {
      }
  
      protected void sep(boolean printFirst, String sep) {
        if (itemCount == 0) {
          if (newlineAfterOpen) {
            newlineAndIndent(currentIndent);
          }
        } else {
          if (newlineBeforeSep) {
            newlineAndIndent(currentIndent + sepIndent);
          }
        }
        if ((itemCount > 0) || printFirst) {
          keyword(sep);
          nextWhitespace = newlineAfterSep ? NL : " ";
        }
        ++itemCount;
      }
  
      /** Returns the extra indent required for a given type of sub-frame. */
      int extraIndent(FrameType subFrameType) {
        if (this.frameType == FrameTypeEnum.ORDER_BY
            && subFrameType == FrameTypeEnum.ORDER_BY_LIST) {
          return config.indentation();
        }
        if (subFrameType.needsIndent()) {
          return extraIndent;
        }
        return 0;
      }
  
      void list(SqlNodeList list, SqlBinaryOperator sepOp) {
        final Save save;
        switch (lineFolding) {
        case CHOP:
        case FOLD:
          save = new Save();
          if (!list2(list, sepOp)) {
            save.restore();
            final boolean newlineAfterOpen = config.clauseEndsLine();
            final SqlWriterConfig.LineFolding lineFolding;
            final int chopLimit;
            if (this.lineFolding == SqlWriterConfig.LineFolding.CHOP) {
              lineFolding = SqlWriterConfig.LineFolding.TALL;
              chopLimit = -1;
            } else {
              lineFolding = SqlWriterConfig.LineFolding.FOLD;
              chopLimit = this.chopLimit;
            }
            final boolean newline =
                lineFolding == SqlWriterConfig.LineFolding.TALL;
            final boolean newlineBeforeSep;
            final boolean newlineAfterSep;
            final int sepIndent;
            if (config.leadingComma() && newline) {
              newlineBeforeSep = true;
              newlineAfterSep = false;
              sepIndent = -", ".length();
            } else if (newline) {
              newlineBeforeSep = false;
              newlineAfterSep = true;
              sepIndent = this.sepIndent;
            } else {
              newlineBeforeSep = false;
              newlineAfterSep = false;
              sepIndent = this.sepIndent;
            }
            final FrameImpl frame2 =
                new FrameImpl(frameType, keyword, open, close, left, extraIndent,
                    chopLimit, lineFolding, newlineAfterOpen, newlineBeforeSep,
                    sepIndent, newlineAfterSep, newlineBeforeClose,
                    newlineAfterClose);
            frame2.list2(list, sepOp);
          }
          break;
        default:
          list2(list, sepOp);
        }
      }
  
      /** Tries to write a list. If the line runs too long, returns false,
       * indicating to retry. */
      private boolean list2(SqlNodeList list, SqlBinaryOperator sepOp) {
        // The precedence pulling on the LHS of a node is the
        // right-precedence of the separator operator. Similarly RHS.
        //
        // At the start and end of the list precedence should be 0, but non-zero
        // precedence is useful, because it forces parentheses around
        // sub-queries and empty lists, e.g. "SELECT a, (SELECT * FROM t), b",
        // "GROUP BY ()".
        final int lprec = sepOp.getRightPrec();
        final int rprec = sepOp.getLeftPrec();
        if (chopLimit < 0) {
          for (SqlNode node : list) {
            sep(false, sepOp.getName());
            node.unparse(SqlPrettyWriter.this, lprec, rprec);
          }
        } else if (newlineBeforeSep) {
          for (SqlNode node : list) {
            sep(false, sepOp.getName());
            final Save prevSize = new Save();
            node.unparse(SqlPrettyWriter.this, lprec, rprec);
            if (column() > chopLimit) {
              if (lineFolding == SqlWriterConfig.LineFolding.CHOP
                  || lineFolding == SqlWriterConfig.LineFolding.TALL) {
                return false;
              }
              prevSize.restore();
              newlineAndIndent();
              node.unparse(SqlPrettyWriter.this, lprec, rprec);
            }
          }
        } else {
          for (int i = 0; i < list.size(); i++) {
            SqlNode node = list.get(i);
            if (i == 0) {
              sep(false, sepOp.getName());
            }
            final Save save = new Save();
            node.unparse(SqlPrettyWriter.this, lprec, rprec);
            if (i + 1 < list.size()) {
              sep(false, sepOp.getName());
            }
            if (column() > chopLimit) {
              switch (lineFolding) {
              case CHOP:
                return false;
              case FOLD:
                if (newlineAfterOpen != config.clauseEndsLine()) {
                  return false;
                }
                break;
              default:
                break;
              }
              save.restore();
              newlineAndIndent();
              node.unparse(SqlPrettyWriter.this, lprec, rprec);
              if (i + 1 < list.size()) {
                sep(false, sepOp.getName());
              }
            }
          }
        }
        return true;
      }
  
      /** Remembers the state of the current frame and writer.
       *
       * <p>You can call {@link #restore} to restore to that state, or just
       * continue. It is useful if you wish to re-try with different options
       * (for example, with lines wrapped). */
      class Save {
        final int bufLength;
  
        Save() {
          this.bufLength = buf.length();
        }
  
        void restore() {
          buf.setLength(bufLength);
        }
      }
    }
  
    /**
     * Helper class which exposes the get/set methods of an object as
     * properties.
     */
    private static class Bean {
      private final SqlPrettyWriter o;
      private final Map<String, Method> getterMethods = new HashMap<>();
      private final Map<String, Method> setterMethods = new HashMap<>();
  
      Bean(SqlPrettyWriter o) {
        this.o = o;
  
        // Figure out the getter/setter methods for each attribute.
        for (Method method : o.getClass().getMethods()) {
          if (method.getName().startsWith("set")
              && (method.getReturnType() == Void.class)
              && (method.getParameterCount() == 1)) {
            String attributeName =
                stripPrefix(
                    method.getName(),
                    3);
            setterMethods.put(attributeName, method);
          }
          if (method.getName().startsWith("get")
              && (method.getReturnType() != Void.class)
              && (method.getParameterCount() == 0)) {
            String attributeName =
                stripPrefix(
                    method.getName(),
                    3);
            getterMethods.put(attributeName, method);
          }
          if (method.getName().startsWith("is")
              && (method.getReturnType() == Boolean.class)
              && (method.getParameterCount() == 0)) {
            String attributeName =
                stripPrefix(
                    method.getName(),
                    2);
            getterMethods.put(attributeName, method);
          }
        }
      }
  
      private static String stripPrefix(String name, int offset) {
        return name.substring(offset, offset + 1).toLowerCase(Locale.ROOT)
            + name.substring(offset + 1);
      }
  
      public void set(String name, String value) {
        final Method method =
            requireNonNull(setterMethods.get(name),
                () -> "setter method " + name + " not found");
        try {
          method.invoke(o, value);
        } catch (IllegalAccessException | InvocationTargetException e) {
          throw Util.throwAsRuntime(Util.causeOrSelf(e));
        }
      }
  
      public @Nullable Object get(String name) {
        final Method method =
            requireNonNull(getterMethods.get(name),
                () -> "getter method " + name + " not found");
        try {
          return method.invoke(o);
        } catch (IllegalAccessException | InvocationTargetException e) {
          throw Util.throwAsRuntime(Util.causeOrSelf(e));
        }
      }
  
      public String[] getPropertyNames() {
        final Set<String> names = new HashSet<>();
        names.addAll(getterMethods.keySet());
        names.addAll(setterMethods.keySet());
        return names.toArray(new String[0]);
      }
    }
  
  }
  