/*******************************************************************************
 * Copyright (c) 2008, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.gef.internal.ui.palette.editparts;

import java.util.Iterator;

import org.eclipse.draw2d.AbstractLayout;
import org.eclipse.draw2d.Animation;
import org.eclipse.draw2d.BorderLayout;
import org.eclipse.draw2d.ButtonModel;
import org.eclipse.draw2d.ChangeEvent;
import org.eclipse.draw2d.ChangeListener;
import org.eclipse.draw2d.Clickable;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.FlowLayout;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.MarginBorder;
import org.eclipse.draw2d.StackLayout;
import org.eclipse.draw2d.Toggle;
import org.eclipse.draw2d.ToolbarLayout;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.Rectangle;

import org.eclipse.gef.internal.ui.palette.PaletteColorUtil;
import org.eclipse.gef.internal.ui.palette.editparts.ToolEntryEditPart.ToolEntryToggle;
import org.eclipse.gef.ui.palette.PaletteViewerPreferences;

/**
 * A pinnable palette stack figure.
 * 
 * @author crevells
 * @since 3.4
 */
public class PinnablePaletteStackFigure extends Figure {

	private static final Dimension EMPTY_DIMENSION = new Dimension(0, 0);

	static final int ARROW_WIDTH = 9;

	/**
	 * A toggle with a triangle figure.
	 */
	class RolloverArrow extends Toggle {

		RolloverArrow() {
			super();
			setRolloverEnabled(true);
			setBorder(null);
			setOpaque(false);
			setPreferredSize(ARROW_WIDTH, -1);
		}

		/**
		 * @return false so that the focus rectangle is not drawn.
		 */
		public boolean hasFocus() {
			return false;
		}

		public void paintFigure(Graphics graphics) {
			Rectangle rect = getClientArea();

			ButtonModel model = getModel();
			if (isRolloverEnabled() && model.isMouseOver()) {
				graphics.setBackgroundColor(PaletteColorUtil.ARROW_HOVER);
				graphics.fillRoundRectangle(rect, 3, 3);
			}

			graphics.translate(getLocation());

			// fill the arrow
			int[] points = new int[8];

			int middleY = rect.height / 2;
			if (isSelected()
					|| layoutMode == PaletteViewerPreferences.LAYOUT_ICONS
					|| layoutMode == PaletteViewerPreferences.LAYOUT_COLUMNS) {
				// pointing down
				int startingX = (ARROW_WIDTH - 5) / 2; // triangle width = 5
				points[0] = startingX;
				points[1] = middleY;
				points[2] = startingX + 5;
				points[3] = middleY;
				points[4] = startingX + 2;
				points[5] = middleY + 3;
				points[6] = startingX;
				points[7] = middleY;
			} else {
				// pointing to the right
				int startingX = (ARROW_WIDTH - 3) / 2; // triangle width = 3
				points[0] = startingX;
				points[1] = middleY - 2;
				points[2] = startingX + 3;
				points[3] = middleY + 1;
				points[4] = startingX;
				points[5] = middleY + 4;
				points[6] = startingX;
				points[7] = middleY - 2;
			}

			graphics.setBackgroundColor(PaletteColorUtil.WIDGET_DARK_SHADOW);
			graphics.fillPolygon(points);

			graphics.translate(getLocation().getNegated());
		}
	}

	/**
	 * Layout manager for the palette stack header figure that handles the
	 * layout of the <code>headerFigure</code> (<code>arrowFigure</code> and the
	 * active tool figure) when in icons or columns layout mode.
	 */
	private class HeaderIconLayout extends StackLayout {

		protected boolean isSensitiveVertically(IFigure container) {
			return false;
		}

		public void layout(IFigure parent) {

			Rectangle r = parent.getClientArea();

			if (activeToolFigure != null) {
				activeToolFigure.setBounds(r);
			}

			// All tool figures have saved an area in its margin for the arrow
			// figure in
			// case that tool figure is in a stack (see the BORDER variables in
			// ToolEntryEditPart). Calculate the arrow figure bounds by using
			// the insets
			// of the active tool figure.
			r.x = r.right() - ToolEntryEditPart.ICON_HIGHLIGHT_INSETS.right
					- ARROW_WIDTH;
			r.y += ToolEntryEditPart.ICON_HIGHLIGHT_INSETS.top;
			r.width = ARROW_WIDTH;
			r.height -= ToolEntryEditPart.ICON_HIGHLIGHT_INSETS.getHeight();
			arrowFigure.setBounds(r);
		}
	}

