package com.actelion.research.gui.dock;

import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.*;

public class JDockingPanel extends JPanel implements ActionListener {
	private static final long serialVersionUID = 0x20070720;

	public  static final int DOCK_CENTER = 0;
	public  static final int DOCK_TOP = 1;
	public  static final int DOCK_LEFT = 2;
	public  static final int DOCK_BOTTOM = 3;
	public  static final int DOCK_RIGHT = 4;

	public static final int ALLOWED_DRAG_DROP_ACTIONS = DnDConstants.ACTION_MOVE;   // currently no copy

	private TreeMap<String,Dockable> mDockableMap;
	private TreeMap<String,TreeLeaf> mLeafMap;
	private Dockable mPreviousTargetDockable;
	private TreeRoot mTreeRoot;
	private TreeLeaf mTargetLeaf;
	private int mTargetPosition,mPreviousTargetPosition;
	private GhostPreview mPreview;
	private Dockable mMaximizedView;
	private Vector<DividerChangeListener> mDividerChangeListeners;

	/**
	 * Creates a docking panel to which any Dockables may be added by
	 * the respective dock methods.
	 */
	public JDockingPanel() {
		mDockableMap = new TreeMap<>();
		mLeafMap = new TreeMap<>();
		mTargetPosition = -1;
		mPreview = new GhostPreview();

		// Unfortunately, this cannot be used to catch drag events of default JTable with active DropTarget
//		Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_MOTION_EVENT_MASK);

		setLayout(new OverlayLayout(this));

		new DropTarget(this, ALLOWED_DRAG_DROP_ACTIONS, new DropTargetAdapter() {
			@Override
			public void dragOver(DropTargetDragEvent dtde) {
				Dockable dockable = getDraggedDockable(dtde);
				if (dockable != null) {
					Point p = dtde.getLocation();
					Rectangle b = dockable.getHeader().getBounds();
					p.translate(b.x, b.y);  // make p relative to Dockable
					updatePreview(p, dockable);
					}
				}

			@Override
			public void drop(DropTargetDropEvent dtde) {
				Transferable transferable = dtde.getTransferable();
				if (mTargetPosition == -1
				 || !transferable.isDataFlavorSupported(TransferableDockable.DF_DOCKABLE_DEF)) {
// TODO do we need this?				fireDockableSelected(mDraggedDockable);
					dtde.rejectDrop();
					}
				else {
					dtde.acceptDrop(ALLOWED_DRAG_DROP_ACTIONS);
					try {
						String draggedTitle = (String)transferable.getTransferData(TransferableDockable.DF_DOCKABLE_DEF);
						for (int i=0; i<mTargetLeaf.getDockableCount(); i++) {
							if (!draggedTitle.equals(mTargetLeaf.getDockable(i).getTitle())) {
								String targetTitle = mTargetLeaf.getDockable(i).getTitle();
								relocateView(draggedTitle, targetTitle, mTargetPosition, 0.5f);
								break;
								}
							}
						dtde.dropComplete(true);
						}
					catch (Exception e) {}
					}

				mTargetPosition = -1;
				}
			}, true);
		}

	public void addDividerChangeLister(DividerChangeListener l) {
		if (mDividerChangeListeners == null)
			mDividerChangeListeners = new Vector<>();

		mDividerChangeListeners.add(l);
		if (mTreeRoot != null)
			mTreeRoot.setDividerChangeListeners(mDividerChangeListeners);
		}

	public void removeDividerChangeLister(DividerChangeListener l) {
		if (mDividerChangeListeners != null) {
			mDividerChangeListeners.remove(l);
			if (mTreeRoot != null)
				mTreeRoot.setDividerChangeListeners(mDividerChangeListeners);
			}
		}

	public void actionPerformed(ActionEvent e) {
		if (e.getActionCommand().startsWith("close_")) {
			String title = e.getActionCommand().substring(6);
			undock(title, false);
			}
		else if (e.getActionCommand().startsWith("max_")) {
			String title = e.getActionCommand().substring(4);
			maximize(title, null);
			}
		}

