package se.ansman.kotshi.renderer

import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.moshi.JsonAdapter
import se.ansman.kotshi.*
import se.ansman.kotshi.model.GeneratedAnnotation
import se.ansman.kotshi.model.JsonAdapterFactory
import se.ansman.kotshi.model.render

internal class JsonAdapterFactoryRenderer(
    private val factory: JsonAdapterFactory,
    private val createAnnotationsUsingConstructor: Boolean
) {
    private val nameAllocator = NameAllocator()

    fun render(generatedAnnotation: GeneratedAnnotation?, typeSpecModifier: TypeSpec.Builder.() -> Unit): FileSpec {
        val annotations = mutableSetOf<AnnotationSpec>()
        val properties = mutableSetOf<PropertySpec>()
        val createFunction = makeCreateFunction(
            typeParam = ParameterSpec(nameAllocator.newName("type"), Types.Java.type),
            annotationsParam = ParameterSpec(
                nameAllocator.newName("annotations"),
                Types.Kotlin.set.parameterizedBy(Types.Kotlin.annotation)
            ),
            moshiParam = ParameterSpec(nameAllocator.newName("moshi"), Types.Moshi.moshi),
            annotations = annotations,
            properties = properties,
        )
        return FileSpec.builder(factory.factoryClassName.packageName, factory.factoryClassName.simpleName)
            .addFileComment("Code generated by Kotshi. Do not edit.")
            .addAnnotation(
                AnnotationSpec.builder(Types.Kotlin.suppress)
                    .addMember("%S", "EXPERIMENTAL_IS_NOT_ENABLED")
                    .addMember("%S", "REDUNDANT_PROJECTION")
                    .build()
            )
            .addType(
                TypeSpec.objectBuilder(factory.factoryClassName)
                    .addModifiers(KModifier.INTERNAL)
                    .apply { generatedAnnotation?.toAnnotationSpec()?.let(::addAnnotation) }
                    .apply {
                        when (factory.usageType) {
                            JsonAdapterFactory.UsageType.Standalone -> addSuperinterface(Types.Moshi.jsonAdapterFactory)
                            is JsonAdapterFactory.UsageType.Subclass -> {
                                if (factory.usageType.parentIsInterface) {
                                    addSuperinterface(factory.usageType.parent)
                                } else {
                                    superclass(factory.usageType.parent)
                                }
                            }
                        }
                    }
                    .addAnnotations(annotations)
                    .addProperties(properties)
                    .addFunction(createFunction)
                    .apply(typeSpecModifier)
                    .addAnnotation(
                        AnnotationSpec.builder(Types.Kotlin.optIn)
                            .addMember("%T::class", Types.Kotshi.internalKotshiApi)
                            .build()
                    )
                    .build()
            )
            .build()
    }

    private fun makeCreateFunction(
        typeParam: ParameterSpec,
        annotationsParam: ParameterSpec,
        moshiParam: ParameterSpec,
        annotations: MutableSet<AnnotationSpec>,
        properties: MutableSet<PropertySpec>,
    ): FunSpec {
        val createSpec = FunSpec.builder("create")
            .addModifiers(KModifier.OVERRIDE)
            .returns(JsonAdapter::class.asClassName().parameterizedBy(STAR).nullable())
            .addParameter(typeParam)
            .addParameter(annotationsParam)
            .addParameter(moshiParam)

        if (factory.isEmpty) {
            return createSpec
                .addStatement("return null")
                .build()
        }


        val rawType = PropertySpec.builder(nameAllocator.newName("rawType"), Types.Java.clazz.parameterizedBy(STAR))
            .initializer("%M(%N)", Functions.Moshi.getRawType, typeParam)
            .build()

        return createSpec
            .addCode("%L", rawType)
            .applyIf(factory.manuallyRegisteredAdapters.isNotEmpty()) {
                addRegisteredAdapters(typeParam, annotationsParam, moshiParam, rawType, annotations, properties)
            }
            .addGeneratedAdapters(typeParam, annotationsParam, moshiParam, rawType)
            .build()
    }

    private fun FunSpec.Builder.addRegisteredAdapters(
        typeParam: ParameterSpec,
        annotationsParam: ParameterSpec,
        moshiParam: ParameterSpec,
        rawType: PropertySpec,
        annotations: MutableSet<AnnotationSpec>,
        properties: MutableSet<PropertySpec>,
    ): FunSpec.Builder = addControlFlow("when") {
        for (adapter in factory.manuallyRegisteredAdapters.sorted()) {
            addCode("%N·==·%T::class.java", rawType, adapter.targetType.rawType())
            if (adapter.qualifiers.isNotEmpty()) {
                val qualifiers = PropertySpec
                    .builder(
                        nameAllocator.newName(adapter.adapterClassName.simpleName.replaceFirstChar(Char::lowercaseChar) + "Qualifiers"),
                        Set::class.parameterizedBy(Annotation::class)
                    )
                    .addModifiers(KModifier.PRIVATE)
                    .initializer(CodeBlock.Builder()
                        .add("%M(⇥", Functions.Kotlin.setOf)
                        .applyEachIndexed(adapter.qualifiers) { i, qualifier ->
                            if (i > 0) add(",")
                            add("\n")
                            add(qualifier.render(createAnnotationsUsingConstructor))
                        }
                        .add("⇤\n)")
                        .build())
                    .build()
                    .also(properties::add)
                addCode("·&&·\n⇥%N == %N⇤", annotationsParam, qualifiers)
            }
            if (adapter.requiresDeepTypeCheck) {
                addCode(
                    "·&&·\n⇥%M(%M<%T>().%M, %N)⇤",
                    Functions.Kotshi.matches,
                    Functions.Kotlin.typeOf,
                    adapter.targetType.unwrapTypeVariables(),
                    Functions.Kotlin.javaType,
                    typeParam
                )
                annotations.add(AnnotationSpec.builder(Types.Kotlin.experimentalStdlibApi).build())
            }
            addCode("·->« return·")
            val constructor = adapter.constructor
            if (constructor == null) {
                addCode("%T", adapter.adapterTypeName)
            } else {
                addAdapterConstructorCall(
                    adapterTypeName = adapter.adapterTypeName,
                    constructor = adapter.constructor,
                    moshiParam = moshiParam,
                    typeParam = typeParam,
                )
            }
            addCode("»\n")
        }
    }

    private fun FunSpec.Builder.addGeneratedAdapters(
        typeParam: ParameterSpec,
        annotationsParam: ParameterSpec,
        moshiParam: ParameterSpec,
        rawType: PropertySpec,
    ): FunSpec.Builder =
        if (factory.generatedAdapters.isEmpty()) {
            addCode("return·null")
        } else {
            addStatement("if·(%N.isNotEmpty()) return·null", annotationsParam)
                .addCode("\n")
                .addControlFlow("return·when·(%N)", rawType) {
                    for (adapter in factory.generatedAdapters.sorted()) {
                        addCode("%T::class.java·->«\n", adapter.adapter.targetType.rawType())
                        addAdapterConstructorCall(
                            adapterTypeName = adapter.adapter.adapterTypeName,
                            constructor = adapter.constructor,
                            moshiParam = moshiParam,
                            typeParam = typeParam,
                        )
                        addCode("»\n")
                    }
                    addStatement("else -> null")
                }
        }

    private fun FunSpec.Builder.addAdapterConstructorCall(
        adapterTypeName: TypeName,
        constructor: KotshiConstructor,
        moshiParam: ParameterSpec,
        typeParam: ParameterSpec,
    ): FunSpec.Builder = apply {
        addCode("%T(", adapterTypeName.mapTypeArguments { NOTHING })
        if (constructor.hasParameters) {
            addCode("⇥")
        }

        if (constructor.moshiParameterName != null) {
            addCode("\n%N = %N", constructor.moshiParameterName, moshiParam)
        }
        if (constructor.typesParameterName != null) {
            if (constructor.moshiParameterName != null) {
                addCode(",")
            }
            addCode(
                "\n%N = %N.%M",
                constructor.typesParameterName,
                typeParam,
                Functions.Kotshi.typeArgumentsOrFail
            )
        }
        if (constructor.hasParameters) {
            addCode("⇤\n")
        }
        addCode(")")
    }
}