package prolog.treeview;

import java.io.*;
import java.util.*;
import java.awt.*;
import javax.swing.*;

/**
 * the node class implements a node storing a
 * object as value. this class also contains a
 * static renderer map mapping a Class to a
 * renderer. So the node knows how to render
 * the content.
 */
public class Node
{
	/**
	 * static node renderer map, key is a node
	 * object's class and value is the INodeRenderer
	 */
	protected static Map gvNodeRendererMap = new Hashtable();

	/**
	 * is the default renderer used when no
	 * renderer was found in the renderer map
	 */
	protected static INodeRenderer defaultRenderer = new DefaultNodeRenderer();

	/**
	 * the nodes layout in the tree space
	 */
	protected Rectangle rect = new Rectangle();

	/**
	 * list of all childs of this node
	 */
	protected java.util.List childs = new Vector();

	/**
	 * the parent node of this node
	 */
	protected Node parent = null;

	/**
	 * the user content stored in this node
	 */
	protected Object content = null;

	/**
	 * constructor
	 */
	public Node()
	{
	}

	/**
	 * constructor - the userString will become the node's user object
	 * @param s
	 */
	public Node( String userString )
	{
		this();
		content = userString;
	}

	/**
	 * constructor
	 * @param parent
	 */
	public Node( Node parent )
	{
		this();
		this.parent = parent;
	}

	/**
	 * returns the parent of this node
	 */
	public Node getParent()
	{
		return this.parent;
	}

	/**
	 * sets the parent of this node
	 */
	public void setParent( Node parent )
	{
		this.parent = parent;
	}

	/**
	 * return the content of this map
	 */
	public Object getContent()
	{
		return this.content;
	}

	/**
	 * sets the content of this map
	 */
	public void setContent( Object content )
	{
		this.content = content;
		this.fireTreeChanged();
	}

	/**
	 * adds a child node to this node
	 */
	public void addChild( Node node )
	{
		if( hasChild( node ) )
			throw new RuntimeException( "can not add a child twice" );
		node.setParent( this );
		this.childs.add( node );
		boolean flashed = this.fireTreeChanged();
	}

	/**
	 * removes the specified child node from this node
	 */
	public synchronized void removeChild( Node node )
	{
		if( ! this.hasChild( node ) )
			throw new RuntimeException(
				"INTERNAL GUI ERROR: Node.removeChild failed! " +
				"Can not remove a non existing child!" );

		this.childs.remove( node );
		this.fireTreeChanged();
	}

	/**
	 * returns the number of childs direct of this node
	 */
	public int getChildCount()
	{
		return this.childs.size();
	}

	/**
	 * returns true if the given node is a direct child of this node
	 */
	public boolean hasChild( Node node )
	{
		return childs.contains( node );
	}

	/**
	 * return the node at the specified index
	 */
	public Node getChild( int index )
	{
		return (Node) this.childs.get( index );
	}

	/**
	 * returns a iterator over all childs
	 */
	public Iterator getChilds()
	{
		return this.childs.iterator();
	}

	/**
	 * returns the root of this node
	 */
	public RootNode getRoot()
	{
		Node next = this;
		while( next.getParent() != null )
			next = next.getParent();

		RootNode back = null;
		if( next instanceof RootNode )
			back = (RootNode) next;

		return back;
	}

	/**
	 * returns the tree this node is contained in
	 */
	public Tree getTree()
	{
		RootNode r = this.getRoot();
		if( r != null )
			return r.getTree();
		else
			return null;
	}

	/**
	 * returns the rect of this node in tree space
	 */
	public synchronized Rectangle getRect()
	{
		return this.rect;
	}

	/**
	 * paints this node and all recursive childs
	 */
	public synchronized void paint( Graphics2D g )
	{
		INodeRenderer renderer = null;
		Object toRender = null;
		if( content == null )
			toRender = this;
		else
		{
			toRender = content;
			renderer = getRenderer( content.getClass() );
		}

		if( renderer == null )
			renderer = this.defaultRenderer;

		renderer.renderNode(
			g,
			rect,
			toRender );

		Iterator i=getChilds();
		while( i.hasNext() )
			((Node)i.next()).paint( g );
	}

	/**
	 * paints all lines between all recursive childs
	 */
	public synchronized void paintLine( Graphics2D g )
	{
		g.setColor( Color.black );

		Point center = this.getCenterBottom();

		Iterator i=getChilds();
		while( i.hasNext() )
		{
			Node child = ((Node)i.next());

			Point cCenter = child.getCenterTop();
			g.drawLine(
				center.x,
				center.y,
				cCenter.x,
				cCenter.y );

			child.paintLine( g );
		}
	}

