/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.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://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */

package com.sun.jini.discovery;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import net.jini.core.constraint.ConstraintAlternatives;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.MethodConstraints;
import net.jini.io.UnsupportedConstraintException;

/**
 * Class for processing constraints which apply to the discovery protocol:
 * {@link DiscoveryProtocolVersion}, {@link MulticastMaxPacketSize},
 * {@link MulticastTimeToLive}, and {@link UnicastSocketTimeout}.
 *
 * @author Sun Microsystems, Inc.
 * @since 2.0
 */
public class DiscoveryConstraints {

    /** Method object for the multicastRequest method of this class. */
    public static final Method multicastRequestMethod;
    /** Method object for the multicastAnnouncement method of this class. */
    public static final Method multicastAnnouncementMethod;
    /** Method object for the unicastDiscovery method of this class. */
    public static final Method unicastDiscoveryMethod;
    static {
	try {
	    multicastRequestMethod = DiscoveryConstraints.class.getMethod(
		"multicastRequest", null);
	    multicastAnnouncementMethod = DiscoveryConstraints.class.getMethod(
		"multicastAnnouncement", null);
	    unicastDiscoveryMethod = DiscoveryConstraints.class.getMethod(
		"unicastDiscovery", null);
	} catch (NoSuchMethodException e) {
	    throw new AssertionError(e);
	}
    }

    private static final Set supportedProtocols = new HashSet(2);
    static {
	supportedProtocols.add(DiscoveryProtocolVersion.ONE);
	supportedProtocols.add(DiscoveryProtocolVersion.TWO);
    }

    private final InvocationConstraints unfulfilled;
    private final Set protocolVersions;
    private final int preferredProtocolVersion;
    private final MulticastMaxPacketSize maxPacketSize;
    private final MulticastTimeToLive timeToLive;
    private final UnicastSocketTimeout socketTimeout;

    /**
     * Empty method which serves as a {@link MethodConstraints} key for looking
     * up {@link InvocationConstraints} that apply to multicast requests.
     */
    public static void multicastRequest() {
    }

    /**
     * Empty method which serves as a {@link MethodConstraints} key for looking
     * up {@link InvocationConstraints} that apply to multicast announcements.
     */
    public static void multicastAnnouncement() {
    }

    /**
     * Empty method which serves as a {@link MethodConstraints} key for looking
     * up {@link InvocationConstraints} that apply to unicast discovery.
     */
    public static void unicastDiscovery() {
    }

    /**
     * Processes the discovery-related constraints in the given set of
     * constraints, returning a <code>DiscoveryConstraints</code> instance from
     * which the constraint results can be queried.
     *
     * @param constraints the constraints to process
     * @return an instance representing processed constraints
     * @throws UnsupportedConstraintException if the discovery-related
     * constraints contain conflicts, or otherwise cannot be processed
     * @throws NullPointerException if <code>constraints</code> is
     * <code>null</code>
     */
    public static DiscoveryConstraints process(
					   InvocationConstraints constraints)
	throws UnsupportedConstraintException
    {
	return new DiscoveryConstraints(constraints);
    }

    private DiscoveryConstraints(InvocationConstraints constraints)
	throws UnsupportedConstraintException
    {
	unfulfilled = new InvocationConstraints(
	    getUnfulfilled(constraints.requirements()),
	    getUnfulfilled(constraints.preferences()));

	ConstraintReducer cr = 
	    new ConstraintReducer(DiscoveryProtocolVersion.class);
	protocolVersions = cr.reduce(
	    new InvocationConstraints(constraints.requirements(), null));
	if (!protocolVersions.isEmpty() &&
	    intersect(protocolVersions, supportedProtocols).isEmpty())
	{
	    throw new UnsupportedConstraintException(
		"no supported protocols: " + protocolVersions);
	}
	preferredProtocolVersion = chooseProtocolVersion(
	    protocolVersions, cr.reduce(constraints), unfulfilled);

	Set s = new MulticastMaxPacketSizeReducer().reduce(constraints);
	maxPacketSize = s.isEmpty() ? 
	    null : (MulticastMaxPacketSize) getElement(s);

	s = new ConstraintReducer(
	    MulticastTimeToLive.class).reduce(constraints);
	timeToLive = s.isEmpty() ? null : (MulticastTimeToLive) getElement(s);

	s = new ConstraintReducer(
	    UnicastSocketTimeout.class).reduce(constraints);
	socketTimeout = s.isEmpty() ? 
	    null : (UnicastSocketTimeout) getElement(s);
    }

