/*******************************************************************************
 * 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.util.Iterator;

import org.eclipse.draw2d.geometry.Rectangle;

/**
 * A Clickable responds to mouse clicks in some way (determined by a
 * ClickBehavior) and fires action events. No visual appearance of feedback is
 * offered. Depends on a model holder and an event handler which understands the
 * model and updates the model accordingly. {@link ButtonModel} is used by
 * default. Any figure can be set as contents to a Clickable.
 * Clickable->EventHandler->Model->ModelObserver->Listeners of actions.
 */
public class Clickable extends Figure {

	private static final int ROLLOVER_ENABLED_FLAG = Figure.MAX_FLAG << 1,
			STYLE_BUTTON_FLAG = Figure.MAX_FLAG << 2,
			STYLE_TOGGLE_FLAG = Figure.MAX_FLAG << 3;

	/**
	 * The highest reserved flag used by this class
	 */
	protected static int MAX_FLAG = STYLE_TOGGLE_FLAG;

	/**
	 * Style constant that defines a push button. The button will be pressed
	 * when the mouse is pressed on top of it. The button will be released when
	 * the mouse is released or is move off of the button.
	 * 
	 */
	public static final int STYLE_BUTTON = STYLE_BUTTON_FLAG;

	/**
	 * Style constant that defines a toggle button. The button will toggle
	 * between 2 states when the mouse is clicked on the button.
	 */
	public static final int STYLE_TOGGLE = STYLE_TOGGLE_FLAG;

	/**
	 * An action is performed every time the mouse is released.
	 */
	public static final int DEFAULT_FIRING = 0;

	/**
	 * Firing starts as soon as the mouse is pressed on this Clickable, and
	 * keeps firing at prefixed intervals until the mouse is released.
	 */
	public static final int REPEAT_FIRING = 1;

	/**
	 * Observes the model for action and state changes.
	 * 
	 * @see ActionListener
	 * @see ChangeListener
	 */
	static interface ModelObserver extends ActionListener, ChangeListener {
	}

	private ClickableEventHandler eventHandler;

	private ButtonModel model;

	private ModelObserver modelObserver;

	{
		init();
		setRequestFocusEnabled(true);
		setFocusTraversable(true);
	}

	/**
	 * Constructs a Clickable with no contents.
	 */
	public Clickable() {
	}

	/**
	 * Constructs a Clickable whose contents are provided as input. The content
	 * figure occupies the entire region of the Clickable.
	 * 
	 * @param contents
	 *            The content figure
	 */
	public Clickable(IFigure contents) {
		this(contents, 0);
	}

	/**
	 * Constructs a Clickable whose contents are provided as input. The content
	 * figure occupies the entire region of the Clickable. Sets the style to the
	 * given <i>style</i> (either {@link #STYLE_BUTTON} or {@link #STYLE_TOGGLE}
	 * ).
	 * 
	 * @param contents
	 *            The content figure
	 * @param style
	 *            The button style
	 */
	public Clickable(IFigure contents, int style) {
		setContents(contents);
		setStyle(style);
	}

	/**
	 * Adds the given listener to the list of action listeners of this Figure.
	 * Listener is called whenever an action is performed.
	 * 
	 * @param listener
	 *            The ActionListener to be added
	 * @since 2.0
	 */
	public void addActionListener(ActionListener listener) {
		addListener(ActionListener.class, listener);
	}

	/**
	 * Adds the given listener to the list of state change listeners of this
	 * figure. A ChangeListener is informed if there is any state change in the
	 * model requiring action by the listener.
	 * 
	 * @param listener
	 *            The ChangeListener to be added
	 * @since 2.0
	 */
	public void addChangeListener(ChangeListener listener) {
		addListener(ChangeListener.class, listener);
	}

	/**
	 * Returns a newly created ButtonModel as the default model to be used by
	 * this Clickable based on the button style.
	 * 
	 * @return The model to be used by default
	 * @since 2.0
	 */
	protected ButtonModel createDefaultModel() {
		if (isStyle(STYLE_TOGGLE))
			return new ToggleModel();
		else
			return new ButtonModel();
	}

	/**
	 * Returns a newly created event handler for this Clickable and its model.
	 * 
	 * @return The event handler
	 * @since 2.0
	 */
	protected ClickableEventHandler createEventHandler() {
		return new ClickableEventHandler();
	}

	/**
	 * Returns a newly created model observer which listens to the model, and
	 * fires any action or state changes. A ModelObserver holds both an action
	 * listener and a state change listener.
	 * 
	 * @return The newly created model observer
	 * @since 2.0
	 */
	protected ModelObserver createModelObserver() {
		return new ModelObserver() {
			public void actionPerformed(ActionEvent action) {
				fireActionPerformed();
			}

			public void handleStateChanged(ChangeEvent change) {
				fireStateChanged(change);
			}
		};
	}

