/*FreeMind - A Program for creating and viewing Mindmaps
 *Copyright (C) 2000-2001  Joerg Mueller <joergmueller@bigfoot.com>
 *See COPYING for Details
 *
 *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.
 */
/*$Id: MindMapLayout.java,v 1.15.14.5 2005/06/26 21:41:58 christianfoltin Exp $*/

package freemind.view.mindmapview;

import freemind.main.FreeMindMain;
import freemind.modes.MindMapCloud;

import java.awt.IllegalComponentStateException;
import java.awt.LayoutManager;
import java.awt.Container;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.NoSuchElementException;

import javax.swing.JLabel;

/**
 * This class will Layout the Nodes and Edges of an MapView.
 */
public class MindMapLayout implements LayoutManager {

    private final static int BORDER = 30;//width of the border around the map.
	// minimal width for input field of leaf or folded node (PN)
	// the MINIMAL_LEAF_WIDTH is reserved by calculation of the map width
	public final static int MINIMAL_LEAF_WIDTH = 150;
	public final static int MINIMAL_WIDTH = 50;


    private MapView map;
    private int ySize;
    
    // Call xSize and ySize in the same manner
    private int xSize;
    
    // save root node coordinates
	private int rootX = 0;
	private int rootY = 0;

    public MindMapLayout(MapView map) {
        this.map = map;
    // properties "mapxsize" and "mapysize" are ignored
    }

    public void addLayoutComponent(String name, Component comp){ }

    public void removeLayoutComponent(Component comp) {  }

    public void layoutContainer(Container parent) {
       layout(true); }
   




    //
    // Absolute positioning
    //

     /**
     * This funcion resizes the map and do the layout.
     * All tree heights, widths and shifts should be already calculated.
     */
	public void layout(boolean holdSelected) {
		NodeView selected = map.getSelected();
		 holdSelected =  holdSelected &&  
		    (selected != null && selected.getX() != 0 && selected.getY() != 0);
		int oldRootX = getRoot().getX();
		int oldRootY = holdSelected ? selected.getY() : getRoot().getY();
		resizeMap(getRoot().getTreeWidth(), getRoot().getTreeHeight());
        layout(map.getRoot());
		try{
			int rootX = getRoot().getX();
			int rootY = holdSelected ? selected.getY() : getRoot().getY();
			getMapView().scrollBy(rootX - oldRootX, rootY - oldRootY, true );
		}
		catch(IllegalComponentStateException e){
		}

	}

    /**
     * This places the node's subtree if the relative
     * position of every node to its parent is known.
     */
    private void layout(NodeView node) {
        // The overall task of layout is: give me the sizes and I'll set the positions.

        // Relative Y positions of nodes are already calculated.  Here we
        // only calculate relative x positions of nodes.  Whenever we talk
        // about relative coordinates / positions, we always mean relative to
        // the coordinates of node's parent.

        int x = 0;
        int hgap = node.getHGap();
        if ( node.isRoot() ) {
           //System.err.println("layoutroot"+node.getModel()); 
           x = 0; }
        else if ( node.isLeft() ) {
           x = - hgap - node.getPreferredSize().width; }
        else {
           x = node.getParentView().getPreferredSize().width + hgap; }
        
        placeNode(node, x, node.relYPos);

        //Iterations
        for ( ListIterator e = node.getChildrenViews().listIterator(); e.hasNext(); ) {
           layout( (NodeView)e.next() ); }
    }

    /**
     * Set the position and the size of the node.
     * Set the position of the edge of the node (the edge of the node is the edge
     * connecting the node to its parent).
     *
     * Preconditions: Absolute position of the parent is already set correctly.
     *                Relative positions and TreeHeights are up to dare.
     */
    private void placeNode(NodeView node, int relativeX, int relativeY) {
        // The placeNode is currently called only by layout().

        // I don't undertand why I reset the size of the node according to its preferred size.
        // This should not be the task of this method.

        // relativeX, relativeY - already calculated coordinates of node relative to its parent.;
        if (node.isRoot()) {
            node.setBounds(getRootX(),
                           getRootY()); 
            }
        else {
            //place the node-label
            int x = node.getParentView().getX() + relativeX;
            int y = node.getParentView().getY() + relativeY;
            node.setBounds(x, y);
            
            // It seems that there is a piece of coding ready for having labelled edges.
            // Having labelled edges is a nice thing, as sure as hell, but we do not
            // have the support yet.

            //place the edge-label
            JLabel label = node.getEdge().getLabel();
            /* fc, 26.06.2005 */
            Point end = node.getInPoint();
            Point start = node.getParentView().getOutPoint(end, node.isLeft());
            /* end fc, 26.06.2005 */
        

            node.getEdge().start = start;
            node.getEdge().end = end;

            int relX = (start.x - end.x) / 2; 
            int absX = start.x - relX;
            
            int relY = (start.y - end.y) / 2;
            int absY = start.y - relY;
                    
            label.setBounds(absX - label.getPreferredSize().width  / 2,
                            absY - label.getPreferredSize().height / 2,
                            label.getPreferredSize().width, label.getPreferredSize().height);
        }
    }

