/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.interview;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 * InterviewSet is an interview that is also a container for an ordered 
 * set of child interviews. The default execution order for the children 
 * is the order in which they are added to this container, but the 
 * order can be modified by specifying dependencies between child interviews:
 * all dependencies for any child will be executed before the child itself.
 * Child interviews are added into an interview set by specifying this interview
 * as their parent.
 * The interview is invoked by using {@link #callInterview} in the usual way.
 * @see Interview#Interview(Interview, String)
 */
public class InterviewSet
    extends Interview
{
    /**
     * This exception will be thrown when an attempt to made to specify a dependency
     * that would create a dependency cycle. In other words, A cannot be a dependent
     * of B if B is already a dependent of A (either directly or indirectly.)
     */
    public static class CycleFault extends Fault
    {
	CycleFault(Interview dependent, Interview dependency) {
	    super(i18n, "iset.cycle", 
		  new Object[] { dependent.getTag(), dependency.getTag() } );
	}
    }

    /** 
     * Create an interview set. 
     * @param parent the parent interview for this interview
     * @param baseTag A name that will be used to qualify the tags of any
     * sub-interviews in this interview, to help ensure uniqueness of those
     * tags. 
     */
    protected InterviewSet(Interview parent, String baseTag) {
	super(parent, baseTag);

	setFirstQuestion(sorter);
    }

    // extend Interview.add(Interview)
    void add(Interview child) {
	super.add(child);
	children.add(child);
	sortedCalls = null;
    }

    // disallow Interview.add(Question)
    void add(Question q) {
	throw new UnsupportedOperationException();
    }

    /**
     * Specify a dependency for a child interview. 
     * When the interview is executed, all dependencies for each child interview
     * will be invoked before that child.
     * @param child the interview which depends on (and will be executed after)
     * the dependency
     * @param dependency the interview on which the child interview depends, 
     * and which will be executed before the child interview
     * @throws InterviewSet.CycleFault if a dependecy cycle would be created
     * @see #removeDependency
     */
    protected void addDependency(Interview child, Interview dependency) 
	throws CycleFault
    {
	if (child == null)
	    throw new NullPointerException();

	if (dependency == null)
	    throw new NullPointerException();

	Set allDeps = getAllDependencies(dependency);
	if (allDeps != null && allDeps.contains(child))
	    throw new CycleFault(child, dependency);

	Set deps = getDependencies(child, true);
	deps.add(dependency);

	sortedCalls = null;
    }
    
    /**
     * Remove any dependency between two interviews, and hence any ordering 
     * constraint between these two interviews.
     * @param child the interview which depends on the dependency
     * @param dependency the interview on which the child interview depends
     */
    protected void removeDependency(Interview child, Interview dependency) {
	if (child == null)
	    throw new NullPointerException();

	if (dependency == null)
	    throw new NullPointerException();

	Set deps = getDependencies(child, false);

	if (deps != null) 
	    deps.remove(dependency);

	if (deps.size() == 0)
	    dependencies.remove(child);

	sortedCalls = null;
    }

    private Set getDependencies(Interview child, boolean create) {
	Set deps = (Set) (dependencies.get(child));

	if (deps == null && create) {
	    deps = new TreeSet(new ChildComparator());
	    dependencies.put(child, deps);
	}

	return deps;
    }

    private Set getAllDependencies(Interview child) {
	Set s = new HashSet();
	getAllDependencies(child, s);
	return s;
    }

    private void getAllDependencies(Interview child, Set s) {
	if (s.contains(child))
	    return;

	Set deps = getDependencies(child, false);
	if (deps != null) {
	    for (Iterator iter = deps.iterator(); iter.hasNext(); ) {
		Interview i = (Interview) (iter.next());
		getAllDependencies(i, s);
		s.add(i);
	    }
	}
    }

    private Interview[] sortChildren() {
	List list = new ArrayList();
	Set cycleSet = new HashSet();

	for (Iterator iter = children.iterator(); iter.hasNext(); ) {
	    Interview child = (Interview) (iter.next());
	    if (!list.contains(child))
		addToList(list, child, cycleSet);
	}

	for (Iterator iter = list.iterator(); iter.hasNext(); ) {
	    Interview i = (Interview) (iter.next());
	    System.err.println(i.getTag() + " " + i);
	}

	return (Interview[]) (list.toArray(new Interview[list.size()]));
    }

    private void addToList(List list, Interview child, Set cycleSet) {
	// assert !cycleSet.contains(child);
	if (cycleSet.contains(child))
	    throw new IllegalArgumentException();

	cycleSet.add(child);
	
	Set deps = (Set) (dependencies.get(child));
	if (deps != null) {
	    for (Iterator iter = deps.iterator(); iter.hasNext(); ) {
		Interview dep = (Interview) (iter.next());
		addToList(list, dep, cycleSet);
	    }
	}

	list.add(child);
	
	cycleSet.remove(child);
    }

    private NullQuestion sorter = new NullQuestion(this) {
	    public boolean isEnabled() { 
		return false; // always hide this question
	    }

	    public Question getNext() {
		if (sortedCalls == null) {
		    Interview[] cc = sortChildren();

		    // have to build the list from the end, backwards,
		    // because of the way InterviewQuestion works
		    Question q = qEnd;
		    for (int i = cc.length - 1; i >= 0; i--)
			q = callInterview(cc[i], q);

		    sortedCalls = q;
		}

		return sortedCalls;
	    }
	};

    private FinalQuestion qEnd = new FinalQuestion(this);

    private List children = new ArrayList();
    private Map dependencies = new HashMap();
    private Question sortedCalls;

    private class ChildComparator implements Comparator
    {
	public int compare(Object o1, Object o2) {
	    if (!children.contains(o1) || !children.contains(o2))
		throw new IllegalArgumentException();

	    if (o1 == o2)
		return 0;

	    for (Iterator iter = children.iterator(); iter.hasNext(); ) {
		Object o = iter.next();
		if (o == o1)
		    return -1;
		if (o == o2)
		    return 1;
	    }

	    throw new IllegalStateException();
	}
    }
}
