/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.facebook.ktfmt.format

import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.kdoc.psi.impl.KDocImpl
import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink
import org.jetbrains.kotlin.kdoc.psi.impl.KDocName
import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection
import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtImportList
import org.jetbrains.kotlin.psi.KtPackageDirective
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType

internal class RedundantImportDetector(val enabled: Boolean) {
  companion object {
    private val OPERATORS =
        setOf(
            // Unary prefix operators
            "unaryPlus",
            "unaryMinus",
            "not",
            // Increments and decrements
            "inc",
            "dec",
            // Arithmetic operators
            "plus",
            "minus",
            "times",
            "div",
            "rem",
            "mod", // deprecated
            "rangeTo",
            // 'In' operator
            "contains",
            // Indexed access operator
            "get",
            "set",
            // Invoke operator
            "invoke",
            // Augmented assignments
            "plusAssign",
            "minusAssign",
            "timesAssign",
            "divAssign",
            "remAssign",
            "modAssign", // deprecated
            // Equality and inequality operators
            "equals",
            // Comparison operators
            "compareTo",
            // Iterator operators
            "iterator",
            "next",
            "hasNext",
            // Bitwise operators
            "and",
            "or",
            // Property delegation operators
            "getValue",
            "setValue",
            "provideDelegate")

    private val COMPONENT_OPERATOR_REGEX = Regex("component\\d+")

    private val KDOC_TAG_SKIP_FIRST_REFERENCE_REGEX = Regex("^@(param|property) (.+)")
  }

  private var thisPackage: FqName? = null

  private val usedReferences = OPERATORS.toMutableSet()

  private lateinit var importCleanUpCandidates: Set<KtImportDirective>

  private var isPackageElement = false
  private var isImportElement = false

  fun takePackageDirective(directive: KtPackageDirective, superBlock: () -> Unit) {
    if (!enabled) {
      return superBlock.invoke()
    }

    thisPackage = directive.fqName

    isPackageElement = true
    superBlock.invoke()
    isPackageElement = false
  }

  fun takeImportList(importList: KtImportList, superBlock: () -> Unit) {
    if (!enabled) {
      return superBlock.invoke()
    }

    importCleanUpCandidates =
        importList.imports
            .filter { import ->
              import.isValidImport &&
                  !import.isAllUnder &&
                  import.identifier != null &&
                  requireNotNull(import.identifier) !in OPERATORS &&
                  !COMPONENT_OPERATOR_REGEX.matches(import.identifier.orEmpty())
            }
            .toSet()

    isImportElement = true
    superBlock.invoke()
    isImportElement = false
  }

  fun takeKdoc(kdoc: KDocImpl) {
    kdoc.getChildrenOfType<KDocSection>().forEach { kdocSection ->
      val tagLinks =
          kdocSection.getChildrenOfType<KDocTag>().flatMap { tag ->
            val tagLinks = tag.getChildrenOfType<KDocLink>().toList()
            when {
              KDOC_TAG_SKIP_FIRST_REFERENCE_REGEX.matches(tag.text) -> tagLinks.drop(1)
              else -> tagLinks
            }
          }

      val links = kdocSection.getChildrenOfType<KDocLink>() + tagLinks

      val references =
          links.flatMap { link ->
            link.getChildrenOfType<KDocName>().mapNotNull {
              it.getQualifiedName().firstOrNull()?.trim('[', ']')
            }
          }

      usedReferences += references
    }
  }

  fun takeReferenceExpression(expression: KtReferenceExpression) {
    if (!enabled) return

    if (!isPackageElement && !isImportElement && expression.children.isEmpty()) {
      usedReferences += expression.text.trim('`')
    }
  }

  fun getRedundantImportElements(): List<PsiElement> {
    if (!enabled) return emptyList()

    val redundantImports = mutableListOf<PsiElement>()

    // Collect unused imports
    for (import in importCleanUpCandidates) {
      val isUnused = import.aliasName !in usedReferences && import.identifier !in usedReferences
      val isFromSamePackage = import.importedFqName?.parent() == thisPackage && import.alias == null
      if (isUnused || isFromSamePackage) {
        redundantImports += import
      }
    }

    return redundantImports
  }

  private inline val KtImportDirective.identifier: String?
    get() = importPath?.importedName?.identifier?.trim('`')
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.facebook.ktfmt.format

import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.psi.KtContainerNodeForControlStructureBody
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtEnumEntry
import org.jetbrains.kotlin.psi.KtIfExpression
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.KtStringTemplateEntry
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import org.jetbrains.kotlin.psi.KtWhileExpression
import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments
import org.jetbrains.kotlin.psi.psiUtil.prevLeaf
import org.jetbrains.kotlin.psi.psiUtil.siblings

internal class RedundantSemicolonDetector {
  private val extraSemicolons = mutableListOf<PsiElement>()

  fun getRedundantSemicolonElements(): List<PsiElement> = extraSemicolons

  /** returns **true** if this element was an extra comma, **false** otherwise. */
  fun takeElement(element: PsiElement, superBlock: () -> Unit) {
    if (isExtraSemicolon(element)) {
      extraSemicolons += element
    } else {
      superBlock.invoke()
    }
  }