	/**
	 * Toggle the maximization state of the dockable in the docking panel.
	 * If it is maximizing, then a toolbar may be provided that is shown in the maximized state.
	 * Note that a null toolbar causes the maximized state to use the default toolbar with
	 * maximize and close buttons only.
	 * @param maximizeToolBar null for default or toolbar that shall be shown in maximized state
	 */
	public void maximize(String title, JToolBar maximizeToolBar) {
		Dockable dockable = mDockableMap.get(title);
		if (mMaximizedView != null) {
			remove(mMaximizedView);
			dockable.endBorrowContent();
			mTreeRoot.getChild().getComponent().setVisible(true);
			mMaximizedView = null;
			validate();
			}
		else if (!(mTreeRoot.getComponent() instanceof Dockable)) {
			mTreeRoot.getChild().getComponent().setVisible(false);
			selectDockable(dockable);
			JComponent content = dockable.borrowContent();

			// create dummy dockable to hold the maximized view
			mMaximizedView = new Dockable(this, content, title, maximizeToolBar);
			mMaximizedView.setPopupProvider(dockable.getPopupProvider());
			mMaximizedView.setVisible(true);
			add(mMaximizedView);

			validate();
			}
		}

	/**
	 * Sends ActionEvent to listeners when the user interactively selected another dockable
	 * @param dockable
	 */
	protected void fireDockableSelected(Dockable dockable) {
		actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "selected_"+dockable.getTitle()));
		}

	private void updatePreview(Point p, Dockable draggedDockable) {
		mTargetPosition = -1;
		Dockable targetDockable = null;

		for (Dockable d:mDockableMap.values()) {
			if (d.isVisible()) {
				Rectangle bounds = getAbsoluteBounds(d.getContent());
				if (bounds.contains(p)) {
					targetDockable = d;
					mTargetPosition = getPosition(p, bounds);
					break;
					}
				}
			}

		updatePreview(draggedDockable, targetDockable);
		}

	/**
	 * Calculates bounds of the dockable relative to this JDockingPanel
	 * @param jc
	 * @return
	 */
	public Rectangle getAbsoluteBounds(Component jc) {
		Rectangle b = jc.getBounds();
		Component c = jc.getParent();
		while (c != this) {
			Point p = c.getLocation();
			b.translate(p.x, p.y);
			c = c.getParent();
			}
		return b;
		}

	private int getPosition(Point p, Rectangle bounds) {
		int x = p.x - bounds.x;
		int y = p.y - bounds.y;
		if (x > bounds.width/4  && x < bounds.width*3/4
		 && y > bounds.height/4 && y < bounds.height*3/4)
			return DOCK_CENTER;

		boolean isTopOrRight = (((double)x / (double)bounds.width) > ((double)y / (double)bounds.height));
		boolean isTopOrLeft =  (((double)x / (double)bounds.width) + ((double)y / (double)bounds.height) < 1.0);

		if (isTopOrRight)
			return isTopOrLeft ? DOCK_TOP : DOCK_RIGHT;

		return isTopOrLeft ? DOCK_LEFT : DOCK_BOTTOM;
		}

	private void updatePreview(Dockable draggedDockable, Dockable targetDockable) {
		if (targetDockable == null) {
			mTargetPosition = -1;
			}
		else if (targetDockable == draggedDockable) {
			TreeLeaf sourceLeaf = mLeafMap.get(draggedDockable.getTitle());
			if (mTargetPosition == DOCK_CENTER
			 || sourceLeaf.getDockableCount() == 1)
				mTargetPosition = -1;
			else
				mTargetLeaf = sourceLeaf;
			}
		else {
			mTargetLeaf = mLeafMap.get(targetDockable.getTitle());
			}

		if (mPreviousTargetDockable != targetDockable
		 || mPreviousTargetPosition != mTargetPosition) {
			if (mTargetPosition != -1)
				mPreview.createPreview(draggedDockable, targetDockable, mTargetPosition, this);
			mPreviousTargetDockable = targetDockable;
			mPreviousTargetPosition = mTargetPosition;
			repaint();
			}
		}

	@Override
	public void paint(Graphics g) {
		super.paint(g);
		if (mTargetPosition != -1)
			mPreview.drawPreview((Graphics2D)g);
		}

