/*
 * #%L
 * Protempa Framework
 * %%
 * Copyright (C) 2012 - 2013 Emory University
 * %%
 * 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.
 * #L%
 */
package org.protempa.dest.table;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.protempa.KnowledgeSource;
import org.protempa.KnowledgeSourceCache;
import org.protempa.KnowledgeSourceReadException;
import org.protempa.PropositionDefinition;
import org.protempa.ProtempaUtil;
import org.protempa.proposition.Proposition;
import org.protempa.proposition.UniqueId;
import org.protempa.proposition.value.Value;
import org.protempa.proposition.value.ValueComparator;
import org.protempa.dest.QueryResultsHandlerValidationFailedException;

/**
 * Representation of links between propositions in the PROTEMPA virtual data
 * model. Links may be derivations or references. This class represents a link
 * as being from one proposition to a collection of propositions. The 
 * collection of propositions may be filtered by proposition id, the values of 
 * its properties, and then by index (according to some specified ordering of 
 * the propositions).
 * 
 * @author Andrew Post
 */
public abstract class Link {

    private static final PropertyConstraint[] EMPTY_PROPERTY_CONSTRAINT_ARR = 
            new PropertyConstraint[0];
    
    private final Set<String> propIdsAsSet;
    private final PropertyConstraint[] constraints;
    private final Comparator<Proposition> comparator;
    private final int fromIndex;
    private final int toIndex;

    /**
     * Specifies a link with proposition ids, constraints, 
     * an index range for selecting from the list of propositions that match 
     * the proposition ids and constraints, and a comparator for ordering the 
     * list.
     * 
     * @param propositionIds
     *            a {@link String[]} of proposition ids.
     * @param constraints
     *            a {@link PropertyConstraint[]} of constraints.
     * @param comparator
     *            a {@link Comparator<Proposition>}.
     * @param fromIndex
     *            the lower bound of the range (a negative number or zero is
     *            interpreted as the beginning of the list).
     * @param toIndex
     *            the upper bound of the range exclusive (a negative number is
     *            interpreted as the end of the list).
     */
    Link(String[] propositionIds, PropertyConstraint[] constraints,
            Comparator<Proposition> comparator, int fromIndex, int toIndex) {
        if (propositionIds == null) {
            this.propIdsAsSet = Collections.emptySet();
        } else {
            ProtempaUtil.checkArrayForNullElement(propositionIds,
                    "propositionIds");
            this.propIdsAsSet = new HashSet<>();
            for (String propId : propositionIds) {
                this.propIdsAsSet.add(propId.intern());
            }
        }
        if (constraints == null) {
            this.constraints = EMPTY_PROPERTY_CONSTRAINT_ARR;
        } else {
            ProtempaUtil.checkArrayForNullElement(constraints, "constraints");
            this.constraints = constraints.clone();
        }
        if (fromIndex >= 0 && toIndex >= 0 && fromIndex >= toIndex) {
            throw new IllegalArgumentException(
                    "fromIndex cannot be greater than or equal to toIndex");
        }

        this.comparator = comparator;
        this.fromIndex = fromIndex;
        this.toIndex = toIndex;
    }

    /**
     * The ids of the propositions to traverse to. An empty array indicates 
     * that all propositions are of interest.
     * 
     * @return a proposition id {@link String[]} Guaranteed not 
     * <code>null</code>.
     */
    public final String[] getPropositionIds() {
        return this.propIdsAsSet.toArray(new String[this.propIdsAsSet.size()]);
    }
    
    /**
     * Aggregates and returns the possible proposition ids on the right-hand
     * side of the link.
     * 
     * @param inPropIds the proposition ids inferred from the previous link,
     * or the row proposition id if this is the first link.
     * 
     * @return an array of proposition id {@link String}s. 
     */
    public abstract String[] getInferredPropositionIds(
            KnowledgeSource knowledgeSource, String[] inPropIds)
            throws KnowledgeSourceReadException;
    
    /**
     * Returns whether a proposition has one of the proposition ids specified
     * by the link.
     * 
     * @param proposition a {@link Proposition}. Cannot be <code>null</code>.
     * @return <code>true</code> or <code>false</code>.
     */
    protected boolean isMatch(Proposition proposition) {
        return this.propIdsAsSet.isEmpty() || 
                this.propIdsAsSet.contains(proposition.getId());
    }

