// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.plugins.markdown.lang.psi.impl

import com.intellij.lang.ASTNode
import com.intellij.navigation.ColoredItemPresentation
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.*
import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.psi.util.elementType
import org.intellij.plugins.markdown.lang.MarkdownTokenTypeSets
import org.intellij.plugins.markdown.lang.psi.MarkdownElementVisitor
import org.intellij.plugins.markdown.lang.psi.util.children
import org.intellij.plugins.markdown.lang.psi.util.childrenOfType
import org.intellij.plugins.markdown.lang.psi.util.hasType
import org.intellij.plugins.markdown.lang.stubs.impl.MarkdownHeaderStubElement
import org.intellij.plugins.markdown.lang.stubs.impl.MarkdownHeaderStubElementType
import org.intellij.plugins.markdown.structureView.MarkdownStructureColors
import org.jetbrains.annotations.ApiStatus
import javax.swing.Icon

/**
 * Corresponds to both ATX and SETEXT headers.
 *
 * Use [contentElement] to agnostically obtain a content element for current header.
 */
@Suppress("DEPRECATION")
class MarkdownHeader: MarkdownHeaderImpl {
  constructor(node: ASTNode): super(node)
  constructor(stub: MarkdownHeaderStubElement, type: MarkdownHeaderStubElementType): super(stub, type)

  val level
    get() = calculateHeaderLevel()

  /**
   * Anchor reference to this header.
   * Should be compatible with anchors generated by GitHub.
   *
   * ```markdown
   * # Some header text -> #some-header-text
   * ```
   * Exception: multiple headers with same text won't be adjusted with it's occurence number.
   */
  val anchorText: String?
    get() = obtainAnchorText(this)

  val contentElement: MarkdownHeaderContent?
    get() = childrenOfType(MarkdownTokenTypeSets.HEADER_CONTENT).filterIsInstance<MarkdownHeaderContent>().firstOrNull()

  override fun accept(visitor: PsiElementVisitor) {
    when (visitor) {
      is MarkdownElementVisitor -> visitor.visitHeader(this)
      else -> super.accept(visitor)
    }
  }

  override fun getReferences(): Array<PsiReference?> {
    return ReferenceProvidersRegistry.getReferencesFromProviders(this)
  }

  override fun getPresentation(): ItemPresentation {
    val headerText = getHeaderText()
    val text = headerText ?: "Invalid header: $text"
    return object: ColoredItemPresentation {
      override fun getPresentableText(): String {
        val prevSibling = prevSibling
        if (Registry.`is`("markdown.structure.view.list.visibility") && MarkdownTokenTypeSets.LIST_MARKERS.contains(prevSibling.elementType)) {
          return prevSibling.text + text
        }
        return text
      }

      override fun getIcon(open: Boolean): Icon? = null

      override fun getTextAttributesKey(): TextAttributesKey? {
        return when (level) {
          1 -> MarkdownStructureColors.MARKDOWN_HEADER_BOLD
          else -> MarkdownStructureColors.MARKDOWN_HEADER
        }
      }
    }
  }

  override fun getName(): String? {
    return getHeaderText()
  }

  private fun findContentHolder(): PsiElement? {
    return findChildByType(MarkdownTokenTypeSets.INLINE_HOLDING_ELEMENT_TYPES)
  }

  private fun getHeaderText(): String? {
    if (!isValid) {
      return null
    }
    val contentHolder = findContentHolder() ?: return null
    return StringUtil.trim(contentHolder.text)
  }

  /**
   * Builds visible text for this header.
   * Visible text includes text content of all children without starting hash and a whitespace after hash.
   *
   * For a child inline link this method will only take it's visible part ([MarkdownLink.linkText]).
   *
   * @param hideImages - if true, all child images will be ignored,
   *  otherwise images will be included as is, with it's visible and invisible parts
   *  (so rendered result will show actual image).
   */
  @ApiStatus.Experimental
  fun buildVisibleText(hideImages: Boolean = true): String? {
    val contentHolder = findContentHolder() ?: return null
    val builder = StringBuilder()
    val children = contentHolder.children().dropWhile { it.hasType(MarkdownTokenTypeSets.WHITE_SPACES) }
    traverseNameText(builder, children, hideImages)
    return builder.toString().trim(' ')
  }

  private fun traverseNameText(builder: StringBuilder, elements: Sequence<PsiElement>, hideImages: Boolean) {
    for (child in elements) {
      when (child) {
        is LeafPsiElement -> builder.append(child.text)
        is MarkdownInlineLink -> traverseNameText(builder, child.linkText?.contentElements.orEmpty(), hideImages)
        is MarkdownImage -> {
          if (!hideImages) {
            builder.append(child.text)
          }
        }
        else -> traverseNameText(builder, child.children(), hideImages)
      }
    }
  }

  private fun buildAnchorText(includeStartingHash: Boolean = false): String? {
    val contentHolder = findContentHolder() ?: return null
    val children = contentHolder.children().filter { !it.hasType(MarkdownTokenTypeSets.WHITE_SPACES) }
    val text = buildString {
      if (includeStartingHash) {
        append("#")
      }
      var count = 0
      for (child in children) {
        if (count >= 1) {
          append(" ")
        }
        when (child) {
          is MarkdownImage -> append("")
          is MarkdownInlineLink -> processInlineLink(child)
          else -> append(child.text)
        }
        count += 1
      }
    }
    val replaced = text.lowercase().replace(garbageRegex, "").replace(additionalSymbolsRegex, "")
    return replaced.replace(" ", "-")
  }

  private fun StringBuilder.processInlineLink(element: MarkdownInlineLink) {
    val contentElements = element.linkText?.contentElements.orEmpty()
    val withoutWhitespaces = contentElements.filterNot { it.hasType(MarkdownTokenTypeSets.WHITE_SPACES) }
    withoutWhitespaces.joinTo(this, separator = " ") {
      when (it) {
        is MarkdownImage -> ""
        else -> it.text
      }
    }
  }

  companion object {
    internal val garbageRegex = Regex("[^\\w\\- ]")
    internal val additionalSymbolsRegex = Regex("[^-_a-z0-9\\s]")

    fun obtainAnchorText(header: MarkdownHeader): String? {
      return CachedValuesManager.getCachedValue(header) {
        CachedValueProvider.Result.create(header.buildAnchorText(false), PsiModificationTracker.MODIFICATION_COUNT)
      }
    }
  }
}