	/**
	 * Layout manager for the palette stack header figure that handles the
	 * layout of the <code>headerFigure</code> (<code>pinFigure</code>,
	 * <code>arrowFigure</code>, and the active tool figure) when in list or
	 * details layout mode.
	 */
	private class HeaderListLayout extends StackLayout {

		protected boolean isSensitiveVertically(IFigure container) {
			return false;
		}

		protected Dimension calculatePreferredSize(IFigure parent, int wHint,
				int hHint) {
			if (isExpanded()) {
				Dimension pinSize = pinFigure.getSize();
				Dimension preferredSize = super.calculatePreferredSize(parent,
						wHint - pinSize.width, hHint);
				preferredSize.width += pinSize.width;
				return preferredSize;
			} else {
				return super.calculatePreferredSize(parent, wHint, hHint);
			}
		}

		public void layout(IFigure parent) {

			Rectangle r = parent.getClientArea();
			Dimension pinSize = isExpanded() ? pinFigure.getPreferredSize()
					: EMPTY_DIMENSION;

			// layout the pin figure
			Rectangle.SINGLETON.setSize(pinSize);
			Rectangle.SINGLETON.setLocation(r.right() - pinSize.width,
					r.getCenter().y - (pinSize.height / 2));
			pinFigure.setBounds(Rectangle.SINGLETON);

			if (activeToolFigure != null) {
				activeToolFigure.setBounds(r.getResized(-pinSize.width, 0));
			}

			// All tool figures have saved an area in its margin for the arrow
			// figure in
			// case that tool figure is in a stack (see the BORDER variables in
			// ToolEntryEditPart). Calculate the arrow figure bounds by using
			// the insets
			// of the active tool figure.
			r.x += ToolEntryEditPart.LIST_HIGHLIGHT_INSETS.left;
			r.y += ToolEntryEditPart.LIST_HIGHLIGHT_INSETS.top;
			r.width = ARROW_WIDTH;
			r.height -= ToolEntryEditPart.LIST_HIGHLIGHT_INSETS.getHeight();
			arrowFigure.setBounds(r);

		}
	}

	/**
	 * Layout manager for the palette stack figure that handles the layout of
	 * the <code>headerFigure</code>, <code>expandablePane</code>, and
	 * <code>pinFigure</code> when in icons or columns layout mode.
	 */
	private class PaletteStackIconLayout extends AbstractLayout {

		protected Dimension calculatePreferredSize(IFigure parent, int wHint,
				int hHint) {
			return parent.getSize();
		}

		public void layout(IFigure parent) {
			if (isExpanded()) {
				headerFigure.setBounds(headerBoundsLayoutHint);

				Rectangle paneBounds = parent.getClientArea();
				paneBounds.y += headerBoundsLayoutHint.height;
				paneBounds.height -= headerBoundsLayoutHint.height;
				expandablePane.setBounds(paneBounds);

				Rectangle pinBounds = Rectangle.SINGLETON;
				Dimension pinSize = pinFigure.getPreferredSize();
				pinBounds.setSize(pinSize);
				int pinFigureAreaHeight = expandablePane.getInsets().top;
				pinBounds.setLocation(expandablePane.getClientArea().right()
						- pinSize.width,
						(paneBounds.y + pinFigureAreaHeight / 2)
								- (pinSize.height / 2));
				pinFigure.setBounds(pinBounds);
			} else {
				headerFigure.setBounds(parent.getClientArea());
			}
		}
	}

