/*
 * Copyright (C) 2004 NNL Technology AB
 * Visit www.infonode.net for information about InfoNode(R) 
 * products and how to contact NNL Technology AB.
 *
 * 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: AbstractTabWindow.java,v 1.74 2009/02/05 15:57:55 jesper Exp $
package api.infonode.docking;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import api.infonode.docking.drop.InsertTabDropInfo;
import api.infonode.docking.internal.ReadContext;
import api.infonode.docking.internal.WindowAncestors;
import api.infonode.docking.internal.WriteContext;
import api.infonode.docking.internalutil.DropAction;
import api.infonode.docking.model.AbstractTabWindowItem;
import api.infonode.docking.model.ViewReader;
import api.infonode.docking.model.ViewWriter;
import api.infonode.docking.model.WindowItem;
import api.infonode.docking.properties.TabWindowProperties;
import api.infonode.docking.properties.WindowTabProperties;
import api.infonode.properties.propertymap.PropertyMapManager;
import api.infonode.tabbedpanel.*;
import api.infonode.util.ChangeNotifyList;

/**
 * Abstract base class for windows containing a tabbed panel.
 *
 * @author $Author: jesper $
 * @version $Revision: 1.74 $
 */
abstract public class AbstractTabWindow extends DockingWindow {

	private static int MINIMUM_SIZE = 7;

	private final DropAction dropAction = new DropAction() {
		@Override
		public boolean showTitle() {
			return false;
		}

		@Override
		public void execute(DockingWindow window, MouseEvent mouseEvent) {
			if (window.getWindowParent() == AbstractTabWindow.this) {
				// Tab moved inside window
				updateWindowItem(window);
			} else {
				int index = tabbedPanel.getTabIndex(dragTab);
				stopDrag();

				try {
					window.beforeDrop(AbstractTabWindow.this);

					if (mouseEvent.isShiftDown()) {
						addTabNoSelect(window, index);
					} else {
						addTab(window, index);
					}
				} catch (OperationAbortedException e) {
					// Ignore
				}
			}
		}

		@Override
		public void clear(DockingWindow window, DropAction newDropAction) {
			if (newDropAction != this) {
				if (window.getWindowParent() == AbstractTabWindow.this) {
					WindowTab tab = window.getTab();
					boolean selected = tab.isSelected();
					tabbedPanel.removeTab(tab);
					tabbedPanel.insertTab(tab, draggedTabIndex);

					if (selected) {
						tab.setSelected(true);
					}
				} else {
					stopDrag();
				}
			}
		}
	};

	private TabbedPanel tabbedPanel;

	/**
	 * Temporary drag tab.
	 */
	private WindowTab dragTab;

	private int ignoreSelected;

	private int draggedTabIndex;

	private java.util.List tabAreaComponents;

	/**
	 * Returns the properties for this tab window.
	 *
	 * @return the properties for this tab window
	 */
	abstract public TabWindowProperties getTabWindowProperties();

	protected AbstractTabWindow(boolean showContent, WindowItem windowItem) {
		super(windowItem);
		if (showContent) {
			TabContentPanel contentPanel = new TabContentPanel() {
				@Override
				public Dimension getMinimumSize() {
					if (getTabWindowProperties().getRespectChildWindowMinimumSize()) {
						return super.getMinimumSize();
					}

					return new Dimension(0, 0);
				}
			};
			tabbedPanel = new TabbedPanel(contentPanel, true) {
				@Override
				public Dimension getMinimumSize() {
					if (getTabWindowProperties().getRespectChildWindowMinimumSize()) {
						return super.getMinimumSize();
					}

					return getTabbedPanelMinimumSize(super.getMinimumSize());
				}
			};
			contentPanel.setTabbedPanel(tabbedPanel);
		} else {
			tabbedPanel = new TabbedPanel(null) {
				@Override
				public Dimension getMinimumSize() {
					if (getTabWindowProperties().getRespectChildWindowMinimumSize()) {
						return super.getMinimumSize();
					}

					return getTabbedPanelMinimumSize(super.getMinimumSize());
				}
			};
		}

		tabbedPanel.addTabListener(new TabWindowMover(this, tabbedPanel));
		setComponent(tabbedPanel);

		getTabbedPanel().addTabListener(new TabAdapter() {
			@Override
			public void tabAdded(final TabEvent event) {
				SwingUtilities.invokeLater(() -> {
					updateButtonVisibility();
				});
			}

			@Override
			public void tabRemoved(final TabRemovedEvent event) {
				SwingUtilities.invokeLater(() -> {
					updateButtonVisibility();
				});
			}

			@Override
			public void tabSelected(TabStateChangedEvent event) {
				AbstractTabWindow.this.tabSelected((WindowTab) event.getTab());
				DockingWindow selectedWindow = getSelectedWindow();

				if (!getIgnoreSelected() && selectedWindow != null) {
					selectedWindow.fireWindowShown(selectedWindow);
				}
			}

			@Override
			public void tabDeselected(TabStateChangedEvent event) {
				WindowTab tab = (WindowTab) event.getTab();

				if (tab != null && !getIgnoreSelected()) {
					tab.getWindow().fireWindowHidden(tab.getWindow());
				}
			}

			@Override
			public void tabMoved(TabEvent event) {
				if (!getIgnoreSelected()) {
					fireTitleChanged();
				}
			}
		});
	}