    /**
     * Constraints on properties of the propositions at the end of the 
     * traversal.
     * 
     * @return a {@link PropertyConstraint[]}. Guaranteed not <code>null</code>.
     */
    public final PropertyConstraint[] getConstraints() {
        return this.constraints.clone();
    }

    /**
     * A comparator for ordering the propositions at the end of the traversal. 
     * If <code>null</code>, the propositions will not be provided in any
     * particular order.
     * 
     * @return a {@link Comparator<Proposition>}.
     */
    public final Comparator<Proposition> getComparator() {
        return this.comparator;
    }

    /**
     * The start index of the propositions of interest at the end of the 
     * traversal step, after applying the proposition id constraints, property 
     * constraints and a comparator.
     * 
     * @return an <code>int</code>, with zero or a negative number indicating
     *         the beginning of the list.
     */
    public final int getFromIndex() {
        return this.fromIndex;
    }

    /**
     * The last index of the propositions of interest at the end of the
     * traversal step exclusive, after applying the proposition id constraints, 
     * property constraints and a comparator.
     * 
     * @return an <code>int</code>, with a negative number indicating the end of
     *         the list. Must be greater than the <code>fromIndex</code>.
     */
    public final int getToIndex() {
        return this.toIndex;
    }
    
    /**
     * Validates the fields of this link specification against the
     * knowledge source.
     * 
     * @param knowledgeSource a {@link KnowledgeSource}. Guaranteed not
     * <code>null</code>.
     * 
     * @throws QueryResultsHandlerValidationFailedException if validation
     * failed.
     * @throws KnowledgeSourceReadException if the knowledge source could
     * not be read.
     */
    void validate(KnowledgeSource knowledgeSource) throws 
            LinkValidationFailedException, KnowledgeSourceReadException {
        List<String> invalidPropIds = new ArrayList<>();
        for (String propId : this.propIdsAsSet) {
            if (!knowledgeSource.hasPropositionDefinition(propId)) {
                invalidPropIds.add(propId);
            }
        }
        if (!invalidPropIds.isEmpty()) {
            throw new LinkValidationFailedException(
                    "Invalid proposition id(s): " + 
                    StringUtils.join(invalidPropIds, ", "));
        }
    }

    /**
     * Generates a string for 
     * {@link org.protempa.query.handler.TableQueryResultsHandler} column 
     * headers.
     * 
     * @return a {@link String}.
     */
    abstract String headerFragment();

    /**
     * Returns the default header fragment for this link.
     * 
     * @param ref
     *            the name {@link String} of the reference of this link.
     * @return a header fragment {@link String}.
     * @see #headerFragment() 
     */
    final String createHeaderFragment(String ref) {
        int size = this.propIdsAsSet.size();
        boolean sep1Needed = size > 0 && this.constraints.length > 0;
        String sep1 = sep1Needed ? ", " : "";
        String id = size > 0 ? "id=" : "";
        boolean parenNeeded = size > 0 || this.constraints.length > 0;
        String startParen = parenNeeded ? "(" : "";
        String finishParen = parenNeeded ? ")" : "";

        String range = rangeString();

        boolean sep2Needed = sep1Needed && range.length() > 0;
        String sep2 = sep2Needed ? ", " : "";

        return '.' + ref + startParen + id
                + StringUtils.join(this.propIdsAsSet, ',') + sep1
                + constraintHeaderString(this.constraints) + finishParen + sep2
                + range;
    }

    /**
     * Filters out propositions by applying the property constraint and index
     * constraints. Modifies <code>propositions</code> in-place!
     * 
     * @param propositions
     *            a {@link Collection<Proposition>}. If <code>null</code>, an
     *            empty list is returned.
     * 
     * @return a {@link List<Proposition>}. Not guaranteed to be modifiable.
     */
    protected final List<Proposition> createResults(
            List<Proposition> propositions) {
        List<Proposition> result;
        if (propositions != null) {
            if (this.constraints.length > 0) {
                result = new ArrayList<>();
                for (Proposition prop : propositions) {
                    applyPropertyConstraints(prop, result);
                }
            } else {
                result = propositions;
            }

            result = applyComparatorAndIndices(result);
        } else {
            result = Collections.emptyList();
        }

        return result;
    }

    private String constraintHeaderString(PropertyConstraint[] constraints) {
        List<String> constraintsL = new ArrayList<>(constraints.length);
        for (int i = 0; i < constraints.length; i++) {
            PropertyConstraint ccc = constraints[i];
            constraintsL.add(ccc.getFormatted());
        }
        return StringUtils.join(constraintsL, ',');
    }