    /**
     * Returns the protocol version to use for sending multicast requests or
     * announcements, or initiating unicast discovery.
     *
     * @return the protocol version to use
     */
    public int chooseProtocolVersion() {
	return preferredProtocolVersion;
    }

    /**
     * Checks the protocol version of an incoming multicast request,
     * announcement, or unicast discovery attempt, throwing an {@link
     * UnsupportedConstraintException} if handling of the given protocol does
     * not satisfy the constraints of this instance.
     *
     * @param version protocol version to check
     * @throws UnsupportedConstraintException if handling of the given protocol
     * does not satisfy the constraints of this instance
     */
    public void checkProtocolVersion(int version)
	throws UnsupportedConstraintException
    {
	if (!(protocolVersions.isEmpty() ||
	      protocolVersions.contains(
		  DiscoveryProtocolVersion.getInstance(version))))
	{
	    throw new UnsupportedConstraintException(
		"disallowed protocol: " + version);
	}
    }

    /**
     * Returns the maximum multicast packet size to allow, or the specified
     * default value if not constrained.
     *
     * @param defaultValue the value to return if the multicast packet size is
     * unconstrained
     * @return the maximum multicast packet size to allow
     */
    public int getMulticastMaxPacketSize(int defaultValue) {
	return (maxPacketSize != null) ? 
	    maxPacketSize.getSize() : defaultValue;
    }

    /**
     * Returns the multicast time to live value to use, or the specified
     * default value if not constrained.
     *
     * @param defaultValue the value to return if the multicast time to live
     * value is unconstrained
     * @return the multicast time to live value to use
     */
    public int getMulticastTimeToLive(int defaultValue) {
	return (timeToLive != null) ?
	    timeToLive.getTimeToLive() : defaultValue;
    }

    /**
     * Returns socket timeout to use for unicast discovery, or the specified
     * default value if not constrained.
     *
     * @param defaultValue the value to return if the socket timeout is
     * unconstrained
     * @return the socket timeout to use
     */
    public int getUnicastSocketTimeout(int defaultValue) {
	return (socketTimeout != null) ? 
	    socketTimeout.getTimeout() : defaultValue;
    }

    /**
     * Returns the constraints for this instance which are not, or do not
     * contain as alternatives, instances of the "fulfillable" (by this layer)
     * constraint types <code>DiscoveryProtocolVersion</code>,
     * <code>MulticastMaxPacketSize</code>, <code>MulticastTimeToLive</code>,
     * and <code>UnicastSocketTimeout</code>.  Constraint alternatives
     * containing both fulfillable and unfulfillable constraints are treated
     * optimistically--it is assumed that the one of the fulfillable
     * alternatives will be satisfied, so the unfulfillable alternatives are
     * not included in the returned constraints.
     *
     * @return the unfulfilled constraints for this instance
     */
    public InvocationConstraints getUnfulfilledConstraints() {
	return unfulfilled;
    }

    /**
     * Utility for reducing constraints of a given type into a base set of
     * alternatives.  For each type of constraints that it reduces, this class
     * makes the simplifying assumption that failure to reduce constraints of
     * that type into a single (perhaps one element) set of alternatives
     * represents a constraint conflict--i.e., that two disjoint instances of
     * the constraint type cannot be applied to the same operation.  While this
     * restriction does not hold across all possible constraints, it is
     * satisfied by the particular constraint types that DiscoveryConstraints
     * handles.
     *
     * If this utility encounters a set of alternatives containing instances of
     * both the type to reduce as well as other constraint types, then it
     * ignores the instances of the other types, treating the set as if it can
     * only be satisfied by the alternatives of the targeted type.  This may in
     * some cases cause false positives when detecting conflicts in constraints
     * containing alternatives of mixed type; however, it should never result
     * in false negatives.
     */
    private static class ConstraintReducer {

	private final Class targetClass;

	/**
	 * Creates reducer that operates on instances of the given constraint
	 * class.
	 */
	ConstraintReducer(Class targetClass) {
	    this.targetClass = targetClass;
	}

	/**
	 * Returns the reduction of the given constraints into a single set of
	 * alternatives for the target class.  Returns an empty set if no
	 * constraints of the target class are specified.  Throws
	 * UnsupportedConstraintException if the constraints conflict.
	 */
	Set reduce(InvocationConstraints constraints)
	    throws UnsupportedConstraintException
	{
	    Set reduced = reduce(null, constraints.requirements(), true);
	    reduced = reduce(reduced, constraints.preferences(), false);
	    return (reduced != null) ? reduced : Collections.EMPTY_SET;
	}

