/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 net.jini.core.constraint;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamField;
import java.io.Serializable;
import java.util.Collection;
import java.util.Set;

/**
 * Combines two or more constraint alternatives into a single overall
 * constraint. The semantics of this aggregate constraint are that at least
 * one of the individual constraint alternatives must be satisfied. The
 * alternatives do not have to be instances of the same type, but they
 * cannot themselves be <code>ConstraintAlternatives</code> instances.
 * <p>
 * Note that this class implements {@link RelativeTimeConstraint} even though
 * the constraint elements might not implement
 * <code>RelativeTimeConstraint</code>.
 * <p>
 * An instance containing an exhaustive list of alternatives (for example,
 * an instance containing both <code>ClientAuthentication.YES</code> and
 * <code>ClientAuthentication.NO</code>) serves no useful purpose, as a
 * requirement or as a preference. A <i>don't care</i> condition should
 * be expressed by the <i>absence</i> of constraints.
 *
 * 
 * @since 2.0
 */
public final class ConstraintAlternatives
				implements RelativeTimeConstraint, Serializable
{
    private static final long serialVersionUID = 7214615235302870613L;

    /**
     * @serialField constraints InvocationConstraint[]
     * The alternative constraints.
     */
    private static final ObjectStreamField[] serialPersistentFields = {
        new ObjectStreamField("constraints", InvocationConstraint[].class,
			      true)
    };

    /**
     * The alternative constraints.
     */
    private final InvocationConstraint[] constraints;

    /**
     * Indicates whether any of the constraints are based on relative time.
     */
    private transient boolean rel = false;

    /**
     * Creates an instance containing the specified alternative constraints,
     * with duplicate constraints removed. The argument passed to this
     * constructor is neither modified nor retained; subsequent changes to
     * that argument have no effect on the instance created.
     *
     * @param constraints the alternative constraints
     * @throws NullPointerException if the argument is <code>null</code> or
     * any element is <code>null</code>
     * @throws IllegalArgumentException if any of the elements are instances
     * of <code>ConstraintAlternatives</code>, or if fewer than two elements
     * remain after duplicate constraints are removed
     */
    public ConstraintAlternatives(InvocationConstraint[] constraints) {
	this.constraints =
	    reduce((InvocationConstraint[]) constraints.clone());
	setRelative();
    }

    /**
     * Creates an instance containing the specified alternative constraints,
     * with duplicate constraints removed. The argument passed to this
     * constructor is neither modified nor retained; subsequent changes to
     * that argument have no effect on the instance created.
     *
     * @param c the alternative constraints
     * @throws NullPointerException if the argument is <code>null</code> or
     * any element is <code>null</code>
     * @throws IllegalArgumentException if any of the elements are instances
     * of <code>ConstraintAlternatives</code>, or if the elements are not all
     * instances of <code>InvocationConstraint</code>, or if fewer than two
     * elements remain after duplicate constraints are removed
     */
    public ConstraintAlternatives(Collection c) {
	try {
	    constraints = reduce((InvocationConstraint[]) c.toArray(
					  new InvocationConstraint[c.size()]));
	} catch (ArrayStoreException e) {
	    throw new IllegalArgumentException(
		       "element of collection is not an InvocationConstraint");
	}
	setRelative();
    }

    /**
     * Returns a constraint representing the specified alternative constraints,
     * with duplicate constraints removed. If a single constraint remains after
     * duplicates are removed, then that constraint is returned, otherwise an
     * instance of <code>ConstraintAlternatives</code> containing the remaining
     * constraints is returned. The argument passed to this method is neither
     * modified nor retained; subsequent changes to that argument have no
     * effect on the instance created.
     *
     * @param constraints the alternative constraints
     * @return a constraint representing the specified alternative constraints,
     * with duplicate constraints removed
     * @throws NullPointerException if the argument is <code>null</code> or
     * any element is <code>null</code>
     * @throws IllegalArgumentException if the argument is empty, or if any
     * of the elements are instances of <code>ConstraintAlternatives</code>
     */
    public static InvocationConstraint create(
					   InvocationConstraint[] constraints)
    {
	return reduce((InvocationConstraint[]) constraints.clone(), false);
    }

    /**
     * Returns a constraint representing the specified alternative constraints,
     * with duplicate constraints removed. If a single constraint remains after
     * duplicates are removed, then that constraint is returned, otherwise an
     * instance of <code>ConstraintAlternatives</code> containing the remaining
     * constraints is returned. The argument passed to this method is neither
     * modified nor retained; subsequent changes to that argument have no
     * effect on the instance created.
     *
     * @param c the alternative constraints
     * @return a constraint representing the specified alternative constraints,
     * with duplicate constraints removed
     * @throws NullPointerException if the argument is <code>null</code> or
     * any element is <code>null</code>
     * @throws IllegalArgumentException if the argument is empty, or if any
     * of the elements are instances of <code>ConstraintAlternatives</code>,
     * or if the elements are not all instances of
     * <code>InvocationConstraint</code>
     */
    public static InvocationConstraint create(Collection c) {
	try {
	    return reduce((InvocationConstraint[]) c.toArray(
					  new InvocationConstraint[c.size()]),
			  false);
	} catch (ArrayStoreException e) {
	    throw new IllegalArgumentException(
		       "element of collection is not an InvocationConstraint");
	}
    }

    /**
     * Creates a constraint containing the specified alternative constraints,
     * and computes the rel field if allAbs is false.
     */
    private ConstraintAlternatives(InvocationConstraint[] constraints,
				   boolean allAbs)
    {
	this.constraints = constraints;
	if (!allAbs) {
	    setRelative();
	}
    }

    /**
     * Sets the rel field to true if any of the constraints are relative.
     */
    private void setRelative() {
	for (int i = constraints.length; --i >= 0; ) {
	    if (constraints[i] instanceof RelativeTimeConstraint) {
		rel = true;
		return;
	    }
	}
    }

    /**
     * Returns true if any of the constraints are relative, false otherwise.
     */
    boolean relative() {
	return rel;
    }

    /**
     * Verifies that the array is non-empty, and that the elements are all
     * non-null and not ConstraintAlternatives instances. Removes duplicates
     * and returns a single constraint if there's only one left, otherwise
     * returns an ConstraintAlternatives containing the remaining constraints.
     * The argument is modified in place.
     */
    private static InvocationConstraint reduce(
					    InvocationConstraint[] constraints,
					    boolean allAbs)
    {
	verify(constraints, 1);
	int n = reduce0(constraints);
	if (n == 1) {
	    return constraints[0];
	}
	return new ConstraintAlternatives(
		      (InvocationConstraint[]) Constraint.trim(constraints, n),
		      allAbs);
    }

    /**
     * Verifies that the array has at least min elements, and that the
     * elements are all non-null and not ConstraintAlternatives instances.
     */
    private static void verify(InvocationConstraint[] constraints, int min) {
	if (constraints.length < min) {
	    throw new IllegalArgumentException(
				 "cannot create constraint with " +
				 (min == 1 ? "no" : ("less than " + min)) +
				 " elements");
	}
	for (int i = constraints.length; --i >= 0; ) {
	    InvocationConstraint c = constraints[i];
	    if (c == null) {
		throw new NullPointerException("elements cannot be null");
	    } else if (c instanceof ConstraintAlternatives) {
		throw new IllegalArgumentException(
			"elements cannot be ConstraintAlternatives instances");
	    }
	}
    }

    /**
     * Verifies that the array has at least 2 elements, and that the elements
     * are all non-null and not ConstraintAlternatives instances, removes
     * duplicates, modifying the array in place, verifies that there are still
     * at least 2 elements, and returns an array containing the remaining
     * elements.
     */
    private static InvocationConstraint[] reduce(
					    InvocationConstraint[] constraints)
    {
	verify(constraints, 2);
	int n = reduce0(constraints);
	if (n == 1) {
	    throw new IllegalArgumentException(
					   "reduced to less than 2 elements");
	}
	return (InvocationConstraint[]) Constraint.trim(constraints, n);
    }

    /**
     * Eliminates duplicates, modifying the array in place, and returns
     * the resulting number of elements.
     */
    private static int reduce0(InvocationConstraint[] constraints) {
	int i = 0;
	for (int j = 0; j < constraints.length; j++) {
	    InvocationConstraint c = constraints[j];
	    if (!Constraint.contains(constraints, i, c)) {
		constraints[i++] = c;
	    }
	}
	return i;
    }

    /**
     * Returns an immutable set of all of the constraints. Any attempt to
     * modify this set results in an {@link UnsupportedOperationException}
     * being thrown.
     *
     * @return an immutable set of all of the constraints
     */
    public Set elements() {
	return new ArraySet(constraints);
    }

    /**
     * Returns the elements, without copying.
     */
    InvocationConstraint[] getConstraints() {
	return constraints;
    }

    /**
     * Returns a constraint equal to the result of taking the constraints in
     * this instance, replacing each constraint that is an instance of
     * {@link RelativeTimeConstraint} with the result of invoking that
     * constraint's <code>makeAbsolute</code> method with the specified base
     * time, and invoking the <code>create</code> method of this class with
     * the revised collection of constraints.
     */
    public InvocationConstraint makeAbsolute(long baseTime) {
	if (!rel) {
	    return this;
	}
	InvocationConstraint[] vals =
	    new InvocationConstraint[constraints.length];
	for (int i = vals.length; --i >= 0; ) {
	    InvocationConstraint c = constraints[i];
	    if (c instanceof RelativeTimeConstraint) {
		c = ((RelativeTimeConstraint) c).makeAbsolute(baseTime);
	    }
	    vals[i] = c;
	}
	return reduce(vals, true);
    }

    /**
     * Returns a hash code value for this object.
     */
    public int hashCode() {
	return Constraint.hash(constraints);
    }

    /**
     * Two instances of this class are equal if they have the same constraints
     * (ignoring order).
     */
    public boolean equals(Object obj) {
	return (obj instanceof ConstraintAlternatives &&
		Constraint.equal(constraints,
				 ((ConstraintAlternatives) obj).constraints));
    }

    /**
     * Returns a string representation of this object.
     */
    public String toString() {
	return "ConstraintAlternatives" + Constraint.toString(constraints);
    }

    /**
     * Verifies that there are at least two constraints, that none are
     * <code>null</code> and none are instances of this class, and that
     * there are no duplicates.
     *
     * @throws InvalidObjectException if there are less than two constraints,
     * or any constraint is <code>null</code> or an instance of this class,
     * or if there are duplicates
     */
    private void readObject(ObjectInputStream s)
	throws IOException, ClassNotFoundException
    {
	s.defaultReadObject();
	if (constraints == null) {
	    throw new InvalidObjectException(
				  "cannot create constraint with no elements");
	}
	try {
	    verify(constraints, 2);
	} catch (RuntimeException e) {
	    if (e instanceof NullPointerException ||
		e instanceof IllegalArgumentException)
	    {
		InvalidObjectException ee =
		    new InvalidObjectException(e.getMessage());
		ee.initCause(e);
		throw ee;
	    }
	    throw e;
	}
	for (int i = constraints.length; --i >= 0; ) {
	    if (Constraint.contains(constraints, i, constraints[i])) {
		throw new InvalidObjectException(
			  "cannot create constraint with duplicate elements");
	    }
	}
	setRelative();
    }
}
