/*
 * Created on Apr 28, 2005
 */
package org.flexdock.docking.state;

import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Window;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.state.tree.SplitNode;
import org.flexdock.util.DockingUtility;
import org.flexdock.util.SwingUtility;

/**
 * @author Christopher Butler
 */
public class DockingPath implements Cloneable, DockingConstants, Serializable {
    private static Log log = LogFactory.getLog(DockingPath.class);
    
    public static final String RESTORE_PATH_KEY = "DockingPath.RESTORE_PATH_KEY";
	
    private transient String stringForm;
	private String rootPortId;
	private ArrayList nodes; // contains SplitNode objects
	private String siblingId;
	private boolean tabbed;

	public DockingPath() {
	    nodes = new ArrayList();
    }
    
	public static DockingPath create(String dockableId) {
		Dockable dockable = findDockable(dockableId);
		return create(dockable);
	}
	
	public static DockingPath create(Dockable dockable) {
		if(dockable==null || !isDocked(dockable))
			return null;
		
		DockingPath path = new DockingPath(dockable);
		Component comp = dockable.getComponent();

		Container parent = comp.getParent();
		while(!isDockingRoot(parent)) {
			if(parent instanceof DockingPort) {
				SplitNode node = createNode((DockingPort)parent);
				path.addNode(node);
			}
			parent = parent.getParent();
		}
		if(isDockingRoot(parent))
			path.setRootPortId(((DockingPort)parent).getPersistentId());
		
		path.initialize();
		return path;
	}
	
	public static SplitNode createNode(Dockable dockable) {
		if(dockable==null)
			return null;

		Container parent = dockable.getComponent().getParent();
		return parent instanceof DockingPort? createNode((DockingPort)parent): null;
	}
	
	public static SplitNode createNode(DockingPort port) {
		if(port==null)
			return null;
		
		Component c = ((Component)port).getParent();
		JSplitPane split = c instanceof JSplitPane? (JSplitPane)c: null;
		if(split==null)
			return null;
		
		return createNode(port, split);
	}
	
	private static SplitNode createNode(DockingPort port, JSplitPane split) {
		int orientation = split.getOrientation();
		boolean topLeft = split.getLeftComponent()==port? true: false;
		
		int region = 0;
		String siblingId = null;
		if(topLeft) {
			region = orientation==JSplitPane.VERTICAL_SPLIT? TOP: LEFT;
			siblingId = getSiblingId(split.getRightComponent());
		}
		else {
			region = orientation==JSplitPane.VERTICAL_SPLIT? BOTTOM: RIGHT;
			siblingId = getSiblingId(split.getLeftComponent());
		}
		
		int size = orientation==JSplitPane.VERTICAL_SPLIT? split.getHeight(): split.getWidth();
		int divLoc = split.getDividerLocation();
        
        int testSize = 0;
        if (orientation == JSplitPane.VERTICAL_SPLIT) {
            testSize += split.getTopComponent().getHeight() + split.getBottomComponent().getHeight() + split.getDividerSize();
        } else {
            testSize += split.getLeftComponent().getWidth() + split.getRightComponent().getWidth() + split.getDividerSize();
        }
		float percentage = (float)divLoc / (float)size;
		
		return new SplitNode(orientation, region, percentage, siblingId);
	}
	
	private static String getSiblingId(Component c) {
		if(c instanceof DockingPort)
			c = ((DockingPort)c).getDockedComponent();
		
		Dockable dockable = findDockable(c);
		return dockable==null? null: dockable.getPersistentId();
	}
	


	private static boolean isDockingRoot(Container c) {
		return c instanceof DockingPort && ((DockingPort)c).isRoot();
	}

	public static DockingPath getRestorePath(Dockable dockable) {
		Object obj = dockable==null? null: dockable.getClientProperty(RESTORE_PATH_KEY);
		return obj instanceof DockingPath? (DockingPath)obj: null;
	}
	
	public static DockingPath updateRestorePath_(Dockable dockable, DockingPath restorePath) {
		if(dockable==null || restorePath==null)
			return null;
		dockable.putClientProperty(RESTORE_PATH_KEY, restorePath);
		return restorePath;
	}
	
	private DockingPath(Dockable dockable) {
		siblingId = findSiblingId(dockable);
		tabbed = dockable.getComponent().getParent() instanceof JTabbedPane;
		nodes = new ArrayList();
	}
    
    public boolean isTabbed() {
        return this.tabbed;
    }

    public void setTabbed(boolean isTabbed) {
        this.tabbed = isTabbed;
    }
    