	/**
	 * listens to selection events on the arrow figure
	 */
	private ChangeListener clickableArrowListener = new ChangeListener() {

		public void handleStateChanged(ChangeEvent event) {
			Clickable clickable = (Clickable) event.getSource();
			if (event.getPropertyName() == ButtonModel.MOUSEOVER_PROPERTY
					&& getActiveFigure() instanceof ToolEntryToggle) {
				((ToolEntryToggle) getActiveFigure())
						.setShowHoverFeedback(clickable.getModel()
								.isMouseOver());
			}
			if (event.getPropertyName() == ButtonModel.SELECTED_PROPERTY) {

				Animation.markBegin();
				handleExpandStateChanged();
				Animation.run(150);

				// Now collapse other stacks when they are not pinned open or in
				// the
				// case of columns and icons layout mode (where only one stack
				// can
				// be expanded at a time for layout reasons).
				if (isExpanded()) {
					boolean collapseOtherStacks = (layoutMode == PaletteViewerPreferences.LAYOUT_COLUMNS || layoutMode == PaletteViewerPreferences.LAYOUT_ICONS);

					for (Iterator iterator = getParent().getChildren()
							.iterator(); iterator.hasNext();) {
						Object childFigure = iterator.next();
						if (childFigure instanceof PinnablePaletteStackFigure
								&& childFigure != PinnablePaletteStackFigure.this) {

							if (collapseOtherStacks
									|| (((PinnablePaletteStackFigure) childFigure)
											.isExpanded() && !((PinnablePaletteStackFigure) childFigure)
											.isPinnedOpen())) {

								((PinnablePaletteStackFigure) childFigure)
										.setExpanded(false);
							}
						}
					}
				}
			}
		}
	};

	private IFigure headerFigure;

	private IFigure activeToolFigure;

	private PinFigure pinFigure;

	private RolloverArrow arrowFigure;

	private IFigure expandablePane;

	private int layoutMode = -1;

	private Rectangle headerBoundsLayoutHint = new Rectangle();

	public PinnablePaletteStackFigure() {
		super();

		arrowFigure = new RolloverArrow();
		arrowFigure.addChangeListener(clickableArrowListener);

		headerFigure = new Figure();
		headerFigure.add(arrowFigure);

		pinFigure = new PinFigure();
		pinFigure.setBorder(new MarginBorder(0, 0, 0, 2));

		expandablePane = new Figure();

		add(headerFigure);
	}

