package lu.tudor.santec.gecamed.esante.gui.tab;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

import javax.swing.JTree;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

import lu.tudor.santec.gecamed.esante.ejb.entity.beans.CdaCode;
import lu.tudor.santec.gecamed.esante.ejb.entity.beans.CdaDocument;
import lu.tudor.santec.gecamed.esante.gui.utils.CodeFetcher;

import org.apache.log4j.Logger;

/**
 * @author jens.ferring(at)tudor.lu
 * 
 * @version
 * <br>$Log: CdaTreeNode.java,v $
 * <br>Revision 1.15  2014-02-19 09:58:37  ferring
 * <br>Disintegrate document icon set
 * <br>
 * <br>Revision 1.14  2014-02-04 10:08:32  ferring
 * <br>eSante ID management completed
 * <br>Only those documents will be shown, that are retrieved by the RSQ
 * <br>
 * <br>Revision 1.13  2013-11-28 16:10:29  ferring
 * <br>cleaned
 * <br>
 * <br>Revision 1.12  2013-11-25 08:27:31  ferring
 * <br>Root expanding and notifying of JTree fixed
 * <br>
 * <br>Revision 1.11  2013-11-21 10:51:47  ferring
 * <br>expansion of objects improved
 * <br>
 * <br>Revision 1.10  2013-11-21 09:48:55  ferring
 * <br>*** empty log message ***
 * <br>
 * <br>Revision 1.9  2013-11-12 07:56:37  ferring
 * <br>TreeModel changed
 * <br>
 * <br>Revision 1.8  2013-11-11 11:45:49  ferring
 * <br>load documents from database before synchronising with eSante
 * <br>
 * <br>Revision 1.7  2013-11-08 08:45:46  ferring
 * <br>sorting node correctly and notifying model correctly about changes
 * <br>
 * <br>Revision 1.6  2013-10-28 16:54:12  ferring
 * <br>eSante and GECAMed patient compare dialog added and a lot of bug fixes
 * <br>
 * <br>Revision 1.5  2013-10-21 08:21:15  ferring
 * <br>tree expanding changed
 * <br>
 * <br>Revision 1.4  2013-10-11 12:30:02  ferring
 * <br>Document tree rendering improved
 * <br>
 * <br>Revision 1.3  2013-10-10 13:32:09  ferring
 * <br>notify document tree for changes
 * <br>
 * <br>Revision 1.2  2013-10-09 13:36:35  ferring
 * <br>eSanté icon changed and tree view initialised
 * <br>
 * <br>Revision 1.1  2013-10-08 08:54:14  ferring
 * <br>class name refactoring and tree view implementation (first steps)
 * <br>
 */

public class CdaTreeNode implements MutableTreeNode, Comparable<CdaTreeNode>
{
	/* ======================================== */
	// CONSTANTS
	/* ======================================== */
	
	// tree node level
	public static final int LEVEL_ROOT		= 0;
	public static final int LEVEL_CLASS		= 1;
	public static final int LEVEL_TYPE		= 2;
	public static final int LEVEL_DOCUMENT	= 3;
	
	
	
	/* ======================================== */
	// MEMBERS
	/* ======================================== */
	
	/** the logger Object for this class */
	private static Logger logger = Logger.getLogger(CdaTreeNode.class.getName());
	
	
	protected TreeSet<CdaTreeNode>	children	= new TreeSet<CdaTreeNode>();
	
	protected Object				userObject;
	
	protected CdaTreeRoot			root;
	
	protected CdaTreeNode			parent;
	
	protected int					level;
	
	protected CdaDocument			document;
	
	protected CdaCode				code;
	
	
	
	/* ======================================== */
	// CONSTRUCTORS
	/* ======================================== */
	
	/**
	 * This creates a root node
	 */
	protected CdaTreeNode ()
	{
		this.level			= LEVEL_ROOT;
	}
	
	
	protected CdaTreeNode (CdaDocument document, CdaTreeNode childParent)
	{
		this.parent		= childParent;
		this.root		= parent.root;
		this.level		= parent.level + 1;
		
		switch (level)
		{
			case LEVEL_CLASS:
				
				code	= CodeFetcher.getCdaCode(
						CodeFetcher.CLASS_CODE, document.getClassCode());
				
				if (code == null)
				{
					logger.info("Class code "+document.getTypeCode()+" is not available.");
					code = new CdaCode();
					code.setCodeId(document.getClassCode());
				}
				
				break;
				
			case LEVEL_TYPE:
				
				code	= CodeFetcher.getCdaCode(
						CodeFetcher.TYPE_CODE, document.getTypeCode());
				
				if (code == null)
				{
					logger.info("Type code "+document.getTypeCode()+" is not available.");
					code = new CdaCode();
					code.setCodeId(document.getTypeCode());
				}
				
				break;
				
			case LEVEL_DOCUMENT:
				
				this.document	= document;
				break;
		}
		
		parent.insert(this);
	}