	/**
	 * Fires an action performed event.
	 * 
	 * @since 2.0
	 */
	public void doClick() {
		fireActionPerformed();
	}

	/**
	 * Called when there has been an action performed by this Clickable, which
	 * is determined by the model. Notifies all ActionListener type listeners of
	 * an action performed.
	 * 
	 * @since 2.0
	 */
	protected void fireActionPerformed() {
		ActionEvent action = new ActionEvent(this);
		Iterator listeners = getListeners(ActionListener.class);
		while (listeners.hasNext())
			((ActionListener) listeners.next()) // Leave newline for debug
												// stepping
					.actionPerformed(action);
	}

	/**
	 * Called when there has been a change of state in the model of this
	 * clickable. Notifies all ChangeListener type listeners of the state
	 * change.
	 * 
	 * @param modelChange
	 *            The ChangeEvent
	 * @since 2.0
	 */
	protected void fireStateChanged(ChangeEvent modelChange) {
		ChangeEvent change = new ChangeEvent(this,
				modelChange.getPropertyName());
		Iterator listeners = getListeners(ChangeListener.class);
		while (listeners.hasNext())
			((ChangeListener) listeners.next()) // Leave newline for debug
												// stepping
					.handleStateChanged(change);
	}

	/**
	 * Returns the behavior model used by this Clickable.
	 * 
	 * @return The model used by this Clickable
	 * @since 2.0
	 */
	public ButtonModel getModel() {
		return model;
	}

	/**
	 * Adds the given ClickableEventHandler to this clickable. A
	 * ClickableEventHandler should be a MouseListener, MouseMotionListener,
	 * ChangeListener, KeyListener, and FocusListener.
	 * 
	 * @param handler
	 *            The new event handler
	 * @since 2.0
	 */
	protected void hookEventHandler(ClickableEventHandler handler) {
		if (handler == null)
			return;
		addMouseListener(handler);
		addMouseMotionListener(handler);
		addChangeListener(handler);
		addKeyListener(handler);
		addFocusListener(handler);
	}

	/**
	 * Initializes this Clickable by setting a default model and adding a
	 * clickable event handler for that model.
	 * 
	 * @since 2.0
	 */
	protected void init() {
		setModel(createDefaultModel());
		setEventHandler(createEventHandler());
	}

	/**
	 * Returns <code>true</code> if rollover feedback is enabled.
	 * 
	 * @return <code>true</code> rollover feedback is enabled
	 * @since 2.0
	 */
	public boolean isRolloverEnabled() {
		return (flags & ROLLOVER_ENABLED_FLAG) != 0;
	}

	/**
	 * Returns <code>true</code> if this Clickable is in a selected state. The
	 * model is the one which holds all this state based information.
	 * 
	 * @return <code>true</code> if this Clickable is in a selected state
	 * @since 2.0
	 */
	public boolean isSelected() {
		return getModel().isSelected();
	}

	/**
	 * Returns <code>true</code> if this Clickable's style is the same as the
	 * passed style.
	 * 
	 * @param style
	 *            The style to be checked
	 * @return <code>true</code> if this Clickable's style is the same as the
	 *         passed style
	 * @since 2.0
	 */
	public boolean isStyle(int style) {
		return ((style & flags) == style);
	}

	/**
	 * If this Clickable has focus, this method paints a focus rectangle.
	 * 
	 * @param graphics
	 *            Graphics handle for painting
	 */
	protected void paintBorder(Graphics graphics) {
		super.paintBorder(graphics);
		if (hasFocus()) {
			graphics.setForegroundColor(ColorConstants.black);
			graphics.setBackgroundColor(ColorConstants.white);

			Rectangle area = getClientArea();
			if (isStyle(STYLE_BUTTON))
				graphics.drawFocus(area.x, area.y, area.width, area.height);
			else
				graphics.drawFocus(area.x, area.y, area.width - 1,
						area.height - 1);
		}
	}

	/**
	 * Paints the area of this figure excluded by the borders. Induces a (1,1)
	 * pixel shift in the painting if the mouse is armed, giving it the pressed
	 * appearance.
	 * 
	 * @param graphics
	 *            Graphics handle for painting
	 * @since 2.0
	 */
	protected void paintClientArea(Graphics graphics) {
		if (isStyle(STYLE_BUTTON)
				&& (getModel().isArmed() || getModel().isSelected())) {
			graphics.translate(1, 1);
			graphics.pushState();
			super.paintClientArea(graphics);
			graphics.popState();
			graphics.translate(-1, -1);
		} else
			super.paintClientArea(graphics);
	}

	/**
	 * Removes the given listener from the list of ActionListener's of this
	 * Clickable.
	 * 
	 * @param listener
	 *            Listener to be removed from this figure
	 * @since 2.0
	 */
	public void removeActionListener(ActionListener listener) {
		removeListener(ActionListener.class, listener);
	}

