/*
 * Copyright 2015 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.api.internal.resolve;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeMultimap;
import org.gradle.api.Named;
import org.gradle.internal.Cast;
import org.gradle.language.base.internal.model.*;
import org.gradle.model.internal.manage.schema.ModelSchemaStore;
import org.gradle.platform.base.BinarySpec;
import org.gradle.platform.base.internal.BinarySpecInternal;

import java.util.*;

public class VariantsMatcher {
    private static final Comparator<VariantValue> SPEC_COMPARATOR = new Comparator<VariantValue>() {
        @Override
        public int compare(VariantValue o1, VariantValue o2) {
            return o1.spec.getDisplayName().compareTo(o2.spec.getDisplayName());
        }
    };

    private final List<VariantAxisCompatibilityFactory> factories;
    private final Class<? extends BinarySpec> binarySpecType;
    private final ModelSchemaStore schemaStore;

    public VariantsMatcher(List<VariantAxisCompatibilityFactory> factories, Class<? extends BinarySpec> binarySpecType, ModelSchemaStore schemaStore) {
        this.factories = factories;
        this.binarySpecType = binarySpecType;
        this.schemaStore = schemaStore;
    }

    private VariantAxisCompatibility<Object> createSelector(Object o) {
        for (VariantAxisCompatibilityFactory factory : factories) {
            @SuppressWarnings("unchecked")
            VariantAxisCompatibility<Object> selector = factory.getVariantAxisCompatibility(o);
            if (selector != null) {
                return selector;
            }
        }
        return new DefaultVariantAxisCompatibility();
    }

    public Collection<? extends BinarySpec> filterBinaries(VariantsMetaData variantsMetaData, Collection<BinarySpec> binaries) {
        if (binaries.isEmpty()) {
            return binaries;
        }
        Set<String> resolveDimensions = variantsMetaData.getNonNullVariantAxes();
        TreeMultimap<String, VariantValue> selectedVariants = TreeMultimap.create(String.CASE_INSENSITIVE_ORDER, SPEC_COMPARATOR);
        Set<BinarySpec> removedSpecs = Sets.newHashSet();
        for (BinarySpec binarySpec : binaries) {
            if (binarySpecType.isAssignableFrom(binarySpec.getClass())) {
                VariantsMetaData binaryVariants = DefaultVariantsMetaData.extractFrom(binarySpec, schemaStore.getSchema(((BinarySpecInternal)binarySpec).getPublicType()));
                Set<String> commonsDimensions = Sets.intersection(resolveDimensions, binaryVariants.getNonNullVariantAxes());
                Set<String> incompatibleDimensionTypes = VariantsMetaDataHelper.determineAxesWithIncompatibleTypes(variantsMetaData, binaryVariants, commonsDimensions);
                if (incompatibleDimensionTypes.isEmpty()) {
                    for (String dimension : commonsDimensions) {
                        Class<?> dimensionType = variantsMetaData.getVariantAxisType(dimension).getConcreteClass();
                        boolean isStringType = String.class == dimensionType;
                        Object requestedValue = isStringType ? variantsMetaData.getValueAsString(dimension) : variantsMetaData.getValueAsType(Cast.<Class<? extends Named>>uncheckedCast(dimensionType), dimension);
                        Object binaryValue = isStringType ? binaryVariants.getValueAsString(dimension) : binaryVariants.getValueAsType(Cast.<Class<? extends Named>>uncheckedCast(dimensionType), dimension);
                        VariantAxisCompatibility<Object> selector = createSelector(requestedValue);
                        if (selector.isCompatibleWithRequirement(requestedValue, binaryValue)) {
                            VariantValue value = new VariantValue(binaryValue, binarySpec);
                            SortedSet<VariantValue> variantValues = selectedVariants.get(dimension);
                            for (VariantValue variantValue : variantValues) {
                                // all the values are equal, but we store all the binaries that match that value
                                // and incrementally build a list of binaries which are excluded because of a better match
                                if (selector.betterFit(requestedValue, variantValue.value, binaryValue)) {
                                    // the new value is a better fit than the old one
                                    removedSpecs.add(variantValue.spec);
                                } else if (selector.betterFit(requestedValue, binaryValue, variantValue.value)) {
                                    // the old value is a better fit than the new one, let's ignore the new one altogether
                                    removedSpecs.add(value.spec);
                                }
                            }
                            selectedVariants.put(dimension, value);
                        } else {
                            removedSpecs.add(binarySpec);
                        }
                    }
                }
            }
        }

        Set<BinarySpec> union = null;
        for (String dimension : selectedVariants.keySet()) {
            Set<BinarySpec> variantValues = ImmutableSet.copyOf(Iterables.transform(selectedVariants.get(dimension), VariantValue.SPEC_FUNCTION));
            union = union == null ? variantValues : Sets.union(union, variantValues);
        }
        return union == null ? Collections.<BinarySpec>emptySet() : Sets.difference(union, removedSpecs);
    }

    private static class VariantValue {
        public static final Function<VariantValue, BinarySpec> SPEC_FUNCTION = new Function<VariantValue, BinarySpec>() {
            @Override
            public BinarySpec apply(VariantValue input) {
                return input.spec;
            }
        };

        final Object value;
        final BinarySpec spec;

        private VariantValue(Object value, BinarySpec spec) {
            this.value = value;
            this.spec = spec;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("VariantValue{");
            sb.append("spec=").append(spec);
            sb.append(", value=").append(value);
            sb.append('}');
            return sb.toString();
        }
    }
}
