/*
 * Copyright (c) 2005-2009 Laf-Widget Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *  o Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer. 
 *     
 *  o Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution. 
 *     
 *  o Neither the name of Laf-Widget Kirill Grouchnikov nor the names of 
 *    its contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 *     
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
package org.jvnet.lafwidget.tabbed;

import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;

import javax.swing.*;

import org.jvnet.lafwidget.LafWidgetUtilities2;
import org.jvnet.lafwidget.animation.*;
import org.jvnet.lafwidget.tabbed.TabPreviewThread.TabPreviewCallback;
import org.jvnet.lafwidget.tabbed.TabPreviewThread.TabPreviewInfo;

/**
 * Tab pager manager.
 * 
 * @author Kirill Grouchnikov
 */
public class TabPagerManager {
	/**
	 * Fade kind for hiding the tab pager.
	 */
	public static final FadeKind HIDE_TAB_PAGER_FADE_KIND = new FadeKind(
			"lafwidget.tabbed.hideTabPagerFadeKind", true);

	/**
	 * Singleton instance of the tab pager manager.
	 */
	protected static TabPagerManager instance;

	/**
	 * The tabbed pane that is currently paged.
	 */
	protected JTabbedPane currTabbedPane;

	/**
	 * Index of the central tab.
	 */
	protected int currTabIndex;

	/**
	 * Index of the next tab.
	 */
	protected int nextTabIndex;

	/**
	 * Index of the previous tab.
	 */
	protected int prevTabIndex;

	// protected Map smallPreviewMap;
	//
	// protected Map regularPreviewMap;
	//
	/**
	 * Preview window for the left (previous) tab.
	 */
	protected JWindow prevTabWindow;

	/**
	 * Preview window for the central (current) tab.
	 */
	protected JWindow currTabWindow;

	/**
	 * Preview window for the right (next) tab.
	 */
	protected JWindow nextTabWindow;

	/**
	 * Indicates whether the tab pager windows are visible.
	 */
	protected boolean isVisible;

	/**
	 * Implementation of the tab preview callback for the tab pager.
	 * 
	 * @author Kirill Grouchnikov.
	 */
	public class TabPagerPreviewCallback implements TabPreviewCallback {
		/**
		 * The associated preview window.
		 */
		private JWindow previewWindow;

		/**
		 * The associated tab preview control.
		 */
		private TabPreviewControl previewControl;

		/**
		 * Creates a new tab preview callback for the tab pager.
		 * 
		 * @param previewWindow
		 *            The associated preview window.
		 * @param tabPane
		 *            The associated tab preview control.
		 * @param tabIndex
		 *            Tab index.
		 */
		public TabPagerPreviewCallback(JWindow previewWindow,
				JTabbedPane tabPane, int tabIndex) {
			this.previewWindow = previewWindow;
			this.previewControl = new TabPreviewControl(tabPane, tabIndex);
			this.previewWindow.getContentPane().removeAll();
			this.previewWindow.getContentPane().add(this.previewControl,
					BorderLayout.CENTER);
			this.previewWindow.getContentPane().doLayout();
			this.previewControl.doLayout();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.jvnet.lafwidget.tabbed.TabPreviewThread.TabPreviewCallback#start(javax.swing.JTabbedPane,
		 *      int, org.jvnet.lafwidget.tabbed.TabPreviewThread.TabPreviewInfo)
		 */
		public void start(JTabbedPane tabPane, int tabCount,
				TabPreviewInfo tabPreviewInfo) {
			// Nothing to do since the callback was registered
			// for a specific tab.
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.jvnet.lafwidget.tabbed.TabPreviewThread.TabPreviewCallback#offer(javax.swing.JTabbedPane,
		 *      int, java.awt.image.BufferedImage)
		 */
		public void offer(JTabbedPane tabPane, int tabIndex,
				BufferedImage componentSnap) {
			if (TabPagerManager.this.currTabbedPane != tabPane) {
				// has since been cancelled
				return;
			}

			if (!this.previewWindow.isVisible()) {
				// has since been hidden
				return;
			}

			this.previewControl.setPreviewImage(componentSnap, true);
		}
	}

