/*
 * Created on May 17, 2005
 */
package org.flexdock.perspective;

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.floating.frames.DockingFrame;
import org.flexdock.docking.state.DockingState;
import org.flexdock.docking.state.FloatManager;
import org.flexdock.docking.state.FloatingGroup;
import org.flexdock.docking.state.LayoutNode;
import org.flexdock.event.EventManager;
import org.flexdock.perspective.event.LayoutEvent;
import org.flexdock.perspective.event.LayoutListener;
import org.flexdock.perspective.event.RegistrationEvent;
import org.flexdock.util.DockingUtility;
import org.flexdock.util.RootWindow;
import org.flexdock.util.SwingUtility;

/**
 * @author Christopher Butler
 */
public class Layout implements Cloneable, FloatManager, Serializable {

    private HashMap dockingInfo;  // contains DockingState objects
	private Hashtable floatingGroups;  // contains FloatingGroup objects
	private LayoutNode restorationLayout;

    private transient ArrayList layoutListeners;

	public Layout() {
		this(new HashMap(), new ArrayList(), new Hashtable());
	}
	
	private Layout(HashMap info, ArrayList listeners, Hashtable floatGroups) {
		dockingInfo = info;
		layoutListeners = listeners;
		floatingGroups = floatGroups;
	}
	
	private ArrayList getLayoutListeners() {
		if(layoutListeners==null)
			layoutListeners = new ArrayList();
		return layoutListeners;
	}
	
	public void addListener(LayoutListener listener) {
		if(listener!=null) {
			synchronized(getLayoutListeners()) {
				getLayoutListeners().add(listener);
			}
		}
	}
	
	public void removeListener(LayoutListener listener) {
		if(listener!=null) {
			synchronized(getLayoutListeners()) {
				getLayoutListeners().remove(listener);
			}
		}
	}
	
	public LayoutListener[] getListeners() {
		return (LayoutListener[])getLayoutListeners().toArray(new LayoutListener[0]);
	}
	
	public void add(Dockable dockable) {
		String key = dockable==null? null: dockable.getPersistentId();
		add(key);
	}
	
	public void add(String dockableId) {
		if(dockableId==null)
			return;
		
		DockingState info = null;
		synchronized(dockingInfo) {
            // return if we're already managing this dockable
            if (dockingInfo.containsKey(dockableId)) {
                return;
            }
			
			// create and add dockingstateinfo here
			info = new DockingState(dockableId);
			dockingInfo.put(dockableId, info);
		}

		EventManager.dispatch(new RegistrationEvent(info, this, true));
	}
	
	public DockingState remove(String dockableId) {
		if(dockableId==null)
			return null;
		
		DockingState info = null;
		synchronized(dockingInfo) {
			info = (DockingState)dockingInfo.remove(dockableId);
		}
		// dispatch event notification if we actually removed something
		if(info!=null)
			EventManager.dispatch(new RegistrationEvent(info, this, false));
		return info;
	}
	
	public boolean contains(Dockable dockable) {
		return dockable==null? false: contains(dockable.getPersistentId());
	}
	
	public boolean contains(String dockable) {
		return dockable==null? false: dockingInfo.containsKey(dockable);
	}
	
	public Dockable getDockable(String id) {
		if(dockingInfo.containsKey(id))
			return DockingManager.getDockable(id);
		return null;
	}
		
	public Dockable[] getDockables() {
		ArrayList list = new ArrayList(dockingInfo.size());
		for(Iterator it=dockingInfo.keySet().iterator(); it.hasNext();) {
			String dockingId = (String)it.next();
			Dockable d = DockingManager.getDockable(dockingId);
			if(d!=null)
				list.add(d);
		}
		return (Dockable[])list.toArray(new Dockable[0]);
	}
	
	public DockingState getDockingState(String dockableId) {
		return getDockingState(dockableId, false);
	}
	
	public DockingState getDockingState(Dockable dockable) {
		return getDockingState(dockable, false);
	}

	public DockingState getDockingState(Dockable dockable, boolean load) {
		if(dockable==null)
			return null;

		return getDockingState(dockable.getPersistentId(), load);
	}
	
	public DockingState getDockingState(String dockableId, boolean load) {
		if(dockableId==null)
			return null;
		
		if(load) {
			Dockable dockable = DockingManager.getDockable(dockableId);
			if(dockable!=null) {
				isMaintained(dockable);
			}
		}
		Object obj = dockingInfo.get(dockableId);
		return (DockingState)obj;
	}
    