	protected void initMouseListener() {
		getTabbedPanel().addMouseListener(new MouseAdapter() {
			@Override
			public void mousePressed(MouseEvent e) {
				if (e.isPopupTrigger()) {
					showPopupMenu(e);
				}
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				mousePressed(e);
			}
		});
	}

	private Dimension getTabbedPanelMinimumSize(Dimension d) {
		if (tabbedPanel.getProperties().getTabAreaOrientation().isHorizontal()) {
			return new Dimension(d.width, MINIMUM_SIZE);
		} else {
			return new Dimension(MINIMUM_SIZE, d.height);
		}
	}

	/**
	 * <p>
	 * Returns a list containing the custom tab area components. Changes to the list will be propagated to the tab area.
	 * </p>
	 * <p>
	 * The custom tab area components will between the scroll buttons and the window buttons in the tab area components
	 * panel. The components are shown in the same order as they appear in the list. The tab area components container
	 * layout is rotated with the tab window tab orientation.
	 * </p>
	 *
	 * @return a list containing the custom tab area components, list elements are of type {@link JComponent}
	 * @since IDW 1.3.0
	 */
	public final java.util.List getCustomTabAreaComponents() {
		if (tabAreaComponents == null) {
			tabAreaComponents = new ChangeNotifyList() {
				@Override
				protected void changed() {
					updateTabAreaComponents();
				}
			};
		}

		return tabAreaComponents;
	}

	/**
	 * Returns the currently selected window in the tabbed panel.
	 *
	 * @return the currently selected window in the tabbed panel
	 */
	public DockingWindow getSelectedWindow() {
		WindowTab tab = (WindowTab) tabbedPanel.getSelectedTab();
		return tab == null ? null : tab.getWindow();
	}

	/**
	 * Selects the tab with the index.
	 *
	 * @param index the tab index
	 */
	public void setSelectedTab(int index) {
		beginIgnoreSelected();

		try {
			Tab tab = index == -1 ? null : tabbedPanel.getTabAt(index);
			Tab oldTab = tabbedPanel.getSelectedTab();

			if (tab != oldTab) {
				tabbedPanel.setSelectedTab(tab);
				fireTitleChanged();

				if (oldTab != null) {
					((WindowTab) oldTab).getWindow().fireWindowHidden(((WindowTab) oldTab).getWindow());
				}

				if (tab != null) {
					((WindowTab) tab).getWindow().fireWindowShown(((WindowTab) tab).getWindow());
				}
			}
		} finally {
			endIgnoreSelected();
		}
	}

	/**
	 * Adds a window tab last in this tab window.
	 *
	 * @param window the window
	 */
	public void addTab(DockingWindow window) {
		PropertyMapManager.getInstance().beginBatch();

		try {
			addTab(window, tabbedPanel.getTabCount());
		} finally {
			PropertyMapManager.getInstance().endBatch();
		}
	}

	/**
	 * Inserts a window tab at an index in this tab window.
	 *
	 * @param window the window
	 * @param index the index where to insert the tab
	 * @return the index of the added tab, this might not be the same as
	 * <tt>index</tt> if the tab already is added to this tab window
	 */
	public int addTab(DockingWindow window, int index) {
		PropertyMapManager.getInstance().beginBatch();

		try {
			return addTabNoSelect(window, index);
		} finally {
			PropertyMapManager.getInstance().endBatch();
		}
	}

