/*
 * Copyright 2010-2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.js.translate.declaration

import org.jetbrains.kotlin.backend.common.CodegenUtil
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.naming.NameSuggestion
import org.jetbrains.kotlin.js.translate.context.Namer
import org.jetbrains.kotlin.js.translate.context.TranslationContext
import org.jetbrains.kotlin.js.translate.general.AbstractTranslator
import org.jetbrains.kotlin.js.translate.general.Translation
import org.jetbrains.kotlin.js.translate.utils.*
import org.jetbrains.kotlin.js.translate.utils.TranslationUtils.simpleReturnFunction
import org.jetbrains.kotlin.js.translate.utils.TranslationUtils.translateFunctionAsEcma5PropertyDescriptor
import org.jetbrains.kotlin.psi.KtDelegatedSuperTypeEntry
import org.jetbrains.kotlin.psi.KtPureClassOrObject
import org.jetbrains.kotlin.psi.KtSuperTypeListEntry
import org.jetbrains.kotlin.resolve.DelegationResolver
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.descriptorUtil.isExtensionProperty

class DelegationTranslator(
        classDeclaration: KtPureClassOrObject,
        context: TranslationContext
) : AbstractTranslator(context) {

    private val classDescriptor: ClassDescriptor =
            BindingUtils.getClassDescriptor(context.bindingContext(), classDeclaration)

    private val delegationBySpecifiers =
            classDeclaration.superTypeListEntries.filterIsInstance<KtDelegatedSuperTypeEntry>()

    private class Field (val name: JsName, val generateField: Boolean)
    private val fields = mutableMapOf<KtDelegatedSuperTypeEntry, Field>()

    init {
        for (specifier in delegationBySpecifiers) {
            val expression = specifier.delegateExpression ?:
                             throw IllegalArgumentException("delegate expression should not be null: ${specifier.text}")
            val propertyDescriptor = CodegenUtil.getDelegatePropertyIfAny(expression, classDescriptor, bindingContext())

            if (CodegenUtil.isFinalPropertyWithBackingField(propertyDescriptor, bindingContext())) {
                val delegateName = context.getNameForDescriptor(propertyDescriptor!!)
                fields[specifier] = Field(delegateName, false)
            }
            else {
                val classFqName = DescriptorUtils.getFqName(classDescriptor)
                val idForMangling = classFqName.asString()
                val suggestedName = NameSuggestion.getStableMangledName(Namer.getDelegatePrefix(), idForMangling)
                val delegateName = context.getScopeForDescriptor(classDescriptor).declareFreshName("${suggestedName}_0")
                fields[specifier] = Field(delegateName, true)
            }
        }
    }

    fun addInitCode(statements: MutableList<JsStatement>) {
        for (specifier in delegationBySpecifiers) {
            val field = fields[specifier]!!
            if (field.generateField) {
                val expression = specifier.delegateExpression!!
                val context = context().innerBlock()
                val delegateInitExpr = Translation.translateAsExpression(expression, context)
                statements += context.dynamicContext().jsBlock().statements
                val lhs = JsAstUtils.pureFqn(field.name, JsThisRef())
                statements += JsAstUtils.assignment(lhs, delegateInitExpr)
                        .apply { source = specifier }
                        .makeStmt()
            }
        }
    }

    fun generateDelegated() {
        for (specifier in delegationBySpecifiers) {
            getSuperClass(specifier)?.let {
                generateDelegates(specifier, it, fields[specifier]!!)
            }
        }
    }

    private fun getSuperClass(specifier: KtSuperTypeListEntry): ClassDescriptor? =
            CodegenUtil.getSuperClassBySuperTypeListEntry(specifier, bindingContext())
            ?: error("ClassDescriptor of superType should not be null: ${specifier.text}")

    private fun generateDelegates(specifier: KtSuperTypeListEntry, toClass: ClassDescriptor, field: Field) {
        for ((descriptor, overriddenDescriptor) in DelegationResolver.getDelegates(classDescriptor, toClass)) {
            when (descriptor) {
                is PropertyDescriptor ->
                    generateDelegateCallForPropertyMember(specifier, descriptor, field.name)
                is FunctionDescriptor ->
                    generateDelegateCallForFunctionMember(specifier, descriptor, overriddenDescriptor as FunctionDescriptor, field.name)
                else ->
                    throw IllegalArgumentException("Expected property or function $descriptor")
            }
        }
    }

    private fun generateDelegateCallForPropertyMember(
            specifier: KtSuperTypeListEntry,
            descriptor: PropertyDescriptor,
            delegateName: JsName
    ) {
        val propertyName: String = descriptor.name.asString()

        fun generateDelegateGetterFunction(getterDescriptor: PropertyGetterDescriptor): JsFunction {
            val delegateRef = JsNameRef(delegateName, JsThisRef())

            val returnExpression: JsExpression = if (DescriptorUtils.isExtension(descriptor)) {
                val getterName = context().getNameForDescriptor(getterDescriptor)
                val receiver = Namer.getReceiverParameterName()
                JsInvocation(JsNameRef(getterName, delegateRef), JsNameRef(receiver))
            }
            else {
                JsNameRef(propertyName, delegateRef)
            }

            returnExpression.source(specifier)

            val jsFunction = simpleReturnFunction(context().getScopeForDescriptor(getterDescriptor.containingDeclaration), returnExpression)
            jsFunction.source = specifier
            if (DescriptorUtils.isExtension(descriptor)) {
                val receiverName = jsFunction.scope.declareName(Namer.getReceiverParameterName())
                jsFunction.parameters.add(JsParameter(receiverName))
            }
            return jsFunction
        }

        fun generateDelegateSetterFunction(setterDescriptor: PropertySetterDescriptor): JsFunction {
            val jsFunction = JsFunction(context().program().rootScope,
                                        "setter for " + setterDescriptor.name.asString())
            jsFunction.source = specifier

            assert(setterDescriptor.valueParameters.size == 1) { "Setter must have 1 parameter" }
            val defaultParameter = JsParameter(JsScope.declareTemporary())
            val defaultParameterRef = defaultParameter.name.makeRef()

            val delegateRef = JsNameRef(delegateName, JsThisRef())

            // TODO: remove explicit type annotation when Kotlin compiler works this out
            val setExpression: JsExpression = if (DescriptorUtils.isExtension(descriptor)) {
                val setterName = context().getNameForDescriptor(setterDescriptor)
                val setterNameRef = JsNameRef(setterName, delegateRef)
                val extensionFunctionReceiverName = jsFunction.scope.declareName(Namer.getReceiverParameterName())
                jsFunction.parameters.add(JsParameter(extensionFunctionReceiverName))
                JsInvocation(setterNameRef, JsNameRef(extensionFunctionReceiverName), defaultParameterRef)
            }
            else {
                val propertyNameRef = JsNameRef(propertyName, delegateRef)
                JsAstUtils.assignment(propertyNameRef, defaultParameterRef)
            }

            jsFunction.parameters.add(defaultParameter)
            jsFunction.body = JsBlock(setExpression.apply { source = specifier }.makeStmt())
            return jsFunction
        }

        fun generateDelegateAccessor(accessorDescriptor: PropertyAccessorDescriptor, function: JsFunction): JsPropertyInitializer =
                translateFunctionAsEcma5PropertyDescriptor(function, accessorDescriptor, context())

        fun generateDelegateGetter(): JsPropertyInitializer {
            val getterDescriptor = descriptor.getter ?: throw IllegalStateException("Getter descriptor should not be null")
            return generateDelegateAccessor(getterDescriptor, generateDelegateGetterFunction(getterDescriptor))
        }

        fun generateDelegateSetter(): JsPropertyInitializer {
            val setterDescriptor = descriptor.setter ?: throw IllegalStateException("Setter descriptor should not be null")
            return generateDelegateAccessor(setterDescriptor, generateDelegateSetterFunction(setterDescriptor))
        }

        // TODO: same logic as in AbstractDeclarationVisitor
        if (descriptor.isExtensionProperty || TranslationUtils.shouldAccessViaFunctions(descriptor)) {
            val getter = descriptor.getter!!
            context().addDeclarationStatement(context().addFunctionToPrototype(
                    classDescriptor, getter, generateDelegateGetterFunction(getter)))
            if (descriptor.isVar) {
                val setter = descriptor.setter!!
                context().addDeclarationStatement(
                        context().addFunctionToPrototype(classDescriptor, setter, generateDelegateSetterFunction(setter)))
            }
        }
        else {
            val literal = JsObjectLiteral(true)
            literal.propertyInitializers.addGetterAndSetter(descriptor, ::generateDelegateGetter, ::generateDelegateSetter)
            context().addAccessorsToPrototype(classDescriptor, descriptor, literal)
        }
    }


    private fun generateDelegateCallForFunctionMember(
            specifier: KtSuperTypeListEntry,
            descriptor: FunctionDescriptor,
            overriddenDescriptor: FunctionDescriptor,
            delegateName: JsName
    ) {
        val delegateRef = JsNameRef(delegateName, JsThisRef())
        val statement = generateDelegateCall(classDescriptor, descriptor, overriddenDescriptor, delegateRef, context().newDeclaration(overriddenDescriptor), true, specifier)
        context().addDeclarationStatement(statement)
    }
}