    /**
	 * @return
	 */
	private int getRootY() {
		return calcYBorderSize() / 2 + getRoot().getUpperChildShift();
	}

	/**
	 * @return
	 */
	private int getRootX() {
		return calcXBorderSize() / 2 + getRoot().getLeftTreeWidth();
	}

	/**
     *
     */
        private void resizeMap(int width, int height) {
        // (public to reuse from MapView.scrollNodeToVisible  ...)

        // In principle, resize can be caused by:
        // 1) Unfold
        // 2) Insertion of a node
        // 3) Modification of a node in an enlarging way
            boolean bResized = false;
        	int oldXSize = getXSize();
        	int oldYSize = getYSize();
        
			int minXSize = width + calcXBorderSize();
            int minYSize = height  +  calcYBorderSize();

         	if (minXSize != getXSize()) {
        		setXSize(minXSize);
        		bResized = true;
        	}

        	if (minYSize != getYSize()) {
        		setYSize(minYSize);
        		bResized = true;
        	}

        	if (bResized){
        		getMapView().setSize(getXSize(), getYSize());
        	}
    }

    private int calcYBorderSize() {
        int yBorderSize;
        	{
        	Dimension visibleSize = map.getViewportSize();
        	if (visibleSize != null){
        		yBorderSize = 2 *  Math.max(visibleSize.height, BORDER);
        	}
        	else{
        		yBorderSize = 2 *  BORDER;
        	}
        }
        return yBorderSize;
    }

    private int calcXBorderSize() {
        int xBorderSize;
        {
        	Dimension visibleSize = map.getViewportSize();
        	if (visibleSize != null){
        		xBorderSize = Math.max(visibleSize.width, 2 *(BORDER + MINIMAL_LEAF_WIDTH));
        	}
        	else{
        		xBorderSize = 2 *(BORDER + MINIMAL_LEAF_WIDTH);
        		
        	}
        }
        return xBorderSize;
    }


    void updateTreeHeightsAndRelativeYOfDescendantsAndAncestors(NodeView node) {
       updateTreeHeightsAndRelativeYOfDescendants(node);  
       if (! node.isRoot())
       updateTreeHeightsAndRelativeYOfAncestors(node.getParentView()); 
    }

    /**
     * This is called by treeNodesChanged(), treeNodesRemoved() & treeNodesInserted(), so it's the
     * standard mechanism to update the graphical node structure. It updates the parent of the 
     * significant node, and follows recursivly the hierary upwards to root.
     */

    void updateTreeHeightsAndRelativeYOfAncestors(NodeView node) {
		updateTreeGeometry(node);
       if ( !node.isRoot()){
          updateTreeHeightsAndRelativeYOfAncestors(node.getParentView()); }
    }

    //
    // Relative positioning
    //

    // Definiton: relative vertical is either relative Y coord or Treeheight.

    void updateTreeHeightsAndRelativeYOfWholeMap() {
        updateTreeHeightsAndRelativeYOfDescendants(getRoot()); 
		layout(false);
        }

   
    void updateTreeHeightsAndRelativeYOfDescendants(NodeView node) {
	        for (ListIterator e = node.getChildrenViews().listIterator(); e.hasNext();) {
	           updateTreeHeightsAndRelativeYOfDescendants((NodeView)e.next()); }
        updateTreeGeometry(node);
   	}
    
	private void updateRelativeYOfChildren(NodeView node, LinkedList childrenViews) {
		ListIterator e = childrenViews.listIterator();
		if (! e.hasNext())
			return ;
		int vgap = node.getVGap();
		NodeView child = (NodeView)childrenViews.getFirst();
		int pointer  = 0;  
		int upperShift = 0;
		while (e.hasNext()) {
			child = (NodeView)e.next();
			int shiftUp = getShiftUp(child);
			int shiftDown = getShiftDown(child);
			int iAdditionalCloudHeigth = child.getAdditionalCloudHeigth();
			int upperChildShift = child.getUpperChildShift();
			child.relYPos = pointer + upperChildShift
			+ iAdditionalCloudHeigth / 2  + shiftDown; 
			upperShift += upperChildShift + shiftUp;
			pointer +=  child.getTreeHeight()+ shiftUp + shiftDown + iAdditionalCloudHeigth + vgap;
		}
		upperShift += calcStandardTreeShift(node, childrenViews);
		while (e.hasPrevious()) {
			child = (NodeView)e.previous();
			child.relYPos -= upperShift; 
		}
	}

	  protected int getShiftUp(NodeView node) {
	  	int shift = node.getShift();
	  	if (shift < 0)
	  		return -shift;
	  	else
	  		return 0;
	}