	/**
	 * Removes the given listener from the list of ChangeListener's of this
	 * clickable.
	 * 
	 * @param listener
	 *            Listener to be removed from this figure
	 * @since 2.0
	 */
	public void removeChangeListener(ChangeListener listener) {
		removeListener(ChangeListener.class, listener);
	}

	/**
	 * Sets the Figure which is the contents of this Clickable. This Figure
	 * occupies the entire clickable region.
	 * 
	 * @param contents
	 *            Contents of the clickable
	 * @since 2.0
	 */
	protected void setContents(IFigure contents) {
		setLayoutManager(new StackLayout());
		if (getChildren().size() > 0)
			remove((IFigure) getChildren().get(0));
		add(contents);
	}

	/**
	 * @see org.eclipse.draw2d.IFigure#setEnabled(boolean)
	 */
	public void setEnabled(boolean value) {
		if (isEnabled() == value)
			return;
		super.setEnabled(value);
		getModel().setEnabled(value);
		setChildrenEnabled(value);
	}

	/**
	 * Sets the event handler which interacts with the model to determine the
	 * behavior of this Clickable.
	 * 
	 * @param h
	 *            Event handler for this clickable
	 * @since 2.0
	 */
	public void setEventHandler(ClickableEventHandler h) {
		if (eventHandler != null)
			unhookEventHandler(eventHandler);
		eventHandler = h;
		if (eventHandler != null)
			hookEventHandler(eventHandler);
	}

	/**
	 * Determines how this clickable is to fire notifications to its listeners.
	 * In the default firing method ({@link #DEFAULT_FIRING}), an action is
	 * performed every time the mouse is released. In the repeat firing method (
	 * {@link #REPEAT_FIRING}), firing starts as soon as it is pressed on this
	 * clickable, and keeps firing at prefixed intervals until the mouse is
	 * released.
	 * 
	 * @param type
	 *            Type of firing
	 * @since 2.0
	 */
	public void setFiringMethod(int type) {
		getModel().setFiringBehavior(type);
	}

	/**
	 * Sets the model to be used by this clickable for its state and behavior
	 * determination. This clickable removes any observers from the previous
	 * model before adding new ones to the new model.
	 * 
	 * @param model
	 *            The new model of this Clickable
	 * @since 2.0
	 */
	public void setModel(ButtonModel model) {
		if (this.model != null) {
			this.model.removeChangeListener(modelObserver);
			this.model.removeActionListener(modelObserver);
			modelObserver = null;
		}
		this.model = model;
		if (model != null) {
			modelObserver = createModelObserver();
			model.addActionListener(modelObserver);
			model.addChangeListener(modelObserver);
		}
	}

	/**
	 * Enables or disables rollover feedback of this figure. Generally used in
	 * conjunction with the model to determine if feedback is to be shown.
	 * 
	 * @param value
	 *            The rollover state to be set
	 * @since 2.0
	 */
	public void setRolloverEnabled(boolean value) {
		if (isRolloverEnabled() == value)
			return;
		setFlag(ROLLOVER_ENABLED_FLAG, value);
		repaint();
	}

	/**
	 * Sets the selected state of this Clickable. Since the model is responsible
	 * for all state based information, it is informed of the state change.
	 * Extending classes can choose selection information, if they do not
	 * represent any selection.
	 * 
	 * @param value
	 *            New selected state of this clickable.
	 * @see #isSelected()
	 * @since 2.0
	 */
	public void setSelected(boolean value) {
		getModel().setSelected(value);
	}

	/**
	 * Sets this Clickable's style to the passed value, either
	 * {@link #STYLE_BUTTON} or {@link #STYLE_TOGGLE}.
	 * 
	 * @param style
	 *            The button style
	 * @since 2.0
	 */
	public void setStyle(int style) {
		if ((style & STYLE_BUTTON) != 0) {
			setFlag(STYLE_BUTTON_FLAG, true);
			if (!(getBorder() instanceof ButtonBorder))
				setBorder(new ButtonBorder());
			setOpaque(true);
		} else {
			setFlag(STYLE_BUTTON_FLAG, false);
			setOpaque(false);
		}

		if ((style & STYLE_TOGGLE) != 0) {
			setFlag(STYLE_TOGGLE_FLAG, true);
			setModel(createDefaultModel());
		}
	}

	/**
	 * Removes the given ClickableEventHandler containing listeners from this
	 * Clickable.
	 * 
	 * @param handler
	 *            The event handler to be removed
	 * @since 2.0
	 */
	protected void unhookEventHandler(ClickableEventHandler handler) {
		if (handler == null)
			return;
		removeMouseListener(handler);
		removeMouseMotionListener(handler);
		removeChangeListener(handler);
	}

}