	protected void paintFigure(Graphics g) {
		super.paintFigure(g);

		if (!isExpanded()) {
			return;
		}

		Rectangle headerBounds = headerFigure.getBounds().getCopy();
		Rectangle paneBounds = expandablePane.getClientArea();

		// fill expandable pane background
		g.setBackgroundColor(PaletteColorUtil.WIDGET_BACKGROUND_LIST_BACKGROUND_40);
		g.fillRectangle(paneBounds);

		if (layoutMode == PaletteViewerPreferences.LAYOUT_ICONS
				|| layoutMode == PaletteViewerPreferences.LAYOUT_COLUMNS) {

			int pinHeight = expandablePane.getInsets().top;
			Rectangle pinAreaBounds = new Rectangle(paneBounds.x,
					expandablePane.getBounds().y, paneBounds.width, pinHeight);

			// fill background colors
			g.setForegroundColor(PaletteColorUtil.WIDGET_BACKGROUND_LIST_BACKGROUND_40);
			g.setBackgroundColor(PaletteColorUtil.WIDGET_BACKGROUND_LIST_BACKGROUND_85);
			g.fillGradient(headerBounds, true);

			g.setBackgroundColor(PaletteColorUtil.WIDGET_BACKGROUND_LIST_BACKGROUND_85);
			g.fillRectangle(pinAreaBounds);

			// draw white lines
			g.setForegroundColor(PaletteColorUtil.WIDGET_LIST_BACKGROUND);
			g.drawLine(headerBounds.getTopLeft().getTranslated(1, 1),
					headerBounds.getTopRight().getTranslated(-1, 1));
			g.drawLine(headerBounds.getBottomLeft().getTranslated(1, 0),
					headerBounds.getTopLeft().getTranslated(1, 1));
			g.drawLine(headerBounds.getBottomRight().getTranslated(-2, 0),
					headerBounds.getTopRight().getTranslated(-2, 1));

			g.drawLine(pinAreaBounds.getTopLeft().getTranslated(0, 1),
					pinAreaBounds.getTopRight().getTranslated(-1, 1));
			g.drawLine(pinAreaBounds.getBottomLeft().getTranslated(0, -2),
					pinAreaBounds.getBottomRight().getTranslated(-1, -2));

			// draw grey border around the whole palette stack
			PointList points = new PointList();
			points.addPoint(headerBounds.getBottomLeft());
			points.addPoint(headerBounds.getTopLeft().getTranslated(0, 2));
			points.addPoint(headerBounds.getTopLeft().getTranslated(1, 1));
			points.addPoint(headerBounds.getTopLeft().getTranslated(2, 0));
			points.addPoint(headerBounds.getTopRight().getTranslated(-3, 0));
			points.addPoint(headerBounds.getTopRight().getTranslated(-2, 1));
			points.addPoint(headerBounds.getTopRight().getTranslated(-1, 2));
			points.addPoint(headerBounds.getBottomRight().getTranslated(-1, 0));
			points.addPoint(pinAreaBounds.getTopRight().getTranslated(-1, 0));
			points.addPoint(paneBounds.getBottomRight().getTranslated(-1, -1));
			points.addPoint(paneBounds.getBottomLeft().getTranslated(0, -1));
			points.addPoint(pinAreaBounds.getTopLeft().getTranslated(0, 0));
			points.addPoint(headerBounds.getBottomLeft());

			g.setForegroundColor(PaletteColorUtil.WIDGET_BACKGROUND_NORMAL_SHADOW_40);
			g.drawPolygon(points);

			g.setForegroundColor(PaletteColorUtil.WIDGET_BACKGROUND_NORMAL_SHADOW_80);
			Point pt = headerBounds.getTopLeft().getTranslated(0, 1);
			g.drawPoint(pt.x, pt.y);
			pt = headerBounds.getTopLeft().getTranslated(1, 0);
			g.drawPoint(pt.x, pt.y);
			pt = headerBounds.getTopRight().getTranslated(-2, 0);
			g.drawPoint(pt.x, pt.y);
			pt = headerBounds.getTopRight().getTranslated(-1, 1);
			g.drawPoint(pt.x, pt.y);
		} else {

			// fill header background
			g.setBackgroundColor(PaletteColorUtil.WIDGET_BACKGROUND_LIST_BACKGROUND_85);
			g.fillRectangle(headerBounds);

			// draw top and bottom border lines of header figure
			g.setForegroundColor(PaletteColorUtil.WIDGET_BACKGROUND_NORMAL_SHADOW_65);
			g.drawLine(headerBounds.getTopLeft(), headerBounds.getTopRight());
			g.setForegroundColor(PaletteColorUtil.WIDGET_LIST_BACKGROUND);
			g.drawLine(headerBounds.getBottomLeft().getTranslated(0, -2),
					headerBounds.getBottomRight().getTranslated(0, -2));

			// draw bottom border line of expandable pane
			g.setForegroundColor(PaletteColorUtil.WIDGET_BACKGROUND_NORMAL_SHADOW_65);
			g.drawLine(paneBounds.getBottomLeft().getTranslated(0, -1),
					paneBounds.getBottomRight().getTranslated(0, -1));
		}

	}

	/**
	 * @return The content pane for this figure, i.e. the Figure to which
	 *         children can be added.
	 */
	public IFigure getContentPane(IFigure figure) {
		if (figure == activeToolFigure) {
			return headerFigure;
		}
		return getContentPane();
	}

	public IFigure getContentPane() {
		return expandablePane;
	}

	public IFigure getActiveFigure() {
		return activeToolFigure;
	}

	/**
	 * @return <code>true</code> if the palette stack is expanded
	 */
	public boolean isExpanded() {
		return arrowFigure.getModel().isSelected();
	}

	/**
	 * @return <code>true</code> if the palette stack is expanded and is pinned
	 *         (i.e., it can't be automatically collapsed)
	 */
	public boolean isPinnedOpen() {
		return isExpanded() && pinFigure.getModel().isSelected();
	}

	/**
	 * Pins or unpins the stack. The stack can be pinned open only when it is
	 * expanded. Attempts to pin it when it is collapsed will do nothing.
	 * 
	 * @param pinned
	 *            <code>true</code> if the stack is to be pinned
	 */
	public void setPinned(boolean pinned) {
		if (isExpanded()) {
			pinFigure.setSelected(pinned);
		}
	}

	public void setExpanded(boolean value) {
		arrowFigure.setSelected(value);
		if (!value) {
			pinFigure.setSelected(false);
		}
	}

