package org.jetbrains.dokka

import org.jetbrains.dokka.LanguageService.RenderMode

/**
 * Implements [LanguageService] and provides rendering of symbols in Kotlin language
 */
class KotlinLanguageService : LanguageService {
    private val visibilityModifiers = setOf("public", "protected", "private")

    override fun render(node: DocumentationNode, renderMode: RenderMode): ContentNode {
        return content {
            when (node.kind) {
                DocumentationNode.Kind.Package -> if (renderMode == RenderMode.FULL) renderPackage(node)
                DocumentationNode.Kind.Class,
                DocumentationNode.Kind.Interface,
                DocumentationNode.Kind.Enum,
                DocumentationNode.Kind.AnnotationClass,
                DocumentationNode.Kind.Object -> renderClass(node, renderMode)

                DocumentationNode.Kind.EnumItem,
                DocumentationNode.Kind.ExternalClass -> if (renderMode == RenderMode.FULL) identifier(node.name)

                DocumentationNode.Kind.TypeParameter -> renderTypeParameter(node)
                DocumentationNode.Kind.Type,
                DocumentationNode.Kind.UpperBound -> renderType(node)

                DocumentationNode.Kind.Modifier -> renderModifier(node)
                DocumentationNode.Kind.Constructor,
                DocumentationNode.Kind.Function,
                DocumentationNode.Kind.CompanionObjectFunction -> renderFunction(node, renderMode)
                DocumentationNode.Kind.Property,
                DocumentationNode.Kind.CompanionObjectProperty -> renderProperty(node, renderMode)
                else -> identifier(node.name)
            }
        }
    }

    override fun renderName(node: DocumentationNode): String {
        return when (node.kind) {
            DocumentationNode.Kind.Constructor -> node.owner!!.name
            else -> node.name
        }
    }

    private fun ContentBlock.renderPackage(node: DocumentationNode) {
        keyword("package")
        text(" ")
        identifier(node.name)
    }

    private fun ContentBlock.renderList(nodes: List<DocumentationNode>, separator: String = ", ",
                                        noWrap: Boolean = false, renderItem: (DocumentationNode) -> Unit) {
        if (nodes.none())
            return
        renderItem(nodes.first())
        nodes.drop(1).forEach {
            if (noWrap) {
                symbol(separator.removeSuffix(" "))
                nbsp()
            } else {
                symbol(separator)
            }
            renderItem(it)
        }
    }

    private fun ContentBlock.renderLinked(node: DocumentationNode, body: ContentBlock.(DocumentationNode)->Unit) {
        val to = node.links.firstOrNull()
        if (to == null)
            body(node)
        else
            link(to) {
                body(node)
            }
    }

    private fun ContentBlock.renderType(node: DocumentationNode) {
        val typeArguments = node.details(DocumentationNode.Kind.Type)
        if (node.name == "Function${typeArguments.count() - 1}") {
            // lambda
            symbol("(")
            renderList(typeArguments.take(typeArguments.size() - 1), noWrap = true) {
                renderType(it)
            }
            symbol(")")
            nbsp()
            symbol("->")
            nbsp()
            renderType(typeArguments.last())
            return
        }
        if (node.name == "ExtensionFunction${typeArguments.count() - 2}") {
            // extension lambda
            renderType(typeArguments.first())
            symbol(".")
            symbol("(")
            renderList(typeArguments.drop(1).take(typeArguments.size() - 2), noWrap = true) {
                renderType(it)
            }
            symbol(")")
            nbsp()
            symbol("->")
            nbsp()
            renderType(typeArguments.last())
            return
        }
        renderSingleModifier(node)
        renderLinked(node) { identifier(it.name, IdentifierKind.TypeName) }
        if (typeArguments.any()) {
            symbol("<")
            renderList(typeArguments, noWrap = true) {
                renderType(it)
            }
            symbol(">")
        }
        val nullabilityModifier = node.details(DocumentationNode.Kind.NullabilityModifier).singleOrNull()
        if (nullabilityModifier != null) {
            symbol(nullabilityModifier.name)
        }
    }

    private fun ContentBlock.renderModifier(node: DocumentationNode) {
        when (node.name) {
            "final", "internal", "var" -> {}
            else -> {
                keyword(node.name)
                text(" ")
            }
        }
    }

    private fun ContentBlock.renderTypeParameter(node: DocumentationNode) {
        renderSingleModifier(node)

        identifier(node.name)

        val constraints = node.details(DocumentationNode.Kind.UpperBound)
        if (constraints.any()) {
            nbsp()
            symbol(":")
            nbsp()
            renderList(constraints, noWrap=true) {
                renderType(it)
            }
        }
    }

    private fun ContentBlock.renderSingleModifier(node: DocumentationNode) {
        val modifier = node.details(DocumentationNode.Kind.Modifier).singleOrNull()
        if (modifier != null) {
            keyword(modifier.name)
            nbsp()
        }
    }

    private fun ContentBlock.renderParameter(node: DocumentationNode) {
        renderAnnotationsForNode(node)
        identifier(node.name, IdentifierKind.ParameterName)
        symbol(":")
        nbsp()
        val parameterType = node.detail(DocumentationNode.Kind.Type)
        renderType(parameterType)
        val valueNode = node.details(DocumentationNode.Kind.Value).firstOrNull()
        if (valueNode != null) {
            nbsp()
            symbol("=")
            nbsp()
            text(valueNode.name)
        }
    }