    public void setDockingState(String dockableId, DockingState dockingState) {
        if(dockableId==null || dockingState == null) {
            return;
        }
        this.dockingInfo.put(dockableId, dockingState);
    }
	
	public void apply(DockingPort dockingPort) {
		Component comp = (Component)dockingPort;
		if(comp==null || !isInitialized())
//		if(comp==null || comp.getParent()==null || !isInitialized())
			return;
		
		// clear out the existing components
		PerspectiveManager.clear(dockingPort);
		
		// restore the layout
		boolean listening = PerspectiveManager.isDockingStateListening();
		PerspectiveManager.setDockingStateListening(false);
        try {
            dockingPort.importLayout(restorationLayout);
        } finally {
            PerspectiveManager.setDockingStateListening(listening);
        }
		
		// not restore floating and minimized layouts
		Dockable[] dockables = getDockables();
		
		// if there is no active window into which to restore our minimized
		// dockables, then we'll have to defer restoration until a window appears.
		ArrayList deferredMinimizedDockables = new ArrayList();
		boolean deferMinimized = SwingUtility.getActiveWindow()==null;
		
		boolean restoreFloatOnLoad = PerspectiveManager.isRestoreFloatingOnLoad();
		for(int i=0; i<dockables.length; i++) {
			Dockable dockable = dockables[i];
			if(DockingUtility.isMinimized(dockable)) { 
				if(deferMinimized) {
					deferredMinimizedDockables.add(dockable);
				}
				else {
					RestorationManager.getInstance().restore(dockable);	
				}
				
			} else if(restoreFloatOnLoad && DockingUtility.isFloating(dockable)) {
				RestorationManager.getInstance().restore(dockable);
			}
		}
		
		// if necessary, defer minimized restoration until after a valid window 
		// has been resolved
		restoreDeferredMinimizedDockables(deferredMinimizedDockables);
		
		// send notification
		LayoutEvent evt = new LayoutEvent(this, null, null, LayoutEvent.LAYOUT_APPLIED);
		EventManager.dispatch(evt);
	}
	
