/*
 * Copyright 2010-2020 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.backend.jvm.lower

import org.jetbrains.kotlin.backend.common.FileLoweringPass
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
import org.jetbrains.kotlin.backend.common.phaser.PhaseDescription
import org.jetbrains.kotlin.backend.jvm.JvmBackendContext
import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin
import org.jetbrains.kotlin.backend.jvm.getRequiresMangling
import org.jetbrains.kotlin.backend.jvm.hasMangledReturnType
import org.jetbrains.kotlin.backend.jvm.ir.needsAccessor
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.DescriptorVisibility
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.builders.declarations.buildFun
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrFieldAccessExpression
import org.jetbrains.kotlin.ir.expressions.impl.IrGetFieldImpl
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.classifierOrNull
import org.jetbrains.kotlin.ir.types.makeNullable
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.utils.addIfNotNull

/**
 * Moves fields and accessors for properties to their classes, replaces calls to default property accessors with field accesses,
 * removes unused accessors and creates synthetic methods for property annotations.
 */
@PhaseDescription(name = "Properties")
internal class JvmPropertiesLowering(
    private val backendContext: JvmBackendContext
) : IrElementTransformerVoidWithContext(), FileLoweringPass {
    val uninitializedPropertyAccessExceptionThrower = JvmUninitializedPropertyAccessExceptionThrower(backendContext.symbols)

    override fun lower(irFile: IrFile) {
        irFile.accept(this, null)
    }

    override fun visitClassNew(declaration: IrClass): IrStatement {
        declaration.transformChildrenVoid(this)
        declaration.transformDeclarationsFlat { if (it is IrProperty) lowerProperty(it, declaration.kind) else null }
        return declaration
    }

    override fun visitCall(expression: IrCall): IrExpression {
        val simpleFunction = expression.symbol.owner
        val property = simpleFunction.correspondingPropertySymbol?.owner ?: return super.visitCall(expression)
        expression.transformChildrenVoid()

        if (shouldSubstituteAccessorWithField(property, simpleFunction) ||
            isDefaultAccessorForCompanionPropertyBackingFieldOnCurrentClass(property, simpleFunction)
        ) {
            backendContext.createIrBuilder(currentScope!!.scope.scopeOwnerSymbol, expression.startOffset, expression.endOffset).apply {
                return when (simpleFunction) {
                    property.getter -> substituteGetter(property, expression)
                    property.setter -> substituteSetter(property, expression)
                    else -> error("Orphaned property getter/setter: ${simpleFunction.render()}")
                }
            }
        }

        return expression
    }

    private fun isDefaultAccessorForCompanionPropertyBackingFieldOnCurrentClass(
        property: IrProperty,
        function: IrSimpleFunction
    ): Boolean {
        if (function.origin != IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR) return false
        if (property.isLateinit) return false
        // If this code could end up inlined in another class (either an inline function or an
        // inlined lambda in an inline function) use the companion object accessor. Otherwise,
        // we could break binary compatibility if we only recompile the class with the companion
        // object and change to non-default field accessors. The inlined code would still attempt
        // to get the backing field which would no longer exist.
        val inInlineFunctionScope = allScopes.any { scope -> (scope.irElement as? IrFunction)?.isInline ?: false }
        if (inInlineFunctionScope) return false
        val backingField = property.resolveFakeOverrideOrFail().backingField
        return backingField?.parent == currentClass?.irElement &&
                backingField?.origin == JvmLoweredDeclarationOrigin.COMPANION_PROPERTY_BACKING_FIELD
    }

    private fun IrBuilderWithScope.substituteSetter(irProperty: IrProperty, expression: IrCall): IrExpression {
        val backingField = irProperty.resolveFakeOverrideOrFail().backingField!!
        return patchReceiver(
            irSetField(
                patchFieldAccessReceiver(expression, irProperty),
                backingField,
                expression.arguments.last()!!,
            )
        )
    }

    private fun IrBuilderWithScope.substituteGetter(irProperty: IrProperty, expression: IrCall): IrExpression {
        val backingField = irProperty.resolveFakeOverrideOrFail().backingField!!
        val patchedReceiver = patchFieldAccessReceiver(expression, irProperty)
        return if (irProperty.isLateinit) {
            irBlock {
                val fieldType = expression.type.makeNullable()
                val tmpVal = irTemporary(IrGetFieldImpl(startOffset, endOffset, backingField.symbol, fieldType, patchedReceiver))
                +irIfNull(
                    expression.type,
                    irGet(tmpVal),
                    uninitializedPropertyAccessExceptionThrower.build(this, backingField.name.asString()),
                    irGet(tmpVal)
                )
            }
        } else {
            IrGetFieldImpl(startOffset, endOffset, backingField.symbol, expression.type, patchedReceiver)
        }
    }

    private fun IrBuilderWithScope.patchFieldAccessReceiver(expression: IrCall, irProperty: IrProperty): IrExpression? {
        val receiver = expression.dispatchReceiver
        if (receiver != null) {
            val propertyParent = irProperty.parent
            if (propertyParent is IrClass &&
                receiver.type.classifierOrNull?.isSubtypeOfClass(propertyParent.symbol) != true &&
                expression.superQualifierSymbol == null
            ) {
                return irImplicitCast(receiver, propertyParent.defaultType)
            }
        }
        return receiver
    }

    private fun IrBuilderWithScope.patchReceiver(expression: IrFieldAccessExpression): IrExpression =
        if (expression.symbol.owner.isStatic && expression.receiver != null) {
            irBlock {
                +expression.receiver!!.coerceToUnit(context.irBuiltIns)
                expression.receiver = null
                +expression
            }
        } else {
            expression
        }

    private fun lowerProperty(declaration: IrProperty, kind: ClassKind): List<IrDeclaration> =
        ArrayList<IrDeclaration>(4).apply {
            val field = declaration.backingField

            // JvmFields in a companion object refer to companion's owners and should not be generated within companion.
            if ((kind != ClassKind.ANNOTATION_CLASS || field?.isStatic == true) && field?.parent == declaration.parent) {
                addIfNotNull(field)
            }

            if (!declaration.isConst) {
                declaration.getter?.takeIf { !shouldSubstituteAccessorWithField(declaration, it) }?.let { add(it) }
                declaration.setter?.takeIf { !shouldSubstituteAccessorWithField(declaration, it) }?.let { add(it) }
            }

            if (!declaration.isFakeOverride && declaration.annotations.isNotEmpty()) {
                add(createSyntheticMethodForAnnotations(declaration))
            }
        }

    private fun shouldSubstituteAccessorWithField(property: IrProperty, accessor: IrSimpleFunction?): Boolean =
        accessor != null && !property.needsAccessor(accessor)

    private fun createSyntheticMethodForAnnotations(declaration: IrProperty): IrSimpleFunction =
        backendContext.createSyntheticMethodForProperty(
            declaration,
            JvmAbi.ANNOTATED_PROPERTY_METHOD_NAME_SUFFIX,
            JvmLoweredDeclarationOrigin.SYNTHETIC_METHOD_FOR_PROPERTY_OR_TYPEALIAS_ANNOTATIONS,
            // TODO: technically JVM permits having fields with same name but different type, so we could potentially
            //   generate two properties like that; should this be the getter's return type instead?
            isStatic = true, returnType = backendContext.irBuiltIns.unitType, visibility = declaration.visibility
        ).apply {
            body = backendContext.irFactory.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET)
            annotations = declaration.annotations
        }

    companion object {
        private fun JvmBackendContext.createSyntheticMethodForProperty(
            declaration: IrProperty,
            suffix: String,
            origin: IrDeclarationOrigin,
            isStatic: Boolean,
            returnType: IrType,
            visibility: DescriptorVisibility
        ) = irFactory.buildFun {
            name = Name.identifier(computeSyntheticMethodName(declaration, suffix))
            modality = Modality.OPEN
            this.origin = origin
            this.returnType = returnType
            this.visibility = visibility
        }.apply {
            parameters = declaration.getter?.parameters?.mapNotNull {
                when (it.kind) {
                    // Synthetic methods don't get generic type signatures anyway, so not exactly useful to preserve type parameters.
                    IrParameterKind.DispatchReceiver -> if (!isStatic) it.copyTo(this, type = it.type.eraseTypeParameters()) else null
                    else -> it.copyTo(this, type = it.type.eraseTypeParameters(), kind = IrParameterKind.Regular)
                }
            } ?: emptyList()
            parent = declaration.parent
            metadata = declaration.metadata
        }

        fun JvmBackendContext.createSyntheticMethodForPropertyDelegate(declaration: IrProperty) =
            createSyntheticMethodForProperty(
                declaration,
                JvmAbi.DELEGATED_PROPERTY_NAME_SUFFIX,
                IrDeclarationOrigin.PROPERTY_DELEGATE,
                isStatic = false, returnType = irBuiltIns.anyNType, visibility = DescriptorVisibilities.PRIVATE
            )

        private fun JvmBackendContext.computeSyntheticMethodName(property: IrProperty, suffix: String): String {
            val baseName =
                if (config.languageVersionSettings.supportsFeature(LanguageFeature.UseGetterNameForPropertyAnnotationsMethodOnJvm)) {
                    val getter = property.getter
                    if (getter != null) {
                        val needsMangling =
                            getter.nonDispatchParameters.any { it.type.getRequiresMangling(includeInline = true, includeMFVC = false) } ||
                                    (config.functionsWithInlineClassReturnTypesMangled && getter.hasMangledReturnType)
                        val mangled = if (needsMangling) inlineClassReplacements.getReplacementFunction(getter) else null
                        defaultMethodSignatureMapper.mapFunctionName(mangled ?: getter)
                    } else JvmAbi.getterName(property.name.asString())
                } else {
                    property.name.asString()
                }
            return baseName + suffix
        }
    }
}