	/**
	 * Returns the reduction (intersection and compaction) of a new set of
	 * alternative constraints, all instances of the target class, with a
	 * previously reduced set (null if no other constraints have been
	 * reduced yet).  Returns an empty set if elements in the sets
	 * conflict.  This method can be overridden by subclasses for
	 * constraints with particular reduction semantics; the default
	 * implementation of this method returns the intersection of the two
	 * sets.
	 */
	Set reduce0(Set reduced, Set toReduce) {
	    return (reduced != null) ? intersect(reduced, toReduce) : toReduce;
	}

	private Set reduce(Set reduced, Set constraints, boolean required)
	    throws UnsupportedConstraintException
	{
	    for (Iterator i = constraints.iterator(); i.hasNext(); ) {
		InvocationConstraint c = (InvocationConstraint) i.next();

		Set toReduce = Collections.EMPTY_SET;
		if (targetClass.isInstance(c)) {
		    toReduce = Collections.singleton(c);
		} else if (c instanceof ConstraintAlternatives) {
		    toReduce = getTargetInstances(
			((ConstraintAlternatives) c).elements());
		}
		if (!toReduce.isEmpty()) {
		    Set s = reduce0(reduced, toReduce);
		    if (!s.isEmpty()) {
			reduced = s;
		    } else if (required) {
			throw new UnsupportedConstraintException(
			    "constraints conflict: " + constraints);
		    }
		}
	    }
	    return reduced;
	}

	private Set getTargetInstances(Set set) {
	    Set instances = Collections.EMPTY_SET;
	    for (Iterator i = set.iterator(); i.hasNext(); ) {
		Object obj = i.next();
		if (targetClass.isInstance(obj)) {
		    if (instances.isEmpty()) {
			instances = new HashSet();
		    }
		    instances.add(obj);
		}
	    }
	    return instances;
	}
    }

    private static class MulticastMaxPacketSizeReducer 
	extends ConstraintReducer
    {
	MulticastMaxPacketSizeReducer() {
	    super(MulticastMaxPacketSize.class);
	}

	Set reduce0(Set reduced, Set toReduce) {
	    int size = 0;
	    for (Iterator i = toReduce.iterator(); i.hasNext(); ) {
		size = Math.max(
		    size, ((MulticastMaxPacketSize) i.next()).getSize());
	    }
	    if (reduced != null) {
		size = Math.min(
		    size,
		    ((MulticastMaxPacketSize) getElement(reduced)).getSize());
	    }
	    return Collections.singleton(new MulticastMaxPacketSize(size));
	}
    }

    private static Set getUnfulfilled(Set constraints) {
	Set unfulfilled = new HashSet(constraints.size());
	for (Iterator i = constraints.iterator(); i.hasNext(); ) {
	    InvocationConstraint c = (InvocationConstraint) i.next();
	    if (c instanceof ConstraintAlternatives) {
		Set s = ((ConstraintAlternatives) c).elements();
		Set u = getUnfulfilled(s);
		if (u.size() == s.size()) {
		    unfulfilled.add(c);
		}
	    } else if (!(c instanceof DiscoveryProtocolVersion ||
			 c instanceof MulticastMaxPacketSize ||
			 c instanceof MulticastTimeToLive ||
			 c instanceof UnicastSocketTimeout))
	    {
		unfulfilled.add(c);
	    }
	}
	return unfulfilled;
    }

    private static int chooseProtocolVersion(Set protocolVersions,
					     Set protocolVersionPrefs,
					     InvocationConstraints unfulfilled)
    {
	DiscoveryProtocolVersion bias = unfulfilled.isEmpty() ? 
	    DiscoveryProtocolVersion.ONE : DiscoveryProtocolVersion.TWO;
	Set[] sets = { protocolVersionPrefs, protocolVersions };
	for (int i = 0; i < sets.length; i++) {
	    Set s = sets[i];
	    if (s.contains(bias)) {
		return bias.getVersion();
	    }
	    if (!(s = intersect(s, supportedProtocols)).isEmpty()) {
		return ((DiscoveryProtocolVersion) getElement(s)).getVersion();
	    }
	}
	return bias.getVersion();
    }

    private static Set intersect(Set s1, Set s2) {
	Set intersection = Collections.EMPTY_SET;
	for (Iterator i = s1.iterator(); i.hasNext(); ) {
	    Object obj = i.next();
	    if (s2.contains(obj)) {
		if (intersection.isEmpty()) {
		    intersection = new HashSet();
		}
		intersection.add(obj);
	    }
	}
	return intersection;
    }

    private static Object getElement(Set s) {
	return s.iterator().next();
    }
}