	public void setLayoutMode(int newLayoutMode) {
		if (this.layoutMode == newLayoutMode) {
			return;
		}

		this.layoutMode = newLayoutMode;

		// Only one stack can be expanded in icons and layout mode, therefore
		// for
		// consistency let's always collapse stacks when changing the layout
		// modes.
		setExpanded(false);

		if (newLayoutMode == PaletteViewerPreferences.LAYOUT_LIST
				|| newLayoutMode == PaletteViewerPreferences.LAYOUT_DETAILS) {

			headerFigure.setLayoutManager(new HeaderListLayout());

			expandablePane.setLayoutManager(new ToolbarLayout());
			expandablePane.setBorder(new MarginBorder(1, 0, 1, 0));
			setLayoutManager(new ToolbarLayout());

		} else {
			headerFigure.setLayoutManager(new HeaderIconLayout());
			if (activeToolFigure != null) {
				headerFigure.setConstraint(activeToolFigure,
						BorderLayout.CENTER);
			}

			setLayoutManager(new PaletteStackIconLayout());

			// account for space used by pin figure
			expandablePane.setBorder(new MarginBorder(18, 0, 0, 0));

			if (layoutMode == PaletteViewerPreferences.LAYOUT_COLUMNS) {
				expandablePane.setLayoutManager(new ColumnsLayout());
			} else { // LAYOUT_ICONS
				FlowLayout fl = new FlowLayout();
				fl.setMinorSpacing(0);
				fl.setMajorSpacing(0);
				expandablePane.setLayoutManager(fl);
			}
		}
	}

	public void activeEntryChanged(IFigure oldFigure, int oldFigureIndex,
			IFigure newFigure) {

		if (oldFigure != null) {
			// if figure is null, its no longer a child.
			expandablePane.add(oldFigure, oldFigureIndex);
		}

		if (newFigure != null) {
			activeToolFigure = newFigure;
			headerFigure.add(activeToolFigure, BorderLayout.CENTER, 0);
		} else {
			activeToolFigure = null;
		}
	}

	private void handleExpandStateChanged() {
		if (isExpanded()) {
			if (expandablePane.getParent() != this) {
				add(expandablePane);

				if (layoutMode == PaletteViewerPreferences.LAYOUT_LIST
						|| layoutMode == PaletteViewerPreferences.LAYOUT_DETAILS) {
					headerFigure.add(pinFigure);
				} else {
					add(pinFigure);
				}
			}
		} else {
			if (expandablePane.getParent() == this) {
				remove(expandablePane);
				pinFigure.getParent().remove(pinFigure);
			}
		}
	}

	/**
	 * Gets the preferred size of the expandable pane figure. Used by
	 * <code>PaletteContainerFlowLayout</code> when the layout is icons or
	 * columns mode.
	 * 
	 * @param wHint
	 *            width hint
	 * @param hHint
	 *            height hint
	 * @return the preferred size of the expandable pane figure or (0,0) if the
	 *         pane is collapsed
	 */
	public Dimension getExpandedContainerPreferredSize(int wHint, int hHint) {
		if (isExpanded()) {
			return expandablePane.getPreferredSize(wHint, hHint);
		} else {
			return EMPTY_DIMENSION;
		}
	}

	/**
	 * Sets the header bounds layout hint. Set by
	 * <code>PaletteContainerFlowLayout</code> when the layout is icons or
	 * columns mode and used by <code>PaletteStackIconLayout</code>.
	 * 
	 * @param rect
	 *            the new value
	 */
	public void setHeaderBoundsLayoutHint(Rectangle rect) {
		headerBoundsLayoutHint.setBounds(rect);
	}

	/**
	 * Gets the preferred size of the header figure. Used by
	 * <code>PaletteContainerFlowLayout</code> and <code>ColumnsLayout</code>
	 * when the layout is icons or columns mode.
	 * 
	 * @param wHint
	 *            width hint
	 * @param hHint
	 *            height hint
	 * @return the preferred size of the header figure
	 */
	public Dimension getHeaderPreferredSize(int wHint, int hHint) {
		return headerFigure.getPreferredSize(wHint, hHint);
	}

	public boolean containsPoint(int x, int y) {
		return headerFigure.containsPoint(x, y)
				|| (isExpanded() && expandablePane.containsPoint(x, y));
	}

}
