/*
 * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

package org.jetbrains.dokka.pages

import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.Documentable
import org.jetbrains.dokka.model.WithChildren
import java.util.*

public interface PageNode : WithChildren<PageNode> {
    public val name: String
    override val children: List<PageNode>

    public fun modified(
        name: String = this.name,
        children: List<PageNode> = this.children
    ): PageNode
}

public interface ContentPage : PageNode {
    public val content: ContentNode
    public val dri: Set<DRI>
    public val embeddedResources: List<String>

    @Deprecated("Deprecated. Remove its usages from your code.",
        ReplaceWith("this.documentables.firstOrNull()")
    )
    public val documentable: Documentable?
        get() = if (this is WithDocumentables) this.documentables.firstOrNull() else null

    public fun modified(
        name: String = this.name,
        content: ContentNode = this.content,
        dri: Set<DRI> = this.dri,
        embeddedResources: List<String> = this.embeddedResources,
        children: List<PageNode> = this.children
    ): ContentPage
}

public interface WithDocumentables {
    public val documentables: List<Documentable>
}

public abstract class RootPageNode(
    public val forceTopLevelName: Boolean = false
) : PageNode {
    public val parentMap: Map<PageNode, PageNode> by lazy {
        IdentityHashMap<PageNode, PageNode>().apply {
            fun process(parent: PageNode) {
                parent.children.forEach { child ->
                    put(child, parent)
                    process(child)
                }
            }
            process(this@RootPageNode)
        }
    }

    public fun transformPageNodeTree(operation: (PageNode) -> PageNode): RootPageNode =
        this.transformNode(operation) as RootPageNode

    public fun transformContentPagesTree(operation: (ContentPage) -> ContentPage): RootPageNode = transformPageNodeTree {
        if (it is ContentPage) operation(it) else it
    }

    private fun PageNode.transformNode(operation: (PageNode) -> PageNode): PageNode =
        operation(this).let { newNode ->
            newNode.modified(children = newNode.children.map { it.transformNode(operation) })
        }

    abstract override fun modified(
        name: String,
        children: List<PageNode>
    ): RootPageNode
}

public class ModulePageNode(
    override val name: String,
    override val content: ContentNode,
    override val documentables: List<Documentable> = listOf(),
    override val children: List<PageNode>,
    override val embeddedResources: List<String> = listOf()
) : RootPageNode(), ModulePage {
    override val dri: Set<DRI> = setOf(DRI.topLevel)

    override fun modified(name: String, children: List<PageNode>): ModulePageNode =
        modified(name = name, content = this.content, dri = dri, children = children)

    override fun modified(
        name: String,
        content: ContentNode,
        dri: Set<DRI>,
        embeddedResources: List<String>,
        children: List<PageNode>
    ): ModulePageNode =
        if (name == this.name && content === this.content && embeddedResources === this.embeddedResources && children shallowEq this.children) this
        else ModulePageNode(name, content, documentables, children, embeddedResources)
}

public class PackagePageNode(
    override val name: String,
    override val content: ContentNode,
    override val dri: Set<DRI>,
    override val documentables: List<Documentable> = listOf(),
    override val children: List<PageNode>,
    override val embeddedResources: List<String> = listOf()
) : PackagePage {

    init {
        require(name.isNotBlank()) { "PackagePageNode.name cannot be blank" }
    }

    override fun modified(name: String, children: List<PageNode>): PackagePageNode =
        modified(name = name, content = this.content, children = children)

    override fun modified(
        name: String,
        content: ContentNode,
        dri: Set<DRI>,
        embeddedResources: List<String>,
        children: List<PageNode>
    ): PackagePageNode =
        if (name == this.name && content === this.content && embeddedResources === this.embeddedResources && children shallowEq this.children) this
        else PackagePageNode(name, content, dri, documentables, children, embeddedResources)
}

public class ClasslikePageNode(
    override val name: String,
    override val content: ContentNode,
    override val dri: Set<DRI>,
    override val documentables: List<Documentable> = listOf(),
    override val children: List<PageNode>,
    override val embeddedResources: List<String> = listOf()
) : ClasslikePage {
    override fun modified(name: String, children: List<PageNode>): ClasslikePageNode =
        modified(name = name, content = this.content, children = children)

    override fun modified(
        name: String,
        content: ContentNode,
        dri: Set<DRI>,
        embeddedResources: List<String>,
        children: List<PageNode>
    ): ClasslikePageNode =
        if (name == this.name && content === this.content && embeddedResources === this.embeddedResources && children shallowEq this.children) this
        else ClasslikePageNode(name, content, dri, documentables, children, embeddedResources)
}

public class MemberPageNode(
    override val name: String,
    override val content: ContentNode,
    override val dri: Set<DRI>,
    override val documentables: List<Documentable> = listOf(),
    override val children: List<PageNode> = emptyList(),
    override val embeddedResources: List<String> = listOf()
) : MemberPage {
    override fun modified(name: String, children: List<PageNode>): MemberPageNode =
        modified(name = name, content = this.content, children = children)

    override fun modified(
        name: String,
        content: ContentNode,
        dri: Set<DRI>,
        embeddedResources: List<String>,
        children: List<PageNode>
    ): MemberPageNode =
        if (name == this.name && content === this.content && embeddedResources === this.embeddedResources && children shallowEq this.children) this
        else MemberPageNode(name, content, dri, documentables, children, embeddedResources)
}


public class MultimoduleRootPageNode(
    override val dri: Set<DRI>,
    override val content: ContentNode,
    override val embeddedResources: List<String> = emptyList()
) : RootPageNode(forceTopLevelName = true), MultimoduleRootPage {
    override val name: String = "All modules"

    override val children: List<PageNode> = emptyList()

    override fun modified(name: String, children: List<PageNode>): RootPageNode =
        MultimoduleRootPageNode(dri, content, embeddedResources)

    override fun modified(
        name: String,
        content: ContentNode,
        dri: Set<DRI>,
        embeddedResources: List<String>,
        children: List<PageNode>
    ): ContentPage =
        if (name == this.name && content === this.content && embeddedResources === this.embeddedResources && children shallowEq this.children) this
        else MultimoduleRootPageNode(dri, content, embeddedResources)
}

public inline fun <reified T : PageNode> PageNode.children(): List<T> = children.filterIsInstance<T>()

private infix fun <T> List<T>.shallowEq(other: List<T>) =
    this === other || (this.size == other.size && (this zip other).all { (a, b) -> a === b })