    private fun ContentBlock.renderTypeParametersForNode(node: DocumentationNode) {
        val typeParameters = node.details(DocumentationNode.Kind.TypeParameter)
        if (typeParameters.any()) {
            symbol("<")
            renderList(typeParameters) {
                renderTypeParameter(it)
            }
            symbol(">")
        }
    }

    private fun ContentBlock.renderSupertypesForNode(node: DocumentationNode) {
        val supertypes = node.details(DocumentationNode.Kind.Supertype)
        if (supertypes.any()) {
            nbsp()
            symbol(":")
            nbsp()
            renderList(supertypes) {
                renderType(it)
            }
        }
    }

    private fun ContentBlock.renderModifiersForNode(node: DocumentationNode, renderMode: RenderMode) {
        val modifiers = node.details(DocumentationNode.Kind.Modifier)
        for (it in modifiers) {
            if (node.kind == org.jetbrains.dokka.DocumentationNode.Kind.Interface && it.name == "abstract")
                continue
            if (renderMode == RenderMode.SUMMARY && it.name in visibilityModifiers) {
                continue
            }
            renderModifier(it)
        }
    }

    private fun ContentBlock.renderAnnotationsForNode(node: DocumentationNode) {
        node.annotations.forEach {
            renderAnnotation(it)
        }
    }

    private fun ContentBlock.renderAnnotation(node: DocumentationNode) {
        identifier(node.name, IdentifierKind.AnnotationName)
        val parameters = node.details(DocumentationNode.Kind.Parameter)
        if (!parameters.isEmpty()) {
            symbol("(")
            renderList(parameters) {
                text(it.detail(DocumentationNode.Kind.Value).name)
            }
            symbol(")")
        }
        text(" ")
    }

    private fun ContentBlock.renderClass(node: DocumentationNode, renderMode: RenderMode) {
        renderModifiersForNode(node, renderMode)
        renderAnnotationsForNode(node)
        when (node.kind) {
            DocumentationNode.Kind.Class -> keyword("class ")
            DocumentationNode.Kind.Interface -> keyword("interface ")
            DocumentationNode.Kind.Enum -> keyword("enum class ")
            DocumentationNode.Kind.AnnotationClass -> keyword("annotation class ")
            DocumentationNode.Kind.EnumItem -> keyword("enum val ")
            DocumentationNode.Kind.Object -> keyword("object ")
            else -> throw IllegalArgumentException("Node $node is not a class-like object")
        }

        identifierOrDeprecated(node)
        renderTypeParametersForNode(node)
        renderSupertypesForNode(node)
    }

    private fun ContentBlock.renderFunction(node: DocumentationNode, renderMode: RenderMode) {
        renderModifiersForNode(node, renderMode)
        renderAnnotationsForNode(node)
        when (node.kind) {
            DocumentationNode.Kind.Constructor -> identifier(node.owner!!.name)
            DocumentationNode.Kind.Function,
            DocumentationNode.Kind.CompanionObjectFunction -> keyword("fun ")
            else -> throw IllegalArgumentException("Node $node is not a function-like object")
        }
        renderTypeParametersForNode(node)
        if (node.details(DocumentationNode.Kind.TypeParameter).any()) {
            text(" ")
        }
        val receiver = node.details(DocumentationNode.Kind.Receiver).singleOrNull()
        if (receiver != null) {
            renderType(receiver.detail(DocumentationNode.Kind.Type))
            symbol(".")
        }

        if (node.kind != org.jetbrains.dokka.DocumentationNode.Kind.Constructor)
            identifierOrDeprecated(node)

        symbol("(")
        renderList(node.details(DocumentationNode.Kind.Parameter)) {
            renderParameter(it)
        }
        symbol(")")
        if (needReturnType(node)) {
            symbol(": ")
            renderType(node.detail(DocumentationNode.Kind.Type))
        }
    }

    private fun needReturnType(node: DocumentationNode) = when(node.kind) {
        DocumentationNode.Kind.Constructor -> false
        else -> true
    }

    private fun ContentBlock.renderProperty(node: DocumentationNode, renderMode: RenderMode) {
        renderModifiersForNode(node, renderMode)
        renderAnnotationsForNode(node)
        when (node.kind) {
            DocumentationNode.Kind.Property,
            DocumentationNode.Kind.CompanionObjectProperty -> keyword("${node.getPropertyKeyword()} ")
            else -> throw IllegalArgumentException("Node $node is not a property")
        }
        renderTypeParametersForNode(node)
        if (node.details(DocumentationNode.Kind.TypeParameter).any()) {
            text(" ")
        }
        val receiver = node.details(DocumentationNode.Kind.Receiver).singleOrNull()
        if (receiver != null) {
            renderType(receiver.detail(DocumentationNode.Kind.Type))
            symbol(".")
        }

        identifierOrDeprecated(node)
        symbol(": ")
        renderType(node.detail(DocumentationNode.Kind.Type))
    }

    fun DocumentationNode.getPropertyKeyword() =
            if (details(DocumentationNode.Kind.Modifier).any { it.name == "var" }) "var" else "val"

    fun ContentBlock.identifierOrDeprecated(node: DocumentationNode) {
        if (node.deprecation != null) {
            val strike = ContentStrikethrough()
            strike.identifier(node.name)
            append(strike)
        } else {
            identifier(node.name)
        }
    }
}