package org.jetbrains.dokka.base.renderers.html

import kotlinx.html.*
import kotlinx.html.stream.createHTML
import org.jetbrains.dokka.DokkaSourceSetID
import org.jetbrains.dokka.Platform
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.renderers.*
import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer
import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelFactory
import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelMerger
import org.jetbrains.dokka.base.renderers.html.innerTemplating.DokkaTemplateTypes
import org.jetbrains.dokka.base.renderers.html.innerTemplating.HtmlTemplater
import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint
import org.jetbrains.dokka.base.resolvers.local.DokkaBaseLocationProvider
import org.jetbrains.dokka.base.templating.*
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.model.properties.PropertyContainer
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.pages.HtmlContent
import org.jetbrains.dokka.plugability.*
import org.jetbrains.dokka.utilities.htmlEscape
import org.jetbrains.kotlin.utils.addIfNotNull

internal const val TEMPLATE_REPLACEMENT: String = "###"

open class HtmlRenderer(
    context: DokkaContext
) : DefaultRenderer<FlowContent>(context) {
    private val sourceSetDependencyMap: Map<DokkaSourceSetID, List<DokkaSourceSetID>> =
        context.configuration.sourceSets.associate { sourceSet ->
            sourceSet.sourceSetID to context.configuration.sourceSets
                .map { it.sourceSetID }
                .filter { it in sourceSet.dependentSourceSets }
        }

    private val templateModelFactories = listOf(DefaultTemplateModelFactory(context)) // TODO: Make extension point
    private val templateModelMerger = DefaultTemplateModelMerger()
    private val templater = HtmlTemplater(context).apply {
        setupSharedModel(templateModelMerger.invoke(templateModelFactories) { buildSharedModel() })
    }

    private var shouldRenderSourceSetBubbles: Boolean = false

    override val preprocessors = context.plugin<DokkaBase>().query { htmlPreprocessors }

    private val tabSortingStrategy = context.plugin<DokkaBase>().querySingle { tabSortingStrategy }

    private fun <R> TagConsumer<R>.prepareForTemplates() =
        if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this
        else ImmediateResolutionTagConsumer(this, context)

    private fun <T : ContentNode> sortTabs(strategy: TabSortingStrategy, tabs: Collection<T>): List<T> {
        val sorted = strategy.sort(tabs)
        if (sorted.size != tabs.size)
            context.logger.warn("Tab sorting strategy has changed number of tabs from ${tabs.size} to ${sorted.size}")
        return sorted
    }

    override fun FlowContent.wrapGroup(
        node: ContentGroup,
        pageContext: ContentPage,
        childrenCallback: FlowContent.() -> Unit
    ) {
        val additionalClasses = node.style.joinToString(" ") { it.toString().toLowerCase() }
        return when {
            node.hasStyle(ContentStyle.TabbedContent) -> div(additionalClasses) {
                val secondLevel = node.children.filterIsInstance<ContentComposite>().flatMap { it.children }
                    .filterIsInstance<ContentHeader>().flatMap { it.children }.filterIsInstance<ContentText>()
                val firstLevel = node.children.filterIsInstance<ContentHeader>().flatMap { it.children }
                    .filterIsInstance<ContentText>()

                val renderable = sortTabs(tabSortingStrategy, firstLevel.union(secondLevel))

                div(classes = "tabs-section") {
                    attributes["tabs-section"] = "tabs-section"
                    renderable.forEachIndexed { index, node ->
                        button(classes = "section-tab") {
                            if (index == 0) attributes["data-active"] = ""
                            attributes["data-togglable"] = node.text
                            text(node.text)
                        }
                    }
                }
                div(classes = "tabs-section-body") {
                    childrenCallback()
                }
            }
            node.hasStyle(ContentStyle.WithExtraAttributes) -> div {
                node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue }
                childrenCallback()
            }
            node.dci.kind in setOf(ContentKind.Symbol) -> div("symbol $additionalClasses") {
                childrenCallback()
            }
            node.hasStyle(ContentStyle.KDocTag) -> span("kdoc-tag") { childrenCallback() }
            node.hasStyle(ContentStyle.Footnote) -> div("footnote") { childrenCallback() }
            node.hasStyle(TextStyle.BreakableAfter) -> {
                span { childrenCallback() }
                wbr { }
            }
            node.hasStyle(TextStyle.Breakable) -> {
                span("breakable-word") { childrenCallback() }
            }
            node.hasStyle(TextStyle.Span) -> span { childrenCallback() }
            node.dci.kind == ContentKind.Symbol -> div("symbol $additionalClasses") { childrenCallback() }
            node.dci.kind == SymbolContentKind.Parameters -> {
                span("parameters $additionalClasses") {
                    childrenCallback()
                }
            }
            node.dci.kind == SymbolContentKind.Parameter -> {
                span("parameter $additionalClasses") {
                    childrenCallback()
                }
            }
            node.hasStyle(TextStyle.InlineComment) -> div("inline-comment") { childrenCallback() }
            node.dci.kind == ContentKind.BriefComment -> div("brief $additionalClasses") { childrenCallback() }
            node.dci.kind == ContentKind.Cover -> div("cover $additionalClasses") { //TODO this can be removed
                childrenCallback()
            }
            node.dci.kind == ContentKind.Deprecation -> div("deprecation-content") { childrenCallback() }
            node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() }
            node.hasStyle(TextStyle.Block) -> div(additionalClasses) { childrenCallback() }
            node.hasStyle(TextStyle.Quotation) -> blockQuote(additionalClasses) { childrenCallback() }
            node.hasStyle(TextStyle.FloatingRight) -> span("clearfix") { span("floating-right") { childrenCallback() } }
            node.hasStyle(TextStyle.Strikethrough) -> strike { childrenCallback() }
            node.isAnchorable -> buildAnchor(
                node.anchor!!,
                node.anchorLabel!!,
                node.sourceSetsFilters
            ) { childrenCallback() }
            node.extra[InsertTemplateExtra] != null -> node.extra[InsertTemplateExtra]?.let { templateCommand(it.command) }
                ?: Unit
            node.hasStyle(ListStyle.DescriptionTerm) -> DT(emptyMap(), consumer).visit {
                this@wrapGroup.childrenCallback()
            }
            node.hasStyle(ListStyle.DescriptionDetails) -> DD(emptyMap(), consumer).visit {
                this@wrapGroup.childrenCallback()
            }
            else -> childrenCallback()
        }
    }

    private fun FlowContent.filterButtons(page: PageNode) {
        if (shouldRenderSourceSetBubbles && page is ContentPage) {
            div(classes = "filter-section") {
                id = "filter-section"
                page.content.withDescendants().flatMap { it.sourceSets }.distinct()
                    .sortedBy { it.comparableKey }.forEach {
                        button(classes = "platform-tag platform-selector") {
                            attributes["data-active"] = ""
                            attributes["data-filter"] = it.sourceSetIDs.merged.toString()
                            when (it.platform.key) {
                                "common" -> classes = classes + "common-like"
                                "native" -> classes = classes + "native-like"
                                "jvm" -> classes = classes + "jvm-like"
                                "js" -> classes = classes + "js-like"
                            }
                            text(it.name)
                        }
                    }
            }
        }
    }

    private fun FlowContent.copyButton() = span(classes = "top-right-position") {
        span("copy-icon")
        copiedPopup("Content copied to clipboard", "popup-to-left")
    }

    private fun FlowContent.copiedPopup(notificationContent: String, additionalClasses: String = "") =
        div("copy-popup-wrapper $additionalClasses") {
            span("copy-popup-icon")
            span {
                text(notificationContent)
            }
        }

    override fun FlowContent.buildPlatformDependent(
        content: PlatformHintedContent,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?
    ) =
        buildPlatformDependent(
            content.sourceSets.filter {
                sourceSetRestriction == null || it in sourceSetRestriction
            }.associateWith { setOf(content.inner) },
            pageContext,
            content.extra,
            content.style
        )

    private fun FlowContent.buildPlatformDependent(
        nodes: Map<DisplaySourceSet, Collection<ContentNode>>,
        pageContext: ContentPage,
        extra: PropertyContainer<ContentNode> = PropertyContainer.empty(),
        styles: Set<Style> = emptySet(),
        shouldHaveTabs: Boolean = shouldRenderSourceSetBubbles
    ) {
        val contents = contentsForSourceSetDependent(nodes, pageContext)
        val isOnlyCommonContent = contents.singleOrNull()?.let { (sourceSet, _) ->
            sourceSet.platform == Platform.common
                    && sourceSet.name.equals("common", ignoreCase = true)
                    && sourceSet.sourceSetIDs.all.all { sourceSetDependencyMap[it]?.isEmpty() == true }
        } ?: false

        // little point in rendering a single "common" tab - it can be
        // assumed that code without any tabs is common by default
        val renderTabs = shouldHaveTabs && !isOnlyCommonContent

        val divStyles = "platform-hinted ${styles.joinToString()}" + if (renderTabs) " with-platform-tabs" else ""
        div(divStyles) {
            attributes["data-platform-hinted"] = "data-platform-hinted"
            extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue }
            if (renderTabs) {
                div("platform-bookmarks-row") {
                    attributes["data-toggle-list"] = "data-toggle-list"
                    contents.forEachIndexed { index, pair ->
                        button(classes = "platform-bookmark") {
                            attributes["data-filterable-current"] = pair.first.sourceSetIDs.merged.toString()
                            attributes["data-filterable-set"] = pair.first.sourceSetIDs.merged.toString()
                            if (index == 0) attributes["data-active"] = ""
                            attributes["data-toggle"] = pair.first.sourceSetIDs.merged.toString()
                            text(pair.first.name)
                        }
                    }
                }
            }
            contents.forEach {
                consumer.onTagContentUnsafe { +it.second }
            }
        }
    }

    private fun contentsForSourceSetDependent(
        nodes: Map<DisplaySourceSet, Collection<ContentNode>>,
        pageContext: ContentPage,
    ): List<Pair<DisplaySourceSet, String>> {
        var counter = 0
        return nodes.toList().map { (sourceSet, elements) ->
            val htmlContent = createHTML(prettyPrint = false).prepareForTemplates().div {
                elements.forEach {
                    buildContentNode(it, pageContext, sourceSet.toSet())
                }
            }.stripDiv()
            sourceSet to createHTML(prettyPrint = false).prepareForTemplates()
                .div(classes = "content sourceset-dependent-content") {
                    if (counter++ == 0) attributes["data-active"] = ""
                    attributes["data-togglable"] = sourceSet.sourceSetIDs.merged.toString()
                    unsafe {
                        +htmlContent
                    }
                }
        }.sortedBy { it.first.comparableKey }
    }

    override fun FlowContent.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) {
        if (node.implicitlySourceSetHinted) {
            val groupedInstancesBySourceSet = node.children.flatMap { instance ->
                instance.sourceSets.map { sourceSet -> instance to sourceSet }
            }.groupBy(
                Pair<ContentDivergentInstance, DisplaySourceSet>::second,
                Pair<ContentDivergentInstance, DisplaySourceSet>::first
            )

            val nodes = groupedInstancesBySourceSet.mapValues {
                val distinct =
                    groupDivergentInstancesWithSourceSet(it.value, it.key, pageContext,
                        beforeTransformer = { instance, _, sourceSet ->
                            createHTML(prettyPrint = false).prepareForTemplates().div {
                                instance.before?.let { before ->
                                    buildContentNode(before, pageContext, sourceSet)
                                }
                            }.stripDiv()
                        },
                        afterTransformer = { instance, _, sourceSet ->
                            createHTML(prettyPrint = false).prepareForTemplates().div {
                                instance.after?.let { after ->
                                    buildContentNode(after, pageContext, sourceSet)
                                }
                            }.stripDiv()
                        })

                val isPageWithOverloadedMembers = pageContext is MemberPage && pageContext.documentables().size > 1

                val contentOfSourceSet = mutableListOf<ContentNode>()
                distinct.onEachIndexed{ index, (_, distinctInstances) ->
                    contentOfSourceSet.addIfNotNull(distinctInstances.firstOrNull()?.before)
                    contentOfSourceSet.addAll(distinctInstances.map { it.divergent })
                    contentOfSourceSet.addIfNotNull(
                        distinctInstances.firstOrNull()?.after
                            ?: if (index != distinct.size - 1) ContentBreakLine(it.key) else null
                    )

                    // content kind main is important for declarations list to avoid double line breaks
                    if (node.dci.kind == ContentKind.Main && index != distinct.size - 1) {
                        if (isPageWithOverloadedMembers) {
                            // add some spacing and distinction between function/property overloads.
                            // not ideal, but there's no other place to modify overloads page atm
                            contentOfSourceSet.add(ContentBreakLine(it.key, style = setOf(HorizontalBreakLineStyle)))
                        } else {
                            contentOfSourceSet.add(ContentBreakLine(it.key))
                        }
                    }
                }
                contentOfSourceSet
            }
            buildPlatformDependent(nodes, pageContext)
        } else {
            node.children.forEach {
                buildContentNode(it.divergent, pageContext, it.sourceSets)
            }
        }
    }

    private fun groupDivergentInstancesWithSourceSet(
        instances: List<ContentDivergentInstance>,
        sourceSet: DisplaySourceSet,
        pageContext: ContentPage,
        beforeTransformer: (ContentDivergentInstance, ContentPage, DisplaySourceSet) -> String,
        afterTransformer: (ContentDivergentInstance, ContentPage, DisplaySourceSet) -> String
    ): Map<SerializedBeforeAndAfter, List<ContentDivergentInstance>> =
        instances.map { instance ->
            instance to Pair(
                beforeTransformer(instance, pageContext, sourceSet),
                afterTransformer(instance, pageContext, sourceSet)
            )
        }.groupBy(
            Pair<ContentDivergentInstance, SerializedBeforeAndAfter>::second,
            Pair<ContentDivergentInstance, SerializedBeforeAndAfter>::first
        )

    private fun ContentPage.documentables(): List<Documentable> {
        return (this as? WithDocumentables)?.documentables ?: emptyList()
    }

    override fun FlowContent.buildList(
        node: ContentList,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?
    ) = when {
        node.ordered -> {
            ol { buildListItems(node.children, pageContext, sourceSetRestriction) }
        }
        node.hasStyle(ListStyle.DescriptionList) -> {
            dl { node.children.forEach { it.build(this, pageContext, sourceSetRestriction) } }
        }
        else -> {
            ul { buildListItems(node.children, pageContext, sourceSetRestriction) }
        }
    }

    open fun OL.buildListItems(
        items: List<ContentNode>,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>? = null
    ) {
        items.forEach {
            if (it is ContentList)
                buildList(it, pageContext)
            else
                li { it.build(this, pageContext, sourceSetRestriction) }
        }
    }

    open fun UL.buildListItems(
        items: List<ContentNode>,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>? = null
    ) {
        items.forEach {
            if (it is ContentList)
                buildList(it, pageContext)
            else
                li { it.build(this, pageContext) }
        }
    }

    override fun FlowContent.buildResource(
        node: ContentEmbeddedResource,
        pageContext: ContentPage
    ) = // TODO: extension point there
        if (node.isImage()) {
            img(src = node.address, alt = node.altText)
        } else {
            println("Unrecognized resource type: $node")
        }

    private fun FlowContent.buildRow(
        node: ContentGroup,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?
    ) {
        node.children
            .filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } }
            .takeIf { it.isNotEmpty() }
            ?.let {
                when (pageContext) {
                    is MultimoduleRootPage -> buildRowForMultiModule(node, it, pageContext, sourceSetRestriction)
                    is ModulePage -> buildRowForModule(node, it, pageContext, sourceSetRestriction)
                    else -> buildRowForContent(node, it, pageContext, sourceSetRestriction)
                }
            }
    }

    private fun FlowContent.buildRowForMultiModule(
        contextNode: ContentGroup,
        toRender: List<ContentNode>,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?
    ) {
        buildAnchor(contextNode)
        div(classes = "table-row") {
            div("main-subrow " + contextNode.style.joinToString(separator = " ")) {
                buildRowHeaderLink(toRender, pageContext, sourceSetRestriction, contextNode.anchor, "w-100")
                div {
                    buildRowBriefSectionForDocs(toRender, pageContext, sourceSetRestriction)
                }
            }
        }
    }

    private fun FlowContent.buildRowForModule(
        contextNode: ContentGroup,
        toRender: List<ContentNode>,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?
    ) {
        buildAnchor(contextNode)
        div(classes = "table-row") {
            addSourceSetFilteringAttributes(contextNode)
            div {
                div("main-subrow " + contextNode.style.joinToString(separator = " ")) {
                    buildRowHeaderLink(toRender, pageContext, sourceSetRestriction, contextNode.anchor)
                    div("pull-right") {
                        if (ContentKind.shouldBePlatformTagged(contextNode.dci.kind)) {
                            createPlatformTags(contextNode, cssClasses = "no-gutters")
                        }
                    }
                }
                div {
                    buildRowBriefSectionForDocs(toRender, pageContext, sourceSetRestriction)
                }
            }
        }
    }

    private fun FlowContent.buildRowForContent(
        contextNode: ContentGroup,
        toRender: List<ContentNode>,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?
    ) {
        buildAnchor(contextNode)
        div(classes = "table-row") {
            addSourceSetFilteringAttributes(contextNode)
            div("main-subrow keyValue " + contextNode.style.joinToString(separator = " ")) {
                buildRowHeaderLink(toRender, pageContext, sourceSetRestriction, contextNode.anchor)
                div {
                    toRender.filter { it !is ContentLink && !it.hasStyle(ContentStyle.RowTitle) }
                        .takeIf { it.isNotEmpty() }?.let {
                            div("title") {
                                it.forEach {
                                    it.build(this, pageContext, sourceSetRestriction)
                                }
                            }
                        }
                }
            }
        }
    }

    private fun FlowContent.buildRowHeaderLink(
        toRender: List<ContentNode>,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?,
        anchorDestination: String?,
        classes: String = ""
    ) {
        toRender.filter { it is ContentLink || it.hasStyle(ContentStyle.RowTitle) }.takeIf { it.isNotEmpty() }?.let {
            div(classes) {
                it.filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } }
                    .forEach {
                        span("inline-flex") {
                            div {
                                it.build(this, pageContext, sourceSetRestriction)
                            }
                            if (it is ContentLink && !anchorDestination.isNullOrBlank()) {
                                buildAnchorCopyButton(anchorDestination)
                            }
                        }
                    }
            }
        }
    }

    private fun FlowContent.addSourceSetFilteringAttributes(
        contextNode: ContentGroup,
    ) {
        attributes["data-filterable-current"] = contextNode.sourceSets.joinToString(" ") {
            it.sourceSetIDs.merged.toString()
        }
        attributes["data-filterable-set"] = contextNode.sourceSets.joinToString(" ") {
            it.sourceSetIDs.merged.toString()
        }
    }

    private fun FlowContent.buildRowBriefSectionForDocs(
        toRender: List<ContentNode>,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?,
    ) {
        toRender.filter { it !is ContentLink }.takeIf { it.isNotEmpty() }?.let {
            it.forEach {
                span(classes = if (it.dci.kind == ContentKind.Comment) "brief-comment" else "") {
                    it.build(this, pageContext, sourceSetRestriction)
                }
            }
        }
    }

    private fun FlowContent.createPlatformTagBubbles(sourceSets: List<DisplaySourceSet>, cssClasses: String = "") {
        if (shouldRenderSourceSetBubbles) {
            div("platform-tags $cssClasses") {
                sourceSets.sortedBy { it.name }.forEach {
                    div("platform-tag") {
                        when (it.platform.key) {
                            "common" -> classes = classes + "common-like"
                            "native" -> classes = classes + "native-like"
                            "jvm" -> classes = classes + "jvm-like"
                            "js" -> classes = classes + "js-like"
                        }
                        text(it.name)
                    }
                }
            }
        }
    }

    private fun FlowContent.createPlatformTags(
        node: ContentNode,
        sourceSetRestriction: Set<DisplaySourceSet>? = null,
        cssClasses: String = ""
    ) {
        node.takeIf { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } }?.let {
            createPlatformTagBubbles(node.sourceSets.filter {
                sourceSetRestriction == null || it in sourceSetRestriction
            }.sortedBy { it.name }, cssClasses)
        }
    }

    override fun FlowContent.buildTable(
        node: ContentTable,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?
    ) {
        when {
            node.style.contains(CommentTable) -> buildDefaultTable(node, pageContext, sourceSetRestriction)
            else -> div(classes = "table") {
                node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue }
                node.children.forEach {
                    buildRow(it, pageContext, sourceSetRestriction)
                }
            }
        }

    }

    fun FlowContent.buildDefaultTable(
        node: ContentTable,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?
    ) {
        table {
            thead {
                node.header.forEach {
                    tr {
                        it.children.forEach {
                            th {
                                it.build(this@table, pageContext, sourceSetRestriction)
                            }
                        }
                    }
                }
            }
            tbody {
                node.children.forEach {
                    tr {
                        it.children.forEach {
                            td {
                                it.build(this, pageContext, sourceSetRestriction)
                            }
                        }
                    }
                }
            }
        }
    }


    override fun FlowContent.buildHeader(level: Int, node: ContentHeader, content: FlowContent.() -> Unit) {
        val classes = node.style.joinToString { it.toString() }.toLowerCase()
        when (level) {
            1 -> h1(classes = classes, content)
            2 -> h2(classes = classes, content)
            3 -> h3(classes = classes, content)
            4 -> h4(classes = classes, content)
            5 -> h5(classes = classes, content)
            else -> h6(classes = classes, content)
        }
    }

    private fun FlowContent.buildAnchor(
        anchor: String,
        anchorLabel: String,
        sourceSets: String,
        content: FlowContent.() -> Unit
    ) {
        a {
            attributes["data-name"] = anchor
            attributes["anchor-label"] = anchorLabel
            attributes["id"] = anchor
            attributes["data-filterable-set"] = sourceSets
        }
        content()
    }

    private fun FlowContent.buildAnchor(anchor: String, anchorLabel: String, sourceSets: String) =
        buildAnchor(anchor, anchorLabel, sourceSets) {}

    private fun FlowContent.buildAnchor(node: ContentNode) {
        node.anchorLabel?.let { label -> buildAnchor(node.anchor!!, label, node.sourceSetsFilters) }
    }


    override fun FlowContent.buildNavigation(page: PageNode) =
        div(classes = "breadcrumbs") {
            val path = locationProvider.ancestors(page).filterNot { it is RendererSpecificPage }.asReversed()
            if (path.size > 1) {
                buildNavigationElement(path.first(), page)
                path.drop(1).forEach { node ->
                    span(classes = "delimiter") {
                        text("/")
                    }
                    buildNavigationElement(node, page)
                }
            }
        }

    private fun FlowContent.buildNavigationElement(node: PageNode, page: PageNode) =
        if (node.isNavigable) {
            val isCurrentPage = (node == page)
            if (isCurrentPage) {
                span(classes = "current") {
                    text(node.name)
                }
            } else {
                buildLink(node, page)
            }
        } else {
            text(node.name)
        }

    private fun FlowContent.buildLink(to: PageNode, from: PageNode) =
        locationProvider.resolve(to, from)?.let { path ->
            buildLink(path) {
                text(to.name)
            }
        } ?: span {
            attributes["data-unresolved-link"] = to.name.htmlEscape()
            text(to.name)
        }

    fun FlowContent.buildAnchorCopyButton(pointingTo: String) {
        span(classes = "anchor-wrapper") {
            span(classes = "anchor-icon") {
                attributes["pointing-to"] = pointingTo
            }
            copiedPopup("Link copied to clipboard")
        }
    }

    fun FlowContent.buildLink(
        to: DRI,
        platforms: List<DisplaySourceSet>,
        from: PageNode? = null,
        block: FlowContent.() -> Unit
    ) = locationProvider.resolve(to, platforms.toSet(), from)?.let { buildLink(it, block) }
        ?: run { context.logger.error("Cannot resolve path for `$to` from `$from`"); block() }

    override fun buildError(node: ContentNode) = context.logger.error("Unknown ContentNode type: $node")

    override fun FlowContent.buildLineBreak() = br()
    override fun FlowContent.buildLineBreak(node: ContentBreakLine, pageContext: ContentPage) {
        if (node.style.contains(HorizontalBreakLineStyle)) {
            hr()
        } else {
            buildLineBreak()
        }
    }

    override fun FlowContent.buildLink(address: String, content: FlowContent.() -> Unit) =
        a(href = address, block = content)

    override fun FlowContent.buildDRILink(
        node: ContentDRILink,
        pageContext: ContentPage,
        sourceSetRestriction: Set<DisplaySourceSet>?
    ) = locationProvider.resolve(node.address, node.sourceSets, pageContext)?.let { address ->
        buildLink(address) {
            buildText(node.children, pageContext, sourceSetRestriction)
        }
    } ?: if (isPartial) {
        templateCommand(ResolveLinkCommand(node.address)) {
            buildText(node.children, pageContext, sourceSetRestriction)
        }
    } else {
        span {
            attributes["data-unresolved-link"] = node.address.toString().htmlEscape()
            buildText(node.children, pageContext, sourceSetRestriction)
        }
    }

    override fun FlowContent.buildCodeBlock(
        code: ContentCodeBlock,
        pageContext: ContentPage
    ) {
        div("sample-container") {
            val codeLang = "lang-" + code.language.ifEmpty { "kotlin" }
            val stylesWithBlock = code.style + TextStyle.Block + codeLang
            pre {
                code(stylesWithBlock.joinToString(" ") { it.toString().toLowerCase() }) {
                    attributes["theme"] = "idea"
                    code.children.forEach { buildContentNode(it, pageContext) }
                }
            }
            /*
            Disable copy button on samples as:
             - it is useless
             - it overflows with playground's run button
             */
            if (!code.style.contains(ContentStyle.RunnableSample)) copyButton()
        }
    }

    override fun FlowContent.buildCodeInline(
        code: ContentCodeInline,
        pageContext: ContentPage
    ) {
        val codeLang = "lang-" + code.language.ifEmpty { "kotlin" }
        val stylesWithBlock = code.style + codeLang
        code(stylesWithBlock.joinToString(" ") { it.toString().toLowerCase() }) {
            code.children.forEach { buildContentNode(it, pageContext) }
        }
    }

    override fun FlowContent.buildText(textNode: ContentText) = buildText(textNode, textNode.style)

    private fun FlowContent.buildText(textNode: ContentText, unappliedStyles: Set<Style>) {
        when {
            textNode.extra[HtmlContent] != null -> {
                consumer.onTagContentUnsafe { raw(textNode.text) }
            }
            unappliedStyles.contains(TextStyle.Indented) -> {
                consumer.onTagContentEntity(Entities.nbsp)
                buildText(textNode, unappliedStyles - TextStyle.Indented)
            }
            unappliedStyles.isNotEmpty() -> {
                val styleToApply = unappliedStyles.first()
                applyStyle(styleToApply) {
                    buildText(textNode, unappliedStyles - styleToApply)
                }
            }
            textNode.hasStyle(ContentStyle.RowTitle) || textNode.hasStyle(TextStyle.Cover) ->
                buildBreakableText(textNode.text)
            else -> text(textNode.text)
        }
    }

    private inline fun FlowContent.applyStyle(styleToApply: Style, crossinline body: FlowContent.() -> Unit) {
        when (styleToApply) {
            TextStyle.Bold -> b { body() }
            TextStyle.Italic -> i { body() }
            TextStyle.Strikethrough -> strike { body() }
            TextStyle.Strong -> strong { body() }
            TextStyle.Var -> htmlVar { body() }
            TextStyle.Underlined -> underline { body() }
            is TokenStyle -> span("token ${styleToApply.prismJsClass()}") { body() }
            else -> body()
        }
    }

    private fun TokenStyle.prismJsClass(): String = when(this) {
        // Prism.js parser adds Builtin token instead of Annotation
        // for some reason, so we also add it for consistency and correct coloring
        TokenStyle.Annotation -> "annotation builtin"
        else -> this.toString().toLowerCase()
    }

    override fun render(root: RootPageNode) {
        shouldRenderSourceSetBubbles = shouldRenderSourceSetBubbles(root)
        super.render(root)
    }

    override fun buildPage(page: ContentPage, content: (FlowContent, ContentPage) -> Unit): String =
        buildHtml(page, page.embeddedResources) {
            content(this, page)
        }


    open fun buildHtml(page: PageNode, resources: List<String>, content: FlowContent.() -> Unit): String =
        templater.renderFromTemplate(DokkaTemplateTypes.BASE) {
            val generatedContent =
                createHTML().div("main-content") {
                    id = "content"
                    (page as? ContentPage)?.let {
                        attributes["pageIds"] = "${context.configuration.moduleName}::${page.pageId}"
                    }
                    content()
                }

            templateModelMerger.invoke(templateModelFactories) {
                buildModel(
                    page,
                    resources,
                    locationProvider,
                    shouldRenderSourceSetBubbles,
                    generatedContent
                )
            }
        }

    /**
     * This is deliberately left open for plugins that have some other pages above ours and would like to link to them
     * instead of ours when clicking the logo
     */
    open fun FlowContent.clickableLogo(page: PageNode, pathToRoot: String) {
        if (context.configuration.delayTemplateSubstitution && page is ContentPage) {
            templateCommand(PathToRootSubstitutionCommand(pattern = "###", default = pathToRoot)) {
                a {
                    href = "###index.html"
                    templateCommand(
                        ProjectNameSubstitutionCommand(
                            pattern = "@@@",
                            default = context.configuration.moduleName
                        )
                    ) {
                        span {
                            text("@@@")
                        }
                    }
                }
            }
        } else {
            a {
                href = pathToRoot + "index.html"
                text(context.configuration.moduleName)
            }
        }
    }

    private val ContentNode.isAnchorable: Boolean
        get() = anchorLabel != null

    private val ContentNode.anchorLabel: String?
        get() = extra[SymbolAnchorHint]?.anchorName

    private val ContentNode.anchor: String?
        get() = extra[SymbolAnchorHint]?.contentKind?.let { contentKind ->
            (locationProvider as DokkaBaseLocationProvider).anchorForDCI(DCI(dci.dri, contentKind), sourceSets)
        }

    private val isPartial = context.configuration.delayTemplateSubstitution
}

fun List<SimpleAttr>.joinAttr() = joinToString(" ") { it.extraKey + "=" + it.extraValue }

private fun String.stripDiv() = drop(5).dropLast(6) // TODO: Find a way to do it without arbitrary trims

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

private fun PropertyContainer<ContentNode>.extraHtmlAttributes() = allOfType<SimpleAttr>()

private val ContentNode.sourceSetsFilters: String
    get() = sourceSets.sourceSetIDs.joinToString(" ") { it.toString() }

private val DisplaySourceSet.comparableKey
        get() = sourceSetIDs.merged.let { it.scopeId + it.sourceSetName }