	protected int addTabNoSelect(DockingWindow window, int index) {
		WindowAncestors ancestors = window.storeAncestors();
		beginOptimize(window.getWindowParent());
		beginIgnoreSelected();

		try {
			Tab beforeTab = index >= tabbedPanel.getTabCount() ? null : tabbedPanel.getTabAt(index);
			DockingWindow w = window.getContentWindow(this);
			w.detach();
			updateTab(w);
			WindowTab tab = w.getTab();
			int actualIndex = beforeTab == null ? tabbedPanel.getTabCount() : tabbedPanel.getTabIndex(beforeTab);
			tabbedPanel.insertTab(tab, actualIndex);
			addWindow(w);
			window.notifyListeners(ancestors);
			return actualIndex;
		} finally {
			endIgnoreSelected();
			endOptimize();
		}
	}

	@Override
	protected boolean isChildShowingInRootWindow(DockingWindow child) {
		return super.isChildShowingInRootWindow(child) && child == getSelectedWindow();
	}

	@Override
	protected void showChildWindow(DockingWindow window) {
		setSelectedTab(getChildWindowIndex(window));
		super.showChildWindow(window);
	}

	@Override
	protected boolean childInsideTab() {
		return true;
	}

	protected void setTabWindowProperties(TabWindowProperties properties) {
		getTabbedPanel().getProperties().addSuperObject(properties.getTabbedPanelProperties());
	}

	@Override
	protected void clearFocus(View view) {
		if (getSelectedWindow() != null) {
			getSelectedWindow().clearFocus(view);
		}
	}

	@Override
	protected DockingWindow getPreferredFocusChild() {
		return getSelectedWindow() == null ? super.getPreferredFocusChild() : getSelectedWindow();
	}

	@Override
	protected void clearChildrenFocus(DockingWindow child, View view) {
		if (getSelectedWindow() != child) {
			clearFocus(view);
		}
	}

	protected int getTabAreaComponentCount() {
		return 0;
	}

	@SuppressWarnings("unchecked")
	protected void updateTabAreaComponents() {
		int ls = tabAreaComponents == null ? 0 : tabAreaComponents.size();
		JComponent[] components = new JComponent[ls + getTabAreaComponentCount()];

		if (tabAreaComponents != null) {
			tabAreaComponents.toArray(components);
		}

		getTabAreaComponents(ls, components);
		getTabbedPanel().setTabAreaComponents(components);
	}

	protected void getTabAreaComponents(int index, JComponent[] components) {
	}

	protected final boolean getIgnoreSelected() {
		return ignoreSelected > 0;
	}

	@SuppressWarnings("NonPublicExported")
	protected void tabSelected(WindowTab tab) {
		if (!getIgnoreSelected() && tab != null) {
			final RootWindow root = getRootWindow();

			if (root != null) {
				// Anticipate the focus movement to avoid flicker
				tab.setFocused(true);
				root.addFocusedWindow(tab.getWindow());
				FocusManager.focusWindow(tab.getWindow());
			}
		}

		if (!getIgnoreSelected()) {
			fireTitleChanged();
		}
	}

	protected TabbedPanel getTabbedPanel() {
		return tabbedPanel;
	}

	@Override
	public DockingWindow getChildWindow(int index) {
		return ((WindowTab) tabbedPanel.getTabAt(index)).getWindow();
	}

	@Override
	protected DockingWindow getLocationWindow() {
		return tabbedPanel.getTabCount() == 1 ? getChildWindow(0) : this;
	}

	@Override
	public int getChildWindowCount() {
		return tabbedPanel.getTabCount();
	}

	@Override
	public Icon getIcon() {
		DockingWindow window = getSelectedWindow();
		return window != null ? window.getIcon() : getChildWindowCount() > 0 ? getChildWindow(0).getIcon() : null;
	}

	private void updateTab(DockingWindow window) {
		window.getTab().setProperties(getTabProperties(window));
	}

	private WindowTabProperties getTabProperties(DockingWindow window) {
		WindowTabProperties properties = new WindowTabProperties(getTabWindowProperties().getTabProperties());
		properties.addSuperObject(window.getWindowProperties().getTabProperties());
		return properties;
	}

	@Override
	protected void doReplace(DockingWindow oldWindow, DockingWindow newWindow) {
		beginIgnoreSelected();

		try {
			Tab tab = oldWindow.getTab();
			int tabIndex = tabbedPanel.getTabIndex(tab);

			boolean selected = tab.isSelected();
			tabbedPanel.removeTab(tab);
			tabbedPanel.insertTab(newWindow.getTab(), tabIndex);

			if (selected) {
				tabbedPanel.setSelectedTab(newWindow.getTab());
			}

			updateTab(newWindow);
		} finally {
			endIgnoreSelected();
		}
	}

