/*
 * org.openmicroscopy.shoola.agents.measurement.util.roitable.ROITreeTable 
 *
  *------------------------------------------------------------------------------
 *  Copyright (C) 2006-2007 University of Dundee. All rights reserved.
 *
 *
 * 	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.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 *------------------------------------------------------------------------------
 */
package org.openmicroscopy.shoola.agents.measurement.util.roitable;



//Java imports
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;
import java.util.Map.Entry;

import javax.swing.table.TableColumn;
import javax.swing.tree.TreePath;

//Third-party libraries
import org.jdesktop.swingx.JXTreeTable;
import org.jdesktop.swingx.treetable.MutableTreeTableNode;

//Application-internal dependencies
import org.openmicroscopy.shoola.agents.measurement.util.model.AnnotationField;
import org.openmicroscopy.shoola.agents.measurement.util.roitable.ROITreeNode;
import org.openmicroscopy.shoola.agents.measurement.util.ui.ShapeRenderer;
import org.openmicroscopy.shoola.util.roi.model.ROI;
import org.openmicroscopy.shoola.util.roi.model.ROIShape;
import org.openmicroscopy.shoola.util.roi.model.util.Coord3D;
import org.openmicroscopy.shoola.util.ui.graphutils.ShapeType;
import org.openmicroscopy.shoola.util.ui.treetable.OMETreeTable;

