content: /*
   * Copyright 2016 Google Inc.
   *
   * Licensed 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 com.google.googlejavaformat.java.javadoc;
  
  import static com.google.common.base.Preconditions.checkNotNull;
  import static com.google.common.collect.Comparators.max;
  import static com.google.common.collect.Sets.immutableEnumSet;
  import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.AUTO_INDENT;
  import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.NO_AUTO_INDENT;
  import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.BLANK_LINE;
  import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NEWLINE;
  import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE;
  import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE;
  import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG;
  import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG;
  import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
  
  import com.google.common.collect.ImmutableSet;
  import com.google.googlejavaformat.java.javadoc.Token.Type;
  
  /**
   * Stateful object that accepts "requests" and "writes," producing formatted Javadoc.
   *
   * <p>Our Javadoc formatter doesn't ever generate a parse tree, only a stream of tokens, so the
   * writer must compute and store the answer to questions like "How many levels of nested HTML list
   * are we inside?"
   */
  final class JavadocWriter {
    private final int blockIndent;
    private final StringBuilder output = new StringBuilder();
    /**
     * Whether we are inside an {@code <li>} element, excluding the case in which the {@code <li>}
     * contains a {@code <ul>} or {@code <ol>} that we are also inside -- unless of course we're
     * inside an {@code <li>} element in that inner list :)
     */
    private boolean continuingListItemOfInnermostList;
  
    private boolean continuingFooterTag;
    private final NestingCounter continuingListItemCount = new NestingCounter();
    private final NestingCounter continuingListCount = new NestingCounter();
    private final NestingCounter postWriteModifiedContinuingListCount = new NestingCounter();
    private int remainingOnLine;
    private boolean atStartOfLine;
    private RequestedWhitespace requestedWhitespace = NONE;
    private Token requestedMoeBeginStripComment;
    private int indentForMoeEndStripComment;
    private boolean wroteAnythingSignificant;
  
    JavadocWriter(int blockIndent) {
      this.blockIndent = blockIndent;
    }
  
    /**
     * Requests whitespace between the previously written token and the next written token. The
     * request may be honored, or it may be overridden by a request for "more significant" whitespace,
     * like a newline.
     */
    void requestWhitespace() {
      requestWhitespace(WHITESPACE);
    }
  
    void requestMoeBeginStripComment(Token token) {
      // We queue this up so that we can put it after any requested whitespace.
      requestedMoeBeginStripComment = checkNotNull(token);
    }
  
    void writeBeginJavadoc() {
      /*
       * JavaCommentsHelper will make sure this is indented right. But it seems sensible enough that,
       * if our input starts with ∕✱✱, so too does our output.
       */
      output.append("/**");
      writeNewline();
    }
  
    void writeEndJavadoc() {
      output.append("\n");
      appendSpaces(blockIndent + 1);
      output.append("*/");
    }
  
    void writeFooterJavadocTagStart(Token token) {
      // Close any unclosed lists (e.g., <li> without <ul>).
      // TODO(cpovirk): Actually generate </ul>, etc.?
      /*
       * TODO(cpovirk): Also generate </pre> and </table> if appropriate. This is necessary for
       * idempotency in broken Javadoc. (We don't necessarily need that, but full idempotency may be a
       * nice goal, especially if it helps us use a fuzzer to test.) Unfortunately, the writer doesn't
       * currently know which of those tags are open.
       */
      continuingListItemOfInnermostList = false;
      continuingListItemCount.reset();
      continuingListCount.reset();
      /*
       * There's probably no need for this, since its only effect is to disable blank lines in some
       * cases -- and we're doing that already in the footer.
       */
      postWriteModifiedContinuingListCount.reset();
  
      if (!wroteAnythingSignificant) {
        // Javadoc consists solely of tags. This is frowned upon in general but OK for @Overrides.
      } else if (!continuingFooterTag) {
        // First footer tag after a body tag.
        requestBlankLine();
      } else {
        // Subsequent footer tag.
        continuingFooterTag = false;
        requestNewline();
      }
      writeToken(token);
      continuingFooterTag = true;
    }
  
    void writeListOpen(Token token) {
      requestBlankLine();
  
      writeToken(token);
      continuingListItemOfInnermostList = false;
      continuingListCount.increment();
      postWriteModifiedContinuingListCount.increment();
  
      requestNewline();
    }
  
    void writeListClose(Token token) {
      requestNewline();
  
      continuingListItemCount.decrementIfPositive();
      continuingListCount.decrementIfPositive();
      writeToken(token);
      postWriteModifiedContinuingListCount.decrementIfPositive();
  
      requestBlankLine();
    }
  
    void writeListItemOpen(Token token) {
      requestNewline();
  
      if (continuingListItemOfInnermostList) {
        continuingListItemOfInnermostList = false;
        continuingListItemCount.decrementIfPositive();
      }
      writeToken(token);
      continuingListItemOfInnermostList = true;
      continuingListItemCount.increment();
    }
  
    void writeHeaderOpen(Token token) {
      requestBlankLine();
  
      writeToken(token);
    }
  
    void writeHeaderClose(Token token) {
      writeToken(token);
  
      requestBlankLine();
    }
  
    void writeParagraphOpen(Token token) {
      if (!wroteAnythingSignificant) {
        /*
         * The user included an initial <p> tag. Ignore it, and don't request a blank line before the
         * next token.
         */
        return;
      }
  
      requestBlankLine();
  
      writeToken(token);
    }
  
    void writeBlockquoteOpenOrClose(Token token) {
      requestBlankLine();
  
      writeToken(token);
  
      requestBlankLine();
    }
  
    void writePreOpen(Token token) {
      requestBlankLine();
  
      writeToken(token);
    }
  
    void writePreClose(Token token) {
      writeToken(token);
  
      requestBlankLine();
    }
  
    void writeCodeOpen(Token token) {
      writeToken(token);
    }
  
    void writeCodeClose(Token token) {
      writeToken(token);
    }
  
    void writeTableOpen(Token token) {
      requestBlankLine();
  
      writeToken(token);
    }
  
    void writeTableClose(Token token) {
      writeToken(token);
  
      requestBlankLine();
    }
  
    void writeMoeEndStripComment(Token token) {
      writeLineBreakNoAutoIndent();
      appendSpaces(indentForMoeEndStripComment);
  
      // Or maybe just "output.append(token.getValue())?" I'm kind of surprised this is so easy.
      writeToken(token);
  
      requestNewline();
    }
  
    void writeHtmlComment(Token token) {
      requestNewline();
  
      writeToken(token);
  
      requestNewline();
    }
  
    void writeBr(Token token) {
      writeToken(token);
  
      requestNewline();
    }
  
    void writeLineBreakNoAutoIndent() {
      writeNewline(NO_AUTO_INDENT);
    }
  
    void writeLiteral(Token token) {
      writeToken(token);
    }
  
    @Override
    public String toString() {
      return output.toString();
    }
  
    private void requestBlankLine() {
      requestWhitespace(BLANK_LINE);
    }
  
    private void requestNewline() {
      requestWhitespace(NEWLINE);
    }
  
    private void requestWhitespace(RequestedWhitespace requestedWhitespace) {
      this.requestedWhitespace = max(requestedWhitespace, this.requestedWhitespace);
    }
  
    /**
     * The kind of whitespace that has been requested between the previous and next tokens. The order
     * of the values is significant: It goes from lowest priority to highest. For example, if the
     * previous token requests {@link #BLANK_LINE} after it but the next token requests only {@link
     * #NEWLINE} before it, we insert {@link #BLANK_LINE}.
     */
    enum RequestedWhitespace {
      NONE,
      WHITESPACE,
      NEWLINE,
      BLANK_LINE,
      ;
    }
  
    private void writeToken(Token token) {
      if (requestedMoeBeginStripComment != null) {
        requestNewline();
      }
  
      if (requestedWhitespace == BLANK_LINE
          && (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) {
        /*
         * We don't write blank lines inside lists or footer tags, even in cases where we otherwise
         * would (e.g., before a <p> tag). Justification: We don't write blank lines _between_ list
         * items or footer tags, so it would be strange to write blank lines _within_ one. Of course,
         * an alternative approach would be to go ahead and write blank lines between items/tags,
         * either always or only in the case that an item contains a blank line.
         */
        requestedWhitespace = NEWLINE;
      }
  
      if (requestedWhitespace == BLANK_LINE) {
        writeBlankLine();
        requestedWhitespace = NONE;
      } else if (requestedWhitespace == NEWLINE) {
        writeNewline();
        requestedWhitespace = NONE;
      }
      boolean needWhitespace = (requestedWhitespace == WHITESPACE);
  
      /*
       * Write a newline if necessary to respect the line limit. (But if we're at the beginning of the
       * line, a newline won't help. Or it might help but only by separating "<p>veryverylongword,"
       * which goes against our style.)
       */
      if (!atStartOfLine && token.length() + (needWhitespace ? 1 : 0) > remainingOnLine) {
        writeNewline();
      }
      if (!atStartOfLine && needWhitespace) {
        output.append(" ");
        remainingOnLine--;
      }
  
      if (requestedMoeBeginStripComment != null) {
        output.append(requestedMoeBeginStripComment.getValue());
        requestedMoeBeginStripComment = null;
        indentForMoeEndStripComment = innerIndent();
        requestNewline();
        writeToken(token);
        return;
      }
  
      output.append(token.getValue());
  
      if (!START_OF_LINE_TOKENS.contains(token.getType())) {
        atStartOfLine = false;
      }
  
      /*
       * TODO(cpovirk): We really want the number of "characters," not chars. Figure out what the
       * right way of measuring that is (grapheme count (with BreakIterator?)? sum of widths of all
       * graphemes? I don't think that our style guide is specific about this.). Moreover, I am
       * probably brushing other problems with surrogates, etc. under the table. Hopefully I mostly
       * get away with it by joining all non-space, non-tab characters together.
       *
       * Possibly the "width" question has no right answer:
       * http://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html
       */
      remainingOnLine -= token.length();
      requestedWhitespace = NONE;
      wroteAnythingSignificant = true;
    }
  
    private void writeBlankLine() {
      output.append("\n");
      appendSpaces(blockIndent + 1);
      output.append("*");
      writeNewline();
    }
  
    private void writeNewline() {
      writeNewline(AUTO_INDENT);
    }
  
    private void writeNewline(AutoIndent autoIndent) {
      output.append("\n");
      appendSpaces(blockIndent + 1);
      output.append("*");
      appendSpaces(1);
      remainingOnLine = JavadocFormatter.MAX_LINE_LENGTH - blockIndent - 3;
      if (autoIndent == AUTO_INDENT) {
        appendSpaces(innerIndent());
        remainingOnLine -= innerIndent();
      }
      atStartOfLine = true;
    }
  
    enum AutoIndent {
      AUTO_INDENT,
      NO_AUTO_INDENT
    }
  
    private int innerIndent() {
      int innerIndent = continuingListItemCount.value() * 4 + continuingListCount.value() * 2;
      if (continuingFooterTag) {
        innerIndent += 4;
      }
      return innerIndent;
    }
  
    // If this is a hotspot, keep a String of many spaces around, and call append(string, start, end).
    private void appendSpaces(int count) {
      output.append(" ".repeat(count));
    }
  
    /**
     * Tokens that are always pinned to the following token. For example, {@code <p>} in {@code <p>Foo
     * bar} (never {@code <p> Foo bar} or {@code <p>\nFoo bar}).
     *
     * <p>This is not the only kind of "pinning" that we do: See also the joining of LITERAL tokens
     * done by the lexer. The special pinning here is necessary because these tokens are not of type
     * LITERAL (because they require other special handling).
     */
    private static final ImmutableSet<Type> START_OF_LINE_TOKENS =
        immutableEnumSet(LIST_ITEM_OPEN_TAG, PARAGRAPH_OPEN_TAG, HEADER_OPEN_TAG);
  }
  