/*
Tiamat - a role-playing game framework
TreeNode.java - stores object in a heirarchical (tree) structure
 
 Copyright  (C) 2004-2005 Hugues Johnson
 
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
 
This program 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 for more details.
 
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package com.huguesjohnson.tiamat;

import com.huguesjohnson.serialization.Formatter;
import com.huguesjohnson.serialization.Storable;
import java.lang.String;
import java.lang.StringBuffer;
import java.util.ArrayList;

/** TreeNode - stores object in a heirarchical (tree) structure
 * @author Hugues Johnson
 */
public class TreeNode implements Storable{

/* begin public constants */
    /**
     * Default path delimiter.
     */
    public final static String DEFAULT_PATH_DELIMITER=".";
/* end public constants */
    
/* begin private members */
    //character used to separate names in path, can only be changed in constructor
    private String pathDelimiter;
    /** 
     * Returns the character used to separate names in path, can only be changed in constructor.
     * @return The character used to separate names in path.
     */
    public String getPathDelimiter(){ return(this.pathDelimiter); }
/* end private members */

/* begin protected properties */
    //each node can have at most one parent
    private TreeNode parent;
    /** 
     * Returns the parent of this TreeNode.
     * @return The parent of this TreeNode.
     */
    public TreeNode getParent(){ return(this.parent); }
    /**
     * Sets the parent of this TreeNode.
     * @param parent The new parent of this TreeNode.
     */
    public void setParent(TreeNode parent){ this.parent=parent; parent.addChild(this); }
    //child nodes for this node
    private ArrayList<TreeNode> children;
	/**
	 * Sets the children of this TreeNode.
	 * @param children The new children of this TreeNode.
	 */
	public void setChildren(TreeNode[] children){
		int length=children.length;
		this.children=new ArrayList<TreeNode>(length);
		for(int index=0;index<length;index++){
		        this.children.set(index,children[index]);
		}
	}
    /**
     * Returns the children of this TreeNode.
     * @return All child TreeNodes.
     */
    public TreeNode[] getChildren(){ return((TreeNode[])this.children.toArray()); }
    /**
     * Sets the child TreeNode at the specified index.
     * @param index The index to set the child TreeNode at.
     * @param child The TreeNode to set.
     */
    public void setChild(int index,TreeNode child){ if((index>=0)&&(index<this.children.size())){ this.children.set(index,child); } }
    /** 
     * Returns the child TreeNode at the specified index, null if index is out of bounds.
     * @param index The index of the child TreeNode to return.
     * @return The child TreeNode at the specified index, null if index is out of bounds.
     */
    public TreeNode getChild(int index){
        if((index>=0)&&(index<this.children.size())){
            return(this.children.get(index)); 
        } 
        return(null); 
    }
    //the object this node is storing
    private Object object;
    /**
     * Returns the object this TreeNode is storing.
     * @return The object this TreeNode is storing.
     */
    public Object getObject(){ return(this.object); }
    /**
     * Sets the object this TreeNode is storing.
     * @param object The new object this TreeNode is storing.
     */
    public void setObject(Object object){ this.object=object; }
    //name of this node
    private String name;
    /**
     * Returns the name of this TreeNode.
     * @return The name of this TreeNode.
     */
    public String getName(){ return(this.name); }
    /**
     * Sets the name of this TreeNode.
     * @param name The new name of this TreeNode.
     */
    public void setName(String name){ this.name=name; }
/* end protected properties */
    
/* begin constructors */
    /**
     * Creates a new instance of TreeNode, uses the default path delimiter.
     * @param name The name of this TreeNode.
     */
    public TreeNode(String name){
        this.setName(name);
        this.pathDelimiter=TreeNode.DEFAULT_PATH_DELIMITER;
        this.parent=null;
        this.children=new ArrayList<TreeNode>();
        this.object=null;
    }
    /**
     * Creates a new instance of TreeNode.
     * @param name The name of this TreeNode.
     * @param pathDelimiter The path delimiter to use.
     */
    public TreeNode(String name,String pathDelimiter){
        this.setName(name);
        this.pathDelimiter=pathDelimiter;
        this.parent=null;
        this.children=new ArrayList<TreeNode>();    
        this.object=null;
    }
    /**
     * Creates a new instance of TreeNode, uses the default path delimiter.
     * @param name The name of this TreeNode.
     * @param object The object this TreeNode is storing.
     */
    public TreeNode(String name,Object object){
        this.setObject(object);    
        this.setName(name);
        this.pathDelimiter=TreeNode.DEFAULT_PATH_DELIMITER;
        this.parent=null;
        this.children=new ArrayList<TreeNode>();
    }
    /**
     * Creates a new instance of TreeNode.
     * @param name The name of this TreeNode.
     * @param pathDelimiter The path delimiter to use.
     * @param object The object this TreeNode is storing.
     */
    public TreeNode(String name,String pathDelimiter,Object object){
        this.setObject(object);
        this.setName(name);
        this.pathDelimiter=pathDelimiter;
        this.parent=null;
        this.children=new ArrayList<TreeNode>(); 
    }
    /**
     * Creates a new instance of TreeNode, uses the default path delimiter.
     * @param name The name of this TreeNode.
     * @param object The object this TreeNode is storing.
     * @param parent The parent of this TreeNode.
     */
    public TreeNode(String name,Object object,TreeNode parent){
        this.setObject(object);
        this.setName(name);
        this.pathDelimiter=TreeNode.DEFAULT_PATH_DELIMITER;
        this.setParent(parent);
        this.children=new ArrayList<TreeNode>(); 
    }
    /**
     * Creates a new instance of TreeNode.
     * @param name The name of this TreeNode.
     * @param pathDelimiter The path delimiter to use.
     * @param object The object this TreeNode is storing.
     * @param parent The parent of this TreeNode.
     */
    public TreeNode(String name,String pathDelimiter,Object object,TreeNode parent){
        this.setObject(object);
        this.setName(name);
        this.pathDelimiter=pathDelimiter;
        this.setParent(parent);
        this.children=new ArrayList<TreeNode>(); 
    }
/* end constructors */

/* begin protected methods */
    /** 
     * Should be called by another node when it sets this node as the parent.
     * @param childNode The child node to add.
     */
    protected void addChild(TreeNode childNode){
        if(this.children==null){
            this.children=new ArrayList<TreeNode>();
        }
        this.children.add(childNode);
    }
/* end protected methods */

/* begin public methods */
    /** 
     * Returns the full path to this node. If this is not a root node, dynamically generate the path by recursively calling parent.getPath().
     * @return The full path to this node.
     */
    public String getPath(){
        //if this is not a root node, dynamically generate the path by recursively calling parent.getPath()
        if(this.parent!=null){
            return(this.parent.getPath()+this.pathDelimiter+this.name);
        } 
        //root node, just return the name
        return(this.name);
    }
    /**
     * Returns true if parent is null.
     * @return True if parent is null.
     */
    public boolean isRoot(){
        return(this.parent==null);
    } 
    /**
     * Returns true if no children.
     * @return True if no children.
     */
    public boolean isLeaf(){
        if(this.children==null){
            return(true);
        } 
        return(this.children.size()<1);
    } 
    /**
     * Removes the child TreeNode with the specified path.
     * @param path The path of the child to remove.
     * @param recursive Set to true to recursively search through children, false to only check the direct children of this TreeNode.
     * @return The success of the operation.
     */
    public boolean removeChildNode(String path,boolean recursive){
        String[] pathNames=path.split(this.pathDelimiter);
        int pathNameCount=pathNames.length;
        if(pathNameCount<=0){
            pathNames=new String[1];
            pathNames[0]=path;
            pathNameCount=1;
        }
        if(pathNameCount==1){
            int index=0;
            int childCount=this.children.size();
            boolean removedNode=false;
            while((index<childCount)&&(removedNode==false)){
                TreeNode childNode=this.children.get(index);
                if(childNode.getName().equals(pathNames[0])){
                    this.children.remove(index);
                    removedNode=true;
                } else{
                    index++;
                }
            }
            return(removedNode);
        } else if(recursive==false){
            return(false);
        } else{
            int index=0;
            int childCount=this.children.size();
            boolean removedNode=false;
            String newPathNameToFind=new String(pathNames[1]);
            for(int index1=2;index1<pathNameCount;index1++){
                newPathNameToFind+=this.pathDelimiter+pathNames[index1];
            }
            while((index<childCount)&&(removedNode==false)){
                TreeNode childNode=this.children.get(index);
                if((pathNameCount==2)||(childNode.getName().equals(pathNames[1]))){
                    removedNode=childNode.removeChildNode(newPathNameToFind,true);
                }
                index++;
            }
            return(removedNode);
        }
    }
    /**
     * Returns the child TreeNode with the specified path, null if it is not found.
     * @param path The path of the child to remove.
     * @param recursive Set to true to recursively search through children, false to only check the direct children of this TreeNode.
     * @return The child TreeNode with the specified path, null if it is not found.
     */
    public TreeNode getChildNode(String path,boolean recursive){
        String[] pathNames=path.split(this.pathDelimiter);
        int pathNameCount=pathNames.length;
        if(pathNameCount<=0){
            pathNames=new String[1];
            pathNames[0]=path;
            pathNameCount=1;
        }
        if(pathNameCount==1){
            int index=0;
            int childCount=this.children.size();
            boolean foundNode=false;
            while((index<childCount)&&(foundNode==false)){
                TreeNode childNode=this.children.get(index);
                if(childNode.getName().equals(pathNames[0])){
                    return(childNode);
                } 
                index++;
            }
            return(null);
        } else if(recursive==false){
            return(null);
        } else{
            int index=0;
            int childCount=this.children.size();
            TreeNode foundNode=null;
            String newPathNameToFind=new String(pathNames[1]);
            for(int index1=2;index1<pathNameCount;index1++){
                newPathNameToFind+=this.pathDelimiter+pathNames[index1];
            }
            while((index<childCount)&&(foundNode==null)){
                TreeNode childNode=this.children.get(index);
                if((pathNameCount==2)||(childNode.getName().equals(pathNames[1]))){
                    foundNode=(childNode.getChildNode(newPathNameToFind,true));
                    if(foundNode!=null){
                        return(foundNode);
                    }
                    index++;
                }
            }
            return(foundNode);
        }
    }
    /**
     * Returns the number of children for this node.
     * @param recursive Set to true to recursively search through children, false to only check the direct children of this TreeNode.
     * @return The number of children for this node.
     */
    public int getChildNodeCount(boolean recursive){
        if(this.children==null){
            return(0);
        }
		int count,length;
		count=length=this.children.size();
		if(recursive){
		    for(int index=0;index<length;index++){
		        TreeNode childNode=this.children.get(index);
		        count+=childNode.getChildNodeCount(true);
		    }
		}
		return(count);
    }
    /**
     * Returns the number of leaves for this node.
     * @param recursive Set to true to recursively search through children, false to only check the direct children of this TreeNode.
     * @return The number of leaves for this node.
     */
    public int getChildLeafCount(boolean recursive){
        if(this.children==null){
            return(0);
        }
		int count=0;
		int length=this.children.size();
		for(int index=0;index<length;index++){
		    TreeNode childNode=this.children.get(index);
		    if(childNode.isLeaf()){
		        count++;
		    } else if(recursive){
		        count+=childNode.getChildLeafCount(true);
		    }
		}
		return(count);
    }
    /**
     * Returns the all leaves for this node.
     * @param recursive Set to true to recursively search through children, false to only check the direct children of this TreeNode.
     * @return All leaves for this node.
     */
    public TreeNode[] getChildLeaves(boolean recursive){
        if(this.children==null){
            return(null);
        }
		int length=this.children.size();
		ArrayList<TreeNode> childLeaves=new ArrayList<TreeNode>(length);
		for(int index=0;index<length;index++){
		    TreeNode childNode=this.children.get(index);
		    if(childNode.isLeaf()){
		        childLeaves.add(childNode);
		    } else if(recursive){
		        TreeNode[] rChildLeaves=childNode.getChildLeaves(true);
		        if(rChildLeaves!=null){
		            int length1=rChildLeaves.length;
		            for(int index1=0;index1<length1;index1++){
		                childLeaves.add(rChildLeaves[index1]);
		            }
		        }
		    }
		}
		return(childLeaves.toArray(new TreeNode[0]));    
    }
    /** 
     * Inserts a node at the specified path.
     * @param path The path to insert the node at.
     * @param node The TreeNode to insert.
     */
    public void insertChildNode(String path,TreeNode node){
        String[] pathNames=path.split(this.pathDelimiter);
        int pathNameLength=pathNames.length;
        if(pathNameLength<1){
            node.setParent(this);
        } else{
            int foundChildIndex=-1;
            int index=0;
            int childCount=this.children.size();
            while((index<childCount)&&(foundChildIndex<0)){
                TreeNode currentChild=this.children.get(index);
                if(currentChild.getName().equals(pathNames[0])){
                    foundChildIndex=index;
                } else{
                    index++;
                }
            }
            TreeNode foundChild;
            if(foundChildIndex<0){
                foundChild=new TreeNode(pathNames[0],this.pathDelimiter);
                foundChild.setParent(this);
            } else{
                foundChild=this.children.get(foundChildIndex);
            }
            String newPath=new String(pathNames[0]);
            for(index=1;index<pathNameLength;index++){
                newPath+=this.pathDelimiter+pathNames[index];
            }
            foundChild.insertChildNode(newPath,node);
        }
    }
    /** 
     * Returns a string representation of the object.
     * @return A string representation of the object. 
     */
    public String toString(){
        StringBuffer tostring=new StringBuffer(this.getClass().getName());
        tostring.append("\n  name="+this.name);
        tostring.append("\n  path="+this.getPath());
        tostring.append("\n  pathDelimiter="+this.pathDelimiter);
        if(this.object!=null){
            tostring.append("\n  object="+this.object.toString());
        } else{
            tostring.append("\n  object=null");
        }        
        return(tostring.toString());
    }
    /** The onDeserialize event should be fired after an object is de-serialized.
     * This allows objects to load transient members, set their working directory, and load Storable members if needed.
     * @param formatter the formatter to use to load any storable members
     * @param absoluteDir the working directory since all paths stored in the object should be relative
     */
    public void onDeserialize(Formatter formatter,String absoluteDir){ }
/* end public methods */
}