	private void restoreDeferredMinimizedDockables(final ArrayList deferred) {
		if(deferred==null || deferred.size()==0)
			return;
		
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				restoreMinimizedDockables(deferred);
			}
		});
	}
	
	
	private void restoreMinimizedDockables(ArrayList dockables) {
		if(SwingUtility.getActiveWindow()==null) {
			restoreDeferredMinimizedDockables(dockables);
			return;
		}
		
		for(Iterator it=dockables.iterator(); it.hasNext();) {
			Dockable dockable = (Dockable)it.next();
			RestorationManager.getInstance().restore(dockable);
		}
	}

	private boolean isMaintained(Dockable dockable) {
		if(dockable==null)
			return false;
		
		if(!contains(dockable))
			add(dockable);
		return true;
	}

	public void hide(Dockable dockable) {
		if(!isMaintained(dockable))
			return;
		
		boolean hidden = false;
		if(DockingManager.isDocked((Dockable)dockable)) {
			hidden = DockingManager.undock(dockable);
		} else if (DockingUtility.isMinimized(dockable)) {
			hidden = DockingManager.getMinimizeManager().close(dockable);
		}
		
		if(hidden) {
			LayoutEvent evt = new LayoutEvent(this, null, dockable.getPersistentId(), LayoutEvent.DOCKABLE_HIDDEN);
			EventManager.dispatch(evt);
		}
	}
	
	public void show(Dockable dockable, DockingPort dockingPort) {
		if(!isMaintained(dockable) || DockingManager.isDocked(dockable))
			return;
	}
	
	public Object clone() {
		synchronized(this) {
			ArrayList listeners = (ArrayList)getLayoutListeners().clone();
			HashMap infoMap = (HashMap)dockingInfo.clone();
			for(Iterator it=dockingInfo.keySet().iterator(); it.hasNext();) {
				String key = (String)it.next();
				DockingState info = getDockingState(key);
				infoMap.put(key, info.clone());
			}
			
			Hashtable floatTable = (Hashtable)floatingGroups.clone();
			for(Iterator it=floatingGroups.keySet().iterator(); it.hasNext();) {
				Object key = it.next();
				FloatingGroup group = (FloatingGroup)floatingGroups.get(key);
				floatTable.put(key, group.clone());
			}
			
			// note, we're using a shallow copy of the listener list.
			// it's okay that we share listener references, since we want the
			// cloned Layout to have the same listeners.
			Layout clone = new Layout(infoMap, listeners, floatTable);
			LayoutNode restoreNode = restorationLayout==null? null: (LayoutNode)restorationLayout.clone();
			clone.restorationLayout = restoreNode;
			return clone;
		}

	}

	private DockingFrame getDockingFrame(Dockable dockable, Component frameOwner) {
		FloatingGroup group = getGroup(dockable);
		if(group==null)
			group = new FloatingGroup(getFloatingGroup(dockable));
		
		DockingFrame frame = group.getFrame();
		if(frame==null) {
			frame = DockingFrame.create(frameOwner, group.getName());
			group.setFrame(frame);
			floatingGroups.put(group.getName(), group);
		}
		return frame;
	}
	
	public DockingFrame floatDockable(Dockable dockable, Component frameOwner, Rectangle screenBounds) {
		if(dockable==null || screenBounds==null)
			return null;
		
		// create the frame
		DockingFrame frame = getDockingFrame(dockable, frameOwner);
		if(screenBounds!=null)
			frame.setBounds(screenBounds);

		// undock the current Dockable instance from it's current parent container
		DockingManager.undock(dockable);

		// add to the floating frame
		frame.addDockable(dockable);
		
		// display and return
		if(!frame.isVisible())
			frame.setVisible(true);
		return frame;
	}
	
	public DockingFrame floatDockable(Dockable dockable, Component frameOwner) {
		FloatingGroup group = getGroup(dockable);
		Rectangle bounds = group==null? null: group.getBounds();
		if(bounds==null) {
			if(dockable.getComponent().isValid()) {
				bounds = dockable.getComponent().getBounds();
			}
			else
				bounds = new Rectangle(0, 0, 200, 200);
			
			Rectangle ownerBounds = frameOwner instanceof DockingFrame? 
			((DockingFrame)frameOwner).getOwner().getBounds():
			RootWindow.getRootContainer(frameOwner).getRootContainer().getBounds();

			int x = (ownerBounds.x + ownerBounds.width/2) - bounds.width/2;
			int y = (ownerBounds.y + ownerBounds.height/2) - bounds.height/2;
			bounds.setLocation(x, y);
		}
		
		return floatDockable(dockable, frameOwner, bounds);
	}
	
	public FloatingGroup getGroup(Dockable dockable) {
		if(dockable==null)
			return null;
		
		String groupId = getFloatingGroup(dockable);
		return getGroup(groupId);
	}

    public String[] getFloatingGroupIds() {
        return (String[]) this.floatingGroups.keySet().toArray(new String[]{});
    }
    
	public FloatingGroup getGroup(String groupId) {
		return groupId==null? null: (FloatingGroup)floatingGroups.get(groupId);
	}
    
    public void addFloatingGroup(FloatingGroup floatingGroup) {
        if (floatingGroup == null) {
            return;
        }
        floatingGroups.put(floatingGroup.getName(), floatingGroup);
    }
    
	public void addToGroup(Dockable dockable, String groupId) {
		// floating groups are mutually exclusive
		removeFromGroup(dockable);

		FloatingGroup group = getGroup(groupId);
		if(dockable!=null && group!=null) {
			group.addDockable(dockable.getPersistentId());
			setFloatingGroup(dockable, group.getName());
		}
	}

	public void removeFromGroup(Dockable dockable) {
		FloatingGroup group = getGroup(dockable);
		if(dockable!=null) {
			if(group!=null)
				group.removeDockable(dockable.getPersistentId());
			setFloatingGroup(dockable, null);
		}
		
		// if the group is empty, dispose of it so we don't have 
		// any memory leaks
		if(group!=null && group.getDockableCount()==0) {
			floatingGroups.remove(group.getName());
			group.destroy();
		}
	}
	
	private String getFloatingGroup(Dockable dockable) {
        DockingState info = getDockingState(dockable, false);
		return info.getFloatingGroup();
	}
	
	private void setFloatingGroup(Dockable dockable, String group) {
		DockingState info = getDockingState(dockable, false);
		info.setFloatingGroup(group);
	}
	
	public boolean isInitialized() {
		return restorationLayout!=null;
	}
	
	public LayoutNode getRestorationLayout() {
		return restorationLayout;
	}
	
	public void setRestorationLayout(LayoutNode restorationLayout) {
		this.restorationLayout = restorationLayout;
	}
	
	void update(LayoutSequence sequence) {
		List states = sequence.getDockingStates();
		
		synchronized(dockingInfo) {
			for(Iterator it=states.iterator(); it.hasNext();) {
				DockingState info = (DockingState)it.next();
				dockingInfo.put(info.getDockableId(), info);				
			}
		}
	}
}
