/*
 * Copyright 2019 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.instantexecution

import org.gradle.api.internal.initialization.ClassLoaderScopeIdentifier
import org.gradle.api.internal.initialization.loadercache.ClassLoaderId
import org.gradle.initialization.ClassLoaderScopeId
import org.gradle.initialization.ClassLoaderScopeRegistryListener
import org.gradle.instantexecution.serialization.ClassLoaderRole
import org.gradle.instantexecution.serialization.ScopeLookup
import org.gradle.internal.classpath.ClassPath
import org.gradle.internal.event.ListenerManager


internal
class InstantExecutionClassLoaderScopeRegistryListener : ClassLoaderScopeRegistryListener, ScopeLookup {
    private
    val scopeSpecs = LinkedHashMap<ClassLoaderScopeId, ClassLoaderScopeSpec>()

    private
    val loaders = mutableMapOf<ClassLoader, Pair<ClassLoaderScopeSpec, ClassLoaderRole>>()

    private
    var manager: ListenerManager? = null

    val scopes: Collection<ClassLoaderScopeSpec>
        get() = scopeSpecs.values

    fun attach(manager: ListenerManager) {
        require(this.manager == null)
        this.manager = manager
        manager.addListener(this)
    }

    /**
     * Stops recording [ClassLoaderScopeSpec]s and releases any recorded state.
     */
    fun dispose() {
        // TODO:instant-execution find a way to make `dispose` unnecessary;
        //  maybe by extracting an `InstantExecutionBuildDefinition` service
        //  from DefaultInstantExecutionHost so a decision based on the configured
        //  instant execution strategy (none, store or load) can be taken early on.
        //  The listener only needs to be attached in the `store` state.
        scopeSpecs.clear()
        loaders.clear()
        detach()
    }

    private
    fun detach() {
        manager?.removeListener(this)
        manager = null
    }

    override fun scopeFor(classLoader: ClassLoader?): Pair<ClassLoaderScopeSpec, ClassLoaderRole>? {
        return loaders[classLoader]
    }

    override fun rootScopeCreated(rootScopeId: ClassLoaderScopeId) {
        val root = ClassLoaderScopeSpec(null, rootScopeId.name)
        if (scopeSpecs.containsKey(rootScopeId)) {
            throw IllegalStateException("Duplicate scope $rootScopeId")
        }
        scopeSpecs[rootScopeId] = root
    }

    override fun childScopeCreated(parentId: ClassLoaderScopeId, childId: ClassLoaderScopeId) {
        val parent = scopeSpecs.getValue(parentId)
        val child = ClassLoaderScopeSpec(parent, childId.name)
        if (!scopeSpecs.containsKey(childId)) {
            scopeSpecs[childId] = child
        }
    }

    override fun localClasspathAdded(scopeId: ClassLoaderScopeId, localClassPath: ClassPath) {
        scopeSpecs.getValue(scopeId).localClassPath += localClassPath
    }

    override fun exportClasspathAdded(scopeId: ClassLoaderScopeId, exportClassPath: ClassPath) {
        scopeSpecs.getValue(scopeId).exportClassPath += exportClassPath
    }

    override fun classloaderCreated(scopeId: ClassLoaderScopeId, classLoaderId: ClassLoaderId, classLoader: ClassLoader) {
        val local = scopeId is ClassLoaderScopeIdentifier && scopeId.localId() == classLoaderId
        loaders[classLoader] = Pair(scopeSpecs.getValue(scopeId), ClassLoaderRole(local))
    }
}


internal
class ClassLoaderScopeSpec(
    val parent: ClassLoaderScopeSpec?,
    val name: String
) {
    var localClassPath = ClassPath.EMPTY
    var exportClassPath = ClassPath.EMPTY
}