  private fun isExtraSemicolon(element: PsiElement): Boolean {
    if (element.text != ";") {
      return false
    }

    val parent = element.parent
    if (parent is KtStringTemplateExpression || parent is KtStringTemplateEntry) {
      return false
    }
    if (parent is KtEnumEntry &&
        parent.siblings(forward = true, withItself = false).any { it is KtDeclaration }) {
      return false
    }

    val prevLeaf = element.prevLeaf(false)
    val prevConcreteSibling = element.getPrevSiblingIgnoringWhitespaceAndComments()
    if ((prevConcreteSibling is KtIfExpression || prevConcreteSibling is KtWhileExpression) &&
        prevLeaf is KtContainerNodeForControlStructureBody &&
        prevLeaf.text.isEmpty()) {
      return false
    }

    val nextConcreteSibling = element.getNextSiblingIgnoringWhitespaceAndComments()
    if (nextConcreteSibling is KtLambdaExpression) {
      /**
       * Example: `val x = foo(0) ; { dead -> lambda }`
       *
       * There are a huge number of cases here because the trailing lambda syntax is so flexible.
       * Therefore, we just assume that all semicolons followed by lambdas are meaningful. The cases
       * where they could be removed are too rare to justify the risk of changing behaviour.
       */
      return false
    }

    return true
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.facebook.ktfmt.format

import java.util.regex.Pattern
import org.jetbrains.kotlin.com.intellij.psi.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset

/**
 * Tokenizer traverses a Kotlin parse tree (which blessedly contains whitespaces and comments,
 * unlike Javac) and constructs a list of 'Tok's.
 *
 * <p>The google-java-format infra expects newline Toks to be separate from maximal-whitespace Toks,
 * but Kotlin emits them together. So, we split them using Java's \R regex matcher. We don't use
 * 'split' et al. because we want Toks for the newlines themselves.
 */
class Tokenizer(private val fileText: String, val file: KtFile) : KtTreeVisitorVoid() {

  companion object {
    private val WHITESPACE_NEWLINE_REGEX: Pattern = Pattern.compile("\\R|( )+")
  }

  val toks = mutableListOf<KotlinTok>()
  var index = 0

  override fun visitElement(element: PsiElement) {
    val startIndex = element.startOffset
    when (element) {
      is PsiComment -> {
        toks.add(
            KotlinTok(
                index,
                fileText.substring(startIndex, element.endOffset),
                element.text,
                startIndex,
                0,
                false,
                KtTokens.EOF))
        index++
        return
      }
      is KtStringTemplateExpression -> {
        toks.add(
            KotlinTok(
                index,
                WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone(
                    fileText.substring(startIndex, element.endOffset)),
                element.text,
                startIndex,
                0,
                true,
                KtTokens.EOF))
        index++
        return
      }
      is LeafPsiElement -> {
        val elementText = element.text
        val endIndex = element.endOffset
        if (element is PsiWhiteSpace) {
          val matcher = WHITESPACE_NEWLINE_REGEX.matcher(elementText)
          while (matcher.find()) {
            val text = matcher.group()
            toks.add(
                KotlinTok(
                    -1,
                    fileText.substring(startIndex + matcher.start(), startIndex + matcher.end()),
                    text,
                    startIndex + matcher.start(),
                    0,
                    false,
                    KtTokens.EOF))
          }
        } else {
          toks.add(
              KotlinTok(
                  index,
                  fileText.substring(startIndex, endIndex),
                  elementText,
                  startIndex,
                  0,
                  true,
                  KtTokens.EOF))
          index++
        }
      }
    }
    super.visitElement(element)
  }
}


/*
 * Copyright 2015 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.
 */

// This was copied from https://github.com/google/google-java-format and converted to Kotlin,
// because the original is package-private.

package com.facebook.ktfmt.format

import com.google.common.base.Verify
import java.util.Optional

/** Heuristics for classifying qualified names as types. */
object TypeNameClassifier {

  /** A state machine for classifying qualified names. */
  private enum class TyParseState(val isSingleUnit: Boolean) {

    /** The start state. */
    START(false) {
      override fun next(n: JavaCaseFormat): TyParseState {
        return when (n) {
          JavaCaseFormat.UPPERCASE ->
              // if we see an UpperCamel later, assume this was a class
              // e.g. com.google.FOO.Bar
              AMBIGUOUS
          JavaCaseFormat.LOWER_CAMEL -> REJECT
          JavaCaseFormat.LOWERCASE ->
              // could be a package
              START
          JavaCaseFormat.UPPER_CAMEL -> TYPE
        }
      }
    },

    /** The current prefix is a type. */
    TYPE(true) {
      override fun next(n: JavaCaseFormat): TyParseState {
        return when (n) {
          JavaCaseFormat.UPPERCASE,
          JavaCaseFormat.LOWER_CAMEL,
          JavaCaseFormat.LOWERCASE -> FIRST_STATIC_MEMBER
          JavaCaseFormat.UPPER_CAMEL -> TYPE
        }
      }
    },

    /** The current prefix is a type, followed by a single static member access. */
    FIRST_STATIC_MEMBER(true) {
      override fun next(n: JavaCaseFormat): TyParseState {
        return REJECT
      }
    },

    /** Anything not represented by one of the other states. */
    REJECT(false) {
      override fun next(n: JavaCaseFormat): TyParseState {
        return REJECT
      }
    },

    /** An ambiguous type prefix. */
    AMBIGUOUS(false) {
      override fun next(n: JavaCaseFormat): TyParseState {
        return when (n) {
          JavaCaseFormat.UPPERCASE -> AMBIGUOUS
          JavaCaseFormat.LOWER_CAMEL,
          JavaCaseFormat.LOWERCASE -> REJECT
          JavaCaseFormat.UPPER_CAMEL -> TYPE
        }
      }
    };

    /** Transition function. */
    abstract fun next(n: JavaCaseFormat): TyParseState
  }

  /**
   * Returns the end index (inclusive) of the longest prefix that matches the naming conventions of
   * a type or static field access, or -1 if no such prefix was found.
   *
   * Examples:
   * * ClassName
   * * ClassName.staticMemberName
   * * com.google.ClassName.InnerClass.staticMemberName
   */
  internal fun typePrefixLength(nameParts: List<String>): Optional<Int> {
    var state = TyParseState.START
    var typeLength = Optional.empty<Int>()
    for (i in nameParts.indices) {
      state = state.next(JavaCaseFormat.from(nameParts[i]))
      if (state === TyParseState.REJECT) {
        break
      }
      if (state.isSingleUnit) {
        typeLength = Optional.of(i)
      }
    }
    return typeLength
  }

  /** Case formats used in Java identifiers. */
  enum class JavaCaseFormat {
    UPPERCASE,
    LOWERCASE,
    UPPER_CAMEL,
    LOWER_CAMEL;

    companion object {

      /** Classifies an identifier's case format. */
      internal fun from(name: String): JavaCaseFormat {
        Verify.verify(name.isNotEmpty())
        var firstUppercase = false
        var hasUppercase = false
        var hasLowercase = false
        var first = true
        for (char in name) {
          if (!Character.isAlphabetic(char.code)) {
            continue
          }
          if (first) {
            firstUppercase = Character.isUpperCase(char)
            first = false
          }
          hasUppercase = hasUppercase or Character.isUpperCase(char)
          hasLowercase = hasLowercase or Character.isLowerCase(char)
        }
        return if (firstUppercase) {
          if (hasLowercase) UPPER_CAMEL else UPPERCASE
        } else {
          if (hasUppercase) LOWER_CAMEL else LOWERCASE
        }
      }
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.facebook.ktfmt.format

import java.util.regex.Pattern
import java.util.regex.Pattern.MULTILINE

object WhitespaceTombstones {
  /** See [replaceTrailingWhitespaceWithTombstone]. */
  const val SPACE_TOMBSTONE = '\u0003'

  fun String.indexOfWhitespaceTombstone() = this.indexOf(SPACE_TOMBSTONE)

  /**
   * Google-java-format removes trailing spaces when it emits formatted code, which is a problem for
   * multiline string literals. We trick it by replacing the last trailing space in such cases with
   * a tombstone, a character that's unlikely to be used in a regular program. After formatting, we
   * replace it back to a space.
   */
  fun replaceTrailingWhitespaceWithTombstone(s: String): String {
    return Pattern.compile(" ($)", MULTILINE).matcher(s).replaceAll("$SPACE_TOMBSTONE$1")
  }

  /** See [replaceTrailingWhitespaceWithTombstone]. */
  fun replaceTombstoneWithTrailingWhitespace(s: String): String {
    return s.replace(SPACE_TOMBSTONE, ' ')
  }
}


/*
 * Copyright (c) Tor Norbye.
 *
 * 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.facebook.ktfmt.kdoc

enum class CommentType(
    /** The opening string of the comment. */
    val prefix: String,
    /** The closing string of the comment. */
    val suffix: String,
    /** For multi line comments, the prefix at each comment line after the first one. */
    val linePrefix: String
) {
  KDOC("/**", "*/", " * "),
  BLOCK("/*", "*/", ""),
  LINE("//", "", "// ");

  /**
   * The number of characters needed to fit a comment on a line: the prefix, suffix and a single
   * space padding inside these.
   */
  fun singleLineOverhead(): Int {
    return prefix.length + suffix.length + 1 + if (suffix.isEmpty()) 0 else 1
  }

  /**
   * The number of characters required in addition to the line comment for each line in a multi line
   * comment.
   */
  fun lineOverhead(): Int {
    return linePrefix.length
  }
}

fun String.isKDocComment(): Boolean = startsWith("/**")

fun String.isBlockComment(): Boolean = startsWith("/*") && !startsWith("/**")

fun String.isLineComment(): Boolean = startsWith("//")

fun String.commentType(): CommentType {
  return if (isKDocComment()) {
    CommentType.KDOC
  } else if (isBlockComment()) {
    CommentType.BLOCK
  } else if (isLineComment()) {
    CommentType.LINE
  } else {
    error("Not a comment: $this")
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.facebook.ktfmt.kdoc

object Escaping {

  private const val SLASH_STAR_ESCAPE = "\u0004\u0005"

  private const val STAR_SLASH_ESCAPE = "\u0005\u0004"

  fun indexOfCommentEscapeSequences(s: String) =
      s.indexOfAny(listOf(SLASH_STAR_ESCAPE, STAR_SLASH_ESCAPE))

  /**
   * kotlin-compiler's KDoc lexer doesn't correctly handle nested slash-star comments, so we escape
   * them into tombstones, format, then unescape.
   */
  fun escapeKDoc(s: String): String {
    val startMarkerIndex = s.indexOf("/*")
    val endMarkerIndex = s.lastIndexOf("*/")

    if (startMarkerIndex == -1 || endMarkerIndex == -1) {
      throw RuntimeException("KDoc with no /** and/or */")
    }

    return s.substring(0, startMarkerIndex + 3) +
        s.substring(startMarkerIndex + 3, endMarkerIndex)
            .replace("/*", SLASH_STAR_ESCAPE)
            .replace("*/", STAR_SLASH_ESCAPE) +
        s.substring(endMarkerIndex)
  }

  /** See [escapeKDoc]. */
  fun unescapeKDoc(s: String): String =
      s.replace(SLASH_STAR_ESCAPE, "/*").replace(STAR_SLASH_ESCAPE, "*/")
}


/*
 * Copyright (c) Tor Norbye.
 *
 * 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.facebook.ktfmt.kdoc

class FormattingTask(
    /** Options to format with */
    var options: KDocFormattingOptions,

    /** The original comment to be formatted */
    var comment: String,

    /**
     * The initial indentation on the first line of the KDoc. The reformatted comment will prefix
     * each subsequent line with this string.
     */
    var initialIndent: String,

    /**
     * Indent to use after the first line.
     *
     * This is useful when the comment starts the end of an existing code line. For example,
     * something like this:
     * ```
     *     if (foo.bar.baz()) { // This comment started at column 25
     *         // but the second and subsequent lines are indented 8 spaces
     *         // ...
     * ```
     *
     * (This doesn't matter much for KDoc comments, since the formatter will always push these into
     * their own lines so the indents will match, but for line and block comments it can matter.)
     */
    var secondaryIndent: String = initialIndent,

    /**
     * Optional list of parameters associated with this doc; if set, and if
     * [KDocFormattingOptions.orderDocTags] is set, parameter doc tags will be sorted to match this
     * order. (The intent is for the tool invoking KDocFormatter to pass in the parameter names in
     * signature order here.)
     */
    var orderedParameterNames: List<String> = emptyList(),

    /** The type of comment being formatted. */
    val type: CommentType = comment.commentType()
)


/*
 * Copyright 2015 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.
 */

/*
 * This was copied from https://github.com/google/google-java-format and modified extensively to
 * work for Kotlin formatting
 */

package com.facebook.ktfmt.kdoc

import com.google.common.base.CharMatcher
import com.google.common.base.Strings
import com.google.googlejavaformat.CommentsHelper
import com.google.googlejavaformat.Input.Tok
import com.google.googlejavaformat.Newlines
import com.google.googlejavaformat.java.Formatter
import java.util.ArrayList
import java.util.regex.Pattern

/** `KDocCommentsHelper` extends [CommentsHelper] to rewrite KDoc comments. */
class KDocCommentsHelper(private val lineSeparator: String, maxLineLength: Int) : CommentsHelper {

  private val kdocFormatter =
      KDocFormatter(
          KDocFormattingOptions(maxLineLength, maxLineLength).also {
            it.allowParamBrackets = true // TODO Do we want this?
            it.convertMarkup = false
            it.nestedListIndent = 4
            it.optimal = false // Use greedy line breaking for predictability.
          })

  override fun rewrite(tok: Tok, maxWidth: Int, column0: Int): String {
    if (!tok.isComment) {
      return tok.originalText
    }
    var text = tok.originalText
    if (tok.isJavadocComment) {
      text = kdocFormatter.reformatComment(text, " ".repeat(column0))
    }
    val lines = ArrayList<String>()
    val it = Newlines.lineIterator(text)
    while (it.hasNext()) {
      lines.add(CharMatcher.whitespace().trimTrailingFrom(it.next()))
    }
    return if (tok.isSlashSlashComment) {
      indentLineComments(lines, column0)
    } else if (javadocShaped(lines)) {
      indentJavadoc(lines, column0)
    } else {
      preserveIndentation(lines, column0)
    }
  }

  // For non-javadoc-shaped block comments, shift the entire block to the correct
  // column, but do not adjust relative indentation.
  private fun preserveIndentation(lines: List<String>, column0: Int): String {
    val builder = StringBuilder()

    // find the leftmost non-whitespace character in all trailing lines
    var startCol = -1
    for (i in 1 until lines.size) {
      val lineIdx = CharMatcher.whitespace().negate().indexIn(lines[i])
      if (lineIdx >= 0 && (startCol == -1 || lineIdx < startCol)) {
        startCol = lineIdx
      }
    }

    // output the first line at the current column
    builder.append(lines[0])

    // output all trailing lines with plausible indentation
    for (i in 1 until lines.size) {
      builder.append(lineSeparator).append(Strings.repeat(" ", column0))
      // check that startCol is valid index, e.g. for blank lines
      if (lines[i].length >= startCol) {
        builder.append(lines[i].substring(startCol))
      } else {
        builder.append(lines[i])
      }
    }
    return builder.toString()
  }

  // Wraps and re-indents line comments.
  private fun indentLineComments(lines: List<String>, column0: Int): String {
    val wrappedLines = wrapLineComments(lines, column0)
    val builder = StringBuilder()
    builder.append(wrappedLines[0].trim())
    val indentString = Strings.repeat(" ", column0)
    for (i in 1 until wrappedLines.size) {
      builder.append(lineSeparator).append(indentString).append(wrappedLines[i].trim())
    }
    return builder.toString()
  }

  private fun wrapLineComments(lines: List<String>, column0: Int): List<String> {
    val result = ArrayList<String>()
    for (originalLine in lines) {
      var line = originalLine
      // Add missing leading spaces to line comments: `//foo` -> `// foo`.
      val matcher = LINE_COMMENT_MISSING_SPACE_PREFIX.matcher(line)
      if (matcher.find()) {
        val length = matcher.group(1).length
        line = Strings.repeat("/", length) + " " + line.substring(length)
      }
      if (line.startsWith("// MOE:")) {
        // don't wrap comments for https://github.com/google/MOE
        result.add(line)
        continue
      }
      while (line.length + column0 > Formatter.MAX_LINE_LENGTH) {
        var idx = Formatter.MAX_LINE_LENGTH - column0
        // only break on whitespace characters, and ignore the leading `// `
        while (idx >= 2 && !CharMatcher.whitespace().matches(line[idx])) {
          idx--
        }
        if (idx <= 2) {
          break
        }
        result.add(line.substring(0, idx))
        line = "//" + line.substring(idx)
      }
      result.add(line)
    }
    return result
  }

  // Remove leading whitespace (trailing was already removed), and re-indent.
  // Add a +1 indent before '*', and add the '*' if necessary.
  private fun indentJavadoc(lines: List<String>, column0: Int): String {
    val builder = StringBuilder()
    builder.append(lines[0].trim())
    val indent = column0 + 1
    val indentString = Strings.repeat(" ", indent)
    for (i in 1 until lines.size) {
      builder.append(lineSeparator).append(indentString)
      val line = lines[i].trim()
      if (!line.startsWith("*")) {
        builder.append("* ")
      }
      builder.append(line)
    }
    return builder.toString()
  }

  // Preserve special `//noinspection` and `//$NON-NLS-x$` comments used by IDEs, which cannot
  // contain leading spaces.
  private val LINE_COMMENT_MISSING_SPACE_PREFIX =
      Pattern.compile("^(//+)(?!noinspection|\\\$NON-NLS-\\d+\\$)[^\\s/]")

  // Returns true if the comment looks like javadoc
  private fun javadocShaped(lines: List<String>): Boolean {
    val it = lines.iterator()
    if (!it.hasNext()) {
      return false
    }
    val first = it.next().trim()
    // if it's actually javadoc, we're done
    if (first.startsWith("/**")) {
      return true
    }
    // if it's a block comment, check all trailing lines for '*'
    if (!first.startsWith("/*")) {
      return false
    }
    while (it.hasNext()) {
      if (!it.next().trim().startsWith("*")) {
        return false
      }
    }
    return true
  }
}


/*
 * Copyright (c) Tor Norbye.
 *
 * 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.facebook.ktfmt.kdoc

import kotlin.math.min

/** Formatter which can reformat KDoc comments. */
class KDocFormatter(private val options: KDocFormattingOptions) {
  /** Reformats the [comment], which follows the given [initialIndent] string. */
  fun reformatComment(comment: String, initialIndent: String): String {
    return reformatComment(FormattingTask(options, comment, initialIndent))
  }

  fun reformatComment(task: FormattingTask): String {
    val indent = task.secondaryIndent
    val indentSize = getIndentSize(indent, options)
    val firstIndentSize = getIndentSize(task.initialIndent, options)
    val comment = task.comment
    val lineComment = comment.isLineComment()
    val blockComment = comment.isBlockComment()
    val paragraphs = ParagraphListBuilder(comment, options, task).scan(indentSize)
    val commentType = task.type
    val lineSeparator = "\n$indent${commentType.linePrefix}"
    val prefix = commentType.prefix

    // Collapse single line? If alternate is turned on, use the opposite of the
    // setting
    val collapseLine = options.collapseSingleLine.let { if (options.alternate) !it else it }
    if (paragraphs.isSingleParagraph() && collapseLine && !lineComment) {
      // Does the text fit on a single line?
      val trimmed = paragraphs.firstOrNull()?.text?.trim() ?: ""
      // Subtract out space for "/** " and " */" and the indent:
      val width =
          min(
              options.maxLineWidth - firstIndentSize - commentType.singleLineOverhead(),
              options.maxCommentWidth)
      val suffix = if (commentType.suffix.isEmpty()) "" else " ${commentType.suffix}"
      if (trimmed.length <= width) {
        return "$prefix $trimmed$suffix"
      }
      if (indentSize < firstIndentSize) {
        val nextLineWidth =
            min(
                options.maxLineWidth - indentSize - commentType.singleLineOverhead(),
                options.maxCommentWidth)
        if (trimmed.length <= nextLineWidth) {
          return "$prefix $trimmed$suffix"
        }
      }
    }

    val sb = StringBuilder()

    sb.append(prefix)
    if (lineComment) {
      sb.append(' ')
    } else {
      sb.append(lineSeparator)
    }

    for (paragraph in paragraphs) {
      if (paragraph.separate) {
        // Remove trailing spaces which can happen when we have a paragraph
        // separator
        stripTrailingSpaces(lineComment, sb)
        sb.append(lineSeparator)
      }
      val text = paragraph.text
      if (paragraph.preformatted || paragraph.table) {
        sb.append(text)
        // Remove trailing spaces which can happen when we have an empty line in a
        // preformatted paragraph.
        stripTrailingSpaces(lineComment, sb)
        sb.append(lineSeparator)
        continue
      }

      val lineWithoutIndent = options.maxLineWidth - commentType.lineOverhead()
      val quoteAdjustment = if (paragraph.quoted) 2 else 0
      val maxLineWidth =
          min(options.maxCommentWidth, lineWithoutIndent - indentSize) - quoteAdjustment
      val firstMaxLineWidth =
          if (sb.indexOf('\n') == -1) {
            min(options.maxCommentWidth, lineWithoutIndent - firstIndentSize) - quoteAdjustment
          } else {
            maxLineWidth
          }

      val lines = paragraph.reflow(firstMaxLineWidth, maxLineWidth)
      var first = true
      val hangingIndent = paragraph.hangingIndent
      for (line in lines) {
        sb.append(paragraph.indent)
        if (first && !paragraph.continuation) {
          first = false
        } else {
          sb.append(hangingIndent)
        }
        if (paragraph.quoted) {
          sb.append("> ")
        }
        if (line.isEmpty()) {
          // Remove trailing spaces which can happen when we have a paragraph
          // separator
          stripTrailingSpaces(lineComment, sb)
        } else {
          sb.append(line)
        }
        sb.append(lineSeparator)
      }
    }
    if (!lineComment) {
      if (sb.endsWith("* ")) {
        sb.setLength(sb.length - 2)
      }
      sb.append("*/")
    } else if (sb.endsWith(lineSeparator)) {
      @Suppress("ReturnValueIgnored") sb.removeSuffix(lineSeparator)
    }

    val formatted =
        if (lineComment) {
          sb.trim().removeSuffix("//").trim().toString()
        } else if (blockComment) {
          sb.toString().replace(lineSeparator + "\n", "\n\n")
        } else {
          sb.toString()
        }

    val separatorIndex = comment.indexOf('\n')
    return if (separatorIndex > 0 && comment[separatorIndex - 1] == '\r') {
      // CRLF separator
      formatted.replace("\n", "\r\n")
    } else {
      formatted
    }
  }

  private fun stripTrailingSpaces(lineComment: Boolean, sb: StringBuilder) {
    if (!lineComment && sb.endsWith("* ")) {
      sb.setLength(sb.length - 1)
    } else if (lineComment && sb.endsWith("// ")) {
      sb.setLength(sb.length - 1)
    }
  }
}


/*
 * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 */

/*
 * Copyright (c) Tor Norbye.
 *
 * 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.facebook.ktfmt.kdoc

import kotlin.math.min

/** Options controlling how the [KDocFormatter] will behave. */
class KDocFormattingOptions(
    /** Right hand side margin to write lines at. */
    var maxLineWidth: Int = 72,
    /**
     * Limit comment to be at most [maxCommentWidth] characters even if more would fit on the line.
     */
    var maxCommentWidth: Int = min(maxLineWidth, 72)
) {
  /** Whether to collapse multi-line comments that would fit on a single line into a single line. */
  var collapseSingleLine: Boolean = true

  /** Whether to collapse repeated spaces. */
  var collapseSpaces: Boolean = true

  /** Whether to convert basic markup like **bold** into **bold**, < into <, etc. */
  var convertMarkup: Boolean = true

  /**
   * Whether to add punctuation where missing, such as ending sentences with a period. (TODO: Make
   * sure the FIRST sentence ends with one too! Especially if the subsequent sentence is separated.)
   */
  var addPunctuation: Boolean = false

  /**
   * How many spaces to use for hanging indents in numbered lists and after block tags. Using 4 or
   * more here will result in subsequent lines being interpreted as block formatted by IntelliJ (but
   * not Dokka).
   */
  var hangingIndent: Int = 2

  /** When there are nested lists etc, how many spaces to indent by. */
  var nestedListIndent: Int = 3
    set(value) {
      if (value < 3) {
        error(
            "Nested list indent must be at least 3; if list items are only indented 2 spaces they " +
                "will not be rendered as list items")
      }
      field = value
    }

  /**
   * Don't format with tabs! (See
   * https://kotlinlang.org/docs/reference/coding-conventions.html#formatting)
   *
   * But if you do, this is the tab width.
   */
  var tabWidth: Int = 8

  /** Whether to perform optimal line breaking instead of greeding. */
  var optimal: Boolean = true

  /**
   * If true, reformat markdown tables such that the column markers line up. When false, markdown
   * tables are left alone (except for left hand side cleanup.)
   */
  var alignTableColumns: Boolean = true

  /**
   * If true, moves any kdoc tags to the end of the comment and `@return` tags after `@param` tags.
   */
  var orderDocTags: Boolean = true

  /**
   * If true, perform "alternative" formatting. This is only relevant in the IDE. You can invoke the
   * action repeatedly and it will jump between normal formatting an alternative formatting. For
   * single-line comments it will alternate between single and multiple lines. For longer comments
   * it will alternate between optimal line breaking and greedy line breaking.
   */
  var alternate: Boolean = false

  /**
   * KDoc allows param tag to be specified using an alternate bracket syntax. KDoc formatter ties to
   * unify the format of comments, so it will rewrite them into the canonical syntax unless this
   * option is true.
   */
  var allowParamBrackets: Boolean = false

  /** Creates a copy of this formatting object. */
  fun copy(): KDocFormattingOptions {
    val copy = KDocFormattingOptions()
    copy.maxLineWidth = maxLineWidth
    copy.maxCommentWidth = maxCommentWidth
    copy.collapseSingleLine = collapseSingleLine
    copy.collapseSpaces = collapseSpaces
    copy.hangingIndent = hangingIndent
    copy.tabWidth = tabWidth
    copy.alignTableColumns = alignTableColumns
    copy.orderDocTags = orderDocTags
    copy.addPunctuation = addPunctuation
    copy.convertMarkup = convertMarkup
    copy.nestedListIndent = nestedListIndent
    copy.optimal = optimal
    copy.alternate = alternate

    return copy
  }
}


/*
 * 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.
 */

/*
 * This was copied from https://github.com/google/google-java-format and modified extensively to
 * work for Kotlin formatting
 */

package com.facebook.ktfmt.kdoc

/**
 * KDoc token. Our idea of what constitutes a token is often larger or smaller than what you'd
 * naturally expect. The decision is usually pragmatic rather than theoretical. Most of the details
 * are in [KDocFormatter].
 */
internal class KDocToken(val type: Type, val value: String) {
  /**
   * KDoc token type.
   *
   * The general idea is that every token that requires special handling (extra line breaks,
   * indentation, forcing or forbidding whitespace) from [KDocWriter] gets its own type. But I
   * haven't been super careful about it, so I'd imagine that we could merge or remove some of these
   * if we wanted. (For example, PARAGRAPH_CLOSE_TAG and LIST_ITEM_CLOSE_TAG could share a common
   * IGNORABLE token type. But their corresponding OPEN tags exist, so I've kept the CLOSE tags.)
   *
   * Note, though, that tokens of the same type may still have been handled differently by [ ] when
   * it created them. For example, LITERAL is used for both plain text and inline tags, even though
   * the two affect the lexer's state differently.
   */
  internal enum class Type {
    /** âˆ•âœ±âœ± */
    BEGIN_KDOC,
    /** âœ±âˆ• */
    END_KDOC,
    LIST_ITEM_OPEN_TAG,
    HEADER_OPEN_TAG,
    PARAGRAPH_OPEN_TAG,
    PRE_OPEN_TAG,
    PRE_CLOSE_TAG,
    CODE_OPEN_TAG,
    CODE_CLOSE_TAG,
    TABLE_OPEN_TAG,
    TABLE_CLOSE_TAG,
    /** Things such as `@param` or `@see` */
    TAG,
    /** Code between two markers of three backticks */
    CODE,
    /** three backticks */
    CODE_BLOCK_MARKER,
    /** A link in brackets such as [KDocToken] */
    MARKDOWN_LINK,
    BLANK_LINE,
    /**
     * Whitespace that is not in a `<pre>` or `<table>` section. Whitespace includes leading
     * newlines, asterisks, and tabs and spaces. In the output, it is translated to newlines (with
     * leading spaces and asterisks) or spaces.
     */
    WHITESPACE,
    /**
     * Anything else: `foo`, `<b>`, `{ foo}` etc. [KDocFormatter] sometimes creates adjacent literal
     * tokens, which it then merges into a single, larger literal token before returning its output.
     *
     * This also includes whitespace in a `<pre>` or `<table>` section. We preserve user formatting
     * in these sections, including arbitrary numbers of spaces. By treating such whitespace as a
     * literal, we can merge it with adjacent literals, preventing us from autowrapping inside these
     * sections -- and doing so naively, to boot. The wrapped line would have no indentation after
     * "* " or, possibly worse, it might begin with an arbitrary amount of whitespace that didn't
     * fit on the previous line. Of course, by doing this, we're potentially creating lines of more
     * than 100 characters. But it seems fair to call in the humans to resolve such problems.
     */
    LITERAL
  }

  fun length(): Int = value.length

  override fun toString(): String = "KDocToken{$type: \"$value\"}"
}


/*
 * 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.
 */

/*
 * This was copied from https://github.com/google/google-java-format and modified extensively to
 * work for Kotlin formatting
 */

package com.facebook.ktfmt.kdoc

import com.facebook.ktfmt.kdoc.KDocToken.Type.CODE_BLOCK_MARKER
import com.facebook.ktfmt.kdoc.KDocToken.Type.HEADER_OPEN_TAG
import com.facebook.ktfmt.kdoc.KDocToken.Type.LIST_ITEM_OPEN_TAG
import com.facebook.ktfmt.kdoc.KDocToken.Type.PARAGRAPH_OPEN_TAG
import com.facebook.ktfmt.kdoc.KDocWriter.RequestedWhitespace.BLANK_LINE
import com.facebook.ktfmt.kdoc.KDocWriter.RequestedWhitespace.CONDITIONAL_WHITESPACE
import com.facebook.ktfmt.kdoc.KDocWriter.RequestedWhitespace.NEWLINE
import com.facebook.ktfmt.kdoc.KDocWriter.RequestedWhitespace.NONE
import com.facebook.ktfmt.kdoc.KDocWriter.RequestedWhitespace.WHITESPACE
import com.google.common.base.Strings
import com.google.common.collect.Ordering
import com.google.common.collect.Sets.immutableEnumSet

/**
 * Stateful object that accepts "requests" and "writes," producing formatted Javadoc.
 *
 * 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?"
 */
internal class KDocWriter(blockIndentCount: Int, private val maxLineLength: Int) {

  /**
   * Tokens that are always pinned to the following token. For example, `<p>` in `<p>Foo bar` (never
   * `<p> Foo bar` or `<p>\nFoo bar`).
   *
   * 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 val START_OF_LINE_TOKENS =
      immutableEnumSet(LIST_ITEM_OPEN_TAG, PARAGRAPH_OPEN_TAG, HEADER_OPEN_TAG)

  private val output = StringBuilder()
  private val blockIndent = Strings.repeat(" ", blockIndentCount + 1)

  private var remainingOnLine: Int = 0
  private var atStartOfLine: Boolean = false
  private var inCodeBlock: Boolean = false
  private var requestedWhitespace = NONE

  /**
   * 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.
   */
  fun requestWhitespace() {
    requestWhitespace(WHITESPACE)
  }

  fun 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.
     */
    appendTrackingLength("/**")
  }

  fun writeEndJavadoc() {
    requestCloseCodeBlockMarker()
    output.append("\n")
    appendTrackingLength(blockIndent)
    appendTrackingLength("*/")
  }

  fun writeListItemOpen(token: KDocToken) {
    requestCloseCodeBlockMarker()
    requestNewline()
    writeToken(token)
  }

  fun writePreOpen(token: KDocToken) {
    requestBlankLine()

    writeToken(token)
  }

  fun writePreClose(token: KDocToken) {
    writeToken(token)

    requestBlankLine()
  }

  fun writeCodeOpen(token: KDocToken) {
    writeToken(token)
  }

  fun writeCodeClose(token: KDocToken) {
    writeToken(token)
  }

  fun writeTableOpen(token: KDocToken) {
    requestBlankLine()

    writeToken(token)
  }

  fun writeTableClose(token: KDocToken) {
    writeToken(token)

    requestBlankLine()
  }

  fun writeTag(token: KDocToken) {
    requestNewline()
    writeToken(token)
  }

  fun writeCodeLine(token: KDocToken) {
    requestOpenCodeBlockMarker()
    requestNewline()
    if (token.value.isNotEmpty()) {
      writeToken(token)
    }
  }

  /** Adds a code block marker if we are not in a code block currently */
  private fun requestCloseCodeBlockMarker() {
    if (inCodeBlock) {
      this.requestedWhitespace = NEWLINE
      writeExplicitCodeBlockMarker(KDocToken(CODE_BLOCK_MARKER, "```"))
    }
  }

  /** Adds a code block marker if we are in a code block currently */
  private fun requestOpenCodeBlockMarker() {
    if (!inCodeBlock) {
      this.requestedWhitespace = NEWLINE
      writeExplicitCodeBlockMarker(KDocToken(CODE_BLOCK_MARKER, "```"))
    }
  }

  fun writeExplicitCodeBlockMarker(token: KDocToken) {
    requestNewline()
    writeToken(token)
    requestNewline()
    inCodeBlock = !inCodeBlock
  }

  fun writeLiteral(token: KDocToken) {
    requestCloseCodeBlockMarker()

    writeToken(token)
  }

  fun writeMarkdownLink(token: KDocToken) {
    writeToken(token)
  }

  override fun toString(): String {
    return output.toString()
  }

  fun requestBlankLine() {
    requestWhitespace(BLANK_LINE)
  }

  fun requestNewline() {
    requestWhitespace(NEWLINE)
  }

  private fun requestWhitespace(requestedWhitespace: RequestedWhitespace) {
    this.requestedWhitespace =
        Ordering.natural<Comparable<*>>().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 [.BLANK_LINE] after it but the next token requests only [ ][.NEWLINE]
   * before it, we insert [.BLANK_LINE].
   */
  internal enum class RequestedWhitespace {
    NONE,

    /**
     * Add one space, only if the next token seems like a word In contrast, punctuation like a dot
     * does need a space before it.
     */
    CONDITIONAL_WHITESPACE,

    /** Add one space, e.g. " " */
    WHITESPACE,

    /** Break to the next line */
    NEWLINE,

    /** Add a whole blank line between the two lines of content */
    BLANK_LINE,
  }

  private fun writeToken(token: KDocToken) {
    if (requestedWhitespace == BLANK_LINE) {
      writeBlankLine()
      requestedWhitespace = NONE
    } else if (requestedWhitespace == NEWLINE) {
      writeNewline()
      requestedWhitespace = NONE
    }

    val needWhitespace =
        when (requestedWhitespace) {
          WHITESPACE -> true
          CONDITIONAL_WHITESPACE -> token.value.first().isLetterOrDigit()
          else -> false
        }
    /*
     * 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() + (if (needWhitespace) 1 else 0) > remainingOnLine) {
      writeNewline()
    }
    if (!atStartOfLine && needWhitespace) {
      appendTrackingLength(" ")
    }

    appendTrackingLength(token.value)
    requestedWhitespace = NONE

    if (!START_OF_LINE_TOKENS.contains(token.type)) {
      atStartOfLine = false
    }
  }

  private fun writeBlankLine() {
    output.append("\n")
    appendTrackingLength(blockIndent)
    appendTrackingLength("*")
    writeNewline()
  }

  private fun writeNewline() {
    output.append("\n")
    remainingOnLine = maxLineLength
    appendTrackingLength(blockIndent)
    appendTrackingLength("* ")
    atStartOfLine = true
  }

  /*
   * 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
   */
  private fun appendTrackingLength(str: String) {
    output.append(str)
    remainingOnLine -= str.length
  }
}


/*
 * 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.
 */

/*
 * This was copied from https://github.com/google/google-java-format and modified extensively to
 * work for Kotlin formatting
 */

package com.facebook.ktfmt.kdoc

/** Mutable integer for tracking the level of nesting. */
internal class NestingCounter {
  private var value: Int = 0

  val isPositive: Boolean
    get() = value > 0

  fun value(): Int {
    return value
  }

  fun increment() {
    value++
  }

  fun incrementIfPositive() {
    if (value > 0) {
      value++
    }
  }

  fun decrementIfPositive() {
    if (value > 0) {
      value--
    }
  }

  fun reset() {
    value = 0
  }
}


/*
 * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 */

/*
 * Copyright (c) Tor Norbye.
 *
 * 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.facebook.ktfmt.kdoc

import kotlin.math.min

class Paragraph(private val task: FormattingTask) {
  private val options: KDocFormattingOptions
    get() = task.options

  var content = StringBuilder()
  val text
    get() = content.toString()

  var prev: Paragraph? = null
  var next: Paragraph? = null

  /** If true, this paragraph should be preceded by a blank line. */
  var separate = false

  /**
   * If true, this paragraph is a continuation of the previous paragraph (so should be indented with
   * the hanging indent, including line 1)
   */
  var continuation = false

  /**
   * Whether this paragraph is allowed to be empty. Paragraphs are normally merged if this is not
   * set. This allows the line breaker to call [ParagraphListBuilder.newParagraph] repeatedly
   * without introducing more than one new paragraph. But for preformatted text we do want to be
   * able to express repeated blank lines.
   */
  var allowEmpty = false

  /** Is this paragraph preformatted? */
  var preformatted = false

  /** Is this paragraph a block paragraph? If so, it must start on its own line. */
  var block = false

  /** Is this paragraph specifying a kdoc tag like @param? */
  var doc = false

  /**
   * Is this line quoted? (In the future make this an int such that we can support additional
   * levels.)
   */
  var quoted = false

  /** Is this line part of a table? */
  var table = false

  /** Is this a separator line? */
  var separator = false

  /** Should this paragraph use a hanging indent? (Implies [block] as well). */
  var hanging = false
    set(value) {
      block = true
      field = value
    }

  var originalIndent = 0

  // The indent to use for all lines in the paragraph.
  var indent = ""

  // The indent to use for all lines in the paragraph if [hanging] is true,
  // or the second and subsequent lines if [hanging] is false
  var hangingIndent = ""

  fun isEmpty(): Boolean {
    return content.isEmpty()
  }

  fun cleanup() {
    val original = text

    if (preformatted) {
      return
    }

    var s = original
    if (options.convertMarkup) {
      s = convertMarkup(text)
    }
    if (!options.allowParamBrackets) {
      s = rewriteParams(s)
    }

    if (s != original) {
      content.clear()
      content.append(s)
    }
  }

  private fun rewriteParams(s: String): String {
    var start = 0
    val length = s.length
    while (start < length && s[start].isWhitespace()) {
      start++
    }
    if (s.startsWith("@param", start)) {
      start += "@param".length
      while (start < length && s[start].isWhitespace()) {
        start++
      }
      if (start < length && s[start++] == '[') {
        while (start < length && s[start].isWhitespace()) {
          start++
        }
        var end = start
        while (end < length && s[end].isJavaIdentifierPart()) {
          end++
        }
        if (end > start) {
          val name = s.substring(start, end)
          while (end < length && s[end].isWhitespace()) {
            end++
          }
          if (end < length && s[end++] == ']') {
            while (end < length && s[end].isWhitespace()) {
              end++
            }
            return "@param $name ${s.substring(end)}"
          }
        }
      }
    }

    return s
  }

  private fun convertMarkup(s: String): String {
    // Whether the tag starts with a capital letter and needs to be cleaned, e.g. `@See` -> `@see`.
    // (isKDocTag only allows the first letter to be capitalized.)
    val convertKDocTag = s.isKDocTag() && s[1].isUpperCase()

    if (!convertKDocTag && s.none { it == '<' || it == '&' || it == '{' }) {
      return s
    }

    val sb = StringBuilder(s.length)
    var i = 0
    val n = s.length

    if (convertKDocTag) {
      sb.append('@').append(s[1].lowercaseChar())
      i += 2
    }

    var code = false
    var brackets = 0
    while (i < n) {
      val c = s[i++]
      if (c == '\\') {
        sb.append(c)
        if (i < n - 1) {
          sb.append(s[i++])
        }
        continue
      } else if (c == '`') {
        code = !code
        sb.append(c)
        continue
      } else if (c == '[') {
        brackets++
        sb.append(c)
        continue
      } else if (c == ']') {
        brackets--
        sb.append(c)
        continue
      } else if (code || brackets > 0) {
        sb.append(c)
        continue
      } else if (c == '<') {
        if (s.startsWith("b>", i, false) || s.startsWith("/b>", i, false)) {
          // "<b>" or </b> -> "**"
          sb.append('*').append('*')
          if (s[i] == '/') i++
          i += 2
          continue
        }
        if (s.startsWith("i>", i, false) || s.startsWith("/i>", i, false)) {
          // "<i>" or </i> -> "*"
          sb.append('*')
          if (s[i] == '/') i++
          i += 2
          continue
        }
        if (s.startsWith("em>", i, false) || s.startsWith("/em>", i, false)) {
          // "<em>" or </em> -> "_"
          sb.append('_')
          if (s[i] == '/') i++
          i += 3
          continue
        }
        // (We don't convert <pre> here because those tags appear in paragraphs
        // marked preformatted, and preformatted paragraphs are never passed to
        // convertTags)
      } else if (c == '&') {
        if (s.startsWith("lt;", i, true)) { // "<" -> "<"
          sb.append('<')
          i += 3
          continue
        }
        if (s.startsWith("gt;", i, true)) { // ">" -> ">"
          sb.append('>')
          i += 3
          continue
        }
      } else if (c == '{') {
        if (s.startsWith("@param", i, true)) {
          val curr = i + 6
          var end = s.indexOf('}', curr)
          if (end == -1) {
            end = n
          }
          sb.append('[')
          sb.append(s.substring(curr, end).trim())
          sb.append(']')
          i = end + 1
          continue
        } else if (s.startsWith("@link", i, true)
        // @linkplain is similar to @link, but kdoc does *not* render a [symbol]
        // into a {@linkplain} in HTML, so converting these would change the output.
        && !s.startsWith("@linkplain", i, true)) {
          // {@link} or {@linkplain}
          sb.append('[')
          var curr = i + 5
          while (curr < n) {
            val ch = s[curr++]
            if (ch.isWhitespace()) {
              break
            }
            if (ch == '}') {
              curr--
              break
            }
          }
          var skip = false
          while (curr < n) {
            val ch = s[curr]
            if (ch == '}') {
              sb.append(']')
              curr++
              break
            } else if (ch == '(') {
              skip = true
            } else if (!skip) {
              if (ch == '#') {
                if (!sb.endsWith('[')) {
                  sb.append('.')
                }
              } else {
                sb.append(ch)
              }
            }
            curr++
          }
          i = curr
          continue
        }
      }
      sb.append(c)
    }

    return sb.toString()
  }

  fun reflow(firstLineMaxWidth: Int, maxLineWidth: Int): List<String> {
    val lineWidth = maxLineWidth - getIndentSize(indent, options)
    val hangingIndentSize = getIndentSize(hangingIndent, options) - if (quoted) 2 else 0 // "> "
    if (text.length < (firstLineMaxWidth - hangingIndentSize)) {
      return listOf(text.collapseSpaces())
    }
    // Split text into words
    val words: List<String> = computeWords()

    // See divide & conquer algorithm listed here: https://xxyxyz.org/line-breaking/
    if (words.size == 1) {
      return listOf(words[0])
    }

    if (firstLineMaxWidth < maxLineWidth) {
      // We have ragged text. We'll just greedily place the first
      // words on the first line, and then optimize the rest.
      val line = StringBuilder()
      val firstLineWidth = firstLineMaxWidth - getIndentSize(indent, options)
      for (i in words.indices) {
        val word = words[i]
        if (line.isEmpty()) {
          if (word.length + task.type.lineOverhead() > firstLineMaxWidth) {
            // can't fit anything on the first line: just flow to
            // full width and caller will need to insert comment on
            // the next line.
            return reflow(words, lineWidth, hangingIndentSize)
          }
          line.append(word)
        } else if (line.length + word.length + 1 <= firstLineWidth) {
          line.append(' ')
          line.append(word)
        } else {
          // Break the rest
          val remainingWords = words.subList(i, words.size)
          val reflownRemaining = reflow(remainingWords, lineWidth, hangingIndentSize)
          return listOf(line.toString()) + reflownRemaining
        }
      }
      // We fit everything on the first line
      return listOf(line.toString())
    }

    return reflow(words, lineWidth, hangingIndentSize)
  }

  private fun reflow(words: List<String>, lineWidth: Int, hangingIndentSize: Int): List<String> {
    if (options.alternate ||
        !options.optimal ||
        hanging && hangingIndentSize > 0 ||
        // An unbreakable long word may make other lines shorter and won't look good
        words.any { it.length > lineWidth }) {
      // Switch to greedy if explicitly turned on, and for hanging indent
      // paragraphs, since the current implementation doesn't have support
      // for a different maximum length on the first line from the rest
      // and there were various cases where this ended up with bad results.
      // This is typically used in list items (and kdoc sections) which tend
      // to be short -- and for 2-3 lines the gains of optimal line breaking
      // isn't worth the cases where we have really unbalanced looking text
      return reflowGreedy(lineWidth, options, words)
    }

    val lines = reflowOptimal(lineWidth - hangingIndentSize, words)
    if (lines.size <= 2) {
      // Just 2 lines? We prefer long+short instead of half+half.
      return reflowGreedy(lineWidth, options, words)
    } else {
      // We could just return [lines] here, but the straightforward algorithm
      // doesn't do a great job with short paragraphs where the last line is
      // short; it over-corrects and shortens everything else in order to balance
      // out the last line.

      val maxLine: (String) -> Int = {
        // Ignore lines that are unbreakable
        if (it.indexOf(' ') == -1) {
          0
        } else {
          it.length
        }
      }
      val longestLine = lines.maxOf(maxLine)
      var lastWord = words.size - 1
      while (lastWord > 0) {
        // We can afford to do this because we're only repeating it for a single
        // line's worth of words and because comments tend to be relatively short
        // anyway
        val newLines = reflowOptimal(lineWidth - hangingIndentSize, words.subList(0, lastWord))
        if (newLines.size < lines.size) {
          val newLongestLine = newLines.maxOf(maxLine)
          if (newLongestLine > longestLine &&
              newLines.subList(0, newLines.size - 1).any { it.length > longestLine }) {
            return newLines +
                reflowGreedy(
                    lineWidth - hangingIndentSize, options, words.subList(lastWord, words.size))
          }
          break
        }
        lastWord--
      }

      return lines
    }
  }

  /**
   * Returns true if it's okay to break at the current word.
   *
   * We need to check for this, because a word can have a different meaning at the beginning of a
   * line than in the middle somewhere, so if it just so happens to be at the break boundary, we
   * need to make sure we don't make it the first word on the next line since that would change the
   * documentation.
   */
  private fun canBreakAt(prev: String, word: String): Boolean {
    // Can we start a new line with this without interpreting it in a special
    // way?

    if (word.startsWith("#") ||
        word.startsWith("```") ||
        word.isDirectiveMarker() ||
        word.startsWith("@") || // interpreted as a tag
        word.isTodo() ||
        word.startsWith(">")) {
      return false
    }

    if (prev == "@sample") {
      return false // https://github.com/facebookincubator/ktfmt/issues/310
    }

    if (!word.first().isLetter()) {
      val wordWithSpace = "$word " // for regex matching in below checks
      if (wordWithSpace.isListItem() && !word.equals("<li>", true) || wordWithSpace.isQuoted()) {
        return false
      }
    }

    return true
  }

  /**
   * Split [text] up into individual "words"; in the case where some words are not allowed to span
   * lines, it will combine these into single word. For example, if we have a sentence which ends
   * with a number, e.g. "the sum is 5.", we want to make sure "5." is never placed at the beginning
   * of a new line (which would turn it into a list item), so for this we'll compute the word list
   * "the", "sum", "is 5.".
   */
  fun computeWords(): List<String> {
    val words = text.split(Regex("\\s+")).filter { it.isNotBlank() }.map { it.trim() }
    if (words.size == 1) {
      return words
    }

    if (task.type != CommentType.KDOC) {
      // In block comments and line comments we feel free to break anywhere
      // between words; there isn't a special meaning assigned to certain words
      // if they appear first on a line like there is in KDoc/Markdown.
      return words
    }

    // See if any of the words should never be broken up. We do that for list
    // separators and a few others. We never want to put "1." at the beginning
    // of a line as an overflow.

    val combined = ArrayList<String>(words.size)

    var from = 0
    val end = words.size
    while (from < end) {
      val start =
          if (from == 0 && (quoted || hanging && !text.isKDocTag())) {
            from + 2
          } else {
            from + 1
          }
      var to = words.size
      for (i in start until words.size) {
        val next = words[i]
        if (next.startsWith("[") && !next.startsWith("[[")) {
          // find end
          var j = -1
          for (k in i until words.size) {
            if (']' in words[k]) {
              j = k
              break
            }
          }
          if (j != -1) {
            // combine everything in the string; we can't break link text or @sample tags
            if (start == from + 1 && canBreakAt(words[start - 1], words[start])) {
              combined.add(words[from])
              from = start
            }
            // Maybe not break; what if the next word isn't okay?
            to = j + 1
            if (to == words.size || canBreakAt(words[to - 1], words[to])) {
              break
            }
          } // else: unterminated [, ignore
        } else if (canBreakAt(words[i - 1], next)) {
          to = i
          break
        }
      }

      if (to == from + 1) {
        combined.add(words[from])
      } else if (to > from) {
        combined.add(words.subList(from, to).joinToString(" "))
      }
      from = to
    }

    return combined
  }

  private data class Quadruple(val i0: Int, val j0: Int, val i1: Int, val j1: Int)

  private fun reflowOptimal(maxLineWidth: Int, words: List<String>): List<String> {
    val count = words.size
    val lines = ArrayList<String>()

    val offsets = ArrayList<Int>()
    offsets.add(0)

    for (boxWidth in words.map { it.length }.toList()) {
      offsets.add(offsets.last() + min(boxWidth, maxLineWidth))
    }

    val big = 10 shl 20
    val minimum = IntArray(count + 1) { big }
    val breaks = IntArray(count + 1)
    minimum[0] = 0

    fun cost(i: Int, j: Int): Int {
      val width = offsets[j] - offsets[i] + j - i - 1
      return if (width <= maxLineWidth) {
        val squared = (maxLineWidth - width) * (maxLineWidth - width)
        minimum[i] + squared
      } else {
        big
      }
    }

    fun search(pi0: Int, pj0: Int, pi1: Int, pj1: Int) {
      val stack = ArrayDeque<Quadruple>()
      stack.add(Quadruple(pi0, pj0, pi1, pj1))

      while (stack.isNotEmpty()) {
        val (i0, j0, i1, j1) = stack.removeLast()
        if (j0 < j1) {
          val j = (j0 + j1) / 2

          for (i in i0 until i1) {
            val c = cost(i, j)
            if (c <= minimum[j]) {
              minimum[j] = c
              breaks[j] = i
            }
          }
          stack.add(Quadruple(breaks[j], j + 1, i1, j1))
          stack.add(Quadruple(i0, j0, breaks[j] + 1, j))
        }
      }
    }

    var n = count + 1
    var i = 0
    var offset = 0

    while (true) {
      val r = min(n, 1 shl (i + 1))
      val edge = (1 shl i) + offset
      search(0 + offset, edge, edge, r + offset)
      val x = minimum[r - 1 + offset]
      var flag = true
      for (j in (1 shl i) until (r - 1)) {
        val y = cost(j + offset, r - 1 + offset)
        if (y <= x) {
          n -= j
          i = 0
          offset += j
          flag = false
          break
        }
      }
      if (flag) {
        if (r == n) break
        i++
      }
    }

    var j = count
    while (j > 0) {
      i = breaks[j]
      val sb = StringBuilder()
      for (w in i until j) {
        sb.append(words[w])
        if (w < j - 1) {
          sb.append(' ')
        }
      }
      lines.add(sb.toString())
      j = i
    }

    lines.reverse()
    return lines
  }

  private fun reflowGreedy(
      lineWidth: Int,
      options: KDocFormattingOptions,
      words: List<String>
  ): List<String> {
    // Greedy implementation

    var width = lineWidth
    if (options.hangingIndent > 0 && hanging && continuation) {
      width -= getIndentSize(hangingIndent, options)
    }

    val lines = mutableListOf<String>()
    var column = 0
    val sb = StringBuilder()
    for (word in words) {
      when {
        sb.isEmpty() -> {
          sb.append(word)
          column += word.length
        }
        column + word.length + 1 <= width -> {
          sb.append(' ').append(word)
          column += word.length + 1
        }
        else -> {
          width = lineWidth
          if (options.hangingIndent > 0 && hanging) {
            width -= getIndentSize(hangingIndent, options)
          }
          lines.add(sb.toString())
          sb.setLength(0)
          sb.append(word)
          column = sb.length
        }
      }
    }
    if (sb.isNotEmpty()) {
      lines.add(sb.toString())
    }
    return lines
  }

  override fun toString(): String {
    return "$content, separate=$separate, block=$block, hanging=$hanging, preformatted=$preformatted, quoted=$quoted, continuation=$continuation, allowempty=$allowEmpty, separator=$separator"
  }
}


/*
 * Copyright (c) Tor Norbye.
 *
 * 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.facebook.ktfmt.kdoc

/**
 * A list of paragraphs. Each paragraph should start on a new line and end with a newline. In
 * addition, if a paragraph is marked with "separate=true", we'll insert an extra blank line in
 * front of it.
 */
class ParagraphList(private val paragraphs: List<Paragraph>) : Iterable<Paragraph> {
  fun isSingleParagraph() = paragraphs.size <= 1

  override fun iterator(): Iterator<Paragraph> = paragraphs.iterator()

  override fun toString(): String = paragraphs.joinToString { it.content }
}