	/**
	 * Returns the tab pager instance.
	 * 
	 * @return Tab pager instance.
	 */
	public static synchronized TabPagerManager getPager() {
		if (TabPagerManager.instance == null)
			TabPagerManager.instance = new TabPagerManager();
		return TabPagerManager.instance;
	}

	/**
	 * Constructs a new tab pager manager. Is made private to enforce single
	 * instance.
	 */
	private TabPagerManager() {
		// this.smallPreviewMap = new HashMap();
		// this.regularPreviewMap = new HashMap();

		// Rectangle virtualBounds = new Rectangle();
		// GraphicsEnvironment ge = GraphicsEnvironment
		// .getLocalGraphicsEnvironment();
		// GraphicsDevice[] gds = ge.getScreenDevices();
		// for (int i = 0; i < gds.length; i++) {
		// GraphicsDevice gd = gds[i];
		// GraphicsConfiguration gc = gd.getDefaultConfiguration();
		// virtualBounds = virtualBounds.union(gc.getBounds());
		// }
		//
		// int screenWidth = virtualBounds.width;
		// int screenHeight = virtualBounds.height;

		this.currTabWindow = new JWindow();
		this.currTabWindow.getContentPane().setLayout(new BorderLayout());
		// int currWidth = screenWidth / 3;
		// int currHeight = screenHeight / 3;
		// this.currTabWindow.setSize(currWidth, currHeight);
		// // Fix for issue 178 on Substance (multiple screens)
		// this.currTabWindow.setLocation(currWidth + virtualBounds.x,
		// currHeight);
		this.currTabWindow.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						// fix for issue 177 in Substance (disallowing selection
						// of disabled tabs).
						TabPreviewPainter tpp = LafWidgetUtilities2
								.getTabPreviewPainter(currTabbedPane);
						if (tpp.isSensitiveToEvents(currTabbedPane,
								currTabIndex)) {
							hide();
							currTabbedPane.setSelectedIndex(currTabIndex);
						}
					}
				});
			}
		});
		this.currTabWindow
				.addMouseWheelListener(new TabPagerMouseWheelListener());

		// int smallWidth = 2 * screenWidth / 9;
		// int smallHeight = 2 * screenHeight / 9;
		this.prevTabWindow = new JWindow();
		this.prevTabWindow.getContentPane().setLayout(new BorderLayout());
		// this.prevTabWindow.setSize(smallWidth, smallHeight);
		// // Fix for issue 178 on Substance (multiple screens)
		// this.prevTabWindow.setLocation((screenWidth / 18) + virtualBounds.x,
		// 7 * screenHeight / 18);
		this.prevTabWindow.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						// fix for issue 177 in Substance (disallowing selection
						// of disabled tabs).
						TabPreviewPainter tpp = LafWidgetUtilities2
								.getTabPreviewPainter(currTabbedPane);
						if (tpp.isSensitiveToEvents(currTabbedPane,
								prevTabIndex)) {
							hide();
							currTabbedPane.setSelectedIndex(prevTabIndex);
						}
					}
				});
			}
		});
		this.prevTabWindow
				.addMouseWheelListener(new TabPagerMouseWheelListener());

		this.nextTabWindow = new JWindow();
		// this.nextTabWindow.getContentPane().setLayout(new BorderLayout());
		// this.nextTabWindow.setSize(smallWidth, smallHeight);
		// // Fix for issue 178 on Substance (multiple screens)
		// this.nextTabWindow.setLocation((13 * screenWidth / 18)
		// + virtualBounds.x, 7 * screenHeight / 18);
		this.nextTabWindow.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						// fix for issue 177 in Substance (disallowing selection
						// of disabled tabs).
						TabPreviewPainter tpp = LafWidgetUtilities2
								.getTabPreviewPainter(currTabbedPane);
						if (tpp.isSensitiveToEvents(currTabbedPane,
								nextTabIndex)) {
							hide();
							currTabbedPane.setSelectedIndex(nextTabIndex);
						}
					}
				});
			}
		});
		this.nextTabWindow
				.addMouseWheelListener(new TabPagerMouseWheelListener());

		this.recomputeBounds();

		this.isVisible = false;
	}

	/**
	 * Recomputes the bounds of tab pager windows.
	 */
	private void recomputeBounds() {
		Rectangle virtualBounds = new Rectangle();
		GraphicsEnvironment ge = GraphicsEnvironment
				.getLocalGraphicsEnvironment();
		GraphicsDevice[] gds = ge.getScreenDevices();
		for (int i = 0; i < gds.length; i++) {
			GraphicsDevice gd = gds[i];
			GraphicsConfiguration gc = gd.getDefaultConfiguration();
			virtualBounds = virtualBounds.union(gc.getBounds());
		}

		int screenWidth = virtualBounds.width;
		int screenHeight = virtualBounds.height;

		int currWidth = screenWidth / 3;
		int currHeight = screenHeight / 3;
		this.currTabWindow.setSize(currWidth, currHeight);
		// Fix for issue 178 on Substance (multiple screens)
		this.currTabWindow.setLocation(currWidth + virtualBounds.x, currHeight);

		int smallWidth = 2 * screenWidth / 9;
		int smallHeight = 2 * screenHeight / 9;
		this.prevTabWindow.setSize(smallWidth, smallHeight);
		// Fix for issue 178 on Substance (multiple screens)
		this.prevTabWindow.setLocation((screenWidth / 18) + virtualBounds.x,
				7 * screenHeight / 18);

		this.nextTabWindow.getContentPane().setLayout(new BorderLayout());
		this.nextTabWindow.setSize(smallWidth, smallHeight);
		// Fix for issue 178 on Substance (multiple screens)
		this.nextTabWindow.setLocation((13 * screenWidth / 18)
				+ virtualBounds.x, 7 * screenHeight / 18);
	}

	/**
	 * Sets the tabbed pane on <code>this</code> tab pager manager.
	 * 
	 * @param jtp
	 *            Tabbed pane to page.
	 */
	private void setTabbedPane(JTabbedPane jtp) {
		if (this.currTabbedPane == jtp)
			return;
		this.currTabbedPane = jtp;
		// this.smallPreviewMap.clear();
		// this.regularPreviewMap.clear();
	}

	/**
	 * Flips the pages.
	 * 
	 * @param tabbedPane
	 *            Tabbed pane.
	 * @param isForward
	 *            if <code>true</code>, the tabs are flipped one page (tab)
	 *            forward, if <code>false</code>, the tabs are flipped one
	 *            page (tab) backward.
	 */
	public synchronized void page(JTabbedPane tabbedPane, boolean isForward) {
		this.setTabbedPane(tabbedPane);
		if (!this.isVisible) {
			this.recomputeBounds();
			this.currTabWindow.setVisible(true);
			this.prevTabWindow.setVisible(true);
			this.nextTabWindow.setVisible(true);
			this.isVisible = true;
			this.currTabIndex = this.currTabbedPane.getSelectedIndex();
		}

		int delta = isForward ? 1 : -1;
		this.currTabIndex += delta;
		if (this.currTabIndex == this.currTabbedPane.getTabCount())
			this.currTabIndex = 0;
		if (this.currTabIndex == -1)
			this.currTabIndex = this.currTabbedPane.getTabCount() - 1;

		this.nextTabIndex = this.currTabIndex + 1;
		this.prevTabIndex = this.currTabIndex - 1;
		if (this.nextTabIndex == this.currTabbedPane.getTabCount())
			this.nextTabIndex = 0;
		if (this.prevTabIndex == -1)
			this.prevTabIndex = this.currTabbedPane.getTabCount() - 1;

		TabPreviewThread.TabPreviewInfo currTabPreviewInfo = new TabPreviewThread.TabPreviewInfo();
		currTabPreviewInfo.tabPane = this.currTabbedPane;
		currTabPreviewInfo.tabIndexToPreview = this.currTabIndex;
		currTabPreviewInfo.previewWidth = this.currTabWindow.getWidth() - 4;
		currTabPreviewInfo.previewHeight = this.currTabWindow.getHeight() - 20;
		currTabPreviewInfo.previewCallback = new TabPagerPreviewCallback(
				this.currTabWindow, this.currTabbedPane, this.currTabIndex);
		currTabPreviewInfo.initiator = this;
		TabPreviewPainter previewPainter = LafWidgetUtilities2
				.getTabPreviewPainter(currTabPreviewInfo.tabPane);
		if ((previewPainter != null)
				&& (previewPainter.hasPreviewWindow(this.currTabbedPane,
						this.currTabIndex))) {
			TabPreviewThread.getInstance().queueTabPreviewRequest(
					currTabPreviewInfo);
		}

		TabPreviewThread.TabPreviewInfo prevTabPreviewInfo = new TabPreviewThread.TabPreviewInfo();
		prevTabPreviewInfo.tabPane = this.currTabbedPane;
		prevTabPreviewInfo.tabIndexToPreview = this.prevTabIndex;
		prevTabPreviewInfo.previewWidth = this.prevTabWindow.getWidth() - 4;
		prevTabPreviewInfo.previewHeight = this.prevTabWindow.getHeight() - 20;
		prevTabPreviewInfo.previewCallback = new TabPagerPreviewCallback(
				this.prevTabWindow, this.currTabbedPane, this.prevTabIndex);
		prevTabPreviewInfo.initiator = this;
		if ((previewPainter != null)
				&& (previewPainter.hasPreviewWindow(this.currTabbedPane,
						this.prevTabIndex))) {
			TabPreviewThread.getInstance().queueTabPreviewRequest(
					prevTabPreviewInfo);
		}

		TabPreviewThread.TabPreviewInfo nextTabPreviewInfo = new TabPreviewThread.TabPreviewInfo();
		nextTabPreviewInfo.tabPane = this.currTabbedPane;
		nextTabPreviewInfo.tabIndexToPreview = this.nextTabIndex;
		nextTabPreviewInfo.previewWidth = this.nextTabWindow.getWidth() - 4;
		nextTabPreviewInfo.previewHeight = this.nextTabWindow.getHeight() - 20;
		nextTabPreviewInfo.previewCallback = new TabPagerPreviewCallback(
				this.nextTabWindow, this.currTabbedPane, this.nextTabIndex);
		nextTabPreviewInfo.initiator = this;
		if ((previewPainter != null)
				&& (previewPainter.hasPreviewWindow(this.currTabbedPane,
						this.nextTabIndex))) {
			TabPreviewThread.getInstance().queueTabPreviewRequest(
					nextTabPreviewInfo);
		}

	}

	/**
	 * Flips the pages in the currently shown tabbed pane.
	 * 
	 * @param isForward
	 *            if <code>true</code>, the tabs are flipped one page (tab)
	 *            forward, if <code>false</code>, the tabs are flipped one
	 *            page (tab) backward.
	 */
	public void page(boolean isForward) {
		if (this.currTabbedPane == null)
			return;
		this.page(this.currTabbedPane, isForward);
	}

	/**
	 * Returns indication whether the tab pager windows are showing.
	 * 
	 * @return <code>true</code> if the tab pager windows are visible,
	 *         <code>false</code> otherwise.
	 */
	public boolean isVisible() {
		return this.isVisible;
	}

	/**
	 * Hides the tab pager windows.
	 * 
	 * @return The index of the center (current) tab.
	 */
	public synchronized int hide() {
		int result = this.isVisible ? this.currTabIndex : -1;

		final Point currWindowLocation = this.currTabWindow.getLocation();
		final Dimension currWindowSize = this.currTabWindow.getSize();
		final Point nextWindowLocation = this.nextTabWindow.getLocation();
		final Dimension nextWindowSize = this.nextTabWindow.getSize();
		final Point prevWindowLocation = this.prevTabWindow.getLocation();
		final Dimension prevWindowSize = this.prevTabWindow.getSize();
		FadeTracker.getInstance().trackFadeOut(HIDE_TAB_PAGER_FADE_KIND,
				this.currTabbedPane, false, new UIThreadFadeTrackerAdapter() {
					@Override
					public void fadePerformed(FadeKind fadeKind,
							float fadeCycle) {
						int cx = currWindowLocation.x + currWindowSize.width
								/ 2;
						int cy = currWindowLocation.y + currWindowSize.height
								/ 2;
						int nWidth = (int) (currWindowSize.width * fadeCycle);
						int nHeight = (int) (currWindowSize.height
								* fadeCycle);
						currTabWindow.setBounds(cx - nWidth / 2, cy - nHeight
								/ 2, nWidth, nHeight);

						cx = prevWindowLocation.x + prevWindowSize.width / 2;
						cy = prevWindowLocation.y + prevWindowSize.height / 2;
						nWidth = (int) (prevWindowSize.width * fadeCycle);
						nHeight = (int) (prevWindowSize.height * fadeCycle);
						prevTabWindow.setBounds(cx - nWidth / 2, cy - nHeight
								/ 2, nWidth, nHeight);

						cx = nextWindowLocation.x + nextWindowSize.width / 2;
						cy = nextWindowLocation.y + nextWindowSize.height / 2;
						nWidth = (int) (nextWindowSize.width * fadeCycle);
						nHeight = (int) (nextWindowSize.height * fadeCycle);
						nextTabWindow.setBounds(cx - nWidth / 2, cy - nHeight
								/ 2, nWidth, nHeight);

						currTabWindow.getRootPane().doLayout();
						currTabWindow.repaint();

						nextTabWindow.getRootPane().doLayout();
						nextTabWindow.repaint();

						prevTabWindow.getRootPane().doLayout();
						prevTabWindow.repaint();
					}

					@Override
					public void fadeEnded(FadeKind fadeKind) {
						currTabWindow.setVisible(false);
						currTabWindow.dispose();
						prevTabWindow.setVisible(false);
						prevTabWindow.dispose();
						nextTabWindow.setVisible(false);
						nextTabWindow.dispose();
					}
				});

		// FadeTracker.getInstance().trackFadeOut(HIDE_TAB_PAGER_FADE_KIND,
		// this.prevTabWindow, false, new FadeTrackerCallback() {
		// public void fadePerformed(FadeKind fadeKind,
		// float fadeCycle10) {
		// int cx = prevWindowLocation.x + prevWindowSize.width
		// / 2;
		// int cy = prevWindowLocation.y + prevWindowSize.height
		// / 2;
		// int nWidth = (int) (prevWindowSize.width * fadeCycle);
		// int nHeight = (int) (prevWindowSize.height
		// * fadeCycle);
		// prevTabWindow.setBounds(cx - nWidth / 2, cy - nHeight
		// / 2, nWidth, nHeight);
		// }
		//
		// public void fadeEnded(FadeKind fadeKind) {
		// prevTabWindow.setVisible(false);
		// prevTabWindow.dispose();
		// }
		// });
		//
		// FadeTracker.getInstance().trackFadeOut(HIDE_TAB_PAGER_FADE_KIND,
		// this.nextTabWindow, false, new FadeTrackerCallback() {
		// public void fadePerformed(FadeKind fadeKind,
		// float fadeCycle10) {
		// int cx = nextWindowLocation.x + nextWindowSize.width
		// / 2;
		// int cy = nextWindowLocation.y + nextWindowSize.height
		// / 2;
		// int nWidth = (int) (nextWindowSize.width * fadeCycle);
		// int nHeight = (int) (nextWindowSize.height
		// * fadeCycle);
		// nextTabWindow.setBounds(cx - nWidth / 2, cy - nHeight
		// / 2, nWidth, nHeight);
		// }
		//
		// public void fadeEnded(FadeKind fadeKind) {
		// nextTabWindow.setVisible(false);
		// nextTabWindow.dispose();
		// }
		// });
		//
		// this.prevTabWindow.setVisible(false);
		// this.prevTabWindow.dispose();
		// this.nextTabWindow.setVisible(false);
		// this.nextTabWindow.dispose();
		this.isVisible = false;
		return result;
	}

	/**
	 * Resets the internal caches.
	 */
	public static void reset() {
		// TabPagerManager.instance.regularPreviewMap.clear();
		// TabPagerManager.instance.smallPreviewMap.clear();
	}
}