	/**
	 * calculates the dimension this node requires in tree space
	 */
	public Dimension getDimension()
	{
		INodeRenderer renderer = null;
		if( content != null )
			renderer = getRenderer( content.getClass() );
		if( renderer == null )
			renderer = this.defaultRenderer;

		Dimension d = renderer.calculateDimension( getContent() );
		if( d.width < 50 )
			d.width = 50;
		if( d.height < 20 )
			d.height = 20;

		return d;
	}

	/**
	 * recaltulates and caches the space
	 * required by the renderer to render
	 * this node.
	 */
	public void recursiveDimensionUpdate()
	{
		Dimension d = getDimension();
		this.rect.width = d.width;
		this.rect.height = d.height;

		Iterator i = getChilds();
		while( i.hasNext() )
			((Node) i.next() ).recursiveDimensionUpdate();
	}

	/**
	 * returns the point in the center of this node in tree space
	 */
	public synchronized Point getCenter()
	{
		return
			new Point(
				rect.x + rect.width / 2,
				rect.y + rect.height / 2 );
	}

	/**
	 * returns the point in the middle+top of this node in tree space
	 */
	public synchronized Point getCenterTop()
	{
		return
			new Point(
				rect.x + rect.width / 2,
				rect.y );
	}

	/**
	 * returns the point in the middle+bottom of this node in tree space
	 */
	public synchronized Point getCenterBottom()
	{
		return
			new Point(
				rect.x + rect.width / 2,
				rect.y + rect.height );
	}

	/**
	 * returns a string representation of this object
	 */
	public synchronized String toString()
	{
		if( content != null )
			return content.toString();
		else
			return "x=" + rect.x + " y=" + rect.y;
	}

	/**
	 * relayouts and repaints the tree
	 */
	protected boolean fireTreeChanged()
	{
		Tree t = getTree();
		if( t != null )
		{
			t.doLayout();
			t.repaint();
		}
		return t != null;
	}

	/**
	 * generates a xml representation of this object
	 */
	public void genXml( Writer out )
		throws IOException
	{
		out.write( "<node value=\"" );
		out.write( toString() );
		out.write( "\" x=\"" );
		out.write( Integer.toString( this.rect.x ) );
		out.write( "\" y=\"" );
		out.write( Integer.toString( this.rect.y ) );
		out.write( "\" width=\"" );
		out.write( Integer.toString( this.rect.width ) );
		out.write( "\" height=\"" );
		out.write( Integer.toString( this.rect.height ) );
		out.write( "\">" );

		Iterator i = getChilds();
		while( i.hasNext() )
			((Node)i.next()).genXml( out );

		out.write( "</node>" );
	}

	/**
	 * registers a INodeRenderer for a specified Node content
	 * @param key
	 * @param renderer
	 */
	public static void registerRenderer( Class key, INodeRenderer renderer )
	{
		gvNodeRendererMap.put( key, renderer );
	}

	/**
	 * returns a renderer (if one was found) from the renderer map
	 * @param key
	 * @return
	 */
	public static INodeRenderer getRenderer( Class key )
	{
		return (INodeRenderer) gvNodeRendererMap.get( key );
	}

	/**
	 * returns the default rederer used to render
	 * unknown content.
	 */
	public static INodeRenderer getDefaultRenderer()
	{
		return defaultRenderer;
	}

	/**
	 * recursive method calculating the height
	 * of this node and all childs required for
	 * rendering.
	 */
	public int getMaximalHeight()
	{
		int height = this.rect.height;

		if( this.childs.size() > 0 )
		{
			int maxChildHeight = 0;
			Iterator i=this.getChilds();
			while( i.hasNext() )
			{
				Node child = (Node) i.next();
				int h = child.getMaximalHeight();
				if( h > maxChildHeight )
					maxChildHeight = h;
			}
			height += maxChildHeight + 20;
		}

		return height;
	}

	/**
	 * recursive method calculating the with
	 * this node requires for rendering
	 */
	public int getMaximalWidth()
	{
		int width = this.rect.width;
		int childWidth = 0;
		Iterator i=this.getChilds();
		while( i.hasNext() )
		{
			Node child = (Node) i.next();
			childWidth += child.getMaximalWidth();
			if( i.hasNext() )
				childWidth += 5;
		}
		if( childWidth > width )
			width = childWidth;
		return width;
	}
}
