/*
 * Copyright (c) 2005-2010 Flamingo 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 Flamingo 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.pushingpixels.flamingo.internal.ui.common.popup;

import java.awt.*;
import java.awt.geom.AffineTransform;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ComponentUI;

import org.pushingpixels.flamingo.api.common.*;
import org.pushingpixels.flamingo.api.common.popup.*;
import org.pushingpixels.flamingo.api.common.popup.PopupPanelManager.PopupEvent;
import org.pushingpixels.flamingo.internal.ui.common.BasicCommandButtonPanelUI;
import org.pushingpixels.flamingo.internal.ui.common.CommandButtonLayoutManagerMedium;
import org.pushingpixels.flamingo.internal.utils.FlamingoUtilities;

public class BasicCommandPopupMenuUI extends BasicPopupPanelUI {
	/**
	 * The associated popup menu
	 */
	protected JCommandPopupMenu popupMenu;

	protected ChangeListener popupMenuChangeListener;

	protected PopupPanelManager.PopupListener popupListener;

	protected ScrollableCommandButtonPanel commandButtonPanel;

	protected JScrollablePanel<JPanel> menuItemsPanel;

	public static final String FORCE_ICON = "flamingo.internal.commandButtonLayoutManagerMedium.forceIcon";

	protected static final CommandButtonDisplayState POPUP_MENU = new CommandButtonDisplayState(
			"Popup menu", 16) {
		@Override
		public CommandButtonLayoutManager createLayoutManager(
				AbstractCommandButton commandButton) {
			return new CommandButtonLayoutManagerMedium() {
				@Override
				protected float getIconTextGapFactor() {
					return 2.0f;
				};
			};
		}
	};

	/**
	 * Popup panel that hosts groups of icons.
	 * 
	 * @author Kirill Grouchnikov
	 */
	protected static class ScrollableCommandButtonPanel extends JComponent {
		/**
		 * Maximum dimension of <code>this</code> popup gallery.
		 */
		protected Dimension maxDimension;

		/**
		 * The internal panel that hosts the icon command buttons. Is hosted in
		 * the {@link #scroll}.
		 */
		protected JCommandButtonPanel buttonPanel;

		/**
		 * The maximum number of visible button rows.
		 */
		protected int maxVisibleButtonRows;

		/**
		 * Scroll panel that hosts {@link #buttonPanel}.
		 */
		protected JScrollPane scroll;

		/**
		 * Creates new a icon popup panel.
		 * 
		 * @param iconPanel
		 *            The internal panel that hosts icon command buttons.
		 * @param maxButtonColumns
		 *            The maximum number of button columns.
		 * @param maxVisibleButtonRows
		 *            The maximum number of visible button rows.
		 */
		public ScrollableCommandButtonPanel(JCommandButtonPanel iconPanel,
				int maxButtonColumns, int maxVisibleButtonRows) {
			this.buttonPanel = iconPanel;
			this.buttonPanel.setMaxButtonColumns(maxButtonColumns);
			this.maxVisibleButtonRows = maxVisibleButtonRows;

			int maxButtonWidth = 0;
			int maxButtonHeight = 0;
			int groupCount = iconPanel.getGroupCount();
			for (int i = 0; i < groupCount; i++) {
				for (AbstractCommandButton button : iconPanel
						.getGroupButtons(i)) {
					maxButtonWidth = Math.max(maxButtonWidth, button
							.getPreferredSize().width);
					maxButtonHeight = Math.max(maxButtonHeight, button
							.getPreferredSize().height);
				}
			}

			updateMaxDimension();

			this.scroll = new JScrollPane(this.buttonPanel,
					JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
					JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
			this.scroll.setBorder(new EmptyBorder(0, 0, 0, 0));
			this.buttonPanel.setBorder(new EmptyBorder(0, 0, 0, 0));
			this.scroll.setOpaque(false);
			this.scroll.getViewport().setOpaque(false);
			this.setLayout(new IconPopupLayout());

			this.add(this.scroll);

			this.setBorder(new Border() {
				@Override
				public Insets getBorderInsets(Component c) {
					return new Insets(0, 0, 1, 0);
				}

				@Override
				public boolean isBorderOpaque() {
					return true;
				}

				@Override
				public void paintBorder(Component c, Graphics g, int x, int y,
						int width, int height) {
					g.setColor(FlamingoUtilities.getBorderColor());
					g.drawLine(x, y + height - 1, x + width, y + height - 1);
				}
			});
		}

		/**
		 * Updates the max dimension of this panel. This method is for internal
		 * use only.
		 */
		public void updateMaxDimension() {
			if (this.buttonPanel == null)
				return;
			this.buttonPanel.setPreferredSize(null);
			Dimension prefIconPanelDim = this.buttonPanel.getPreferredSize();
			// fix for issue 13 - respect the gaps and insets
			BasicCommandButtonPanelUI panelUI = (BasicCommandButtonPanelUI) buttonPanel
					.getUI();
			int titlePanelCount = buttonPanel.isToShowGroupLabels() ? 1 : 0;
			this.maxDimension = new Dimension(prefIconPanelDim.width, panelUI
					.getPreferredHeight(this.maxVisibleButtonRows,
							titlePanelCount));
			this.setPreferredSize(null);
		}

		/**
		 * Layout manager for <code>this</code> popup gallery.
		 * 
		 * @author Kirill Grouchnikov
		 */
		protected class IconPopupLayout implements LayoutManager {
			/*
			 * (non-Javadoc)
			 * 
			 * @see java.awt.LayoutManager#addLayoutComponent(java.lang.String,
			 * java.awt.Component)
			 */
			public void addLayoutComponent(String name, Component comp) {
			}

			/*
			 * (non-Javadoc)
			 * 
			 * @see
			 * java.awt.LayoutManager#removeLayoutComponent(java.awt.Component)
			 */
			public void removeLayoutComponent(Component comp) {
			}

			/*
			 * (non-Javadoc)
			 * 
			 * @see java.awt.LayoutManager#layoutContainer(java.awt.Container)
			 */
			public void layoutContainer(Container parent) {
				Insets insets = parent.getInsets();
				int left = insets.left;
				int right = insets.right;
				int top = insets.top;
				int bottom = insets.bottom;
				scroll.setBounds(left, top, parent.getWidth() - left - right,
						parent.getHeight() - top - bottom);
			}

			/*
			 * (non-Javadoc)
			 * 
			 * @see java.awt.LayoutManager#minimumLayoutSize(java.awt.Container)
			 */
			public Dimension minimumLayoutSize(Container parent) {
				return this.preferredLayoutSize(parent);
			}

			/*
			 * (non-Javadoc)
			 * 
			 * @see
			 * java.awt.LayoutManager#preferredLayoutSize(java.awt.Container)
			 */
			public Dimension preferredLayoutSize(Container parent) {
				Insets insets = parent.getInsets();
				int left = insets.left;
				int right = insets.right;
				int top = insets.top;
				int bottom = insets.bottom;
				Dimension controlPanelDim = buttonPanel.getPreferredSize();
				if (controlPanelDim == null)
					controlPanelDim = new Dimension(0, 0);
				int w = Math.min(controlPanelDim.width, maxDimension.width)
						+ left + right;
				int h = Math.min(controlPanelDim.height, maxDimension.height)
						+ top + bottom;
				if (h == (maxDimension.height + top + bottom)) {
					int scrollBarWidth = UIManager.getInt("ScrollBar.width");
					if (scrollBarWidth == 0) {
						// Nimbus
						scrollBarWidth = new JScrollBar(JScrollBar.VERTICAL)
								.getPreferredSize().width;
					}
					w += scrollBarWidth;
					// h += 5;
				}
				return new Dimension(w, h);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent)
	 */
	public static ComponentUI createUI(JComponent c) {
		return new BasicCommandPopupMenuUI();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.jvnet.flamingo.common.ui.BasicPopupPanelUI#installUI(javax.swing.
	 * JComponent)
	 */
	@Override
	public void installUI(JComponent c) {
		this.popupMenu = (JCommandPopupMenu) c;
		super.installUI(this.popupMenu);

		this.popupMenu.setLayout(this.createLayoutManager());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jvnet.flamingo.common.ui.BasicPopupPanelUI#installComponents()
	 */
	@Override
	protected void installComponents() {
		super.installComponents();

		syncComponents();
	}

	protected void syncComponents() {
		if (this.popupMenu.hasCommandButtonPanel()) {
			this.commandButtonPanel = createScrollableButtonPanel();
			this.popupMenu.add(this.commandButtonPanel);
		}

		final JPanel menuPanel = this.createMenuPanel();
		menuPanel.setLayout(new LayoutManager() {
			@Override
			public void addLayoutComponent(String name, Component comp) {
			}

			@Override
			public void removeLayoutComponent(Component comp) {
			}

			@Override
			public Dimension preferredLayoutSize(Container parent) {
				int height = 0;
				int width = 0;
				for (int i = 0; i < parent.getComponentCount(); i++) {
					Dimension pref = parent.getComponent(i).getPreferredSize();
					height += pref.height;
					width = Math.max(width, pref.width);
				}

				Insets ins = parent.getInsets();
				return new Dimension(width + ins.left + ins.right, height
						+ ins.top + ins.bottom);
			}

			@Override
			public Dimension minimumLayoutSize(Container parent) {
				return preferredLayoutSize(parent);
			}

			@Override
			public void layoutContainer(Container parent) {
				Insets ins = parent.getInsets();

				int topY = ins.top;
				for (int i = 0; i < parent.getComponentCount(); i++) {
					Component comp = parent.getComponent(i);
					Dimension pref = comp.getPreferredSize();
					comp.setBounds(ins.left, topY, parent.getWidth() - ins.left
							- ins.right, pref.height);
					topY += pref.height;
				}
			}
		});

		this.popupMenu.putClientProperty(BasicCommandPopupMenuUI.FORCE_ICON,
				null);
		java.util.List<Component> menuComponents = this.popupMenu
				.getMenuComponents();
		if (menuComponents != null) {
			for (Component menuComponent : menuComponents) {
				menuPanel.add(menuComponent);
			}

			boolean atLeastOneButtonHasIcon = false;
			for (Component menuComponent : menuComponents) {
				if (menuComponent instanceof JCommandMenuButton) {
					JCommandMenuButton menuButton = (JCommandMenuButton) menuComponent;
					if (menuButton.getIcon() != null) {
						atLeastOneButtonHasIcon = true;
					}
				}
				if (menuComponent instanceof JCommandToggleMenuButton) {
					atLeastOneButtonHasIcon = true;
				}
			}

			this.popupMenu.putClientProperty(
					BasicCommandPopupMenuUI.FORCE_ICON,
					atLeastOneButtonHasIcon ? Boolean.TRUE : null);
			for (Component menuComponent : menuComponents) {
				if (menuComponent instanceof JCommandMenuButton) {
					JCommandMenuButton menuButton = (JCommandMenuButton) menuComponent;
					menuButton.putClientProperty(
							BasicCommandPopupMenuUI.FORCE_ICON,
							atLeastOneButtonHasIcon ? Boolean.TRUE : null);
					menuButton.setDisplayState(POPUP_MENU);
				}
				if (menuComponent instanceof JCommandToggleMenuButton) {
					JCommandToggleMenuButton menuButton = (JCommandToggleMenuButton) menuComponent;
					menuButton.putClientProperty(
							BasicCommandPopupMenuUI.FORCE_ICON, Boolean.TRUE);
					menuButton.setDisplayState(POPUP_MENU);
				}
			}
		}

		this.menuItemsPanel = new JScrollablePanel<JPanel>(menuPanel,
				JScrollablePanel.ScrollType.VERTICALLY);
		final LayoutManager scrollableLm = this.menuItemsPanel.getLayout();
		this.menuItemsPanel.setLayout(new LayoutManager() {
			@Override
			public void addLayoutComponent(String name, Component comp) {
				scrollableLm.addLayoutComponent(name, comp);
			}

			@Override
			public void removeLayoutComponent(Component comp) {
				scrollableLm.removeLayoutComponent(comp);
			}

			@Override
			public Dimension preferredLayoutSize(Container parent) {
				Dimension result = menuPanel.getPreferredSize();
				int maxMenuButtonCount = popupMenu.getMaxVisibleMenuButtons();
				if ((maxMenuButtonCount < 0)
						|| (maxMenuButtonCount >= menuPanel.getComponentCount())) {
					return result;
				}
				// the assumption is that all menu buttons have the
				// same height.
				int singleHeight = menuPanel.getComponent(0).getPreferredSize().height;
				int width = 0;
				for (int i = 0; i < menuPanel.getComponentCount(); i++) {
					width = Math.max(width, menuPanel.getComponent(i)
							.getPreferredSize().width);
				}
				Insets ins = parent.getInsets();
				// add two for scroller buttons
				return new Dimension(width + ins.left + ins.right, singleHeight
						* (maxMenuButtonCount + 2) + ins.top + ins.bottom);
			}

			@Override
			public Dimension minimumLayoutSize(Container parent) {
				return this.preferredLayoutSize(parent);
			}

			@Override
			public void layoutContainer(Container parent) {
				scrollableLm.layoutContainer(parent);
			}
		});
		this.popupMenu.add(this.menuItemsPanel);
	}

	protected ScrollableCommandButtonPanel createScrollableButtonPanel() {
		return new ScrollableCommandButtonPanel(this.popupMenu
				.getMainButtonPanel(), this.popupMenu.getMaxButtonColumns(),
				this.popupMenu.getMaxVisibleButtonRows());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jvnet.flamingo.common.ui.BasicPopupPanelUI#uninstallComponents()
	 */
	@Override
	protected void uninstallComponents() {
		this.popupMenu.removeAll();
		super.uninstallComponents();
	}

	@Override
	protected void installListeners() {
		super.installListeners();

		this.popupMenuChangeListener = new ChangeListener() {
			@Override
			public void stateChanged(ChangeEvent e) {
				popupMenu.removeAll();
				syncComponents();
			}
		};
		this.popupMenu.addChangeListener(this.popupMenuChangeListener);

		this.popupListener = new PopupPanelManager.PopupListener() {
			@Override
			public void popupShown(PopupEvent event) {
			}

			@Override
			public void popupHidden(PopupEvent event) {
				if (event.getSource() instanceof JColorSelectorPopupMenu) {
					((JColorSelectorPopupMenu) event.getSource())
							.getColorSelectorCallback().onColorRollover(null);
				}
			}
		};
		PopupPanelManager.defaultManager().addPopupListener(this.popupListener);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jvnet.flamingo.common.ui.BasicPopupPanelUI#uninstallListeners()
	 */
	@Override
	protected void uninstallListeners() {
		this.popupMenu.removeChangeListener(this.popupMenuChangeListener);
		this.popupMenuChangeListener = null;

		PopupPanelManager.defaultManager().addPopupListener(this.popupListener);
		this.popupListener = null;

		super.uninstallListeners();
	}

	protected JPanel createMenuPanel() {
		return new MenuPanel();
	}

	protected LayoutManager createLayoutManager() {
		return new PopupMenuLayoutManager();
	}

	protected class PopupMenuLayoutManager implements LayoutManager {
		@Override
		public void addLayoutComponent(String name, Component comp) {
		}

		@Override
		public void removeLayoutComponent(Component comp) {
		}

		@Override
		public Dimension minimumLayoutSize(Container parent) {
			return null;
		}

		@Override
		public Dimension preferredLayoutSize(Container parent) {
			int height = 0;
			int width = 0;

			if (commandButtonPanel != null) {
				width = commandButtonPanel.getPreferredSize().width;
				height = commandButtonPanel.getPreferredSize().height;
			}
			Dimension menuItemsPref = (popupMenu.getMaxVisibleMenuButtons() > 0) ? menuItemsPanel
					.getPreferredSize()
					: menuItemsPanel.getView().getPreferredSize();
			width = Math.max(menuItemsPref.width, width);
			height += menuItemsPref.height;

			Insets ins = parent.getInsets();
			return new Dimension(width + ins.left + ins.right, height + ins.top
					+ ins.bottom);
		}

		@Override
		public void layoutContainer(Container parent) {
			Insets ins = parent.getInsets();

			int bottomY = parent.getHeight() - ins.bottom;
			Dimension menuItemsPref = (popupMenu.getMaxVisibleMenuButtons() > 0) ? menuItemsPanel
					.getPreferredSize()
					: menuItemsPanel.getView().getPreferredSize();
			menuItemsPanel.setBounds(ins.left, bottomY - menuItemsPref.height,
					parent.getWidth() - ins.left - ins.right,
					menuItemsPref.height);
			menuItemsPanel.doLayout();
			bottomY -= menuItemsPref.height;

			if (commandButtonPanel != null) {
				commandButtonPanel.setBounds(ins.left, ins.top, parent
						.getWidth()
						- ins.left - ins.right, bottomY - ins.top);
				commandButtonPanel.invalidate();
				commandButtonPanel.validate();
				commandButtonPanel.doLayout();
			}
		}
	}

	protected static class MenuPanel extends JPanel {
		@Override
		public void paintComponent(Graphics g) {
			super.paintComponent(g);
			JCommandPopupMenu menu = (JCommandPopupMenu) SwingUtilities
					.getAncestorOfClass(JCommandPopupMenu.class, this);
			if (Boolean.TRUE.equals(menu.getClientProperty(FORCE_ICON))) {
				this.paintIconGutterBackground(g);
				this.paintIconGutterSeparator(g);
			}
		}

		protected int getSeparatorX() {
			JCommandPopupMenu menu = (JCommandPopupMenu) SwingUtilities
					.getAncestorOfClass(JCommandPopupMenu.class, this);
			if (!Boolean.TRUE.equals(menu.getClientProperty(FORCE_ICON))) {
				return -1;
			}
			java.util.List<Component> menuComponents = menu.getMenuComponents();
			if (menuComponents != null) {
				for (Component menuComponent : menuComponents) {
					if (menuComponent instanceof JCommandMenuButton
							|| menuComponent instanceof JCommandToggleMenuButton) {
						AbstractCommandButton button = (AbstractCommandButton) menuComponent;
						if (!Boolean.TRUE.equals(button
								.getClientProperty(FORCE_ICON))) {
							continue;
						}
						boolean ltr = button.getComponentOrientation()
								.isLeftToRight();
						CommandButtonLayoutManager.CommandButtonLayoutInfo layoutInfo = button
								.getUI().getLayoutInfo();
						if (ltr) {
							int iconRight = layoutInfo.iconRect.x
									+ layoutInfo.iconRect.width;
							int textLeft = button.getWidth();
							for (CommandButtonLayoutManager.TextLayoutInfo tli : layoutInfo.textLayoutInfoList) {
								textLeft = Math.min(textLeft, tli.textRect.x);
							}
							return (iconRight + textLeft) / 2;
						} else {
							int iconLeft = layoutInfo.iconRect.x;
							int textRight = 0;
							for (CommandButtonLayoutManager.TextLayoutInfo tli : layoutInfo.textLayoutInfoList) {
								textRight = Math.max(textRight, tli.textRect.x
										+ tli.textRect.width);
							}
							return (iconLeft + textRight) / 2;
						}
					}
				}
			}
			throw new IllegalStateException(
					"Menu marked to show icons but no menu buttons in it");
		}

		protected void paintIconGutterSeparator(Graphics g) {
			CellRendererPane buttonRendererPane = new CellRendererPane();
			JSeparator rendererSeparator = new JSeparator(JSeparator.VERTICAL);

			buttonRendererPane.setBounds(0, 0, this.getWidth(), this
					.getHeight());
			int sepX = this.getSeparatorX();
			if (this.getComponentOrientation().isLeftToRight()) {
				buttonRendererPane.paintComponent(g, rendererSeparator, this,
						sepX, 2, 2, this.getHeight() - 4, true);
			} else {
				buttonRendererPane.paintComponent(g, rendererSeparator, this,
						sepX, 2, 2, this.getHeight() - 4, true);
			}
		}

		protected void paintIconGutterBackground(Graphics g) {
			Graphics2D g2d = (Graphics2D) g.create();
			g2d.setComposite(AlphaComposite.SrcOver.derive(0.7f));

			int sepX = this.getSeparatorX();
			if (this.getComponentOrientation().isLeftToRight()) {
				g2d.clipRect(0, 0, sepX + 2, this.getHeight());
				AffineTransform at = AffineTransform.getTranslateInstance(0,
						this.getHeight());
				at.rotate(-Math.PI / 2);
				g2d.transform(at);

				FlamingoUtilities.renderSurface(g2d, this, new Rectangle(0, 0,
						this.getHeight(), 50), false, false, false);
			} else {
				g2d.clipRect(this.getWidth() - sepX, 0, sepX + 2, this
						.getHeight());
				AffineTransform at = AffineTransform.getTranslateInstance(0,
						this.getHeight());
				at.rotate(-Math.PI / 2);
				g2d.transform(at);

				FlamingoUtilities.renderSurface(g2d, this, new Rectangle(0,
						sepX, this.getHeight(), this.getWidth() - sepX), false,
						false, false);
			}

			g2d.dispose();
		}
	}
}