    private String rangeString() {
        boolean rangeSpecified = this.fromIndex >= 0 || this.toIndex >= 0;
        String range = rangeSpecified ? "range=" : "";
        if (rangeSpecified) {
            if (this.fromIndex >= 0) {
                range += this.fromIndex;
            } else {
                range += 0;
            }
            range += ",";
            if (this.toIndex >= 0) {
                range += this.toIndex;
            } else {
                range += "end";
            }
        }
        return range;
    }

    private List<Proposition> applyComparatorAndIndices(
            List<Proposition> result) {
        assert result != null : "result should not be null in sliceResults";
        if (!result.isEmpty()) {
            if (this.comparator != null) {
                Collections.sort(result, this.comparator);
            }
            if (this.fromIndex >= 0 || this.toIndex >= 0) {
                int resultSize = result.size();
                return result.subList(
                        this.fromIndex >= 0 ? this.fromIndex : 0,
                        this.toIndex >= 0 ? Math.min(this.toIndex, resultSize) 
                            : resultSize);
            }
        }
        return result;
    }

    private void applyPropertyConstraints(Proposition prop, 
            Collection<Proposition> result) {
        assert prop != null : "prop cannot be null";
        assert result != null : "result cannot be null";

        boolean compatible = constraintsCheckCompatible(prop,this.constraints);
        if (compatible) {
            result.add(prop);
        }
    }

    private boolean constraintsCheckCompatible(Proposition proposition,
            PropertyConstraint[] constraints) {
        Logger logger = Util.logger();
        for (PropertyConstraint ccc : constraints) {
            String propName = ccc.getPropertyName();
            Value value = proposition.getProperty(propName);
            if (value != null) {
                ValueComparator vc = ccc.getValueComparator();
                if (logger.isLoggable(Level.FINER)) {
                    logger.log(Level.FINER,
                                "Proposition is {0}; Property is {1}; Value is {2}; Comparator: {3}",
                                new Object[] { proposition.getId(), propName,
                                        value, vc });
                }
                if (!vc.compare(value, ccc.getValue())) {
                    return false;
                }
            } else {
                return false;
            }
        }
        return true;
    }

    /**
     * Traverses the specified link from a proposition to a collection of 
     * propositions.
     * 
     * @param proposition
     *            a {@link Proposition} at which to start the traversal.
     * @param forwardDerivations
     *            a {@link Map<Proposition,List<Proposition>>} of derived
     *            propositions.
     * @param backwardDerivations 
     *            a {@link Map<Proposition,List<Proposition>>} of derived
     *            propositions.
     * @param references
     *            a {@link Map<Proposition,Proposition>} of unique identifiers
     *            to {@link Proposition}s, used to resolve references.
     * @param knowledgeSource
     *            the {@link KnowledgeSource}.
     * @param cache
     *            a {@link Set<Proposition>} for convenience in checking if
     *            duplicate propositions are traversed to. It is cleared 
     *            in between calls to this method.
     * @return the {@link Collection<Proposition>} at the end of the traversal
     *            step. Not guaranteed to be modifiable.
     */
    abstract Collection<Proposition> traverse(Proposition proposition,
            Map<Proposition, List<Proposition>> forwardDerivations,
            Map<Proposition, List<Proposition>> backwardDerivations,
            Map<UniqueId, Proposition> references,
            KnowledgeSourceCache ksCache, Set<Proposition> cache);

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((comparator == null) ? 0 : comparator.hashCode());
		result = prime * result + Arrays.hashCode(constraints);
		result = prime * result + fromIndex;
		result = prime * result + ((propIdsAsSet == null) ? 0 : propIdsAsSet.hashCode());
		result = prime * result + toIndex;
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Link other = (Link) obj;
		if (comparator == null) {
			if (other.comparator != null)
				return false;
		} else if (!comparator.equals(other.comparator))
			return false;
		if (!Arrays.equals(constraints, other.constraints))
			return false;
		if (fromIndex != other.fromIndex)
			return false;
		if (propIdsAsSet == null) {
			if (other.propIdsAsSet != null)
				return false;
		} else if (!propIdsAsSet.equals(other.propIdsAsSet))
			return false;
		if (toIndex != other.toIndex)
			return false;
		return true;
	}
}
