package org.jetbrains.dokka.base.signatures

import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.model.properties.WithExtraProperties
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet
import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.drisOfAllNestedBounds
import org.jetbrains.dokka.model.AnnotationTarget

interface JvmSignatureUtils {

    fun PageContentBuilder.DocumentableContentBuilder.annotationsBlock(d: AnnotationTarget)

    fun PageContentBuilder.DocumentableContentBuilder.annotationsInline(d: AnnotationTarget)

    fun <T : Documentable> WithExtraProperties<T>.modifiers(): SourceSetDependent<Set<ExtraModifiers>>

    fun Collection<ExtraModifiers>.toSignatureString(): String =
        joinToString("") { it.name.toLowerCase() + " " }

    fun <T : AnnotationTarget> WithExtraProperties<T>.annotations(): SourceSetDependent<List<Annotations.Annotation>> =
        extra[Annotations]?.directAnnotations ?: emptyMap()

    @Suppress("UNCHECKED_CAST")
    operator fun <T : Iterable<*>> SourceSetDependent<T>.plus(other: SourceSetDependent<T>): SourceSetDependent<T> =
        LinkedHashMap(this).apply {
            for ((k, v) in other) {
                put(k, get(k).let { if (it != null) (it + v) as T else v })
            }
        }

    fun DProperty.annotations(): SourceSetDependent<List<Annotations.Annotation>> =
        (extra[Annotations]?.directAnnotations ?: emptyMap()) +
        (getter?.annotations() ?: emptyMap()).mapValues { it.value.map { it.copy( scope = Annotations.AnnotationScope.GETTER) } } +
        (setter?.annotations() ?: emptyMap()).mapValues { it.value.map { it.copy( scope = Annotations.AnnotationScope.SETTER) } }

    private fun PageContentBuilder.DocumentableContentBuilder.annotations(
        d: AnnotationTarget,
        ignored: Set<Annotations.Annotation>,
        styles: Set<Style>,
        operation: PageContentBuilder.DocumentableContentBuilder.(Annotations.Annotation) -> Unit
    ): Unit = when (d) {
        is DFunction -> d.annotations()
        is DProperty -> d.annotations()
        is DClass -> d.annotations()
        is DInterface -> d.annotations()
        is DObject -> d.annotations()
        is DEnum -> d.annotations()
        is DAnnotation -> d.annotations()
        is DTypeParameter -> d.annotations()
        is DEnumEntry -> d.annotations()
        is DTypeAlias -> d.annotations()
        is DParameter -> d.annotations()
        is TypeParameter -> d.annotations()
        is GenericTypeConstructor -> d.annotations()
        is FunctionalTypeConstructor -> d.annotations()
        is JavaObject -> d.annotations()
        else -> null
    }?.let {
        it.entries.forEach {
            it.value.filter { it !in ignored && it.mustBeDocumented }.takeIf { it.isNotEmpty() }?.let { annotations ->
                group(sourceSets = setOf(it.key), styles = styles, kind = ContentKind.Annotations) {
                    annotations.forEach {
                        operation(it)
                    }
                }
            }
        }
    } ?: Unit

    fun PageContentBuilder.DocumentableContentBuilder.toSignatureString(
        a: Annotations.Annotation,
        renderAtStrategy: AtStrategy,
        listBrackets: Pair<Char, Char>,
        classExtension: String
    ) {

        when (renderAtStrategy) {
            is All, is OnlyOnce -> {
                when(a.scope) {
                    Annotations.AnnotationScope.GETTER -> text("@get:", styles = mainStyles + TokenStyle.Annotation)
                    Annotations.AnnotationScope.SETTER -> text("@set:", styles = mainStyles + TokenStyle.Annotation)
                    else -> text("@", styles = mainStyles + TokenStyle.Annotation)
                }
                link(a.dri.classNames!!, a.dri, styles = mainStyles + TokenStyle.Annotation)
            }
            is Never -> link(a.dri.classNames!!, a.dri)
        }
        val isNoWrappedBrackets = a.params.entries.isEmpty() && renderAtStrategy is OnlyOnce
        listParams(
            a.params.entries,
            if (isNoWrappedBrackets) null else Pair('(', ')')
        ) {
            text(it.key)
            text(" = ", styles = mainStyles + TokenStyle.Operator)
            when (renderAtStrategy) {
                is All -> All
                is Never, is OnlyOnce -> Never
            }.let { strategy ->
                valueToSignature(it.value, strategy, listBrackets, classExtension)
            }
        }
    }

    private fun PageContentBuilder.DocumentableContentBuilder.valueToSignature(
        a: AnnotationParameterValue,
        renderAtStrategy: AtStrategy,
        listBrackets: Pair<Char, Char>,
        classExtension: String
    ): Unit = when (a) {
        is AnnotationValue -> toSignatureString(a.annotation, renderAtStrategy, listBrackets, classExtension)
        is ArrayValue -> {
            listParams(a.value, listBrackets) { valueToSignature(it, renderAtStrategy, listBrackets, classExtension) }
        }
        is EnumValue -> link(a.enumName, a.enumDri)
        is ClassValue -> link(a.className + classExtension, a.classDRI)
        is StringValue -> group(styles = setOf(TextStyle.Breakable)) { stringLiteral( "\"${a.text()}\"") }
        is BooleanValue -> group(styles = setOf(TextStyle.Breakable)) { booleanLiteral(a.value) }
        is LiteralValue -> group(styles = setOf(TextStyle.Breakable)) { constant(a.text()) }
    }

