/*
 * Copyright 2018 the original author or authors.
 *
 * 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.gradle.kotlin.dsl.accessors.runtime

import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.internal.DynamicObjectAware
import org.gradle.features.binding.Definition
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderConvertible
import org.gradle.kotlin.dsl.*
import org.gradle.kotlin.dsl.support.mapOfNonNullValuesOf
import org.gradle.kotlin.dsl.support.uncheckedCast
import org.gradle.features.internal.binding.ProjectFeatureSupportInternal
import org.gradle.features.internal.binding.TargetTypeInformationChecks
import java.util.Locale.getDefault

@Suppress("unused") // invoked from generated bytecode
fun extensionOf(target: Any, extensionName: String): Any =
    (target as ExtensionAware).extensions.getByName(extensionName)

@Suppress("unused") // invoked from generated bytecode
fun <T : Dependency> addDependencyTo(
    dependencies: DependencyHandler,
    configuration: String,
    dependencyNotation: Any,
    configurationAction: Action<T>
): T = dependencies.run {
    uncheckedCast<T>(create(dependencyNotation)).also { dependency ->
        configurationAction.execute(dependency)
        add(configuration, dependency)
    }
}

@Suppress("unused") // invoked from generated bytecode
fun addConfiguredDependencyTo(
    dependencies: DependencyHandler,
    configuration: String,
    dependencyNotation: Provider<*>,
    configurationAction: Action<ExternalModuleDependency>
) {
    dependencies.addProvider(configuration, dependencyNotation, configurationAction)
}

@Suppress("unused") // invoked from generated bytecode
fun addConfiguredDependencyTo(
    dependencies: DependencyHandler,
    configuration: String,
    dependencyNotation: ProviderConvertible<*>,
    configurationAction: Action<ExternalModuleDependency>
) {
    dependencies.addProviderConvertible(configuration, dependencyNotation, configurationAction)
}


@Suppress("LongParameterList", "DEPRECATION")
@Deprecated("Use single-string notation instead")
fun addExternalModuleDependencyTo(
    dependencyHandler: DependencyHandler,
    targetConfiguration: String,
    group: String,
    name: String,
    version: String?,
    configuration: String?,
    classifier: String?,
    ext: String?,
    action: Action<ExternalModuleDependency>?
): ExternalModuleDependency = externalModuleDependencyFor(
    dependencyHandler,
    group,
    name,
    version,
    configuration,
    classifier,
    ext
).also {
    action?.execute(it)
    dependencyHandler.add(targetConfiguration, it)
}

@Deprecated("Use single-string notation instead")
fun externalModuleDependencyFor(
    dependencyHandler: DependencyHandler,
    group: String,
    name: String,
    version: String?,
    configuration: String?,
    classifier: String?,
    ext: String?
): ExternalModuleDependency = dependencyHandler.create(
    mapOfNonNullValuesOf(
        "group" to group,
        "name" to name,
        "version" to version,
        "configuration" to configuration,
        "classifier" to classifier,
        "ext" to ext
    )
) as ExternalModuleDependency


@Suppress("unused") // invoked from generated bytecode
fun <T : Any> maybeRegister(
    container: NamedDomainObjectContainer<T>,
    name: String,
    configure: Action<in T>
) {
    with(container) {
        if (name in names) {
            named(name, configure)
        } else {
            register(name, configure)
        }
    }
}


@Suppress("unused") // invoked from generated bytecode
fun applyProjectFeature(
    target: DynamicObjectAware,
    name: String,
    configure: Action<in Any>
) {
    val context = when (target) {
        is ProjectInternal ->
            target.objects.newInstance<ProjectFeatureSupportInternal.DefaultProjectFeatureDefinitionContext.Factory>()
                .create(target)

        is Definition<*> -> ProjectFeatureSupportInternal.getContext(target)
        else -> throw IllegalArgumentException("Unexpected definition object $target, cannot get context to apply the feature")
    }
    val projectFeatures = context.projectFeatureRegistry.projectFeatureImplementations.getValue(name)
    val matchingFeature = projectFeatures.find {
        TargetTypeInformationChecks.isValidBindingType(it.targetDefinitionType, target.javaClass)
    } ?: throw IllegalArgumentException("No project feature '$name' is applicable to target of type ${target::class.qualifiedName}")
    val featureApplication = context.projectFeatureApplicator.registerFeatureApplicationFor(target, matchingFeature)
    configure.invoke(featureApplication.definitionInstance)
    // If the feature is a project type (i.e. the outermost declarative block) we can go ahead and apply all features.
    // Otherwise, defer application until the outermost project type is done being configured.
    if (featureApplication.isProjectType) {
        context.projectFeatureApplicator.applyFeatures()
    }
}

@Suppress("unused") // invoked from generated bytecode
fun configureNestedModel(target: Any, name: String, configure: Action<in Any>) {
    val methodName = "get${name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() }}"
    val nestedModel = target::class.java.getMethod(methodName).invoke(target)
    configure.invoke(nestedModel)
}

@Suppress("unused") // invoked from generated bytecode
fun functionToAction(f: (Any?) -> Unit): Action<Any> =
    Action { f(this) }