/**
* The ROITable is the class extending the JXTreeTable, this shows the 
* ROI as the parent object and the ROIShapes belonging to the ROI as the
* child objects. 
*
* @author  Jean-Marie Burel &nbsp;&nbsp;&nbsp;&nbsp;
* 	<a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a>
* @author	Donald MacDonald &nbsp;&nbsp;&nbsp;&nbsp;
* 	<a href="mailto:donald@lifesci.dundee.ac.uk">donald@lifesci.dundee.ac.uk</a>
* @version 3.0
* <small>
* (<b>Internal version:</b> $Revision: $Date: $)
* </small>
* @since OME3.0
*/
public class ROITreeTable 
	extends OMETreeTable
{

	/** The root node of the tree. */
	private ROITreeNode			root;
	
	/** Column names of the table. */
	private Vector<String>	columnNames;
	
	/** Collection of column names. */
	private List<AnnotationField>			fields;
	
	/** The map to relate ROI to ROITreeNodes. */
	private Map<ROI, ROITreeNode> ROIMap;
	
	/** The tree model. */
	private ROITreeTableModel	model;
	
	/**
	 * The constructor for the ROITable, taking the root node and
	 * column names as parameters.  
	 * @param model the table model.
	 * @param columnNames the column names.
	 */
	public ROITreeTable(ROITreeTableModel model, Vector columnNames, 
			List<AnnotationField> fields)
	{
		super(model);
		this.model = model;
		this.root = (ROITreeNode) model.getRoot();
		this.fields = fields;
		this.columnNames = columnNames;
		this.setAutoResizeMode(JXTreeTable.AUTO_RESIZE_ALL_COLUMNS);
		ROIMap = new HashMap<ROI, ROITreeNode>();
		for (int i = 0 ; i < model.getColumnCount() ; i++)
			getColumn(i).setResizable(true);
		setDefaultRenderer(ShapeType.class, new ShapeRenderer());
		setTreeCellRenderer(new ROITreeTableCellRenderer());
	}
	
	/* TABLE META METHODS: MANIPULATE FIELDS. */
	
	/**
	 * Set the fields of the Table. Each field represents a column in the table
	 * and will represent an annotation in the ROI, ROIShape. 
	 * @param newFields see above.
	 */
	public void setFields(Vector<String> columnNames, 
			List<AnnotationField> newFields)
	{
		fields = newFields;
	}
	
	
	/* ADD METHODS. */
	
	/**
	 * Add the ROI to the table, placing the ROIShapes in the ROI
	 * to it. This method will create the ROI if it does not exist 
	 * already. This method will also collapse all the ROI in the treetable
	 * and expand the ROI of the ROIShape, moving the viewport to 
	 * the ROIShape. 
	 * @param roi see above.
	 */
	public void addROI(ROI roi)
	{
		TreeMap<Coord3D, ROIShape> shapeMap = roi.getShapes();
		List<ROIShape> shapeList = new ArrayList<ROIShape>();
		
		Iterator<ROIShape> shapeIterator = shapeMap.values().iterator();
		while (shapeIterator.hasNext())
			shapeList.add(shapeIterator.next());
		if (shapeList.size() == 0)
			return;
		addROIShapeList(shapeList);
	}
	
	/**
	 * Add the ROIShape to the table, placing it under the ROI the ROIShape
	 * belongs to. This method will create the ROI if it does not exist 
	 * already. This method will also collapse all the ROI in the treetable
	 * and expand the ROI of the ROIShape, moving the viewport to 
	 * the ROIShape. 
	 * @param shape see above.
	 */
	public void addROIShape(ROIShape shape)
	{
		List<ROIShape> shapeList = new ArrayList<ROIShape>();
		shapeList.add(shape);
		addROIShapeList(shapeList);
	}
	

	/**
	 * Add the ROIShapes in the shapeList to the table, placing it under the 
	 * ROI the ROIShape belongs to. This method will create the ROI if it does 
	 * not exist already. This method will also collapse all the ROI in the 
	 * treetable  and expand the ROI of the ROIShape, moving the viewport to 
	 * the ROIShape. 
	 * @param shapeList see above.
	 */
	public void addROIShapeList(List<ROIShape> shapeList)
	{
		ROITreeNode parent=null;
		for (ROIShape shape : shapeList)
		{
			parent = findParent(shape.getROI());
			if (parent == null)
			{
				parent = new ROITreeNode(shape.getROI());
				parent.setExpanded(true);
				ROIMap.put(shape.getROI(), parent);
				int childCount = root.getChildCount();
				root.insert(parent,childCount);
			}
			ROITreeNode roiShapeNode = parent.findChild(shape.getCoord3D());
			ROITreeNode newNode = new ROITreeNode(shape);
			newNode.setExpanded(true);
			if (roiShapeNode != null)
			{
				int index = parent.getIndex(roiShapeNode);
				parent.remove(shape.getCoord3D());
				parent.insert(newNode, index);
			}
			else
				parent.insert(newNode, 
						parent.getInsertionPoint(shape.getCoord3D()));
		}
		setTreeTableModel(new ROITreeTableModel(root, columnNames, fields));
		if (parent != null)
			expandROIRow(parent);
	}
	
	/* REMOVE METHODS. */
	
	/**
	 * Clear the table. 
	 *
	 */
	public void clear()
	{
		int childCount = root.getChildCount();
		for (int i = 0 ; i < childCount ; i++ )
			root.remove(0);
		this.setTreeTableModel(new ROITreeTableModel(root, 
				columnNames, fields));
		ROIMap = new HashMap<ROI, ROITreeNode>();
		this.invalidate();
		this.repaint();
	}
	
	/**
	 * Remove the ROIShape from the table, and delete the ROI if the 
	 * ROIShape was the last ROIShape in the ROI.
	 * @param shape see above.
	 */
	public void removeROIShape(ROIShape shape)
	{
		ROITreeNode parent = findParent(shape.getROI());
		if (parent == null)
			return;
		ROITreeNode child = parent.findChild(shape);
		parent.remove(child);
		if(parent.getChildCount()==0)
			root.remove(parent);
		this.setTreeTableModel(
				new ROITreeTableModel(root, columnNames, fields));
	}

	/**
	 * Remove the ROI from the table.
	 * @param roi see above.
	 */
	public void removeROI(ROI roi)
	{
		ROITreeNode ROITreeNode = findParent(roi);
		root.remove(ROITreeNode);
		this.setTreeTableModel(
				new ROITreeTableModel(root, columnNames, fields));
	}
	
	/* FIND METHODS. */
	
	/**
	 * Find the parent ROITreeNode of the ROI.
	 * @param roi see above.
	 * @return see above.
	 */
	public ROITreeNode findParent(ROI roi)
	{
		if (ROIMap.containsKey(roi))
			return ROIMap.get(roi);
		return null;
	}
	
	/* GET METHODS. */
	
	/**
	 * Get the ROI Shape at row index.
	 * @param index see above.
	 * @return see above.
	 */
	public ROIShape getROIShapeAtRow(int index)
	{
		TreePath path = this.getPathForRow(index);
		if (path == null) return null;
		ROITreeNode node = (ROITreeNode) path.getLastPathComponent();
		if (node.getUserObject() instanceof ROIShape)
			return (ROIShape) node.getUserObject();
		return null;
	}
	
	/**
	 * Get the ROI at row index.
	 * @param index see above.
	 * @return see above.
	 */
	public ROI getROIAtRow(int index)
	{
		TreePath path = this.getPathForRow(index);
		ROITreeNode node = (ROITreeNode) path.getLastPathComponent();
		if (node.getUserObject() instanceof ROI)
			return (ROI) node.getUserObject();
		return null;
	}
	
	/** 
	 * Returns <code>true</code> if the column is hosting a shape.
	 * @return see above 
	 */
	public boolean isShapeTypeColumn(int column)
	{
		TableColumn col = this.getColumn(column);
		return model.isShapeColumn(col.getModelIndex());
	}

	
	/* SET METHODS. */
	
	/**
	 * Set the value of the object at row and col to value object, 
	 * this will also expand the row of the object set, unless the ROI
	 * of the object is not visible it will then collapse the row.
	 * @param obj see above.
	 * @param row see above.
	 * @param column see above.
	 */
	public void setValueAt(Object obj, int row, int column)
	{
		ROITreeNode node = (ROITreeNode)getNodeAtRow(row);
		super.setValueAt(obj, row, column);
		ROITreeNode expandNode;
		if (node.getUserObject() instanceof ROI)
		{
			ROI roi = (ROI) node.getUserObject();
			expandNode = node;
			if (roi.isVisible())
				expandROIRow(expandNode);
			else
				collapseROIRow(expandNode);
		}
		else
		{
			expandNode = (ROITreeNode) node.getParent();
			ROIShape roiShape = (ROIShape) node.getUserObject();
			if (roiShape.getROI().isVisible())
				expandROIRow(expandNode);
			else
				collapseROIRow(expandNode);
		}
	}
	
	/* TREE VIEW METHODS: EXPANSION, COLLAPSE. */
	
	/** 
	 * Expand the row of the node 
	 * @param node see above.
	 */
	public void expandNode(ROITreeNode node)
	{
		if (node.getUserObject() instanceof ROI)
			expandROIRow((ROI)node.getUserObject());
	}

	/** 
	 * Expand the row with node parent.
	 * @param parent see above.
	 */
	public void expandROIRow(ROITreeNode parent)
	{
		int addedNodeIndex = root.getIndex(parent);
		parent.setExpanded(true);
		this.expandRow(addedNodeIndex);
		ROITreeNode node;
		for (int i = 0; i < root.getChildCount(); i++)
		{
				node = (ROITreeNode) root.getChildAt(i);
				if (node.isExpanded()) 
					expandPath(node.getPath());
		}
	}
	
	/** 
	 * Collapse the row with node parent.
	 * @param parent see above.
	 */
	public void collapseROIRow(ROITreeNode parent)
	{
		int addedNodeIndex = root.getIndex(parent);
		this.collapseRow(addedNodeIndex);
		parent.setExpanded(false);
		ROITreeNode node;
		for (int i=0; i<root.getChildCount(); i++)
		{
				node = (ROITreeNode) root.getChildAt(i);
				if (node.isExpanded()) 
					expandROIRow((ROITreeNode) node);
		}
	}
	
	/** 
	 * Expand the row with ROI.
	 * @param roi see above.
	 */
	public void expandROIRow(ROI roi)
	{
		ROITreeNode selectedNode = findParent(roi);
		this.expandROIRow(selectedNode);
	//	this.scrollCellToVisible(selectedNodeIndex, 0);
	}
	
	
	/**
	 * Scroll to the selected ROIShape. 
	 * @param shape see above.
	 */
	public void scrollToROIShape(ROIShape shape)
	{
		ROITreeNode parent = findParent(shape.getROI());
		if (parent == null)
			return;
		expandROIRow(parent);
		ROITreeNode child = parent.findChild(shape);
		this.scrollPathToVisible(child.getPath());
	}
	
	/* MOUSE HANDLING. */
	
	/**
	 * Extending the mouse pressed event to show menu. 
	 * @param e mouse event.
	 */
	protected void onMousePressed(MouseEvent e)
	{

	}
	
	/* VIEW UPDATE METHODS. */
	
	/**
	 * Refresh the data in the table. 
	 *
	 */
	public void refresh()
	{
		this.setTreeTableModel(new ROITreeTableModel(root, columnNames, fields));
	}
	
	/**
	 * The ROIShape has changed it's properties. 
	 * @param shape the roiShape which has to be updated.
	 */
	public void setROIAttributesChanged(ROIShape shape)
	{
		ROITreeNode parent = findParent(shape.getROI());
		ROITreeNode child = parent.findChild(shape);
		model.nodeUpdated(child);
	}
	
	/* SELECTION METHODS. */
		
	/** 
	 * Select the ROIShape in the TreeTable and move the view port of the 
	 * table to the shape selected. 
	 * @param shape
	 */
	public void selectROIShape(ROIShape shape)
	{
		ROITreeNode parent = findParent(shape.getROI());
		if (parent == null)
			return;
		expandROIRow(parent);
		ROITreeNode child = parent.findChild(shape);
		
		int row = this.getRowForPath(child.getPath());
		this.selectionModel.addSelectionInterval(row, row);
	}
	
	/** 
	 * Select the ROI in the TreeTable and move the view port of the 
	 * table to the ROI selected. 
	 * @param roi
	 */
	public void selectROI(ROI roi)
	{
		ROITreeNode parent = findParent(roi);
		if (parent == null)
			return;
		expandROIRow(parent);
		List<MutableTreeTableNode> childList = parent.getChildList();
		for (MutableTreeTableNode child : childList)
			selectROIShape((ROIShape) ((ROITreeNode) child).getUserObject());
	}	
	
	/**
	 * Create a list of all the roi and roishapes selected in the table.
	 * This will only list an roi even if the roi and roishapes are selected.
	 * @return see above.
	 */
	public List getSelectedObjects()
	{
		int [] selectedRows = this.getSelectedRows();
		TreeMap<Long, Object> roiMap = new TreeMap<Long, Object>(); 
		List selectedList = new ArrayList();
		for(int i = 0 ; i < selectedRows.length ; i++)
		{	
			Object nodeObject = this.getNodeAtRow(
					selectedRows[i]).getUserObject();
			if(nodeObject instanceof ROI)
			{
				ROI roi = (ROI)nodeObject;
				roiMap.put(roi.getID(), roi);
				selectedList.add(roi);
			}
		}
		for (int i = 0 ; i < selectedRows.length ; i++)
		{	
			Object nodeObject = this.getNodeAtRow(
					selectedRows[i]).getUserObject();
			if (nodeObject instanceof ROIShape)
			{
				ROIShape roiShape = (ROIShape) nodeObject;
				if (!roiMap.containsKey(roiShape.getID()))
					selectedList.add(roiShape);
			}
		}
		return selectedList;
	}

	/**
	 * Get the roishapes of the selected objects, this method will split ROI
	 * into their respective ROIshapes. 
	 * @return see above.
	 */
	public List<ROIShape> getSelectedROIShapes()
	{
		int [] selectedRows = this.getSelectedRows();
		TreeMap<Long, Object> roiMap = new TreeMap<Long, Object>(); 
		List<ROIShape> selectedList = new ArrayList<ROIShape>();
		for(int i = 0 ; i < selectedRows.length ; i++)
		{	
			Object nodeObject = this.getNodeAtRow(
					selectedRows[i]).getUserObject();
			if (nodeObject instanceof ROI)
			{
				ROI roi = (ROI) nodeObject;
				roiMap.put(roi.getID(), roi);
				Iterator<ROIShape> shapeIterator = 
					roi.getShapes().values().iterator();
				while(shapeIterator.hasNext())
					selectedList.add(shapeIterator.next());
			}
		}
		for(int i = 0 ; i < selectedRows.length ; i++)
		{	
			Object nodeObject = this.getNodeAtRow(
					selectedRows[i]).getUserObject();
			if (nodeObject instanceof ROIShape)
			{
				ROIShape roiShape = (ROIShape)nodeObject;
				if(!roiMap.containsKey(roiShape.getID()))
					selectedList.add(roiShape);
			}
		}

		return selectedList;
	}
	
	/* OBJECT ORGANISATION, QUERY METHODS. */
	
	/**
	 * Build the plane map from the selected object list. This builds a map
	 * of all the planes that have objects reside on them.
	 * @param objectList see above.
	 * @return see above.
	 */
	public TreeMap<Coord3D, ROIShape> buildPlaneMap(ArrayList objectList)
	{
		TreeMap<Coord3D, ROIShape> planeMap = new TreeMap<Coord3D, ROIShape>
		(new Coord3D());
		for (Object node : objectList)
		{
			if (node instanceof ROI)
			{
				ROI roi = (ROI) node;
				TreeMap<Coord3D, ROIShape> shapeMap =  roi.getShapes();
				Iterator i = shapeMap.entrySet().iterator();
				Entry entry;
				Coord3D coord;
				while (i.hasNext())
				{
					entry = (Entry) i.next();
					coord = (Coord3D) entry.getKey();
					if (planeMap.containsKey(coord))
						return null;
					planeMap.put(coord, (ROIShape) entry.getValue());
				}
			} else if (node instanceof ROIShape)
			{
				ROIShape shape = (ROIShape)node;
				if(planeMap.containsKey(shape.getCoord3D()))
					return null;
				else
					planeMap.put(shape.getCoord3D(), shape);
			}
		}
		return planeMap;
	}

	/**
	 * Get the id of objects in the selected list. 
	 * @param selectedObjects
	 * @return see above.
	 */
	public List<Long> getIDList(List selectedObjects)
	{
		TreeMap<Long,ROI> idMap = new TreeMap<Long, ROI>();
		List<Long> idList = new ArrayList<Long>();
		for (Object node : selectedObjects)
		{
			ROI roi;
			if (node instanceof ROI)
				roi = (ROI)node;
			else
				roi = ((ROIShape)node).getROI();
			if (!idMap.containsKey(roi.getID()))
			{
				idMap.put(roi.getID(), roi);
				idList.add(roi.getID());
			}
		}
		return idList;
	}
	
	/**
	 * Are all the roishapes in the shapelist on separate planes. 
	 * @param shapeList see above.
	 * @return see above.
	 */
	public boolean onSeparatePlanes(ArrayList<ROIShape> shapeList)
	{
		TreeMap<Coord3D, ROIShape> 
		shapeMap = new TreeMap<Coord3D, ROIShape>(new Coord3D());
		for (ROIShape shape : shapeList)
		{
			if (shapeMap.containsKey(shape.getCoord3D()))
				return false;
			else
				shapeMap.put(shape.getCoord3D(), shape);
		}
		return true;
	}
	
	/**
	 * Return true if all the roishapes in the shapelist have the same id. 
	 * @param shapeList see above.
	 * @return see above.
	 */
	public boolean haveSameID(ArrayList<ROIShape> shapeList)
	{
		TreeMap<Long, ROIShape> shapeMap = new TreeMap<Long, ROIShape>();
		for (ROIShape shape : shapeList)
		{
			if (!shapeMap.containsKey(shape.getID()))
			{
				if (shapeMap.size() == 0)
					shapeMap.put(shape.getID(), shape);
				else
					return false;
			}
		}
		return true;
	}
	
	/**
	 * Get the id that the roishapes in the shapelist contain, if they
	 * do not contain the same id return -1;
	 * @param shapeList see above.
	 * @return see above.
	 */
	public long getSameID(ArrayList<ROIShape> shapeList)
	{
		TreeMap<Long, ROIShape> shapeMap = new TreeMap<Long, ROIShape>();
		if (shapeList.size() == 0)
			return -1;
		for (ROIShape shape : shapeList)
		{
			if (!shapeMap.containsKey(shape.getID()))
			{
				if (shapeMap.size() == 0)
					shapeMap.put(shape.getID(), shape);
				else
					return -1;
			}
		}
		return shapeList.get(0).getID();
	}
	
	/**
	 * Check the list of to make sure the ROIShapes in the list are from the
	 * same ROI and if they are then return true;
	 * @param shapeList see above.
	 * @return see above.
	 */
	public boolean shapesInSameROI(ArrayList shapeList)
	{
		long id = -1;
		if (shapeList.size() == 0)
			return false;
		boolean first = true;
		for (Object node : shapeList)
		{
			if (node instanceof ROI)
				return false;
			else if (node instanceof ROIShape)
			{
				ROIShape shape = (ROIShape)node;
				if (first)
				{
					id = shape.getID();
					first = false;
				}
				if (id != shape.getID())
					return false;
			}
		}
		return true;
	}

}