	  protected int getShiftDown(NodeView node) {
	  	int shift = node.getShift();
	  	if (shift > 0)
	  		return shift;
	  	else
	  		return 0;
	}
    protected void updateTreeGeometry(NodeView node) {
    	if (node.isRoot()){
    		LinkedList leftNodeViews = getRoot().getLeft();
			LinkedList rightNodeViews = getRoot().getRight();

			int leftWidth = calcTreeWidth(node, leftNodeViews);
			int rightWidth = calcTreeWidth(node, rightNodeViews);
			getRoot().setRootTreeWidths(leftWidth, rightWidth);
			
			updateRelativeYOfChildren(node, leftNodeViews);
			updateRelativeYOfChildren(node, rightNodeViews);
			
			int leftTreeShift = calcUpperChildShift(node, leftNodeViews);
			int rightTreeShift = calcUpperChildShift(node, rightNodeViews);
			
			getRoot().setRootUpperChildShift(leftTreeShift, rightTreeShift);

			int leftTreeHeight = calcTreeHeight(node, leftTreeShift, leftNodeViews);
			int rightTreeHeight = calcTreeHeight(node, rightTreeShift, rightNodeViews);
			
			getRoot().setRootTreeHeights(leftTreeHeight, rightTreeHeight);
    	}
    	else{
			LinkedList childrenViews = node.getChildrenViews();

			int treeWidth = calcTreeWidth(node, childrenViews);
    		node.setTreeWidth(treeWidth); 

    		updateRelativeYOfChildren(node, childrenViews);

			int treeShift = calcUpperChildShift(node, childrenViews);
			node.setUpperChildShift(treeShift);

			int treeHeight = calcTreeHeight(node, treeShift, childrenViews);        	
    		node.setTreeHeight(treeHeight);
    		
// System.out.println(node.getText()
//    				+ ": treeShift=" + treeShift 
//    				+ ": treeHeight=" + treeHeight 
//					);
    	}

    }

	/**
	 * @param node
	 * @param rightNodeViews
	 * @return
	 */
	private int calcUpperChildShift(NodeView node, LinkedList childrenViews) {
		try{
			NodeView firstChild = (NodeView) childrenViews.getFirst();
			int childShift = -firstChild.relYPos 
				+ firstChild.getAdditionalCloudHeigth()/2
				+ firstChild.getUpperChildShift(); 
			return childShift > 0 ? childShift : 0;			
		}
		catch(NoSuchElementException e)
		{
			return 0;
		}
	}

	private int calcStandardTreeShift( NodeView parent, LinkedList childrenViews ) {
    	int parentHeight = parent.getPreferredSize().height;
       if ( childrenViews == null || childrenViews.size() == 0 ) {
          return 0; }
       int height = 0;
       int vgap = parent.getVGap();
       for (ListIterator e = childrenViews.listIterator(); e.hasNext(); ) {
           NodeView node = (NodeView)e.next();
           if (node != null) {
              height += node.getPreferredSize().height + vgap + node.getAdditionalCloudHeigth(); 
           }
       }
       return Math.max(height - vgap - parentHeight, 0) / 2; 
    }

	/**
	 * @param leftNodeViews
	 * @return
	 */
	private int calcTreeHeight(NodeView parent, int treeShift, LinkedList childrenViews) {
    	int parentHeight = parent.getPreferredSize().height;
		try{
			NodeView firstChild = (NodeView) childrenViews.getFirst();
			NodeView lastChild = (NodeView) childrenViews.getLast();
			int minY = Math.min(
					firstChild.relYPos 
					- firstChild.getUpperChildShift()
					- firstChild.getAdditionalCloudHeigth()/2, 
					0);
			int maxY = Math.max(
					lastChild.relYPos 
					- lastChild.getUpperChildShift()
					+ lastChild.getTreeHeight() + lastChild.getAdditionalCloudHeigth()/2 
					, parentHeight);
			return maxY - minY;
		}
		catch(NoSuchElementException e)
		{
	           return parentHeight; 
		}        	        
  	}

	private int calcTreeWidth(NodeView parent, LinkedList childrenViews) {
		int treeWidth = 0;
		for (ListIterator e = childrenViews.listIterator(); e.hasNext(); ) {
			NodeView childNode = (NodeView)e.next();
			if (childNode != null) {
				int childWidth = childNode.getTreeWidth() + childNode.getHGap();
				if(childWidth > treeWidth){
					treeWidth = childWidth;
				}
			}
		}
		return parent.getPreferredSize().width 
		+ 2 * parent.getAdditionalCloudHeigth() 
		+ treeWidth;
	}
	
    //
    // Get Methods
    //

    private RootNodeView getRoot() {
       return (RootNodeView)map.getRoot(); }

    private MapView getMapView() {
       return map; }

    private FreeMindMain getFrame() {
       return map.getController().getFrame(); }


    // This is actually never used.
    public Dimension minimumLayoutSize(Container parent) {
        return new Dimension(200,200); } //For testing Purposes

    public Dimension preferredLayoutSize(Container parent) {
        return new Dimension(getXSize(), getYSize()); }


    private int getXSize() {
        return xSize;
    }

    /**
     * @return
     */
    private int getYSize() {
        return ySize;
    }

    /**
     * @param i
     */
    private void setXSize(int i) {
        xSize = i;
    }

    /**
     * @param i
     */
    private void setYSize(int i) {
        ySize = i;
    }


}//class MindMapLayout