	/* ======================================== */
	// CLASS BODY
	/* ======================================== */
	
	/**
	 * @return The {@link CdaDocument} if this is a leave or else <code>null</code>.
	 */
	public CdaDocument getDocument ()
	{
		return document;
	}
	
	
	public int getLevel ()
	{
		return level;
	}
	
	
	public TreePath createPath ()
	{
		CdaTreeNode		node	= this;
		CdaTreeNode[]	path	= new CdaTreeNode[level+1];
		
		
		while (node != null)
		{
			path[node.level] = node;
			node = node.getParent();
		}
		
		return new TreePath(path);
	}
	
	
	public void clear ()
	{
		if (!isLeaf())
		{
			for (CdaTreeNode child : children)
				child.clear();
			
			children.clear();
		}
		parent		= null;
	}
	
	
	public void expand (JTree tree, int level)
	{
		if (isLeaf() || this.level > level)
			// you cannot expand a leaf
			return;
		
		tree.expandPath(createPath());
		
		if (this.level < level)
		{
			for (CdaTreeNode child : children)
				child.expand(tree, level);
		}
	}
	
	
	public void collapse (JTree tree, int level)
	{
		if (isLeaf())
			return;
		
		for (CdaTreeNode child : children)
			child.collapse(tree, level);
		
		if (this.level >= level)
		{
			tree.collapsePath(createPath());
		}
	}
	
	
	public void reload ()
	{
		root.treeStructureChanged(this);
	}
	
	
	/**
	 * @return the string representation of this node, depending on the level<br>
	 * <li>
	 * <ul>Root: Empty string</ul>
	 * <ul>Class & Type: The name of the code</ul>
	 * <ul>Document: The description of the document as HTML</ul>
	 * </li>
	 */
	@Override
	public String toString ()
	{
		if (level == LEVEL_ROOT)
			return "root";
		else if (level == LEVEL_CLASS
				|| level == LEVEL_TYPE)
			return String.valueOf(code);
		else if (level == LEVEL_DOCUMENT)
			return document.toHtml(false);
		else
			throw new RuntimeException("CdaTreeNode level "+level+" is not supported!");
	}
	
	
	@Override
	public boolean equals (Object o)
	{
		if (o instanceof CdaTreeNode)
		{
			CdaTreeNode node = (CdaTreeNode) o;
			
			if (this.level == node.level)
			{
				switch (this.level)
				{
					case LEVEL_ROOT:
						return super.equals(o);
					
					case LEVEL_CLASS:
					case LEVEL_TYPE:
						try
						{
							return code.getCodeId().equals(node.code.getCodeId());
						}
						catch (Exception e)
						{
							e.printStackTrace();
						}
						
					case LEVEL_DOCUMENT:
						return document.equals(node.document);
				}
			}
			
			return false;
		}
		else if (o instanceof CdaDocument)
		{
			return document.equals(o);
		}
		else
		{
			return super.equals(o);
		}
	}
	
	
	
	/* ======================================== */
	// TREE NODE METHODS
	/* ======================================== */
	
	public CdaTreeNode getChildAt (int childIndex)
	{
		int index = 0;
		
		
		if (childIndex >= children.size() || childIndex < 0)
		{
			logger.error("Couldn't get child at " + childIndex + " of node \"" 
					+ toString() + "\" (size = " + children.size() + ")");
			return null;
		}
		
		for (Iterator<CdaTreeNode> iter = children.iterator(); iter.hasNext(); index++)
		{
			if (index == childIndex)
				return iter.next();
			iter.next();
		}
		
		return null;
	}
	
	
	public int getChildCount ()
	{
		return children.size();
	}
	
	
	public CdaTreeNode getParent ()
	{
		return parent;
	}
	
	
	public int getIndex (TreeNode node)
	{
		int index = -1;
		
		
		if (node == null)
			return index;
		
		for (CdaTreeNode childNode : children)
		{
			index++;
			
			if (childNode.equals(node))
			{
				return index;
			}
		}
		
		if (children.contains(node))
		{
			// TODO Just for debugging. Remove it, when everything works 
			throw new RuntimeException("Node couldn't be found, but is available. There must be an error in the search algorythm.");
		}
		
		// node not found
		return -1;
	}
	
	
	public boolean getAllowsChildren ()
	{
		return level < LEVEL_DOCUMENT ? true : false;
	}
	
	
	public boolean isLeaf ()
	{
		return document != null; // level < LEVEL_DOCUMENT ? false : true;
	}
	
	
	public Enumeration<CdaTreeNode> children ()
	{
		return new NodeEnumeration(children);
	}
	
	
	