    public String getSiblingId() {
        return this.siblingId;
    }
    
    public void setSiblingId(String siblingId) {
        this.siblingId = siblingId;
    }
	
	private DockingPath(String parent, boolean tabs, ArrayList nodeList) {
		siblingId = parent;
		tabbed = tabs;
		nodes = nodeList;
	}

	public List getNodes() {
		return nodes;
	}

	public DockingPort getRootPort() {
		return DockingManager.getDockingPort(rootPortId);
	}

    public String getRootPortId() {
        return this.rootPortId;
    }
	
    public void setRootPortId(String portId) {
		rootPortId = portId;
	}
	
	private void addNode(SplitNode node) {
		nodes.add(node);
	}
	
	private void initialize() {
		Collections.reverse(nodes);
	}
	
	private String findSiblingId(Dockable dockable) {
		Component comp = dockable.getComponent();
		JSplitPane split = comp.getParent() instanceof JSplitPane? (JSplitPane)comp.getParent(): null;
		if(split==null)
			return null;
		
		Component sibling = split.getLeftComponent();
		if(comp==sibling)
			sibling = split.getRightComponent();
		
		Dockable d = findDockable(sibling);
		return d==null? null: d.getPersistentId();
	}
	
	public String toString() {
		if(stringForm==null) {
			StringBuffer sb = new StringBuffer("/RootPort[id=").append(rootPortId).append("]");
			for(Iterator it=nodes.iterator(); it.hasNext();) {
				SplitNode node = (SplitNode)it.next();
				sb.append("/").append(node.toString());
			}
			sb.append("/Dockable");
			stringForm = sb.toString();
		}
		return stringForm;
	}
	
	public boolean restore(String dockable) {
		return restore(DockingManager.getDockable(dockable));
	}
	
	private DockingPort getRootDockingPort() {
		DockingPort port = DockingManager.getDockingPort(rootPortId);
		if(port!=null)
			return port;
		
		Window activeWindow = SwingUtility.getActiveWindow();
		return DockingManager.getRootDockingPort(activeWindow);
	}
	
	public boolean restore(Dockable dockable) {
		if(dockable==null || isDocked(dockable))
			return false;
		
		DockingPort rootPort = getRootDockingPort();
		String region = CENTER_REGION;
		if(nodes.size()==0) {
			return dockFullPath(dockable, rootPort, region);
		}
		
		DockingPort port = rootPort;
		for(Iterator it=nodes.iterator(); it.hasNext();) {
			SplitNode node = (SplitNode)it.next();
			Component comp = port.getDockedComponent();
			region = getRegion(node, comp);
			
			JSplitPane splitPane = comp instanceof JSplitPane? (JSplitPane)comp: null;
			// path was broken.  we have no SplitPane, or the SplitPane doesn't 
			// match the orientation of the current node, meaning the path was 
			// altered at this point.
			if(splitPane==null || splitPane.getOrientation()!=node.getOrientation()) {
				return dockBrokenPath(dockable, port, region, node);
			}
			
			// assume there is a transient sub-dockingPort in the split pane
			comp = node.getRegion()==LEFT || node.getRegion()==TOP? splitPane.getLeftComponent(): splitPane.getRightComponent();
			port = (DockingPort)comp;
			
			// move on to the next node
		}
		
		return dockFullPath(dockable, port, region);
	}
	
	
	
	private boolean dockBrokenPath(Dockable dockable, DockingPort port, String region, SplitNode ctrlNode) {
		Component current = port.getDockedComponent();
		if(current instanceof JSplitPane) {
			return dockExtendedPath(dockable, port, region, ctrlNode);
		}
		
		if(current instanceof JTabbedPane) {
			return dock(dockable, port, CENTER_REGION, null);
		}
		
		Dockable embedded = findDockable(current);
		if(embedded==null || tabbed) {
			return dock(dockable, port, CENTER_REGION, null);
		}
		
		String embedId = embedded.getPersistentId();
		SplitNode lastNode = getLastNode();
		if(embedId.equals(lastNode.getSiblingId())) {
			region = getRegion(lastNode, current);
			ctrlNode = lastNode;
		}
		
		return dock(dockable, port, region, ctrlNode);
	}
	
