/*
 * $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.Arrays;
import java.util.Map;
import java.util.Vector;

/**
 * A {@link Question question} to which the response is a set of selected
 * nodes within a tree. The nodes are identified by their paths.
 */
public abstract class TreeQuestion extends Question
{
    /**
     * An interface that provides the model for the tree whose nodes
     * are selected by a TreeQuestion.
     */
    public static interface Model {
	/**
	 * Get the root node of the tree.
	 * @return the root node of the tree.
	 */
	Object getRoot();

	/**
	 * Get the number of children of a specified node.
	 * @param node the node for which to get the number of children
	 * @return the number of children of the specified node
	 */
	int getChildCount(Object node);
	
	/**
	 * Get a child of a specified node, or null if no such child exists.
	 * @param node the node for which to get the child
	 * @param index the index of the child; this should be a 
	 * number greater than or equal to zero and less than the number
	 * of children of the node.
	 * @return the specified child, or null if index does not identify a 
	 * valid child.
	 * @see #getChildCount
	 */
	Object getChild(Object node, int index);
	
	/**
	 * Get the name of a specified node. The name will be used to
	 * construct paths. At a minimum, it should not contain '/'.
	 * @param node the node whose name is required
	 * @return the name of the specified node
	 */
	String getName(Object node);
	
	/** 
	 * Get the full path from the root node of a specified node.
	 * @param node the node whose path is required
	 * @return the path of the specified node from the root node,
	 * as composed from the names of this node and its ancestors,
	 * using '/' to separate the individual names.
	 */
	String getPath(Object node);
	
	/**
	 * Determine if this a node is a leaf node. 
	 * This is primarily an attrribute of the node's actual type,
	 * and is not necessarily the same as having zero children.
	 * (A leaf node implies zero children, but zero children does
	 * npot imply a node is a leaf.)
	 * @param node the node to check for being a leaf
	 * @return true if the specified node is a leaf node, and
	 * false otherwise.
	 */
	boolean isLeaf(Object node);
    };

    /**
     * Create a tree question with a nominated tag and tree model.
     * @param interview The interview containinng this question.
     * @param tag A unique tag to identify this specific question.
     * @param model The tree model to which the question's path values relate.
     */
    protected TreeQuestion(Interview interview, String tag, Model model) {
	super(interview, tag);
	this.model = model;
	clear();
	setDefaultValue(value);
    }

    /**
     * Get the tree model to which the question's path values relate.
     * @return the tree model to which the question's path values relate
     */
    public Model getModel() {
	return model;
    }

    /**
     * Get the default response for this question.
     * @return the default response for this question.
     *
     * @see #setDefaultValue
     */
    public String[] getDefaultValue() {
	return defaultValue;
    }

    /** 
     * Set the default response for this question, 
     * used by the clear method. 
     * @param v the default response for this question.
     *
     * @see #getDefaultValue
     */
    public void setDefaultValue(String[] v) {
	defaultValue = v;
    }

    /**
     * Get the current (default or latest) response to this question.
     * The strings should each represent valid paths to nodes within the 
     * tree represented by the tree model.
     * @return the current value for this question
     * @see #setValue
     */
    public String[] getValue() {
	return value;
    }

    /**
     * Verify this question is on the current path, and if it is,
     * return the current value.
     * @return the current value of this question
     * @throws Interview.NotOnPathFault if this question is not on the 
     * current path
     * @see #getValue
     */
    public String[] getValueOnPath() 
	throws Interview.NotOnPathFault
    {
	interview.verifyPathContains(this);
	return getValue();
    }

    public void setValue(String newValue) {
	setValue(split(newValue));
    }

    /**
     * Set the current response to this question.
     * @param newValue a set of strings (or null if none), representing
     * paths to nodes within the tree represented by the tree model
     * @see #getValue
     */
    public void setValue(String[] newValue) {
	String[] oldValue;
	if (newValue == null) {
	    oldValue = value;
	    value = null;
	}
	else {
	    // could arguably validate paths here and throw Fault if invalid
	    oldValue = value;
	    /* leave this for clients to do, if they want
	    // sort and remove duplicates from the array
	    TreeSet ts = new TreeSet(Arrays.asList(newValue));
	    value = (String[]) (ts.toArray(new String[ts.size()]));
	    */
	    value = newValue;
	}
	
	if (!Arrays.equals(value, oldValue)) {
	    interview.updatePath(this);
	    interview.setEdited(true);
	}
    }