	/* ======================================== */
	// MUTABLE TREE NODE METHODS
	/* ======================================== */
	
	/* (non-Javadoc)
	 * @see javax.swing.tree.MutableTreeNode#insert(javax.swing.tree.MutableTreeNode, int)
	 */
	public void insert (MutableTreeNode child, int index)
	{
		insert((CdaTreeNode)child);
	}
	
	
	public void insert (CdaTreeNode child)
	{
		if (children.add(child))
		{
			root.treeNodeInserted(createPath(), child);
			
			// expand root and class branch and type branch, if one of its documents is new 
			if (child.level < LEVEL_TYPE || (child.isLeaf() && child.getDocument().getStatus() <= CdaDocument.STATUS_NEW))
			{
				for (JTree tree : root.getTrees())
					expand(tree, child.level);
			}
			
			if (child.isLeaf())
			{
				CdaTreeNode node = this;
				while (node.getParent() != null)
				{
					root.valueForPathChanged(node.getParent().createPath(), node);
					node = node.getParent();
				}
			}
		}
	}
	
	
	/* (non-Javadoc)
	 * @see javax.swing.tree.MutableTreeNode#remove(int)
	 */
	public void remove (int index)
	{
		remove(getChildAt(index));
	}
	
	
	public void remove (MutableTreeNode node)
	{
		int index = getIndex(node);
		if (children.remove(node))
			root.treeNodeRemoved(createPath(), index, node);
	}
	
	
	public void setUserObject (Object object)
	{
		userObject = object;
	}
	
	
	public void removeFromParent ()
	{
		if (parent == null)
			return;
		
		parent.remove(this);
		
		if (parent.getChildCount() == 0)
			parent.removeFromParent();
		
		parent = null;
	}
	
	
	public void setParent (MutableTreeNode newParent)
	{
		removeFromParent();
		parent = (CdaTreeNode) newParent;
	}
	
	
	/* ======================================== */
	// COMPARABLE METHODS
	/* ======================================== */
	
	public int compareTo (CdaTreeNode o)
	{
		if (this == o)
			return 0;
		
		if (level == LEVEL_DOCUMENT
			&& o.document != null
			&& this.document != null)
		{
			return this.document.compareTo(o.document);
		}
		
		// default compare the string
		if (!this.toString().equals(o.toString()))
		{
			try
			{
				return this.toString().compareTo(o.toString());
			}
			catch (Exception e)
			{
				e.printStackTrace();
			}
		}

		return 0;
	}
	
	
	
	/* ======================================== */
	// CLASS: NODE ENUMERTAION
	/* ======================================== */
	
	private class NodeEnumeration implements Enumeration<CdaTreeNode>
	{
		private Iterator<CdaTreeNode>	iterator;
		
		
		public NodeEnumeration (Collection<CdaTreeNode> nodes)
		{
			this.iterator = new ArrayList<CdaTreeNode>(nodes).iterator();
		}
		
		
		public boolean hasMoreElements ()
		{
			return iterator.hasNext();
		}
		
		
		public CdaTreeNode nextElement ()
		{
			return iterator.next();
		}
	}
	
	
	public Collection<CdaDocument> getDocuments ()
	{
		Set<CdaDocument> docs = new HashSet<CdaDocument>();
		
		
		if (isLeaf())
		{
			docs.add(getDocument());
		}
		else
		{
			CdaTreeNode node;
			for (int index = 0; index < getChildCount(); index++)
			{
				node = getChildAt(index);
				if (node.isLeaf())
					docs.add(node.getDocument());
				else
					docs.addAll(node.getDocuments());
			}
		}
		
		return docs;
	}
	
	
	public int getDocumentCount ()
	{
		if (isLeaf())
			return 1;
		else if (level == LEVEL_TYPE)
			return children.size();
		else 
		{
			int childCount = 0;
			
			for (CdaTreeNode child : children)
			{
				if (child == this)
					throw new RuntimeException("Node "+child.toString()+" contains itself in its children!");
				childCount += child.getDocumentCount();
			}
			
			return childCount;
		}
	}
}
