/*
 * Copyright 2024 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.internal.cc.impl.serialize

import org.gradle.api.file.FileSystemOperations
import org.gradle.api.internal.DocumentationRegistry
import org.gradle.api.internal.GradleInternal
import org.gradle.api.internal.artifacts.ImmutableModuleIdentifierFactory
import org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.ArtifactSetToFileCollectionFactory
import org.gradle.api.internal.artifacts.ivyservice.resolveengine.result.CapabilitySerializer
import org.gradle.api.internal.artifacts.transform.TransformActionScheme
import org.gradle.api.internal.artifacts.transform.TransformParameterScheme
import org.gradle.api.internal.artifacts.transform.TransformStepNode
import org.gradle.api.internal.artifacts.transform.TransformStepNodeFactory
import org.gradle.api.internal.attributes.AttributeDesugaring
import org.gradle.api.internal.attributes.AttributesFactory
import org.gradle.api.internal.attributes.immutable.ImmutableAttributesSchemaFactory
import org.gradle.api.internal.file.FileCollectionFactory
import org.gradle.api.internal.file.FileFactory
import org.gradle.api.internal.file.FileLookup
import org.gradle.api.internal.file.FileOperations
import org.gradle.api.internal.file.FilePropertyFactory
import org.gradle.api.internal.file.FileResolver
import org.gradle.api.internal.file.collections.DirectoryFileTreeFactory
import org.gradle.api.internal.provider.PropertyFactory
import org.gradle.api.internal.tasks.TaskDependencyFactory
import org.gradle.api.problems.internal.ProblemsInternal
import org.gradle.api.tasks.util.internal.PatternSetFactory
import org.gradle.composite.internal.BuildTreeWorkGraphController
import org.gradle.internal.build.BuildStateRegistry
import org.gradle.internal.buildtree.BuildModelParameters
import org.gradle.internal.execution.InputFingerprinter
import org.gradle.internal.hash.ClassLoaderHierarchyHasher
import org.gradle.internal.instantiation.InstantiatorFactory
import org.gradle.internal.isolation.IsolatableFactory
import org.gradle.internal.model.CalculatedValueContainerFactory
import org.gradle.internal.operations.BuildOperationRunner
import org.gradle.internal.reflect.Instantiator
import org.gradle.internal.serialize.BaseSerializerFactory.HASHCODE_SERIALIZER
import org.gradle.internal.serialize.codecs.core.BooleanValueSnapshotCodec
import org.gradle.internal.serialize.codecs.core.BuildServiceParameterCodec
import org.gradle.internal.serialize.codecs.core.BuildServiceProviderCodec
import org.gradle.internal.serialize.codecs.core.CalculatedValueContainerCodec
import org.gradle.internal.serialize.codecs.core.ConfigurableFileCollectionCodec
import org.gradle.internal.serialize.codecs.core.ConfigurableFileTreeCodec
import org.gradle.internal.serialize.codecs.core.DefaultContextAwareTaskLoggerCodec
import org.gradle.internal.serialize.codecs.core.DefaultCopySpecCodec
import org.gradle.internal.serialize.codecs.core.DestinationRootCopySpecCodec
import org.gradle.internal.serialize.codecs.core.DirectoryCodec
import org.gradle.internal.serialize.codecs.core.DirectoryPropertyCodec
import org.gradle.internal.serialize.codecs.core.FileCollectionCodec
import org.gradle.internal.serialize.codecs.core.FileTreeCodec
import org.gradle.internal.serialize.codecs.core.FileValueSnapshotCodec
import org.gradle.internal.serialize.codecs.core.FixedValueReplacingProviderCodec
import org.gradle.internal.serialize.codecs.core.FlowProvidersCodec
import org.gradle.internal.serialize.codecs.core.GradlePropertiesCodec
import org.gradle.internal.serialize.codecs.core.IntegerValueSnapshotCodec
import org.gradle.internal.serialize.codecs.core.IntersectionPatternSetCodec
import org.gradle.internal.serialize.codecs.core.IsolateContextSource
import org.gradle.internal.serialize.codecs.core.IsolatedArrayCodec
import org.gradle.internal.serialize.codecs.core.IsolatedEnumValueSnapshotCodec
import org.gradle.internal.serialize.codecs.core.IsolatedImmutableManagedValueCodec
import org.gradle.internal.serialize.codecs.core.IsolatedJavaSerializedValueSnapshotCodec
import org.gradle.internal.serialize.codecs.core.IsolatedListCodec
import org.gradle.internal.serialize.codecs.core.IsolatedManagedValueCodec
import org.gradle.internal.serialize.codecs.core.IsolatedMapCodec
import org.gradle.internal.serialize.codecs.core.IsolatedSetCodec
import org.gradle.internal.serialize.codecs.core.ListPropertyCodec
import org.gradle.internal.serialize.codecs.core.LoggerCodec
import org.gradle.internal.serialize.codecs.core.MapEntrySnapshotCodec
import org.gradle.internal.serialize.codecs.core.MapPropertyCodec
import org.gradle.internal.serialize.codecs.core.NamedCodec
import org.gradle.internal.serialize.codecs.core.NullValueSnapshotCodec
import org.gradle.internal.serialize.codecs.core.OrdinalNodeCodec
import org.gradle.internal.serialize.codecs.core.PathToFileResolverCodec
import org.gradle.internal.serialize.codecs.core.PatternSetCodec
import org.gradle.internal.serialize.codecs.core.PropertyCodec
import org.gradle.internal.serialize.codecs.core.ProviderCodec
import org.gradle.internal.serialize.codecs.core.RegisteredFlowActionCodec
import org.gradle.internal.serialize.codecs.core.RegularFileCodec
import org.gradle.internal.serialize.codecs.core.RegularFilePropertyCodec
import org.gradle.internal.serialize.codecs.core.SerializedLambdaParametersCheckingCodec
import org.gradle.internal.serialize.codecs.core.SetPropertyCodec
import org.gradle.internal.serialize.codecs.core.StringValueSnapshotCodec
import org.gradle.internal.serialize.codecs.core.TaskInAnotherBuildCodec
import org.gradle.internal.serialize.codecs.core.TaskNodeCodec
import org.gradle.internal.serialize.codecs.core.TaskReferenceCodec
import org.gradle.internal.serialize.codecs.core.UnsupportedFingerprintBuildServiceProviderCodec
import org.gradle.internal.serialize.codecs.core.UnsupportedFingerprintFlowProviders
import org.gradle.internal.serialize.codecs.core.UserCodeSourceCodec
import org.gradle.internal.serialize.codecs.core.ValueSourceProviderCodec
import org.gradle.internal.serialize.codecs.core.WorkNodeActionCodec
import org.gradle.internal.serialize.codecs.core.WorkNodeCodec
import org.gradle.internal.serialize.codecs.core.baseTypes
import org.gradle.internal.serialize.codecs.core.defaultCodecForProviderWithChangingValue
import org.gradle.internal.serialize.codecs.core.groovyCodecs
import org.gradle.internal.serialize.codecs.core.jos.ExternalizableCodec
import org.gradle.internal.serialize.codecs.core.jos.JavaObjectSerializationCodec
import org.gradle.internal.reflection.access.ObjectOpener
import org.gradle.internal.serialize.codecs.core.jos.JavaSerializationEncodingLookup
import org.gradle.internal.serialize.codecs.core.unsupportedTypes
import org.gradle.internal.serialize.codecs.dm.ArtifactCollectionCodec
import org.gradle.internal.serialize.codecs.dm.AttributeContainerCodec
import org.gradle.internal.serialize.codecs.dm.DefaultComponentArtifactsResultCodec
import org.gradle.internal.serialize.codecs.dm.DefaultGraphStructureCodec
import org.gradle.internal.serialize.codecs.dm.DefaultResolvableArtifactCodec
import org.gradle.internal.serialize.codecs.dm.DefaultResolvedArtifactResultCodec
import org.gradle.internal.serialize.codecs.dm.DefaultUnresolvedComponentResultCodec
import org.gradle.internal.serialize.codecs.dm.ImmutableAttributesCodec
import org.gradle.internal.serialize.codecs.dm.ImmutableAttributesSchemaCodec
import org.gradle.internal.serialize.codecs.dm.ImmutableCapabilitiesCodec
import org.gradle.internal.serialize.codecs.dm.LocalFileDependencyBackedArtifactSetCodec
import org.gradle.internal.serialize.codecs.dm.PublishArtifactLocalArtifactMetadataCodec
import org.gradle.internal.serialize.codecs.dm.ResolveArtifactNodeCodec
import org.gradle.internal.serialize.codecs.dm.ResolvedComponentResultCodec
import org.gradle.internal.serialize.codecs.dm.ResolvedGraphResultCodec
import org.gradle.internal.serialize.codecs.dm.transform.CalculateArtifactsCodec
import org.gradle.internal.serialize.codecs.dm.transform.ChainedTransformStepNodeCodec
import org.gradle.internal.serialize.codecs.dm.transform.ComponentVariantIdentifierCodec
import org.gradle.internal.serialize.codecs.dm.transform.DefaultTransformCodec
import org.gradle.internal.serialize.codecs.dm.transform.FinalizeTransformDependenciesNodeCodec
import org.gradle.internal.serialize.codecs.dm.transform.InitialTransformStepNodeCodec
import org.gradle.internal.serialize.codecs.dm.transform.IsolateTransformParametersCodec
import org.gradle.internal.serialize.codecs.dm.transform.TransformChainCodec
import org.gradle.internal.serialize.codecs.dm.transform.TransformStepCodec
import org.gradle.internal.serialize.codecs.dm.transform.TransformStepSpecCodec
import org.gradle.internal.serialize.codecs.dm.transform.TransformedArtifactCodec
import org.gradle.internal.serialize.codecs.dm.transform.TransformedExternalArtifactSetCodec
import org.gradle.internal.serialize.codecs.dm.transform.TransformedProjectArtifactSetCodec
import org.gradle.internal.serialize.codecs.stdlib.ProxyCodec
import org.gradle.internal.serialize.graph.Codec
import org.gradle.internal.serialize.graph.codecs.BeanCodec
import org.gradle.internal.serialize.graph.codecs.BeanSpecCodec
import org.gradle.internal.serialize.graph.codecs.Bindings
import org.gradle.internal.serialize.graph.codecs.BindingsBuilder
import org.gradle.internal.serialize.graph.codecs.DelegatingCodec
import org.gradle.internal.serialize.graph.codecs.NotImplementedCodec
import org.gradle.internal.serialize.graph.codecs.ServicesCodec
import org.gradle.internal.serialize.graph.codecs.ValueObjectCodec
import org.gradle.internal.serialize.graph.reentrant
import org.gradle.internal.service.scopes.Scope
import org.gradle.internal.service.scopes.ServiceScope
import org.gradle.internal.state.ManagedFactoryRegistry


@ServiceScope(Scope.Build::class)
interface ConfigurationCacheCodecs {

    fun userTypesCodec(): Codec<Any?>

    fun fingerprintTypesCodec(): Codec<Any?>

    fun internalTypesCodec(): Codec<Any?>

    fun workNodeCodecFor(gradle: GradleInternal, contextSource: IsolateContextSource): WorkNodeCodec

}


@Suppress("LongParameterList")
internal
class DefaultConfigurationCacheCodecs(
    modelParameters: BuildModelParameters,
    directoryFileTreeFactory: DirectoryFileTreeFactory,
    fileCollectionFactory: FileCollectionFactory,
    artifactSetConverter: ArtifactSetToFileCollectionFactory,
    fileLookup: FileLookup,
    propertyFactory: PropertyFactory,
    filePropertyFactory: FilePropertyFactory,
    fileResolver: FileResolver,
    instantiator: Instantiator,
    instantiatorFactory: InstantiatorFactory,
    fileSystemOperations: FileSystemOperations,
    inputFingerprinter: InputFingerprinter,
    buildOperationRunner: BuildOperationRunner,
    classLoaderHierarchyHasher: ClassLoaderHierarchyHasher,
    isolatableFactory: IsolatableFactory,
    managedFactoryRegistry: ManagedFactoryRegistry,
    parameterScheme: TransformParameterScheme,
    actionScheme: TransformActionScheme,
    attributesFactory: AttributesFactory,
    attributeDesugaring: AttributeDesugaring,
    attributeSchemaFactory: ImmutableAttributesSchemaFactory,
    calculatedValueContainerFactory: CalculatedValueContainerFactory,
    patternSetFactory: PatternSetFactory,
    fileOperations: FileOperations,
    fileFactory: FileFactory,
    includedTaskGraph: BuildTreeWorkGraphController,
    buildStateRegistry: BuildStateRegistry,
    documentationRegistry: DocumentationRegistry,
    taskDependencyFactory: TaskDependencyFactory,
    moduleIdentifierFactory: ImmutableModuleIdentifierFactory,
    val javaSerializationEncodingLookup: JavaSerializationEncodingLookup,
    transformStepNodeFactory: TransformStepNodeFactory,
    problems: ProblemsInternal,
    private val objectOpener: ObjectOpener
) : ConfigurationCacheCodecs {

    private
    val parallelStore: Boolean = modelParameters.isConfigurationCacheParallelStore

    private
    val parallelLoad: Boolean = modelParameters.isConfigurationCacheParallelLoad

    private
    val userTypesBindings: Bindings

    private
    val fingerprintUserTypesBindings: Bindings

    init {
        fun makeUserTypeBindings(providersBlock: BindingsBuilder.() -> Unit) = Bindings.of {
            unsupportedTypes()

            baseTypes(objectOpener)

            bind(HASHCODE_SERIALIZER)

            bind(BuildServiceParameterCodec)

            providersBlock()

            bind(DefaultContextAwareTaskLoggerCodec)
            bind(LoggerCodec)

            fileCollectionTypes(directoryFileTreeFactory, fileCollectionFactory, artifactSetConverter, fileOperations, fileFactory, patternSetFactory, fileLookup, taskDependencyFactory)

            bind(org.gradle.internal.serialize.codecs.core.ApiTextResourceAdapterCodec)

            groovyCodecs()
            bind(SerializedLambdaParametersCheckingCodec(objectOpener))

            // Dependency management types
            val immutableAttributesCodec = ImmutableAttributesCodec(attributesFactory, managedFactoryRegistry)
            val immutableCapabilitiesCodec = ImmutableCapabilitiesCodec()
            bind(immutableAttributesCodec)
            bind(immutableCapabilitiesCodec)
            bind(ArtifactCollectionCodec(calculatedValueContainerFactory, artifactSetConverter, attributeDesugaring, taskDependencyFactory))
            bind(AttributeContainerCodec(attributesFactory, managedFactoryRegistry))
            bind(ImmutableAttributesSchemaCodec(instantiatorFactory, attributeSchemaFactory))
            bind(ComponentVariantIdentifierCodec)
            bind(InitialTransformStepNodeCodec(transformStepNodeFactory, buildOperationRunner, calculatedValueContainerFactory))
            bind(ChainedTransformStepNodeCodec(transformStepNodeFactory, buildOperationRunner, calculatedValueContainerFactory))
            bind(TransformStepCodec(inputFingerprinter))
            bind(TransformChainCodec())
            bind(DefaultTransformCodec(fileLookup, actionScheme))
            bind(DefaultResolvableArtifactCodec(calculatedValueContainerFactory))
            bind(TransformStepSpecCodec)
            bind(PublishArtifactLocalArtifactMetadataCodec)
            bind(TransformedProjectArtifactSetCodec())
            bind(TransformedExternalArtifactSetCodec())
            bind(CalculateArtifactsCodec(calculatedValueContainerFactory))
            bind(TransformedArtifactCodec(calculatedValueContainerFactory))
            bind(LocalFileDependencyBackedArtifactSetCodec(attributesFactory, calculatedValueContainerFactory))
            bind(CalculatedValueContainerCodec(calculatedValueContainerFactory))
            bind(IsolateTransformParametersCodec(parameterScheme, isolatableFactory, buildOperationRunner, classLoaderHierarchyHasher, fileCollectionFactory, documentationRegistry, problems))
            bind(FinalizeTransformDependenciesNodeCodec())
            bind(ResolveArtifactNodeCodec)
            bind(WorkNodeActionCodec)
            bind(CapabilitySerializer())
            bind(DefaultComponentArtifactsResultCodec())
            bind(DefaultResolvedArtifactResultCodec(immutableAttributesCodec, immutableCapabilitiesCodec))
            bind(DefaultUnresolvedComponentResultCodec())

            val graphStructureCodec = DefaultGraphStructureCodec(
                immutableAttributesCodec,
                immutableCapabilitiesCodec,
                moduleIdentifierFactory,
            )
            val resolvedGraphResultCodec = ResolvedGraphResultCodec(
                graphStructureCodec
            )
            val resolvedComponentResultCodec = ResolvedComponentResultCodec(resolvedGraphResultCodec)
            bind(resolvedComponentResultCodec)

            bind(DefaultCopySpecCodec(patternSetFactory, fileCollectionFactory, propertyFactory, instantiator, fileSystemOperations))
            bind(DestinationRootCopySpecCodec(fileResolver))

            bind(TaskReferenceCodec)
            bind(UserCodeSourceCodec)

            bind(IsolatedManagedValueCodec(managedFactoryRegistry))
            bind(IsolatedImmutableManagedValueCodec(managedFactoryRegistry))
            bind(IsolatedJavaSerializedValueSnapshotCodec)
            bind(IsolatedArrayCodec)
            bind(IsolatedSetCodec)
            bind(IsolatedListCodec)
            bind(IsolatedMapCodec)
            bind(MapEntrySnapshotCodec)
            bind(IsolatedEnumValueSnapshotCodec)
            bind(StringValueSnapshotCodec)
            bind(IntegerValueSnapshotCodec)
            bind(FileValueSnapshotCodec)
            bind(BooleanValueSnapshotCodec)
            bind(NullValueSnapshotCodec)

            bind(GradlePropertiesCodec)
            bind(ServicesCodec)

            bind(ProxyCodec)

            bind(BeanSpecCodec)

            bind(RegisteredFlowActionCodec)

            bind(NamedCodec(managedFactoryRegistry))
        }

        userTypesBindings = makeUserTypeBindings {
            providerTypes(
                propertyFactory,
                filePropertyFactory,
                nestedProviderCodec(
                    buildStateRegistry
                )
            )
        }

        fingerprintUserTypesBindings = makeUserTypeBindings {
            providerTypes(
                propertyFactory,
                filePropertyFactory,
                nestedProviderCodecForFingerprint()
            )
        }
    }

    private
    fun Bindings.completeWithStatefulCodecs() = append {
        bind(ExternalizableCodec)
        bind(JavaObjectSerializationCodec(javaSerializationEncodingLookup, objectOpener))
        bind(ValueObjectCodec)

        // This protects the BeanCodec against StackOverflowErrors, but
        // we can still get them for the other codecs, for instance,
        // with deeply nested Lists, deeply nested Maps, etc.
        // The reentrant codec is stateful, and cannot be cached because of it.
        bind(reentrant(BeanCodec))
    }.build()

    override fun userTypesCodec(): Codec<Any?> = userTypesBindings.completeWithStatefulCodecs()

    override fun fingerprintTypesCodec(): Codec<Any?> = fingerprintUserTypesBindings.completeWithStatefulCodecs()

    private
    val internalTypesBindings = Bindings.of {
        baseTypes(objectOpener)

        providerTypes(propertyFactory, filePropertyFactory, nestedProviderCodec(buildStateRegistry))
        fileCollectionTypes(directoryFileTreeFactory, fileCollectionFactory, artifactSetConverter, fileOperations, fileFactory, patternSetFactory, fileLookup, taskDependencyFactory)

        bind(TaskInAnotherBuildCodec(includedTaskGraph))

        bind(DefaultResolvableArtifactCodec(calculatedValueContainerFactory))
    }

    override fun internalTypesCodec(): Codec<Any?> = internalTypesBindings.append {
        val userTypesCodec = userTypesCodec()

        bind(TaskNodeCodec(userTypesCodec))
        bind(DelegatingCodec<TransformStepNode>(userTypesCodec))
        bind(org.gradle.internal.serialize.codecs.core.ActionNodeCodec(userTypesCodec))
        bind(OrdinalNodeCodec)

        bind(NotImplementedCodec)
    }.build()

    private
    fun BindingsBuilder.providerTypes(
        propertyFactory: PropertyFactory,
        filePropertyFactory: FilePropertyFactory,
        nestedCodec: FixedValueReplacingProviderCodec
    ) {
        bind(ListPropertyCodec(propertyFactory, nestedCodec))
        bind(SetPropertyCodec(propertyFactory, nestedCodec))
        bind(MapPropertyCodec(propertyFactory, nestedCodec))
        bind(DirectoryPropertyCodec(filePropertyFactory, nestedCodec))
        bind(RegularFilePropertyCodec(filePropertyFactory, nestedCodec))
        bind(PropertyCodec(propertyFactory, nestedCodec))
        bind(ProviderCodec(nestedCodec))
    }

    /**
     * Returns a Codec for Provider implementations suitable for all Provider implementations.
     */
    private
    fun nestedProviderCodec(
        buildStateRegistry: BuildStateRegistry
    ) = FixedValueReplacingProviderCodec(
        defaultCodecForProviderWithChangingValue(
            ValueSourceProviderCodec(::userTypesCodec),
            BuildServiceProviderCodec(buildStateRegistry),
            FlowProvidersCodec
        )
    )

    /**
     * Returns a Codec for Provider implementations supported in the fingerprinting context. For example, BuildServiceProviders are not supported.
     */
    private
    fun nestedProviderCodecForFingerprint() = FixedValueReplacingProviderCodec(
        defaultCodecForProviderWithChangingValue(
            ValueSourceProviderCodec(::fingerprintTypesCodec),
            UnsupportedFingerprintBuildServiceProviderCodec,
            UnsupportedFingerprintFlowProviders
        )
    )

    private
    fun BindingsBuilder.fileCollectionTypes(
        directoryFileTreeFactory: DirectoryFileTreeFactory,
        fileCollectionFactory: FileCollectionFactory,
        artifactSetConverter: ArtifactSetToFileCollectionFactory,
        fileOperations: FileOperations,
        fileFactory: FileFactory,
        patternSetFactory: PatternSetFactory,
        fileLookup: FileLookup,
        taskDependencyFactory: TaskDependencyFactory
    ) {
        bind(PathToFileResolverCodec(fileLookup))
        bind(DirectoryCodec(fileFactory))
        bind(RegularFileCodec(fileFactory))
        bind(ConfigurableFileTreeCodec(fileCollectionFactory))
        bind(FileTreeCodec(fileCollectionFactory, directoryFileTreeFactory, fileOperations))
        val fileCollectionCodec = FileCollectionCodec(fileCollectionFactory, artifactSetConverter, taskDependencyFactory)
        bind(ConfigurableFileCollectionCodec(fileCollectionCodec, fileCollectionFactory))
        bind(fileCollectionCodec)
        bind(IntersectionPatternSetCodec)
        bind(PatternSetCodec(patternSetFactory))
    }

    override fun workNodeCodecFor(gradle: GradleInternal, contextSource: IsolateContextSource) =
        WorkNodeCodec(gradle, internalTypesCodec(), contextSource, parallelStore, parallelLoad)
}