	@Override
	protected void doRemoveWindow(DockingWindow window) {
		beginIgnoreSelected();

		try {
			WindowTab tab = window.getTab();
			tab.unsetProperties();
			tabbedPanel.removeTab(tab);
		} finally {
			endIgnoreSelected();
		}
	}

	private void beginIgnoreSelected() {
		ignoreSelected++;
	}

	private void endIgnoreSelected() {
		ignoreSelected--;
	}

	protected boolean isInsideTabArea(Point p2) {
		return tabbedPanel.tabAreaContainsPoint(p2);
	}

	@Override
	protected DropAction acceptInteriorDrop(Point p, DockingWindow window) {
		if (getChildWindowCount() == 1 && window == getChildWindow(0) && dragTab == null) {
			return null;
		}

		Point p2 = SwingUtilities.convertPoint(this, p, tabbedPanel);

		if ((getRootWindow().getRootWindowProperties().getRecursiveTabsEnabled() || window.getChildWindowCount() <= 1)
				&& isInsideTabArea(p2)) {
			getRootWindow().setDragRectangle(null);

			if (window.getWindowParent() == this) {
				tabbedPanel.moveTab(window.getTab(), p2);
			} else {
				if (!getInsertTabDropFilter().acceptDrop(new InsertTabDropInfo(window, this, p))) {
					return null;
				}

				if (dragTab == null) {
					dragTab = createGhostTab(window);
					tabbedPanel.insertTab(dragTab, p2);
				} else {
					tabbedPanel.moveTab(dragTab, p2);
				}
			}

			return dropAction;
		}

		return null;
	}

	WindowTab createGhostTab(DockingWindow window) {
		WindowTab tab = new WindowTab(window, true);
		tab.setProperties(getTabProperties(window));
		return tab;
	}

	private void stopDrag() {
		if (dragTab != null) {
			tabbedPanel.removeTab(dragTab);
			dragTab = null;
		}
	}

	@Override
	protected boolean showsWindowTitle() {
		return true;
	}

	@Override
	protected DockingWindow oldRead(ObjectInputStream in, ReadContext context) throws IOException {
		int size = in.readInt();
		int selectedIndex = in.readInt();

		while (getChildWindowCount() > 0) {
			removeChildWindow(getChildWindow(0));
		}

		for (int i = 0; i < size; i++) {
			DockingWindow window = WindowDecoder.decodeWindow(in, context);

			if (window != null) {
				addTab(window);
			} else if (i < selectedIndex) {
				selectedIndex--;
			}
		}

		super.oldRead(in, context);

		if (tabbedPanel.getTabCount() > 0) {
			if (selectedIndex >= 0) {
				setSelectedTab(Math.min(tabbedPanel.getTabCount() - 1, selectedIndex));
			}

			return this;
		} else {
			return null;
		}
	}

	@Override
	protected void write(ObjectOutputStream out, WriteContext context, ViewWriter viewWriter) throws IOException {
		out.writeInt(getChildWindowCount());

		for (int i = 0; i < getChildWindowCount(); i++) {
			getChildWindow(i).write(out, context, viewWriter);
		}
	}

	protected DockingWindow newRead(ObjectInputStream in, ReadContext context, ViewReader viewReader) throws IOException {
		int size = in.readInt();

		while (getChildWindowCount() > 0) {
			removeChildWindow(getChildWindow(0));
		}

		for (int i = 0; i < size; i++) {
			DockingWindow window = WindowDecoder.decodeWindow(in, context, viewReader);

			if (window != null) {
				addTab(window);
			}
		}

		updateSelectedTab();
		return getChildWindowCount() > 0 ? this : null;
	}

	protected void updateSelectedTab() {
		WindowItem selectedItem = ((AbstractTabWindowItem) getWindowItem()).getSelectedItem();

		for (int i = 0; i < getChildWindowCount(); i++) {
			if (getChildWindow(i).getWindowItem().hasAncestor(selectedItem)) {
				setSelectedTab(i);
				return;
			}
		}
	}

	void setDraggedTabIndex(int index) {
		draggedTabIndex = index;
	}

	@Override
	void removeWindowComponent(DockingWindow window) {
		window.getTab().setContentComponent(null);
	}

	@Override
	void restoreWindowComponent(DockingWindow window) {
		window.getTab().setContentComponent(window);
	}

}