/*	private void performDrag() {
		undock(mDraggedDockable.getTitle(), true);
		dock(mDraggedDockable, mTargetPosition, mTargetLeaf, 0.5, true);
		} */

	/**
	 * May be overriden to catch user initiated drag&drop
	 * @param movedDockableName
	 * @param targetDockableName
	 * @param targetPosition
	 */
	public void relocateView(String movedDockableName, String targetDockableName, int targetPosition, float dividerPosition) {
		Dockable movedDockable = getDockable(movedDockableName);
		undock(movedDockableName, true);
		dock(movedDockable, targetPosition, mLeafMap.get(targetDockableName), dividerPosition, true);
		}

	/**
	 * Dock the dockable to the center of the selected view.
	 * @param dockable
	 */
	public void dock(Dockable dockable) {
		dock(dockable, DOCK_CENTER);
		}

	/**
	 * Dock the Dockable at the given position to the selected view.
	 * @param dockable
	 * @param position
	 */
	public void dock(Dockable dockable, int position) {
		dock(dockable, position, getSelectedLeaf(), 0.5, false);
		}

	/**
	 * Dock the dockable either as a new root dockable or
	 * to the treeElement that contains dockable specified as part dockInfo.
	 * The dockInfo parameter is either "root" or has the following format:
	 * "title[\tposition[\tdividerlocation]]" with position is one of center,top,left,bottom,right
	 * and dividerlocation is between 0.0 and 1.0 giving the percentage of space given to the left/top
	 * element.
	 * @param dockable
	 * @param dockInfo encoded position and parent obtainable by getDockInfo(title)
	 */
	public void dock(Dockable dockable, String dockInfo) {
		if (dockInfo == null || dockInfo.equals("root")) {
			dock(dockable, DOCK_CENTER, getSelectedLeaf(), Double.NaN, false);
			return;
			}

		int index1 = dockInfo.indexOf('\t');
		int index2 = dockInfo.indexOf('\t', index1+1);
		String title = dockInfo.substring(0, index1);
		String p = (index2 == -1) ? dockInfo.substring(index1+1) : dockInfo.substring(index1+1, index2);
		String dp = (index2 == -1) ? null : dockInfo.substring(index2);

		int position = "center".equals(p) ? DOCK_CENTER
					 : "top".equals(p)	? DOCK_TOP
					 : "left".equals(p)   ? DOCK_LEFT
					 : "bottom".equals(p) ? DOCK_BOTTOM
					 : "right".equals(p)  ? DOCK_RIGHT
					 : -1;
		double dividerPosition = (dp != null) ? Double.parseDouble(dp)
				: (position == DOCK_CENTER) ? Double.NaN : 0.5;

		dock(dockable, position, mLeafMap.get(title), dividerPosition, false);
		}

	/**
	 * Dock the dockable at the given position to the treeElement's component.
	 * If component is the first to dock then treeElement may be null.
	 * @param dockable
	 * @param position DOCK_CENTER,DOCK_LEFT,...
	 * @param treeLeaf may be null, if the dockable is the first in the JDockingPanel
	 * @param dividerPosition position of divider of new JSplitPane if position!=DOCK_CENTER
	 */
	private void dock(Dockable dockable, int position, TreeLeaf treeLeaf, double dividerPosition, boolean isDragging) {
		mDockableMap.put(dockable.getTitle(), dockable);

		if (treeLeaf == null) {
			TreeLeaf leaf = new TreeLeaf(dockable, this, isDragging);
			mTreeRoot = new TreeRoot(this, leaf);
			mLeafMap.put(dockable.getTitle(), leaf);
			}
		else if (position == DOCK_CENTER) {
			treeLeaf.addContent(dockable, isDragging);
			mLeafMap.put(dockable.getTitle(), treeLeaf);
			}
		else {
			TreeLeaf newLeaf = new TreeLeaf(dockable, this, isDragging);
			TreeContainer parent = treeLeaf.getParent();
			TreeFork treeFork = new TreeFork(treeLeaf, newLeaf, position, dividerPosition, mDividerChangeListeners);
			parent.replaceChildElement(treeLeaf, treeFork);
			mLeafMap.put(dockable.getTitle(), newLeaf);
			}

		selectDockable(dockable);
		validate();
//System.out.println("*****Status after dock()");
//mTreeRoot.printStatus();
		}

	public void undock(String title) {
		undock(title, false);
		}

	public void undock(String title, boolean isDragging) {
		Dockable dockable = mDockableMap.get(title);
		if (dockable != null) {
			if(mDockableMap.size()==1) {
				undockAll();
				return;				
			}
			boolean isSelected = dockable.isSelected();
			TreeLeaf leaf = mLeafMap.get(title);
			mLeafMap.remove(title);
			if (leaf.removeContent(dockable, isDragging)) {
				if (isSelected) {
					TreeLeaf newSelected = mLeafMap.get(mLeafMap.firstKey());
					if (newSelected != null)
						newSelected.setSelected(true);
					}
				}
			else {
				if (isSelected)
					leaf.setSelected(true);
				}

			mDockableMap.remove(title);

			validate();
			repaint();
			}
//System.out.println("*****Status after undock()");
//mTreeRoot.printStatus();
		}

	public void undockAll() {
		mTreeRoot = null;
		mDockableMap.clear();
		mLeafMap.clear();
		removeAll();
		validate();
		repaint();
		}

	public Set<String> getDockableTitles() {
		return mDockableMap.keySet();
		}

	public Collection<Dockable> getDockables() {
		return mDockableMap.values();
		}

	public Dockable getDockable(String title) {
		return mDockableMap.get(title);
		}

	public String getTitle(Dockable dockable) {
		for (String title:mDockableMap.keySet())
			if (mDockableMap.get(title) == dockable)
				return title;
		return null;
		}

	public int getDockableCount() {
		return mDockableMap.size();
		}

	/**
	 * returns the persistence information necessary to recreate the current docking state.
	 * To recreate a docking state create an empty JDockingPanel and call dock(dockable, dockInfo)
	 * once for every dockable keeping the order of the title[] array and pass the respective
	 * dockInfo from the array returned by this function.
	 * @return dockInfo[] array with a docking instruction for every dockable
	 */
	public String[] getDockInfoSequence() {
		if (mTreeRoot == null)
			return null;

		return mTreeRoot.createStateInfo().toArray(new String[0]);
		}

	/**
	 * @param title
	 * @return true if a dockable with the given title exists and if it is the selected component of a tabbed pane
	 */
	public boolean isInFrontInTabbedPane(String title) {
		Dockable dockable = mDockableMap.get(title);
		if (dockable == null)
			return false;

		Component parent = dockable.getParent();
		if (parent instanceof JTabbedPane)
			return (dockable == ((JTabbedPane)parent).getSelectedComponent());

		return false;
		}

	/**
	 * @param title
	 */
	public void setToFrontInTabbedPane(String title) {
		Dockable dockable = mDockableMap.get(title);
		if (dockable != null) {
			Component parent = dockable.getParent();
			if (parent instanceof JTabbedPane)
				((JTabbedPane)parent).setSelectedComponent(dockable);

			}
		}

	/**
	 * Checks, whether a dockable with the given title exists, and whether it is visible (selected if in a JTabbedPane)
	 * and whether it is in the left or right branch (depending on parameter left) of a JSplitPane.
	 * @param title
	 * @param left if true, then
	 * @return true the respective dockable is visible and in the left part of a JSplitPane
	 */
	public boolean isVisibleInSplitPane(String title, boolean left) {
		Component view = mDockableMap.get(title);
		if (view == null)
			return false;

		Component pane = view.getParent();
		if (pane instanceof JTabbedPane) {
			if (view != ((JTabbedPane)pane).getSelectedComponent())
				return false;

			view = pane;
			pane = pane.getParent();
			}

		if (pane instanceof JSplitPane)
			return (left && view == ((JSplitPane)pane).getLeftComponent())
				|| (!left && view == ((JSplitPane)pane).getRightComponent());

		return false;
		}

	/**
	 * changes a title of a docked Dockable
	 * @param oldTitle existing title
	 * @param newTitle must be a unique title and must not contain a TAB
	 * @return true in case of success
	 */
	public boolean changeTitle(String oldTitle, String newTitle) {
		if (mDockableMap.containsKey(newTitle) || newTitle.indexOf('\t') != -1)
			return false;

		Dockable dockable = mDockableMap.get(oldTitle);
		mDockableMap.remove(oldTitle);
		mDockableMap.put(newTitle, dockable);
		dockable.setTitle(newTitle);

		TreeLeaf leaf = mLeafMap.get(oldTitle);
		leaf.changeTitle(oldTitle, newTitle);
		mLeafMap.remove(oldTitle);
		mLeafMap.put(newTitle, leaf);

		if (mMaximizedView != null
		 && mMaximizedView.getTitle().equals(oldTitle))
			mMaximizedView.setTitle(newTitle);

		return true;
		}

	/**
	 * Returns the Dockable that contains the given content.
	 * @param content the content to be looked for
	 * @return content owning Dockable or null if content is not found
	 */
	public Dockable getDockable(Component content) {
		if (content != null)
			for (Dockable d:mDockableMap.values())
				if (d.getContent() == content)
					return d;

		return null;
		}

	/**
	 * Returns the currently active Dockable, i.e. the one with the
	 * highlighted header area.
	 * @return active Dockable or null if there are no Dockables
	 */
	public Dockable getSelectedDockable() {
		for (Dockable d:mDockableMap.values())
			if (d.isSelected())
				return d;

		return null;
		}

	public Dockable getDraggedDockable(DropTargetDragEvent dtde) {
		Transferable transferable = dtde.getTransferable();
		if (transferable.isDataFlavorSupported(TransferableDockable.DF_DOCKABLE_DEF)) {
			try {
				return getDockable((String)transferable.getTransferData(TransferableDockable.DF_DOCKABLE_DEF));
				}
			catch (Exception e) {}
			}
		return null;
		}

	/**
	 * This is called when a Dockable's visibility changes, i.e.<br>
	 * - when a new Dockable is docked<br>
	 * - when a new Dockable is undocked<br>
	 * - when the user drags & docks a Dockable<br>
	 * - when the user actively switches tabs in a tabbed pane<br>
	 * It may be overridden in order to react on a visibility change due
	 * to the user selecting a different tab of dragging & docking a
	 * Dockable.
	 * @param dockable
	 * @param isVisible
	 */
	public void visibilityChanged(Dockable dockable, boolean isVisible) {
		dockable.notifyVisibility(isVisible);
		}

	public void selectDockable(Dockable dockable) {
		if (dockable != null) {
			for (Dockable d:mDockableMap.values())
				d.setSelected(d == dockable);
	
			TreeLeaf leaf = mLeafMap.get(dockable.getTitle());
			if (leaf != null)
				leaf.setSelectedDockable(dockable);
			}
		}

	private TreeLeaf getSelectedLeaf() {
		for (TreeLeaf leaf:mLeafMap.values())
			if (leaf.isSelected())
				return leaf;

		return null;
		}

	/**
	 * @return whether one of the dockable views is currently maximized
	 */
	public boolean isMaximized() {
		return (mMaximizedView != null);
		}

	public void redistribute() {
		List<Dockable> dockables = new ArrayList<Dockable>(getDockables());
		Collections.sort(dockables, new Comparator<Dockable>() {
			public int compare(Dockable o1, Dockable o2) {
				return o1.getTitle().compareTo(o2.getTitle());
				}
			});

		if(dockables.size()<=1)
			return;

		mTreeRoot = null;
		mLeafMap.clear();
		mDockableMap.clear();
		removeAll();
		validate();
		
		for (Dockable dockable : dockables) {
			dockable.borrowContent();
			}
		
		if(dockables.size()>0) {
			Dockable dockable = dockables.get(0);
			TreeLeaf leaf = new TreeLeaf(dockable, this, false);
			mTreeRoot = new TreeRoot(this, leaf);
			mLeafMap.put(dockable.getTitle(), leaf);
			mDockableMap.put(dockable.getTitle(), dockable);
			redistribute(dockables, 0, dockables.size(), leaf);
			}

		for (Dockable dockable : dockables) {
			dockable.endBorrowContent();
			}

		validate();
		repaint();
		}
	
	private void redistribute(List<Dockable> dockables, int startIndex, int endIndex, TreeLeaf topLeft) {

		//int size = (endIndex-startIndex + 3)/4;
		//if(size>1 && size%2==1) size++;
		//size = (int)(Math.sqrt(size)+.99);
		//size*= size;
		//System.out.println("JDockingPanel.redistribute() "+startIndex+" "+endIndex+" size="+size);

		int size1 = startIndex + (endIndex-startIndex+3)/4;
		int size2 = size1 + (endIndex-size1+2)/3;
		int size3 = size2 + (endIndex-size2+1)/2;
		System.out.println(startIndex+" "+size1+" "+size2+" "+size3+" "+endIndex );

		TreeLeaf topRight = null;
		TreeLeaf bottomLeft = null;
		TreeLeaf bottomRight = null;

		if(size2>size1 && size2<dockables.size()) {
			Dockable dockable = dockables.get(size2);
			bottomLeft = new TreeLeaf(dockable, this, false);
			TreeContainer parent = topLeft.getParent();
			TreeFork treeFork = new TreeFork(topLeft, bottomLeft, DOCK_BOTTOM, .5, mDividerChangeListeners);
			parent.replaceChildElement(topLeft, treeFork);
			mLeafMap.put(dockable.getTitle(), bottomLeft);
			mDockableMap.put(dockable.getTitle(), dockable);
			}

		if(size1>startIndex && size1<dockables.size()) {
			Dockable dockable = dockables.get(size1);
			topRight = new TreeLeaf(dockable, this, false);
			TreeContainer parent = topLeft.getParent();
			TreeFork treeFork = new TreeFork(topLeft, topRight, DOCK_RIGHT, .5, mDividerChangeListeners);
			parent.replaceChildElement(topLeft, treeFork);
			mLeafMap.put(dockable.getTitle(), topRight);
			mDockableMap.put(dockable.getTitle(), dockable);
			}

		if(size3>size2 && size3<dockables.size()) {
			Dockable dockable = dockables.get(size3);
			bottomRight = new TreeLeaf(dockable, this, false);
			TreeContainer parent = bottomLeft.getParent();
			TreeFork treeFork = new TreeFork(bottomLeft, bottomRight, DOCK_RIGHT, .5, mDividerChangeListeners);
			parent.replaceChildElement(bottomLeft, treeFork);
			mLeafMap.put(dockable.getTitle(), bottomRight);
			mDockableMap.put(dockable.getTitle(), dockable);
			}

		if (size1>startIndex+1)
			redistribute(dockables, startIndex, size1, topLeft);
		if (size2>size1+1)
			redistribute(dockables, size1, size2, topRight);
		if (size3>size2+1)
			redistribute(dockables, size2, size3, bottomLeft);
		if (endIndex>size3+1)
			redistribute(dockables, size3, endIndex, bottomRight);
		}
	}
