/*
 * 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.context

import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.backend.ast.metadata.exportedTag
import org.jetbrains.kotlin.js.backend.ast.metadata.staticRef
import org.jetbrains.kotlin.js.descriptorUtils.shouldBeExported
import org.jetbrains.kotlin.js.translate.utils.AnnotationsUtils.isLibraryObject
import org.jetbrains.kotlin.js.translate.utils.AnnotationsUtils.isNativeObject
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils.assignment
import org.jetbrains.kotlin.js.translate.utils.JsDescriptorUtils
import org.jetbrains.kotlin.js.translate.utils.TranslationUtils
import org.jetbrains.kotlin.js.translate.utils.definePackageAlias
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.inline.isEffectivelyInlineOnly
import org.jetbrains.kotlin.resolve.source.getPsi

internal class DeclarationExporter(val context: StaticContext) {
    private val objectLikeKinds = setOf(ClassKind.OBJECT, ClassKind.ENUM_ENTRY)
    private val exportedDeclarations = mutableSetOf<MemberDescriptor>()
    private val localPackageNames = mutableMapOf<FqName, JsName>()
    private val statements: MutableList<JsStatement>
        get() = context.fragment.exportBlock.statements

    fun export(descriptor: MemberDescriptor, force: Boolean) {
        if (exportedDeclarations.contains(descriptor)) return
        if (descriptor is ConstructorDescriptor && descriptor.isPrimary) return
        if (isNativeObject(descriptor) || isLibraryObject(descriptor)) return
        if (descriptor.isEffectivelyInlineOnly()) return

        val suggestedName = context.nameSuggestion.suggest(descriptor) ?: return

        val container = suggestedName.scope
        if (!descriptor.shouldBeExported(force)) return
        exportedDeclarations.add(descriptor)

        val qualifier = when {
            container is PackageFragmentDescriptor -> {
                getLocalPackageName(container.fqName).makeRef()
            }
            DescriptorUtils.isObject(container) -> {
                JsAstUtils.prototypeOf(context.getInnerNameForDescriptor(container).makeRef())
            }
            else -> {
                context.getInnerNameForDescriptor(container).makeRef()
            }
        }

        when {
            descriptor is ClassDescriptor && descriptor.kind in objectLikeKinds -> {
                exportObject(descriptor, qualifier)
            }
            descriptor is PropertyDescriptor && container is PackageFragmentDescriptor -> {
                exportProperty(descriptor, qualifier)
            }
            else -> {
                assign(descriptor, qualifier)
            }
        }
    }

    private fun assign(descriptor: DeclarationDescriptor, qualifier: JsExpression) {
        val exportedName = context.getInnerNameForDescriptor(descriptor)
        val expression = exportedName.makeRef()
        val propertyName = context.getNameForDescriptor(descriptor)
        if (propertyName.staticRef == null && exportedName != propertyName) {
            propertyName.staticRef = expression
        }
        statements += assignment(JsNameRef(propertyName, qualifier), expression).exportStatement(descriptor)
    }

    private fun exportObject(declaration: ClassDescriptor, qualifier: JsExpression) {
        val name = context.getNameForDescriptor(declaration)
        val expression = JsAstUtils.defineGetter(qualifier, name.ident,
                                                 context.getNameForObjectInstance(declaration).makeRef())
        statements += expression.exportStatement(declaration)
    }

    private fun exportProperty(declaration: PropertyDescriptor, qualifier: JsExpression) {
        val propertyLiteral = JsObjectLiteral(true)

        val name = context.getNameForDescriptor(declaration).ident
        val simpleProperty = JsDescriptorUtils.isSimpleFinalProperty(declaration) &&
                             !TranslationUtils.shouldAccessViaFunctions(declaration)

        val exportedName: JsName
        val getterBody: JsExpression = if (simpleProperty) {
            exportedName = context.getInnerNameForDescriptor(declaration)
            val accessToField = JsReturn(exportedName.makeRef())
            JsFunction(context.fragment.scope, JsBlock(accessToField), "$declaration getter")
        }
        else {
            exportedName = context.getInnerNameForDescriptor(declaration.getter!!)
            exportedName.makeRef()
        }
        propertyLiteral.propertyInitializers += JsPropertyInitializer(JsNameRef("get"), getterBody)

        if (declaration.isVar) {
            val setterBody: JsExpression = if (simpleProperty) {
                val statements = mutableListOf<JsStatement>()
                val function = JsFunction(context.fragment.scope, JsBlock(statements), "$declaration setter")
                function.source = declaration.source.getPsi()
                val valueName = JsScope.declareTemporaryName("value")
                function.parameters += JsParameter(valueName)
                statements += assignment(context.getInnerNameForDescriptor(declaration).makeRef(), valueName.makeRef()).makeStmt()
                function
            }
            else {
                context.getInnerNameForDescriptor(declaration.setter!!).makeRef()
            }
            propertyLiteral.propertyInitializers += JsPropertyInitializer(JsNameRef("set"), setterBody)
        }

        statements += JsAstUtils.defineProperty(qualifier, name, propertyLiteral).exportStatement(declaration)
    }

    fun getLocalPackageName(packageName: FqName): JsName {
        if (packageName.isRoot) {
            return context.fragment.scope.declareName(Namer.getRootPackageName())
        }
        var name = localPackageNames[packageName]
        if (name == null) {
            name = JsScope.declareTemporaryName("package$" + packageName.shortName().asString())
            localPackageNames[packageName] = name
            statements += definePackageAlias(packageName.shortName().asString(), name, packageName.asString(),
                                             getLocalPackageName(packageName.parent()).makeRef())
        }
        return name
    }

    private fun JsExpression.exportStatement(declaration: DeclarationDescriptor) = JsExpressionStatement(this).also {
        it.exportedTag = context.getTag(declaration)
    }

    private fun MemberDescriptor.shouldBeExported(force: Boolean) = force || shouldBeExported(context.config)
}

