/*
 * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.lombok

import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.impl.PropertyDescriptorImpl
import org.jetbrains.kotlin.load.java.lazy.LazyJavaResolverContext
import org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaClassDescriptor
import org.jetbrains.kotlin.load.java.lazy.descriptors.SyntheticJavaClassDescriptor
import org.jetbrains.kotlin.lombok.config.LombokConfig
import org.jetbrains.kotlin.lombok.processor.*
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.jvm.SyntheticJavaPartsProvider
import java.util.*

/**
 * Provides synthetic parts to java classes (from current compilation unit), which will be generated by lombok AnnotationProcessor
 * So kotlin can reference lombok members.
 */
@Suppress("IncorrectFormatting") // KTIJ-22227
class LombokSyntheticJavaPartsProvider(config: LombokConfig) : SyntheticJavaPartsProvider {

    private val processors = listOf(
        GetterProcessor(config),
        SetterProcessor(config),
        WithProcessor(),
        NoArgsConstructorProcessor(),
        AllArgsConstructorProcessor(),
        RequiredArgsConstructorProcessor(),
        BuilderProcessor(config)
    )

    private val valueFieldModifier = ValueFieldModifier(config)

    /**
     * kotlin resolve references in two calls - first it gets names, then actual member descriptor
     * but for us it is much easier to run full generation for class once
     * hence we cache results and reuse it
     */
    private val partsCache: MutableMap<ClassDescriptor, SyntheticParts> = HashMap()

    override fun getMethodNames(thisDescriptor: ClassDescriptor, c: LazyJavaResolverContext): List<Name> =
        c.getSyntheticParts(thisDescriptor).methods.map { it.name }

    override fun generateMethods(
        thisDescriptor: ClassDescriptor,
        name: Name,
        result: MutableCollection<SimpleFunctionDescriptor>,
        c: LazyJavaResolverContext,
    ) {
        val methods = c.getSyntheticParts(thisDescriptor).methods.filter { it.name == name }
        addNonExistent(result, methods)
    }

    override fun getStaticFunctionNames(thisDescriptor: ClassDescriptor, c: LazyJavaResolverContext): List<Name> =
        c.getSyntheticParts(thisDescriptor).staticFunctions.map { it.name }

    override fun generateStaticFunctions(
        thisDescriptor: ClassDescriptor,
        name: Name,
        result: MutableCollection<SimpleFunctionDescriptor>,
        c: LazyJavaResolverContext,
    ) {
        val functions = c.getSyntheticParts(thisDescriptor).staticFunctions.filter { it.name == name }
        addNonExistent(result, functions)
    }

    override fun generateConstructors(
        thisDescriptor: ClassDescriptor,
        result: MutableList<ClassConstructorDescriptor>,
        c: LazyJavaResolverContext,
    ) {
        val constructors = c.getSyntheticParts(thisDescriptor).constructors
        addNonExistent(result, constructors)
    }

    override fun getNestedClassNames(thisDescriptor: ClassDescriptor, c: LazyJavaResolverContext): List<Name> {
        return c.getSyntheticParts(thisDescriptor).classes.map { it.name }
    }

    override fun generateNestedClass(
        thisDescriptor: ClassDescriptor,
        name: Name,
        result: MutableList<ClassDescriptor>,
        c: LazyJavaResolverContext,
    ) {
        result += c.getSyntheticParts(thisDescriptor).classes.filter { it.name == name }
    }

    private fun LazyJavaResolverContext.getSyntheticParts(descriptor: ClassDescriptor): SyntheticParts {
        if (descriptor !is LazyJavaClassDescriptor && descriptor !is SyntheticJavaClassDescriptor) return SyntheticParts.Empty
        return partsCache.getOrPut(descriptor) {
            computeSyntheticParts(descriptor)
        }
    }

    private fun LazyJavaResolverContext.computeSyntheticParts(descriptor: ClassDescriptor): SyntheticParts {
        val builder = SyntheticPartsBuilder()
        processors.forEach { it.contribute(descriptor, builder, this) }
        return builder.build()
    }

    override fun modifyField(
        thisDescriptor: ClassDescriptor,
        propertyDescriptor: PropertyDescriptorImpl,
        c: LazyJavaResolverContext
    ): PropertyDescriptorImpl {
        return valueFieldModifier.modifyField(thisDescriptor, propertyDescriptor) ?: propertyDescriptor
    }

    /**
     * Deduplicates generated functions using name and argument counts, as lombok does
     */
    private fun <T : FunctionDescriptor> addNonExistent(result: MutableCollection<T>, toAdd: List<T>) {
        toAdd.forEach { f ->
            if (result.none { sameSignature(it, f) }) {
                result += f
            }
        }
    }

    companion object {
        /**
         * Lombok treat functions as having the same signature by arguments count only
         * Corresponding code in lombok - https://github.com/projectlombok/lombok/blob/v1.18.20/src/core/lombok/javac/handlers/JavacHandlerUtil.java#L752
         */
        private fun sameSignature(a: FunctionDescriptor, b: FunctionDescriptor): Boolean {
            val aVararg = a.valueParameters.any { it.varargElementType != null }
            val bVararg = b.valueParameters.any { it.varargElementType != null }
            return aVararg && bVararg ||
                    aVararg && b.valueParameters.size >= (a.valueParameters.size - 1) ||
                    bVararg && a.valueParameters.size >= (b.valueParameters.size - 1) ||
                    a.valueParameters.size == b.valueParameters.size
        }
    }
}
