/*
 * Copyright 2017 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.model;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.attributes.HasAttributes;
import org.gradle.api.internal.attributes.AttributeContainerInternal;
import org.gradle.api.internal.attributes.AttributeValue;
import org.gradle.api.internal.attributes.ImmutableAttributes;

import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * This is the heart of the attribute matching algorithm and is used whenever there are multiple candidates to choose from.
 * <p>
 * <ol>
 * <li>
 * For each candidate, check whether its attribute values are compatible (according to the {@link AttributeSelectionSchema)} with the values that were requested.
 * Any missing or extra attributes on the candidate are ignored at this point. If there are 0 or 1 compatible candidates after this, return that as the result.
 * </li>
 * <li>
 * If there are multiple candidates matching the requested values, check whether one of the candidates is a strict superset of all the others, i.e. it matched more
 * of the requested attributes than any other one. Missing or extra attributes don't count. If such a strict superset candidate exists, it is returned as the single match.
 * </li>
 * <li>
 * Otherwise continue with disambiguation. Disambiguation iterates through the attributes and presents the different values to the {@link AttributeSelectionSchema}.
 * The schema can declare a subset of these values as preferred. Candidates whose value is not in that subset are rejected. If a single candidate remains after
 * disambiguating the requested attributes, this candidate is returned. Otherwise disambiguation continues with extra attributes.
 * </li>
 * <li>
 * If we run out of candidates during this process, the intersection of preferred values is not satisfied by any of them, so there could be multiple valid choices.
 * In that case return all compatible candidates, as none of them is preferable over any other.
 * </li>
 * <li>
 * If there are one or more candidates left after disambiguation, return those.
 * </li>
 * </ol>
 * </p>
 *
 * <p>
 * Implementation notes:
 *
 * For matching and disambiguating the requested values, we keep a table of values to avoid recomputing them. The table has one row for each candidate and one column for each attribute.
 * The cells contain the values of the candidate for the given attribute. The first row contains the requested values. This table is packed into a single flat array in order to reduce
 * memory usage and increase data locality.
 *
 * The information which candidates are compatible and which candidates are still valid during disambiguation is kept in two {@link BitSet}s. The nth bit is set if the nth candidate
 * is compatible. The longest match is kept using two integers, one containing the length of the match, the other containing the index of the candidate that was the longest.
 *
 * </p>
 */
class MultipleCandidateMatcher<T extends HasAttributes> {
    private final AttributeSelectionSchema schema;
    private final ImmutableAttributes requested;
    private final List<? extends T> candidates;
    private final ImmutableAttributes[] candidateAttributeSets;

    private final List<Attribute<?>> requestedAttributes;
    private final BitSet compatible;
    private final Object[] requestedAttributeValues;

    private int candidateWithLongestMatch;
    private int lengthOfLongestMatch;

    private BitSet remaining;
    private Attribute<?>[] extraAttributes;

    MultipleCandidateMatcher(AttributeSelectionSchema schema, Collection<? extends T> candidates, ImmutableAttributes requested) {
        this.schema = schema;
        this.requested = requested;
        this.candidates = (candidates instanceof List) ? (List<? extends T>) candidates : ImmutableList.copyOf(candidates);
        candidateAttributeSets = new ImmutableAttributes[candidates.size()];
        for (int i = 0; i < candidates.size(); i++) {
            candidateAttributeSets[i] = ((AttributeContainerInternal) this.candidates.get(i).getAttributes()).asImmutable();
        }
        this.requestedAttributes = requested.keySet().asList();
        requestedAttributeValues = new Object[(1 + candidates.size()) * requestedAttributes.size()];
        compatible = new BitSet(candidates.size());
        compatible.set(0, candidates.size());
    }

    public List<T> getMatches() {
        fillRequestedValues();
        findCompatibleCandidates();
        if (compatible.cardinality() <= 1) {
            return getCandidates(compatible);
        }
        if (longestMatchIsSuperSetOfAllOthers()) {
            return Collections.singletonList(candidates.get(candidateWithLongestMatch));
        }
        return disambiguateCompatibleCandidates();
    }

    private void fillRequestedValues() {
        for (int a = 0; a < requestedAttributes.size(); a++) {
            Attribute<?> attribute = requestedAttributes.get(a);
            AttributeValue<?> attributeValue = requested.findEntry(attribute);
            setRequestedValue(a, attributeValue.isPresent() ? attributeValue.get() : null);
        }
    }

    private void findCompatibleCandidates() {
        for (int c = 0; c < candidates.size(); c++) {
            matchCandidate(c);
        }
    }

    private void matchCandidate(int c) {
        int matchLength = 0;

        for (int a = 0; a < requestedAttributes.size(); a++) {
            MatchResult result = recordAndMatchCandidateValue(c, a);
            if (result == MatchResult.NO_MATCH) {
                compatible.clear(c);
                return;
            }
            if (result == MatchResult.MATCH) {
                matchLength++;
            }
        }

        if (matchLength > lengthOfLongestMatch) {
            lengthOfLongestMatch = matchLength;
            candidateWithLongestMatch = c;
        }
    }

    private MatchResult recordAndMatchCandidateValue(int c, int a) {
        Object requestedValue = getRequestedValue(a);
        Attribute<?> attribute = requestedAttributes.get(a);
        AttributeValue<?> candidateValue = candidateAttributeSets[c].findEntry(attribute.getName());

        if (!candidateValue.isPresent()) {
            setCandidateValue(c, a, null);
            return MatchResult.MISSING;
        }

        Object coercedValue = candidateValue.coerce(attribute);
        setCandidateValue(c, a, coercedValue);

        return schema.matchValue(attribute, requestedValue, coercedValue) ? MatchResult.MATCH : MatchResult.NO_MATCH;
    }


