package org.jetbrains.dokka.base.translators.documentables

import org.jetbrains.dokka.base.signatures.SignatureProvider
import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.model.doc.DocTag
import org.jetbrains.dokka.model.properties.PropertyContainer
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.utilities.DokkaLogger

@DslMarker
annotation class ContentBuilderMarker

open class PageContentBuilder(
    val commentsConverter: CommentsToContentConverter,
    val signatureProvider: SignatureProvider,
    val logger: DokkaLogger
) {
    fun contentFor(
        dri: DRI,
        sourceSets: Set<SourceSetData>,
        kind: Kind = ContentKind.Main,
        styles: Set<Style> = emptySet(),
        extra: PropertyContainer<ContentNode> = PropertyContainer.empty(),
        block: DocumentableContentBuilder.() -> Unit
    ): ContentGroup =
        DocumentableContentBuilder(setOf(dri), sourceSets, styles, extra)
            .apply(block)
            .build(sourceSets, kind, styles, extra)

    fun contentFor(
        dri: Set<DRI>,
        sourceSets: Set<SourceSetData>,
        kind: Kind = ContentKind.Main,
        styles: Set<Style> = emptySet(),
        extra: PropertyContainer<ContentNode> = PropertyContainer.empty(),
        block: DocumentableContentBuilder.() -> Unit
    ): ContentGroup =
        DocumentableContentBuilder(dri, sourceSets, styles, extra)
            .apply(block)
            .build(sourceSets, kind, styles, extra)

    fun contentFor(
        d: Documentable,
        kind: Kind = ContentKind.Main,
        styles: Set<Style> = emptySet(),
        extra: PropertyContainer<ContentNode> = PropertyContainer.empty(),
        sourceSets: Set<SourceSetData> = d.sourceSets.toSet(),
        block: DocumentableContentBuilder.() -> Unit = {}
    ): ContentGroup =
        DocumentableContentBuilder(setOf(d.dri), sourceSets, styles, extra)
            .apply(block)
            .build(sourceSets, kind, styles, extra)

    @ContentBuilderMarker
    open inner class DocumentableContentBuilder(
        val mainDRI: Set<DRI>,
        val mainPlatformData: Set<SourceSetData>,
        val mainStyles: Set<Style>,
        val mainExtra: PropertyContainer<ContentNode>
    ) {
        protected val contents = mutableListOf<ContentNode>()

        fun build(
            sourceSets: Set<SourceSetData>,
            kind: Kind,
            styles: Set<Style>,
            extra: PropertyContainer<ContentNode>
        ) = ContentGroup(
            contents.toList(),
            DCI(mainDRI, kind),
            sourceSets,
            styles,
            extra
        )

        operator fun ContentNode.unaryPlus() {
            contents += this
        }

        operator fun Collection<ContentNode>.unaryPlus() {
            contents += this
        }

        fun header(
            level: Int,
            text: String,
            kind: Kind = ContentKind.Main,
            platformData: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit = {}
        ) {
            contents += ContentHeader(
                level,
                contentFor(
                    mainDRI,
                    platformData,
                    kind,
                    styles,
                    extra + SimpleAttr("anchor", text.replace("\\s".toRegex(), "").toLowerCase())
                ) {
                    text(text)
                    block()
                }
            )
        }

        fun cover(
            text: String,
            platformData: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles + TextStyle.Cover,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit = {}
            ) {
            header(1, text, platformData = platformData, styles = styles, extra = extra, block = block)
        }

        fun text(
            text: String,
            kind: Kind = ContentKind.Main,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra
        ) {
            contents += createText(text, kind, sourceSets, styles, extra)
        }

        fun buildSignature(d: Documentable) = signatureProvider.signature(d)

        fun linkTable(
            elements: List<DRI>,
            kind: Kind = ContentKind.Main,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra
        ) {
            contents += ContentTable(
                emptyList(),
                elements.map {
                    contentFor(it, sourceSets, kind, styles, extra) {
                        link(it.classNames ?: "", it)
                    }
                },
                DCI(mainDRI, kind),
                sourceSets, styles, extra
            )
        }

        fun table(
            kind: Kind = ContentKind.Main,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            operation: DocumentableContentBuilder.() -> List<ContentGroup>
        ) {
            contents += ContentTable(
                emptyList(),
                operation(),
                DCI(mainDRI, kind),
                sourceSets, styles, extra
            )
        }

        fun <T : Documentable> block(
            name: String,
            level: Int,
            kind: Kind = ContentKind.Main,
            elements: Iterable<T>,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            renderWhenEmpty: Boolean = false,
            operation: DocumentableContentBuilder.(T) -> Unit
        ) {
            if (renderWhenEmpty || elements.any()) {
                header(level, name) { }
                contents += ContentTable(
                    emptyList(),
                    elements.map {
                        buildGroup(setOf(it.dri), it.sourceSets.toSet(), kind, styles, extra) {
                            operation(it)
                        }
                    },
                    DCI(mainDRI, kind),
                    sourceSets, styles, extra
                )
            }
        }

        fun <T> list(
            elements: List<T>,
            prefix: String = "",
            suffix: String = "",
            separator: String = ", ",
            sourceSets: Set<SourceSetData> = mainPlatformData, // TODO: children should be aware of this platform data
            operation: DocumentableContentBuilder.(T) -> Unit
        ) {
            if (elements.isNotEmpty()) {
                if (prefix.isNotEmpty()) text(prefix, sourceSets = sourceSets)
                elements.dropLast(1).forEach {
                    operation(it)
                    text(separator, sourceSets = sourceSets)
                }
                operation(elements.last())
                if (suffix.isNotEmpty()) text(suffix, sourceSets = sourceSets)
            }
        }

        fun link(
            text: String,
            address: DRI,
            kind: Kind = ContentKind.Main,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra
        ) {
            contents += linkNode(text, address, kind, sourceSets, styles, extra)
        }

        fun linkNode(
            text: String,
            address: DRI,
            kind: Kind = ContentKind.Main,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra
        ) = ContentDRILink(
            listOf(createText(text, kind, sourceSets, styles, extra)),
            address,
            DCI(mainDRI, kind),
            sourceSets
        )

        fun link(
            text: String,
            address: String,
            kind: Kind = ContentKind.Main,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra
        ) =
            ContentResolvedLink(
                children = listOf(createText(text, kind, sourceSets, styles, extra)),
                address = address,
                extra = PropertyContainer.empty(),
                dci = DCI(mainDRI, kind),
                sourceSets = sourceSets,
                style = emptySet()
            )

        fun link(
            address: DRI,
            kind: Kind = ContentKind.Main,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit
        ) {
            contents += ContentDRILink(
                contentFor(mainDRI, sourceSets, kind, styles, extra, block).children,
                address,
                DCI(mainDRI, kind),
                sourceSets
            )
        }

        fun comment(
            docTag: DocTag,
            kind: Kind = ContentKind.Comment,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra
        ) {
            val content = commentsConverter.buildContent(
                docTag,
                DCI(mainDRI, kind),
                sourceSets
            )
            contents += ContentGroup(content, DCI(mainDRI, kind), sourceSets, styles, extra)
        }

        fun group(
            dri: Set<DRI> = mainDRI,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            kind: Kind = ContentKind.Main,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit
        ) {
            contents += buildGroup(dri, sourceSets, kind, styles, extra, block)
        }

        fun divergentGroup(
            groupID: ContentDivergentGroup.GroupID,
            dri: Set<DRI> = mainDRI,
            kind: Kind = ContentKind.Main,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            implicitlySourceSetHinted: Boolean = true,
            block: DivergentBuilder.() -> Unit
        ) {
            contents +=
                DivergentBuilder(dri, kind, styles, extra)
                    .apply(block)
                    .build(groupID = groupID, implicitlySourceSetHinted = implicitlySourceSetHinted)
        }

        fun buildGroup(
            dri: Set<DRI> = mainDRI,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            kind: Kind = ContentKind.Main,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit
        ): ContentGroup = contentFor(dri, sourceSets, kind, styles, extra, block)

        fun sourceSetDependentHint(
            dri: Set<DRI> = mainDRI,
            sourceSets: Set<SourceSetData> = mainPlatformData,
            kind: Kind = ContentKind.Main,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit
        ) {
            contents += PlatformHintedContent(
                buildGroup(dri, sourceSets, kind, styles, extra, block),
                sourceSets
            )
        }

        fun sourceSetDependentHint(
            dri: DRI,
            platformData: Set<SourceSetData> = mainPlatformData,
            kind: Kind = ContentKind.Main,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit
        ) {
            contents += PlatformHintedContent(
                buildGroup(setOf(dri), platformData, kind, styles, extra, block),
                platformData
            )
        }

        protected fun createText(
            text: String,
            kind: Kind,
            sourceSets: Set<SourceSetData>,
            styles: Set<Style>,
            extra: PropertyContainer<ContentNode>
        ) =
            ContentText(text, DCI(mainDRI, kind), sourceSets, styles, extra)

        fun <T> platformText(
            value: SourceSetDependent<T>,
            platforms: Set<SourceSetData> = value.keys,
            transform: (T) -> String
        ) = value.entries.filter { it.key in platforms }.mapNotNull { (p, v) ->
            transform(v).takeIf { it.isNotBlank() }?.let { it to p }
        }.groupBy({ it.first }) { it.second }.forEach {
            text(it.key, sourceSets = it.value.toSet())
        }
    }

    @ContentBuilderMarker
    open inner class DivergentBuilder(
        private val mainDRI: Set<DRI>,
        private val mainKind: Kind,
        private val mainStyles: Set<Style>,
        private val mainExtra: PropertyContainer<ContentNode>
    ) {
        private val instances: MutableList<ContentDivergentInstance> = mutableListOf()
        fun instance(
            dri: Set<DRI>,
            sourceSets: Set<SourceSetData>,  // Having correct PlatformData is crucial here, that's why there's no default
            kind: Kind = mainKind,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DivergentInstanceBuilder.() -> Unit
        ) {
            instances += DivergentInstanceBuilder(dri, sourceSets, styles, extra)
                .apply(block)
                .build(kind)
        }

        fun build(
            groupID: ContentDivergentGroup.GroupID,
            implicitlySourceSetHinted: Boolean,
            kind: Kind = mainKind,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra
        ) = ContentDivergentGroup(
            instances.toList(),
            DCI(mainDRI, kind),
            styles,
            extra,
            groupID,
            implicitlySourceSetHinted
        )
    }

    @ContentBuilderMarker
    open inner class DivergentInstanceBuilder(
        private val mainDRI: Set<DRI>,
        private val mainSourceSets: Set<SourceSetData>,
        private val mainStyles: Set<Style>,
        private val mainExtra: PropertyContainer<ContentNode>
    ) {
        private var before: ContentNode? = null
        private var divergent: ContentNode? = null
        private var after: ContentNode? = null

        fun before(
            dri: Set<DRI> = mainDRI,
            sourceSets: Set<SourceSetData> = mainSourceSets,
            kind: Kind = ContentKind.Main,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit
        ) {
            contentFor(dri, sourceSets, kind, styles, extra, block)
                .takeIf { it.hasAnyContent() }
                .also { before = it }
        }

        fun divergent(
            dri: Set<DRI> = mainDRI,
            sourceSets: Set<SourceSetData> = mainSourceSets,
            kind: Kind = ContentKind.Main,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit
        ) {
            divergent = contentFor(dri, sourceSets, kind, styles, extra, block)
        }

        fun after(
            dri: Set<DRI> = mainDRI,
            sourceSets: Set<SourceSetData> = mainSourceSets,
            kind: Kind = ContentKind.Main,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra,
            block: DocumentableContentBuilder.() -> Unit
        ) {
            contentFor(dri, sourceSets, kind, styles, extra, block)
                .takeIf { it.hasAnyContent() }
                .also { after = it }
        }


        fun build(
            kind: Kind,
            sourceSets: Set<SourceSetData> = mainSourceSets,
            styles: Set<Style> = mainStyles,
            extra: PropertyContainer<ContentNode> = mainExtra
        ) =
            ContentDivergentInstance(
                before,
                divergent!!,
                after,
                DCI(mainDRI, kind),
                sourceSets,
                styles,
                extra
            )
    }
}