    /**
     * Set the current response to this question. The response is
     * set to the paths of a set of specified nodes within the tree 
     * represented by the tree model.
     * @param nodes a set of nodes (or null if none) within 
     * the tree represented by the tree model, whose paths will
     * be set as the current response to the question
     * @see #getValue
     */
    public void setValue(Object[] nodes) {
	if (nodes == null)
	    setValue((String[]) null);

	String[] paths = new String[nodes.length];
	for (int i = 0; i < nodes.length; i++)
	    paths[i] = model.getPath(nodes[i]);
    }

    public String getStringValue() {
	return join(value);
    }

    public boolean isValueValid() {
	return true;
    }

    public boolean isValueAlwaysValid() {
	return false;
    }

    /*
    public Object[] getNodesValue() {
	if (value == null)
	    return null;

	Vector v = new Vector();
	for (int i = 0; i < value.length; i++) {
	    Object node = getNode(value[i]);
	    if (node != null)
		v.add(node);
	}

	Object[] nodes = new Object[v.size()];
	v.copyInto(nodes);
	return nodes;
    }
    */

    /*
    public Object getNode(String path) {
	return model.getNode(getRoot(), path);
    }
    */

    /*
    private Object getNode(Object node, String path) {  
	if (node == null)
	    throw new NullPointerException();
		
	if (path.length() == 0)
	    return node;
	
	String head;
	String tail;
	
	int sep = path.indexOf("/");
	if (sep == -1) {
	    head = path;
	    tail = null;
	}
	else {
	    head = path.substring(0, sep);
	    tail = path.substring(sep + 1);
	}
	
	for (int i = 0; i < model.getChildCount(node); i++) {
	    Object c = model.getChild(node, i);
	    if (model.getName(c).equals(head))
		return (tail == null ? c : getNode(c, tail));
	}
	
	return null;
    }
    */

    /*
    public boolean isSelected(Object node) {
	if (value == null)
	    return false;

	for (int i = 0; i < value.length; i++) {
	    if (node == value[i])
		return true;
	}

	return false;
    }
    */

    /**
     * Clear any response to this question, resetting the value
     * back to its initial state.
     */
    public void clear() {
	setValue(defaultValue);
    }

    /**
     * Load the value for this question from a dictionary, using
     * the tag as the key.
     * @param data The map from which to load the value for this question.
     */
    protected void load(Map data) {
	String paths = (String) (data.get(tag));
	setValue(paths);
    }

    /**
     * Save the value for this question in a dictionary, using
     * the tag as the key.
     * @param data The map in which to save the value for this question.
     */
    protected void save(Map data) {
	data.put(tag, join(value));
    }

    private static String[] split(String s) {
	if (s == null)
	    return empty;

	Vector v = new Vector();
	int start = -1;
	for (int i = 0; i < s.length(); i++) {
	    if (white(s.charAt(i))) {
	        if (start != -1)
		    v.addElement(s.substring(start, i));
	        start = -1;
	    } else
	        if (start == -1)
		    start = i;
	}

	if (start != -1)
	    v.addElement(s.substring(start));

	if (v.size() == 0)
	    return empty;

	String[] a = new String[v.size()];
	v.copyInto(a);
	return a;
    }

    private static String join(String[] paths) {
	if (paths == null || paths.length == 0)
	    return "";

	int l = paths.length - 1; // allow for spaces between words
	for (int i = 0; i < paths.length; i++)
	    l += paths[i].length();

	StringBuffer sb = new StringBuffer(l);
	sb.append(paths[0]);
	for (int i = 1; i < paths.length; i++) {
	    sb.append(' ');
	    sb.append(paths[i]);
	}

	return sb.toString();
    }

    private static boolean white(char c) {
        return (c == ' '  ||  c == '\t'  ||  c == '\n');
    }

    private static String[] empty = { };

    private Model model;

    /**
     * The current response for this question.
     */
    protected String[] value;

    /**
     * The default response for this question.
     */
    private String[] defaultValue;
}