    private boolean longestMatchIsSuperSetOfAllOthers() {
        for (int c = compatible.nextSetBit(0); c >= 0; c = compatible.nextSetBit(c + 1)) {
            if (c == candidateWithLongestMatch) {
                continue;
            }
            int lengthOfOtherMatch = 0;
            for (int a = 0; a < requestedAttributes.size(); a++) {
                if (getCandidateValue(c, a) == null) {
                    continue;
                }
                lengthOfOtherMatch++;
                if (getCandidateValue(candidateWithLongestMatch, a) == null) {
                    return false;
                }
            }
            if (lengthOfOtherMatch == lengthOfLongestMatch) {
                return false;
            }
        }
        return true;
    }


    private List<T> disambiguateCompatibleCandidates() {
        remaining = new BitSet(candidates.size());
        remaining.or(compatible);

        disambiguateWithRequestedAttributes();
        if (remaining.cardinality() > 1) {
            disambiguateWithExtraAttributes();
        }

        return remaining.cardinality() == 0 ? getCandidates(compatible) : getCandidates(remaining);
    }

    private void disambiguateWithRequestedAttributes() {
        for (int a = 0; a < requestedAttributes.size(); a++) {
            disambiguateWithAttribute(a);
            if (remaining.cardinality() == 0) {
                return;
            }
        }
    }

    private void disambiguateWithAttribute(int a) {
        Set<Object> candidateValues = getCandidateValues(a);
        if (candidateValues.size() == 1) {
            return;
        }

        Set<Object> matches = schema.disambiguate(getAttribute(a), getRequestedValue(a), candidateValues);
        removeCandidatesWithValueNotIn(a, matches);
    }

    private Set<Object> getCandidateValues(int a) {
        Set<Object> candidateValues = Sets.newHashSetWithExpectedSize(compatible.cardinality());
        for (int c = compatible.nextSetBit(0); c >= 0; c = compatible.nextSetBit(c + 1)) {
            candidateValues.add(getCandidateValue(c, a));
        }
        return candidateValues;
    }

    private void removeCandidatesWithValueNotIn(int a, Set<Object> matchedValues) {
        for (int c = remaining.nextSetBit(0); c >= 0; c = remaining.nextSetBit(c + 1)) {
            if (!matchedValues.contains(getCandidateValue(c, a))) {
                remaining.clear(c);
            }
        }
    }

    private void disambiguateWithExtraAttributes() {
        collectExtraAttributes();
        int allAttributes = requestedAttributes.size() + extraAttributes.length;
        for (int a = requestedAttributes.size(); a < allAttributes; a++) {
            disambiguateWithAttribute(a);
            if (remaining.cardinality() == 0) {
                return;
            }
        }
    }

    /**
     * Collects attributes that were present on the candidates, but which the consumer did
     * not ask for. Some of these attributes might be weakly typed, e.g. coming as Strings
     * from an artifact repository. We always check whether the schema has a more strongly
     * typed version of an attribute and use that one instead to apply its disambiguation
     * rules.
     */
    private void collectExtraAttributes() {
        Set<Attribute<?>> extraAttributes = Sets.newLinkedHashSet();
        for (ImmutableAttributes attributes : candidateAttributeSets) {
            extraAttributes.addAll(attributes.keySet());
        }
        extraAttributes.removeAll(requested.keySet());
        this.extraAttributes = extraAttributes.toArray(new Attribute[0]);
        for (int i = 0; i < this.extraAttributes.length; i++) {
            Attribute<?> extraAttribute = this.extraAttributes[i];
            Attribute<?> schemaAttribute = schema.getAttribute(extraAttribute.getName());
            if (schemaAttribute != null) {
                this.extraAttributes[i] = schemaAttribute;
            }
        }
    }

    private List<T> getCandidates(BitSet liveSet) {
        if (liveSet.cardinality() == 0) {
            return Collections.emptyList();
        }
        if (liveSet.cardinality() == 1) {
            return Collections.singletonList(this.candidates.get(liveSet.nextSetBit(0)));
        }
        ImmutableList.Builder<T> builder = ImmutableList.builder();
        for (int c = liveSet.nextSetBit(0); c >= 0; c = liveSet.nextSetBit(c + 1)) {
            builder.add(this.candidates.get(c));
        }
        return builder.build();
    }

    private Attribute<?> getAttribute(int a) {
        if (a < requestedAttributes.size()) {
            return requestedAttributes.get(a);
        } else {
            return extraAttributes[a - requestedAttributes.size()];
        }
    }

    private Object getRequestedValue(int a) {
        if (a < requestedAttributes.size()) {
            return requestedAttributeValues[a];
        } else {
            return null;
        }
    }

    private Object getCandidateValue(int c, int a) {
        if (a < requestedAttributes.size()) {
            return requestedAttributeValues[getValueIndex(c, a)];
        } else {
            Attribute<?> extraAttribute = getAttribute(a);
            AttributeValue attributeValue = candidateAttributeSets[c].findEntry(extraAttribute.getName());
            return attributeValue.isPresent() ? attributeValue.coerce(extraAttribute) : null;
        }
    }

    private void setRequestedValue(int a, Object value) {
        requestedAttributeValues[a] = value;
    }

    private void setCandidateValue(int c, int a, Object value) {
        requestedAttributeValues[getValueIndex(c, a)] = value;
    }


    private int getValueIndex(int c, int a) {
        return (1 + c) * requestedAttributes.size() + a;
    }

    private enum MatchResult {
        MATCH,
        MISSING,
        NO_MATCH
    }
}