	private boolean dockFullPath(Dockable dockable, DockingPort port, String region) {
		// the docking layout was altered since the last time our dockable we embedded within
		// it, and we were able to fill out the full docking path.  this means there is already
		// something within the target dockingPort where we expect to dock our dockable.  
		
		// first, check to see if we need to use a tabbed layout
		Component current = port.getDockedComponent();
		if(current instanceof JTabbedPane) {
			return dock(dockable, port, CENTER_REGION, null);
		}

		// check to see if we dock outside the current port or outside of it
		Dockable docked = findDockable(current);
		if(docked!=null) {
			Component comp = dockable.getComponent();
			if(port.isDockingAllowed(comp, CENTER_REGION)) {
				return dock(dockable, port, CENTER_REGION, null);
			}
			DockingPort superPort = (DockingPort)SwingUtilities.getAncestorOfClass(DockingPort.class, (Component)port);
			if(superPort!=null)
				port = superPort;
			return dock(dockable, port, region, getLastNode());
		}
		
		// if we were't able to dock above, then the path changes means our current path
		// does not extend all the way down into to docking layout.  try to determine 
		// an extended path and dock into it
		return dockExtendedPath(dockable, port, region, getLastNode());
	}
	
	private boolean dockExtendedPath(Dockable dockable, DockingPort port, String region, SplitNode ctrlNode) {
		Component docked = port.getDockedComponent();
		
        //I don't think this code will matter any more, given the null check, but leaving for now.
        //null is returned when a dockingport is empty, so we need to dock to an empty port
        
		// if 'docked' is not a split pane, then I don't know what it is.  let's print a
		// stacktrace and see who sends in an error report.
		if(docked != null && !(docked instanceof JSplitPane)) {
			Throwable t = new Throwable("Docked: " + docked);
            log.warn(t.getMessage(), t);
			return false;
		}
				
        //begin code that matters.
        
		SplitNode lastNode = getLastNode();
		String lastSibling = lastNode==null? null: lastNode.getSiblingId();
		
		Set dockables = port.getDockables();
		for(Iterator it=dockables.iterator(); lastSibling!=null && it.hasNext();) {
			Dockable d = (Dockable)it.next();
			if(d.getPersistentId().equals(lastSibling)) {
				DockingPort embedPort = d.getDockingPort();
				String embedRegion = getRegion(lastNode, d.getComponent());
				return dock(dockable, embedPort, embedRegion, ctrlNode);
			}
		}
		
		
		return dock(dockable, port, region, ctrlNode);
	}
	
	private String getRegion(SplitNode node, Component dockedComponent) {
		if(dockedComponent==null)
			return CENTER_REGION;
		return DockingUtility.getRegion(node.getRegion());
	}
	
	public SplitNode getLastNode() {
		return nodes.size()==0? null: (SplitNode)nodes.get(nodes.size()-1);
	}
	
	private boolean dock(Dockable dockable, DockingPort port, String region, SplitNode ctrlNode) {
		boolean ret = DockingManager.dock(dockable, port, region);
		if(tabbed || ctrlNode==null)
			return ret;
		
		final float percent = ctrlNode.getPercentage();
		final Component docked = dockable.getComponent();
		
		EventQueue.invokeLater(new Runnable() {
			public void run() {
			    resizeSplitPane(docked, percent);
			}
		});
		return ret;
	}
	
	private void resizeSplitPane(Component comp, float percentage) {
		Container parent = comp.getParent();
		Container grandParent = parent==null? null: parent.getParent();
		if(!(grandParent instanceof JSplitPane))
			return;
		
		JSplitPane split = (JSplitPane)grandParent;
//		int splitSize = split.getOrientation()==DockingConstants.VERTICAL? split.getHeight(): split.getWidth();
//		int divLoc = (int)(percentage * (float)splitSize);
		split.setDividerLocation(percentage);
	}
	
	private static Dockable findDockable(Component c) {
		return DockingManager.getDockable(c);
	}
	
	private static Dockable findDockable(String id) {
		return DockingManager.getDockable(id);
	}
	
	private static boolean isDocked(Dockable dockable) {
		return DockingManager.isDocked(dockable);
	}
	
	public int getDepth() {
		return nodes.size();
	}
	
	public SplitNode getNode(int indx) {
		return indx<0 || indx>=getDepth()? null: (SplitNode)nodes.get(indx);
	}
	
	public Object clone() {
		ArrayList nodeList = null;
		if(nodes!=null) {
			nodeList = new ArrayList(nodes.size());
			for(Iterator it=nodes.iterator(); it.hasNext();) {
				SplitNode node = (SplitNode)it.next();
				nodeList.add(node.clone());
			}
		}
		
		DockingPath path = new DockingPath(siblingId, tabbed, nodeList);
		path.rootPortId = rootPortId;
		return path;
	}
    
}
