/*******************************************************************************
 * Copyright (c) 2000, 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.draw2d;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import org.eclipse.swt.graphics.Color;

import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.draw2d.geometry.Transposer;

/**
 * Provides for the scrollbars used by the {@link ScrollPane}. A ScrollBar is
 * made up of five essential Figures: An 'Up' arrow button, a 'Down' arrow
 * button, a draggable 'Thumb', a 'Pageup' button, and a 'Pagedown' button.
 */
public class ScrollBar extends Figure implements Orientable,
		PropertyChangeListener {

	private static final int ORIENTATION_FLAG = Figure.MAX_FLAG << 1;
	/** @see Figure#MAX_FLAG */
	protected static final int MAX_FLAG = ORIENTATION_FLAG;

	private static final Color COLOR_TRACK = FigureUtilities.mixColors(
			ColorConstants.white, ColorConstants.button);

	private RangeModel rangeModel = null;
	private IFigure thumb;
	private Clickable pageUp, pageDown;
	private Clickable buttonUp, buttonDown;
	/**
	 * Listens to mouse events on the scrollbar to take care of scrolling.
	 */
	protected ThumbDragger thumbDragger = new ThumbDragger();

	private boolean isHorizontal = false;

	private int pageIncrement = 50;
	private int stepIncrement = 10;

	/**
	 * Transposes from vertical to horizontal if needed.
	 */
	protected final Transposer transposer = new Transposer();

	{
		setRangeModel(new DefaultRangeModel());
	}

	/**
	 * Constructs a ScrollBar. ScrollBar orientation is vertical by default.
	 * Call {@link #setHorizontal(boolean)} with <code>true</code> to set
	 * horizontal orientation.
	 * 
	 * @since 2.0
	 */
	public ScrollBar() {
		initialize();
	}

	/**
	 * Creates the default 'Up' ArrowButton for the ScrollBar.
	 * 
	 * @return the up button
	 * @since 2.0
	 */
	protected Clickable createDefaultUpButton() {
		Button buttonUp = new ArrowButton();
		buttonUp.setBorder(new ButtonBorder(
				ButtonBorder.SCHEMES.BUTTON_SCROLLBAR));
		return buttonUp;
	}

	/**
	 * Creates the default 'Down' ArrowButton for the ScrollBar.
	 * 
	 * @return the down button
	 * @since 2.0
	 */
	protected Clickable createDefaultDownButton() {
		Button buttonDown = new ArrowButton();
		buttonDown.setBorder(new ButtonBorder(
				ButtonBorder.SCHEMES.BUTTON_SCROLLBAR));
		return buttonDown;
	}

	/**
	 * Creates the pagedown Figure for the Scrollbar.
	 * 
	 * @return the page down figure
	 * @since 2.0
	 */
	protected Clickable createPageDown() {
		return createPageUp();
	}

	/**
	 * Creates the pageup Figure for the Scrollbar.
	 * 
	 * @return the page up figure
	 * @since 2.0
	 */
	protected Clickable createPageUp() {
		final Clickable clickable = new Clickable();
		clickable.setOpaque(true);
		clickable.setBackgroundColor(COLOR_TRACK);
		clickable.setRequestFocusEnabled(false);
		clickable.setFocusTraversable(false);
		clickable.addChangeListener(new ChangeListener() {
			public void handleStateChanged(ChangeEvent evt) {
				if (clickable.getModel().isArmed())
					clickable.setBackgroundColor(ColorConstants.black);
				else
					clickable.setBackgroundColor(COLOR_TRACK);
			}
		});
		return clickable;
	}

	/**
	 * Creates the Scrollbar's "thumb", the draggable Figure that indicates the
	 * Scrollbar's position.
	 * 
	 * @return the thumb figure
	 * @since 2.0
	 */
	protected IFigure createDefaultThumb() {
		Panel thumb = new Panel();
		thumb.setMinimumSize(new Dimension(6, 6));
		thumb.setBackgroundColor(ColorConstants.button);

		thumb.setBorder(new SchemeBorder(SchemeBorder.SCHEMES.BUTTON_CONTRAST));
		return thumb;
	}

	/**
	 * Returns the figure used as the up button.
	 * 
	 * @return the up button
	 */
	protected IFigure getButtonUp() {
		// TODO: The set method takes a Clickable while the get method returns
		// an IFigure.
		// Change the get method to return Clickable (since that's what it's
		// typed as).
		return buttonUp;
	}

	/**
	 * Returns the figure used as the down button.
	 * 
	 * @return the down button
	 */
	protected IFigure getButtonDown() {
		// TODO: The set method takes a Clickable while the get method returns
		// an IFigure.
		// Change the get method to return Clickable (since that's what it's
		// typed as).
		return buttonDown;
	}

	/**
	 * Returns the extent.
	 * 
	 * @return the extent
	 * @see RangeModel#getExtent()
	 */
	public int getExtent() {
		return getRangeModel().getExtent();
	}

	/**
	 * Returns the minumum value.
	 * 
	 * @return the minimum
	 * @see RangeModel#getMinimum()
	 */
	public int getMinimum() {
		return getRangeModel().getMinimum();
	}

	/**
	 * Returns the maximum value.
	 * 
	 * @return the maximum
	 * @see RangeModel#getMaximum()
	 */
	public int getMaximum() {
		return getRangeModel().getMaximum();
	}

	/**
	 * Returns the figure used for page down.
	 * 
	 * @return the page down figure
	 */
	protected IFigure getPageDown() {
		// TODO: The set method takes a Clickable while the get method returns
		// an IFigure.
		// Change the get method to return Clickable (since that's what it's
		// typed as).
		return pageDown;
	}

	/**
	 * Returns the the amound the scrollbar will move when the page up or page
	 * down areas are pressed.
	 * 
	 * @return the page increment
	 */
	public int getPageIncrement() {
		return pageIncrement;
	}

	/**
	 * Returns the figure used for page up.
	 * 
	 * @return the page up figure
	 */
	protected IFigure getPageUp() {
		// TODO: The set method takes a Clickable while the get method returns
		// an IFigure.
		// Change the get method to return Clickable (since that's what it's
		// typed as).
		return pageUp;
	}

	/**
	 * Returns the range model for this scrollbar.
	 * 
	 * @return the range model
	 */
	public RangeModel getRangeModel() {
		return rangeModel;
	}

	/**
	 * Returns the amount the scrollbar will move when the up or down arrow
	 * buttons are pressed.
	 * 
	 * @return the step increment
	 */
	public int getStepIncrement() {
		return stepIncrement;
	}

	/**
	 * Returns the figure used as the scrollbar's thumb.
	 * 
	 * @return the thumb figure
	 */
	protected IFigure getThumb() {
		return thumb;
	}

	/**
	 * Returns the current scroll position of the scrollbar.
	 * 
	 * @return the current value
	 * @see RangeModel#getValue()
	 */
	public int getValue() {
		return getRangeModel().getValue();
	}

	/**
	 * Returns the size of the range of allowable values.
	 * 
	 * @return the value range
	 */
	protected int getValueRange() {
		return getMaximum() - getExtent() - getMinimum();
	}

	/**
	 * Initilization of the ScrollBar. Sets the Scrollbar to have a
	 * ScrollBarLayout with vertical orientation. Creates the Figures that make
	 * up the components of the ScrollBar.
	 * 
	 * @since 2.0
	 */
	protected void initialize() {
		setLayoutManager(new ScrollBarLayout(transposer));
		setUpClickable(createDefaultUpButton());
		setDownClickable(createDefaultDownButton());
		setPageUp(createPageUp());
		setPageDown(createPageDown());
		setThumb(createDefaultThumb());
	}

	/**
	 * Returns <code>true</code> if this scrollbar is orientated horizontally,
	 * <code>false</code> otherwise.
	 * 
	 * @return whether this scrollbar is horizontal
	 */
	public boolean isHorizontal() {
		return isHorizontal;
	}

	private void pageDown() {
		setValue(getValue() + getPageIncrement());
	}

	private void pageUp() {
		setValue(getValue() - getPageIncrement());
	}

	/**
	 * @see PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
	 */
	public void propertyChange(PropertyChangeEvent event) {
		if (event.getSource() instanceof RangeModel) {
			setEnabled(getRangeModel().isEnabled());
			if (RangeModel.PROPERTY_VALUE.equals(event.getPropertyName())) {
				firePropertyChange("value", event.getOldValue(), //$NON-NLS-1$
						event.getNewValue());
				revalidate();
			}
			if (RangeModel.PROPERTY_MINIMUM.equals(event.getPropertyName())) {
				firePropertyChange("value", event.getOldValue(), //$NON-NLS-1$
						event.getNewValue());
				revalidate();
			}
			if (RangeModel.PROPERTY_MAXIMUM.equals(event.getPropertyName())) {
				firePropertyChange("value", event.getOldValue(), //$NON-NLS-1$
						event.getNewValue());
				revalidate();
			}
			if (RangeModel.PROPERTY_EXTENT.equals(event.getPropertyName())) {
				firePropertyChange("value", event.getOldValue(), //$NON-NLS-1$
						event.getNewValue());
				revalidate();
			}
		}
	}

	/**
	 * @see IFigure#revalidate()
	 */
	public void revalidate() {
		// Override default revalidate to prevent going up the parent chain.
		// Reason for this
		// is that preferred size never changes unless orientation changes.
		invalidate();
		getUpdateManager().addInvalidFigure(this);
	}

	/**
	 * Does nothing because this doesn't make sense for a scrollbar.
	 * 
	 * @see Orientable#setDirection(int)
	 */
	public void setDirection(int direction) {
		// Doesn't make sense for Scrollbar.
	}

	/**
	 * Sets the Clickable that represents the down arrow of the Scrollbar to
	 * <i>down</i>.
	 * 
	 * @param down
	 *            the down button
	 * @since 2.0
	 */
	public void setDownClickable(Clickable down) {
		if (buttonDown != null) {
			remove(buttonDown);
		}
		buttonDown = down;
		if (buttonDown != null) {
			if (buttonDown instanceof Orientable)
				((Orientable) buttonDown)
						.setDirection(isHorizontal() ? Orientable.EAST
								: Orientable.SOUTH);
			buttonDown.setFiringMethod(Clickable.REPEAT_FIRING);
			buttonDown.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					stepDown();
				}
			});
			add(buttonDown, ScrollBarLayout.DOWN_ARROW);
		}
	}

	/**
	 * Sets the Clickable that represents the up arrow of the Scrollbar to
	 * <i>up</i>.
	 * 
	 * @param up
	 *            the up button
	 * @since 2.0
	 */
	public void setUpClickable(Clickable up) {
		if (buttonUp != null) {
			remove(buttonUp);
		}
		buttonUp = up;
		if (up != null) {
			if (up instanceof Orientable)
				((Orientable) up).setDirection(isHorizontal() ? Orientable.WEST
						: Orientable.NORTH);
			buttonUp.setFiringMethod(Clickable.REPEAT_FIRING);
			buttonUp.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					stepUp();
				}
			});
			add(buttonUp, ScrollBarLayout.UP_ARROW);
		}
	}

	/**
	 * @see IFigure#setEnabled(boolean)
	 */
	public void setEnabled(boolean value) {
		if (isEnabled() == value)
			return;
		super.setEnabled(value);
		setChildrenEnabled(value);
		if (getThumb() != null) {
			getThumb().setVisible(value);
			revalidate();
		}
	}

	/**
	 * Sets the extent of the Scrollbar to <i>ext</i>
	 * 
	 * @param ext
	 *            the extent
	 * @since 2.0
	 */
	public void setExtent(int ext) {
		if (getExtent() == ext)
			return;
		getRangeModel().setExtent(ext);
	}

	/**
	 * Sets the orientation of the ScrollBar. If <code>true</code>, the
	 * Scrollbar will have a horizontal orientation. If <code>false</code>, the
	 * scrollBar will have a vertical orientation.
	 * 
	 * @param value
	 *            <code>true</code> if the scrollbar should be horizontal
	 * @since 2.0
	 */
	public final void setHorizontal(boolean value) {
		setOrientation(value ? HORIZONTAL : VERTICAL);
	}

	/**
	 * Sets the maximum position to <i>max</i>.
	 * 
	 * @param max
	 *            the maximum position
	 * @since 2.0
	 */
	public void setMaximum(int max) {
		if (getMaximum() == max)
			return;
		getRangeModel().setMaximum(max);
	}

	/**
	 * Sets the minimum position to <i>min</i>.
	 * 
	 * @param min
	 *            the minumum position
	 * @since 2.0
	 */
	public void setMinimum(int min) {
		if (getMinimum() == min)
			return;
		getRangeModel().setMinimum(min);
	}

	/**
	 * @see Orientable#setOrientation(int)
	 */
	public void setOrientation(int value) {
		if ((value == HORIZONTAL) == isHorizontal())
			return;
		isHorizontal = value == HORIZONTAL;
		transposer.setEnabled(isHorizontal);

		setChildrenOrientation(value);
		super.revalidate();
	}

	/**
	 * Sets the ScrollBar to scroll <i>increment</i> pixels when its pageup or
	 * pagedown buttons are pressed. (Note that the pageup and pagedown buttons
	 * are <b>NOT</b> the arrow buttons, they are the figures between the arrow
	 * buttons and the ScrollBar's thumb figure).
	 * 
	 * @param increment
	 *            the new page increment
	 * @since 2.0
	 */
	public void setPageIncrement(int increment) {
		pageIncrement = increment;
	}

	/**
	 * Sets the pagedown button to the passed Clickable. The pagedown button is
	 * the figure between the down arrow button and the ScrollBar's thumb
	 * figure.
	 * 
	 * @param down
	 *            the page down figure
	 * @since 2.0
	 */
	public void setPageDown(Clickable down) {
		if (pageDown != null)
			remove(pageDown);
		pageDown = down;
		if (pageDown != null) {
			pageDown.setFiringMethod(Clickable.REPEAT_FIRING);
			pageDown.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent event) {
					pageDown();
				}
			});
			add(down, ScrollBarLayout.PAGE_DOWN);
		}
	}

	/**
	 * Sets the pageup button to the passed Clickable. The pageup button is the
	 * rectangular figure between the down arrow button and the ScrollBar's
	 * thumb figure.
	 * 
	 * @param up
	 *            the page up figure
	 * @since 2.0
	 */
	public void setPageUp(Clickable up) {
		if (pageUp != null)
			remove(pageUp);
		pageUp = up;
		if (pageUp != null) {
			pageUp.setFiringMethod(Clickable.REPEAT_FIRING);
			pageUp.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent event) {
					pageUp();
				}
			});
			add(pageUp, ScrollBarLayout.PAGE_UP);
		}
	}

	/**
	 * Sets the ScrollBar's RangeModel to the passed value.
	 * 
	 * @param rangeModel
	 *            the new range model
	 * @since 2.0
	 */
	public void setRangeModel(RangeModel rangeModel) {
		if (this.rangeModel != null)
			this.rangeModel.removePropertyChangeListener(this);
		this.rangeModel = rangeModel;
		rangeModel.addPropertyChangeListener(this);
	}

	/**
	 * Sets the ScrollBar's step increment to the passed value. The step
	 * increment indicates how many pixels the ScrollBar will scroll when its up
	 * or down arrow button is pressed.
	 * 
	 * @param increment
	 *            the new step increment
	 * @since 2.0
	 */
	public void setStepIncrement(int increment) {
		stepIncrement = increment;
	}

	/**
	 * Sets the ScrollBar's thumb to the passed Figure. The thumb is the
	 * draggable component of the ScrollBar that indicates the ScrollBar's
	 * position.
	 * 
	 * @param figure
	 *            the thumb figure
	 * @since 2.0
	 */
	public void setThumb(IFigure figure) {
		if (thumb != null) {
			thumb.removeMouseListener(thumbDragger);
			thumb.removeMouseMotionListener(thumbDragger);
			remove(thumb);
		}
		thumb = figure;
		if (thumb != null) {
			thumb.addMouseListener(thumbDragger);
			thumb.addMouseMotionListener(thumbDragger);
			add(thumb, ScrollBarLayout.THUMB);
		}
	}

	/**
	 * Sets the value of the Scrollbar to <i>v</i>
	 * 
	 * @param v
	 *            the new value
	 * @since 2.0
	 */
	public void setValue(int v) {
		getRangeModel().setValue(v);
	}

	/**
	 * Causes the ScrollBar to scroll down (or right) by the value of its step
	 * increment.
	 * 
	 * @since 2.0
	 */
	protected void stepDown() {
		setValue(getValue() + getStepIncrement());
	}

	/**
	 * Causes the ScrollBar to scroll up (or left) by the value of its step
	 * increment.
	 * 
	 * @since 2.0
	 */
	protected void stepUp() {
		setValue(getValue() - getStepIncrement());
	}

	/**
	 * @since 3.6
	 */
	protected class ThumbDragger extends MouseMotionListener.Stub implements
			MouseListener {
		protected Point start;
		protected int dragRange;
		protected int revertValue;
		protected boolean armed;

		public ThumbDragger() {
		}

		public void mousePressed(MouseEvent me) {
			armed = true;
			start = me.getLocation();
			Rectangle area = new Rectangle(transposer.t(getClientArea()));
			Dimension thumbSize = transposer.t(getThumb().getSize());
			if (getButtonUp() != null)
				area.height -= transposer.t(getButtonUp().getSize()).height;
			if (getButtonDown() != null)
				area.height -= transposer.t(getButtonDown().getSize()).height;
			Dimension sizeDifference = new Dimension(area.width, area.height
					- thumbSize.height);
			dragRange = sizeDifference.height;
			revertValue = getValue();
			me.consume();
		}

		public void mouseDragged(MouseEvent me) {
			if (!armed)
				return;
			Dimension difference = transposer.t(me.getLocation().getDifference(
					start));
			int change = getValueRange() * difference.height / dragRange;
			setValue(revertValue + change);
			me.consume();
		}

		public void mouseReleased(MouseEvent me) {
			if (!armed)
				return;
			armed = false;
			me.consume();
		}

		public void mouseDoubleClicked(MouseEvent me) {
		}
	}

}
