package org.jetbrains.dokka.gfm

import org.jetbrains.dokka.CoreExtensions
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.renderers.DefaultRenderer
import org.jetbrains.dokka.base.renderers.PackageListCreator
import org.jetbrains.dokka.base.renderers.RootCreator
import org.jetbrains.dokka.base.resolvers.local.DefaultLocationProvider
import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory
import org.jetbrains.dokka.model.SourceSetData
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.plugability.DokkaPlugin
import org.jetbrains.dokka.plugability.plugin
import org.jetbrains.dokka.plugability.query
import org.jetbrains.dokka.transformers.pages.PageTransformer
import java.lang.StringBuilder


class GfmPlugin : DokkaPlugin() {

    val gfmPreprocessors by extensionPoint<PageTransformer>()

    val renderer by extending {
        CoreExtensions.renderer providing { CommonmarkRenderer(it) } applyIf { format == "gfm" }
    }

    val locationProvider by extending {
        plugin<DokkaBase>().locationProviderFactory providing { MarkdownLocationProviderFactory(it) } applyIf { format == "gfm" }
    }

    val rootCreator by extending {
        gfmPreprocessors with RootCreator
    }

    val packageListCreator by extending {
        gfmPreprocessors providing {
            PackageListCreator(
                it,
                "gfm",
                "md"
            )
        } order { after(rootCreator) }
    }
}

open class CommonmarkRenderer(
    context: DokkaContext
) : DefaultRenderer<StringBuilder>(context) {

    override val preprocessors = context.plugin<GfmPlugin>().query { gfmPreprocessors }

    override fun StringBuilder.buildHeader(level: Int, node: ContentHeader, content: StringBuilder.() -> Unit) {
        buildParagraph()
        append("#".repeat(level) + " ")
        content()
        buildNewLine()
    }

    override fun StringBuilder.buildLink(address: String, content: StringBuilder.() -> Unit) {
        append("[")
        content()
        append("]($address)")
    }

    override fun StringBuilder.buildList(
        node: ContentList,
        pageContext: ContentPage,
        platformRestriction: Set<SourceSetData>?
    ) {
        buildParagraph()
        buildListLevel(node, pageContext)
        buildParagraph()
    }

    private val indent = " ".repeat(4)

    private fun StringBuilder.buildListItem(items: List<ContentNode>, pageContext: ContentPage, bullet: String = "*") {
        items.forEach {
            if (it is ContentList) {
                val builder = StringBuilder()
                builder.append(indent)
                builder.buildListLevel(it, pageContext)
                append(builder.toString().replace(Regex("  \n(?!$)"), "  \n$indent"))
            } else {
                append("$bullet ")
                it.build(this, pageContext)
                buildNewLine()
            }
        }
    }

    private fun StringBuilder.buildListLevel(node: ContentList, pageContext: ContentPage) {
        if (node.ordered) {
            buildListItem(
                node.children,
                pageContext,
                "${node.extra.allOfType<SimpleAttr>().find { it.extraKey == "start" }?.extraValue
                    ?: 1.also { context.logger.error("No starting number specified for ordered list in node ${pageContext.dri.first()}!") }}."
            )
        } else {
            buildListItem(node.children, pageContext, "*")
        }
    }

    override fun StringBuilder.buildNewLine() {
        append("  \n")
    }

    private fun StringBuilder.buildParagraph() {
        append("\n\n")
    }

    override fun StringBuilder.buildPlatformDependent(
        content: PlatformHintedContent,
        pageContext: ContentPage,
        sourceSetRestriction: Set<SourceSetData>?
    ) {
        val distinct = content.sourceSets.map {
            it to StringBuilder().apply {buildContentNode(content.inner, pageContext, setOf(it)) }.toString()
        }.groupBy(Pair<SourceSetData, String>::second, Pair<SourceSetData, String>::first)

        if (distinct.size == 1)
            append(distinct.keys.single())
        else
            distinct.forEach { text, platforms ->
                append(platforms.joinToString(prefix = " [", postfix = "] $text") { "${it.moduleName}/${it.sourceSetID}"  })
            }
    }

    override fun StringBuilder.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) {
        append("Resource")
    }

    override fun StringBuilder.buildTable(
        node: ContentTable,
        pageContext: ContentPage,
        platformRestriction: Set<SourceSetData>?
    ) {

        buildParagraph()
        val size = node.children.firstOrNull()?.children?.size ?: 0

        if (node.header.size > 0) {
            node.header.forEach {
                it.children.forEach {
                    append("| ")
                    it.build(this, pageContext)
                }
                append("|\n")
            }
        } else {
            append("| ".repeat(size))
            if (size > 0) append("|\n")
        }

        append("|---".repeat(size))
        if (size > 0) append("|\n")

        node.children.forEach {
            it.children.forEach {
                append("| ")
                it.build(this, pageContext)
            }
            append("|\n")
        }

        buildParagraph()
    }

    override fun StringBuilder.buildText(textNode: ContentText) {
        val decorators = decorators(textNode.style)
        append(decorators)
        append(textNode.text.replace(Regex("[<>]"), ""))
        append(decorators.reversed())
    }

    override fun StringBuilder.buildNavigation(page: PageNode) {
        locationProvider.ancestors(page).asReversed().forEach { node ->
            append("/")
            if (node.isNavigable) buildLink(node, page)
            else append(node.name)
        }
        buildParagraph()
    }

    override fun buildPage(page: ContentPage, content: (StringBuilder, ContentPage) -> Unit): String =
        StringBuilder().apply {
            content(this, page)
        }.toString()

    override fun buildError(node: ContentNode) {
        context.logger.warn("Markdown renderer has encountered problem. The unmatched node is $node")
    }

    private fun decorators(styles: Set<Style>) = StringBuilder().apply {
        styles.forEach {
            when (it) {
                TextStyle.Bold -> append("**")
                TextStyle.Italic -> append("*")
                TextStyle.Strong -> append("**")
                TextStyle.Strikethrough -> append("~~")
                else -> Unit
            }
        }
    }.toString()

    private val PageNode.isNavigable: Boolean
        get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing

    private fun StringBuilder.buildLink(to: PageNode, from: PageNode) =
        buildLink(locationProvider.resolve(to, from)) {
            append(to.name)
        }

    override suspend fun renderPage(page: PageNode) {
        val path by lazy { locationProvider.resolve(page, skipExtension = true) }
        when (page) {
            is ContentPage -> outputWriter.write(path, buildPage(page) { c, p -> buildPageContent(c, p) }, ".md")
            is RendererSpecificPage -> when (val strategy = page.strategy) {
                is RenderingStrategy.Copy -> outputWriter.writeResources(strategy.from, path)
                is RenderingStrategy.Write -> outputWriter.write(path, strategy.text, "")
                is RenderingStrategy.Callback -> outputWriter.write(path, strategy.instructions(this, page), ".md")
                RenderingStrategy.DoNothing -> Unit
            }
            else -> throw AssertionError(
                "Page ${page.name} cannot be rendered by renderer as it is not renderer specific nor contains content"
            )
        }
    }
}

class MarkdownLocationProviderFactory(val context: DokkaContext) : LocationProviderFactory {

    override fun getLocationProvider(pageNode: RootPageNode) = MarkdownLocationProvider(pageNode, context)
}

class MarkdownLocationProvider(
    pageGraphRoot: RootPageNode,
    dokkaContext: DokkaContext
) : DefaultLocationProvider(
    pageGraphRoot,
    dokkaContext
) {
    override val extension = ".md"
}