    private fun<T> PageContentBuilder.DocumentableContentBuilder.listParams(
        params: Collection<T>,
        listBrackets: Pair<Char, Char>?,
        outFn: PageContentBuilder.DocumentableContentBuilder.(T) -> Unit
    ) {
        listBrackets?.let{ punctuation(it.first.toString()) }
        params.forEachIndexed { i, it ->
            group(styles = setOf(TextStyle.BreakableAfter)) {
                this.outFn(it)
                if (i != params.size - 1) punctuation(", ")
            }
        }
        listBrackets?.let{ punctuation(it.second.toString()) }
    }

    fun PageContentBuilder.DocumentableContentBuilder.annotationsBlockWithIgnored(
        d: AnnotationTarget,
        ignored: Set<Annotations.Annotation>,
        renderAtStrategy: AtStrategy,
        listBrackets: Pair<Char, Char>,
        classExtension: String
    ) {
        annotations(d, ignored, setOf(TextStyle.Block)) {
            group {
                toSignatureString(it, renderAtStrategy, listBrackets, classExtension)
            }
        }
    }

    fun PageContentBuilder.DocumentableContentBuilder.annotationsInlineWithIgnored(
        d: AnnotationTarget,
        ignored: Set<Annotations.Annotation>,
        renderAtStrategy: AtStrategy,
        listBrackets: Pair<Char, Char>,
        classExtension: String
    ) {
        annotations(d, ignored, setOf(TextStyle.Span)) {
            toSignatureString(it, renderAtStrategy, listBrackets, classExtension)
            text(Typography.nbsp.toString())
        }
    }

    fun <T : Documentable> WithExtraProperties<T>.stylesIfDeprecated(sourceSetData: DokkaSourceSet): Set<TextStyle> {
        val directAnnotations = extra[Annotations]?.directAnnotations?.get(sourceSetData) ?: emptyList()
        val hasAnyDeprecatedAnnotation =
            directAnnotations.any { it.dri == DRI("kotlin", "Deprecated") || it.dri == DRI("java.lang", "Deprecated") }

        return if (hasAnyDeprecatedAnnotation) setOf(TextStyle.Strikethrough) else emptySet()
    }

    infix fun DFunction.uses(typeParameter: DTypeParameter): Boolean {
        val parameterDris = parameters.flatMap { listOf(it.dri) + it.type.drisOfAllNestedBounds }
        val receiverDris =
            listOfNotNull(
                receiver?.dri,
                *receiver?.type?.drisOfAllNestedBounds?.toTypedArray() ?: emptyArray()
            )
        val allDris = parameterDris + receiverDris
        return typeParameter.dri in allDris
    }

    /**
     * Builds a distinguishable [function] parameters block, so that it
     * can be processed or custom rendered down the road.
     *
     * Resulting structure:
     * ```
     * SymbolContentKind.Parameters(style = wrapped) {
     *     SymbolContentKind.Parameter(style = indented) { param, }
     *     SymbolContentKind.Parameter(style = indented) { param, }
     *     SymbolContentKind.Parameter(style = indented) { param }
     * }
     * ```
     * Wrapping and indentation of parameters is applied conditionally, see [shouldWrapParams]
     */
    fun PageContentBuilder.DocumentableContentBuilder.parametersBlock(
        function: DFunction, paramBuilder: PageContentBuilder.DocumentableContentBuilder.(DParameter) -> Unit
    ) {
        val shouldWrap = function.shouldWrapParams()
        val parametersStyle = if (shouldWrap) setOf(ContentStyle.Wrapped) else emptySet()
        val elementStyle = if (shouldWrap) setOf(ContentStyle.Indented) else emptySet()
        group(kind = SymbolContentKind.Parameters, styles = parametersStyle) {
            function.parameters.dropLast(1).forEach {
                group(kind = SymbolContentKind.Parameter, styles = elementStyle) {
                    paramBuilder(it)
                    punctuation(", ")
                }
            }
            group(kind = SymbolContentKind.Parameter, styles = elementStyle) {
                paramBuilder(function.parameters.last())
            }
        }
    }

    /**
     * Determines whether parameters in a function (including constructor) should be wrapped
     *
     * Without wrapping:
     * ```
     * class SimpleClass(foo: String, bar: String) {}
     * ```
     * With wrapping:
     * ```
     * class SimpleClass(
     *     foo: String,
     *     bar: String,
     *     baz: String
     * )
     * ```
     */
    private fun DFunction.shouldWrapParams() = this.parameters.size >= 3
}

sealed class AtStrategy
object All : AtStrategy()
object OnlyOnce : AtStrategy()
object Never : AtStrategy()