/*
 * 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.internal.component.external.model.ivy;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import org.gradle.api.artifacts.component.ModuleComponentSelector;
import org.gradle.api.internal.artifacts.ImmutableModuleIdentifierFactory;
import org.gradle.api.internal.artifacts.ivyservice.resolveengine.result.AttributeContainerSerializer;
import org.gradle.api.internal.attributes.ImmutableAttributes;
import org.gradle.internal.component.external.descriptor.Artifact;
import org.gradle.internal.component.external.descriptor.Configuration;
import org.gradle.internal.component.external.descriptor.DefaultExclude;
import org.gradle.internal.component.external.model.AbstractRealisedModuleComponentResolveMetadata;
import org.gradle.internal.component.external.model.AbstractRealisedModuleResolveMetadataSerializationHelper;
import org.gradle.internal.component.external.model.ComponentVariant;
import org.gradle.internal.component.external.model.ConfigurationBoundExternalDependencyMetadata;
import org.gradle.internal.component.external.model.ExternalDependencyDescriptor;
import org.gradle.internal.component.external.model.GradleDependencyMetadata;
import org.gradle.internal.component.external.model.ImmutableCapabilities;
import org.gradle.internal.component.external.model.LazyToRealisedModuleComponentResolveMetadataHelper;
import org.gradle.internal.component.external.model.ModuleComponentArtifactMetadata;
import org.gradle.internal.component.external.model.ModuleComponentResolveMetadata;
import org.gradle.internal.component.external.model.ModuleDependencyMetadata;
import org.gradle.internal.component.external.model.RealisedConfigurationMetadata;
import org.gradle.internal.component.model.ConfigurationMetadata;
import org.gradle.internal.component.model.DefaultIvyArtifactName;
import org.gradle.internal.component.model.DependencyMetadata;
import org.gradle.internal.component.model.Exclude;
import org.gradle.internal.component.model.IvyArtifactName;
import org.gradle.internal.serialize.Decoder;
import org.gradle.internal.serialize.Encoder;

import java.io.IOException;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class RealisedIvyModuleResolveMetadataSerializationHelper extends AbstractRealisedModuleResolveMetadataSerializationHelper {
    public RealisedIvyModuleResolveMetadataSerializationHelper(AttributeContainerSerializer attributeContainerSerializer, ImmutableModuleIdentifierFactory moduleIdentifierFactory) {
        super(attributeContainerSerializer, moduleIdentifierFactory);
    }

    public ModuleComponentResolveMetadata readMetadata(Decoder decoder, DefaultIvyModuleResolveMetadata resolveMetadata) throws IOException {
        Map<String, List<GradleDependencyMetadata>> variantToDependencies = readVariantDependencies(decoder);
        ImmutableList<? extends ComponentVariant> variants = resolveMetadata.getVariants();
        ImmutableList.Builder<AbstractRealisedModuleComponentResolveMetadata.ImmutableRealisedVariantImpl> builder = ImmutableList.builder();
        for (ComponentVariant variant: variants) {
            builder.add(new AbstractRealisedModuleComponentResolveMetadata.ImmutableRealisedVariantImpl(resolveMetadata.getId(), variant.getName(), variant.getAttributes().asImmutable(), variant.getDependencies(), variant.getDependencyConstraints(),
                variant.getFiles(), ImmutableCapabilities.of(variant.getCapabilities().getCapabilities()), variantToDependencies.get(variant.getName())));
        }
        ImmutableList<AbstractRealisedModuleComponentResolveMetadata.ImmutableRealisedVariantImpl> realisedVariants = builder.build();
        return new RealisedIvyModuleResolveMetadata(resolveMetadata, realisedVariants, readIvyConfigurations(decoder, resolveMetadata));
    }

    protected void writeDependencies(Encoder encoder, ConfigurationMetadata configuration) throws IOException {
        List<? extends DependencyMetadata> dependencies = configuration.getDependencies();
        encoder.writeSmallInt(dependencies.size());
        for (DependencyMetadata dependency: dependencies) {
            if (dependency instanceof GradleDependencyMetadata) {
                encoder.writeByte(GRADLE_DEPENDENCY_METADATA);
                writeDependencyMetadata(encoder, (GradleDependencyMetadata) dependency);
            } else if (dependency instanceof ConfigurationBoundExternalDependencyMetadata) {
                ConfigurationBoundExternalDependencyMetadata dependencyMetadata = (ConfigurationBoundExternalDependencyMetadata) dependency;
                ExternalDependencyDescriptor dependencyDescriptor = dependencyMetadata.getDependencyDescriptor();
                if (dependencyDescriptor instanceof IvyDependencyDescriptor) {
                    encoder.writeByte(IVY_DEPENDENCY_METADATA);
                    writeIvyDependency(encoder, (IvyDependencyDescriptor) dependencyDescriptor);
                } else {
                    throw new IllegalStateException("Unknown type of dependency descriptor: " + dependencyDescriptor.getClass());
                }
                encoder.writeNullableString(dependency.getReason());
            }
        }
    }

    private Map<String, ConfigurationMetadata> readIvyConfigurations(Decoder decoder, DefaultIvyModuleResolveMetadata metadata) throws IOException {
        Map<Artifact, ModuleComponentArtifactMetadata> artifacts = new IdentityHashMap<Artifact, ModuleComponentArtifactMetadata>();
        IvyConfigurationHelper configurationHelper = new IvyConfigurationHelper(metadata.getArtifactDefinitions(), artifacts, metadata.getExcludes(), metadata.getDependencies(), metadata.getId());

        ImmutableMap<String, Configuration> configurationDefinitions = metadata.getConfigurationDefinitions();

        int configurationsCount = decoder.readSmallInt();
        Map<String, ConfigurationMetadata> configurations = Maps.newHashMapWithExpectedSize(configurationsCount);
        for (int i = 0; i < configurationsCount; i++) {
            String configurationName = decoder.readString();
            Configuration configuration = configurationDefinitions.get(configurationName);
            assert configuration != null;
            ImmutableSet<String> hierarchy = LazyToRealisedModuleComponentResolveMetadataHelper.constructHierarchy(configuration, configurationDefinitions);
            ImmutableAttributes attributes = getAttributeContainerSerializer().read(decoder);
            ImmutableCapabilities capabilities = readCapabilities(decoder);

            RealisedConfigurationMetadata configurationMetadata = new RealisedConfigurationMetadata(metadata.getId(), configurationName, configuration.isTransitive(), configuration.isVisible(),
                hierarchy, configurationHelper.filterArtifacts(configurationName, hierarchy), configurationHelper.filterExcludes(hierarchy), attributes, capabilities);

            ImmutableList.Builder<ModuleDependencyMetadata> builder = ImmutableList.builder();
            int dependenciesCount = decoder.readSmallInt();
            for (int j = 0; j < dependenciesCount; j++) {
                byte dependencyType = decoder.readByte();
                switch(dependencyType) {
                    case GRADLE_DEPENDENCY_METADATA:
                        builder.add(readDependencyMetadata(decoder));
                        break;
                    case IVY_DEPENDENCY_METADATA:
                        IvyDependencyDescriptor ivyDependency = readIvyDependency(decoder);
                        ModuleDependencyMetadata dependencyMetadata = configurationHelper.contextualize(configurationMetadata, metadata.getId(), ivyDependency);
                        builder.add(dependencyMetadata.withReason(decoder.readNullableString()));
                        break;
                    case MAVEN_DEPENDENCY_METADATA:
                        throw new IllegalStateException("Unexpected Maven dependency for Ivy module");
                    default:
                        throw new IllegalStateException("Unknown dependency type " + dependencyType);
                }
            }
            ImmutableList<ModuleDependencyMetadata> dependencies = builder.build();
            configurationMetadata.setDependencies(dependencies);

            configurations.put(configurationName, configurationMetadata);
        }
        return configurations;
    }

    private IvyDependencyDescriptor readIvyDependency(Decoder decoder) throws IOException {
        ModuleComponentSelector requested = getComponentSelectorSerializer().read(decoder);
        SetMultimap<String, String> configMappings = readDependencyConfigurationMapping(decoder);
        List<Artifact> artifacts = readDependencyArtifactDescriptors(decoder);
        List<Exclude> excludes = readDependencyExcludes(decoder);
        String dynamicConstraintVersion = decoder.readString();
        boolean changing = decoder.readBoolean();
        boolean transitive = decoder.readBoolean();
        boolean optional = decoder.readBoolean();
        return new IvyDependencyDescriptor(requested, dynamicConstraintVersion, changing, transitive,  optional, configMappings, artifacts, excludes);
    }

    private void writeIvyDependency(Encoder encoder, IvyDependencyDescriptor ivyDependency) throws IOException {
        getComponentSelectorSerializer().write(encoder, ivyDependency.getSelector());
        writeDependencyConfigurationMapping(encoder, ivyDependency);
        writeArtifacts(encoder, ivyDependency.getDependencyArtifacts());
        writeExcludeRules(encoder, ivyDependency.getAllExcludes());
        encoder.writeString(ivyDependency.getDynamicConstraintVersion());
        encoder.writeBoolean(ivyDependency.isChanging());
        encoder.writeBoolean(ivyDependency.isTransitive());
        encoder.writeBoolean(ivyDependency.isOptional());
    }

    private void writeExcludeRules(Encoder encoder, List<Exclude> excludes) throws IOException {
        encoder.writeSmallInt(excludes.size());
        for (Exclude exclude : excludes) {
            encoder.writeString(exclude.getModuleId().getGroup());
            encoder.writeString(exclude.getModuleId().getName());
            IvyArtifactName artifact = exclude.getArtifact();
            writeNullableArtifact(encoder, artifact);
            writeStringSet(encoder, exclude.getConfigurations());
            encoder.writeNullableString(exclude.getMatcher());
        }
    }

    private void writeArtifacts(Encoder encoder, List<Artifact> artifacts) throws IOException {
        encoder.writeSmallInt(artifacts.size());
        for (Artifact artifact : artifacts) {
            IvyArtifactName artifactName = artifact.getArtifactName();
            encoder.writeString(artifactName.getName());
            encoder.writeString(artifactName.getType());
            encoder.writeNullableString(artifactName.getExtension());
            encoder.writeNullableString(artifactName.getClassifier());
            writeStringSet(encoder, artifact.getConfigurations());
        }
    }

    private SetMultimap<String, String> readDependencyConfigurationMapping(Decoder decoder) throws IOException {
        int size = decoder.readSmallInt();
        SetMultimap<String, String> result = LinkedHashMultimap.create();
        for (int i = 0; i < size; i++) {
            String from = decoder.readString();
            Set<String> to = readStringSet(decoder);
            result.putAll(from, to);
        }
        return result;
    }

    private void writeDependencyConfigurationMapping(Encoder encoder, IvyDependencyDescriptor dep) throws IOException {
        SetMultimap<String, String> confMappings = dep.getConfMappings();
        encoder.writeSmallInt(confMappings.keySet().size());
        for (String conf : confMappings.keySet()) {
            encoder.writeString(conf);
            writeStringSet(encoder, confMappings.get(conf));
        }
    }

    private List<Artifact> readDependencyArtifactDescriptors(Decoder decoder) throws IOException {
        int size = decoder.readSmallInt();
        List<Artifact> result = Lists.newArrayListWithCapacity(size);
        for (int i = 0; i < size; i++) {
            IvyArtifactName ivyArtifactName = new DefaultIvyArtifactName(decoder.readString(), decoder.readString(), decoder.readNullableString(), decoder.readNullableString());
            result.add(new Artifact(ivyArtifactName, readStringSet(decoder)));
        }
        return result;
    }

    private List<Exclude> readDependencyExcludes(Decoder decoder) throws IOException {
        int len = decoder.readSmallInt();
        List<Exclude> result = Lists.newArrayListWithCapacity(len);
        for (int i = 0; i < len; i++) {
            DefaultExclude rule = readExcludeRule(decoder);
            result.add(rule);
        }
        return result;
    }

}
