/**
 * 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
 *
 *     https://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.avro.compiler.idl;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility class with {@code ThreadLocal} fields that allow the generated
 * classes {@link Idl} and {@link IdlTokenManager} to exchange documentation
 * comments without forcing explicit parsing of documentation comments.
 *
 * The reason this works is that all calls to this class happen within a call to
 * the method {@link Idl#CompilationUnit()} (either directly or indirectly).
 */
public class DocCommentHelper {
  /**
   * Pattern to match the common whitespace indents in a multi-line String.
   * Doesn't match a single-line String, fully matches any multi-line String.
   *
   * To use: match on a {@link String#trim() trimmed} String, and then replace all
   * newlines followed by the group "indent" with a newline.
   */
  private static final Pattern WS_INDENT = Pattern.compile("(?U).*\\R(?<indent>\\h*).*(?:\\R\\k<indent>.*)*");
  /**
   * Pattern to match the whitespace indents plus common stars (1 or 2) in a
   * multi-line String. If a String fully matches, replace all occurrences of a
   * newline followed by whitespace and then the group "stars" with a newline.
   *
   * Note: partial matches are invalid.
   */
  private static final Pattern STAR_INDENT = Pattern.compile("(?U)(?<stars>\\*{1,2}).*(?:\\R\\h*\\k<stars>.*)*");

  private static final ThreadLocal<DocComment> DOC = new ThreadLocal<>();
  private static final ThreadLocal<List<String>> WARNINGS = ThreadLocal.withInitial(ArrayList::new);

  /**
   * Return all warnings that were encountered while parsing, once. Subsequent
   * calls before parsing again will return an empty list.
   */
  static List<String> getAndClearWarnings() {
    List<String> warnings = WARNINGS.get();
    WARNINGS.remove();
    return warnings;
  }

  static void setDoc(Token token) {
    DocComment newDocComment = new DocComment(token);
    DocComment oldDocComment = DOC.get();
    if (oldDocComment != null) {
      WARNINGS.get()
          .add(String.format(
              "Found documentation comment at line %d, column %d. Ignoring previous one at line %d, column %d: \"%s\"\n"
                  + "Did you mean to use a multiline comment ( /* ... */ ) instead?",
              newDocComment.line, newDocComment.column, oldDocComment.line, oldDocComment.column, oldDocComment.text));
    }
    DOC.set(newDocComment);
  }

  /**
   * Clear any documentation (and generate a warning if there was).
   *
   * This method should NOT be used after an optional component in a grammar
   * (i.e., after a @code{[…]} or @code{…*} construct), because the optional
   * grammar part may have already caused parsing a doc comment special token
   * placed after the code block.
   */
  static void clearDoc() {
    DocComment oldDocComment = DOC.get();
    if (oldDocComment != null) {
      WARNINGS.get()
          .add(String.format(
              "Ignoring out-of-place documentation comment at line %d, column %d: \"%s\"\n"
                  + "Did you mean to use a multiline comment ( /* ... */ ) instead?",
              oldDocComment.line, oldDocComment.column, oldDocComment.text));
    }
    DOC.remove();
  }

  static String getDoc() {
    DocComment docComment = DOC.get();
    DOC.remove();
    return docComment == null ? null : docComment.text;
  }

  /* Package private to facilitate testing */
  static String stripIndents(String doc) {
    Matcher starMatcher = STAR_INDENT.matcher(doc);
    if (starMatcher.matches()) {
      return doc.replaceAll("(?U)(?:^|(\\R)\\h*)\\Q" + starMatcher.group("stars") + "\\E\\h?", "$1");
    }

    Matcher whitespaceMatcher = WS_INDENT.matcher(doc);
    if (whitespaceMatcher.matches()) {
      return doc.replaceAll("(?U)(\\R)" + whitespaceMatcher.group("indent"), "$1");
    }

    return doc;
  }

  private static class DocComment {
    private final String text;
    private final int line;
    private final int column;

    DocComment(Token token) {
      // The token is everything after the initial '/**', including all
      // whitespace and the ending '*/'
      int tokenLength = token.image.length();
      this.text = stripIndents(token.image.substring(0, tokenLength - 2).trim());
      this.line = token.beginLine;
      // The preceding token was "/**", and the current token includes
      // everything since (also all whitespace). Thus, we can safely subtract 3
      // from the token column to get the start of the doc comment.
      this.column = token.beginColumn - 3;
    }
  }
}
