/*******************************************************************************
 * Copyright (c) 2000, 2005 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 java.util.Timer;
import java.util.TimerTask;

//import org.eclipse.draw2d.internal.Timer;

/**
 * A model for buttons containing several properties, including enabled, pressed,
 * selected, rollover enabled and mouseover.
 */
public class ButtonModel {

/** Enabled property */
public static final String ENABLED_PROPERTY = "enabled"; //$NON-NLS-1$
/** Pressed property */
public static final String PRESSED_PROPERTY = "pressed"; //$NON-NLS-1$
/** Selected property */
public static final String SELECTED_PROPERTY = "selected"; //$NON-NLS-1$
/** Rollover Enabled property */
public static final String ROLLOVER_ENABLED_PROPERTY = "rollover enabled"; //$NON-NLS-1$
/** Mouseover property */
public static final String MOUSEOVER_PROPERTY = "mouseover"; //$NON-NLS-1$

/** Armed property */
public static final String ARMED_PROPERTY = "armed";  //$NON-NLS-1$


/** Flag for armed button state */
protected static final int ARMED_FLAG 				= 1;
/** Flag for pressed button state */
protected static final int PRESSED_FLAG 			= 2;
/** Flag for mouseOver state */
protected static final int MOUSEOVER_FLAG 			= 4;
/** Flag for selected button state */
protected static final int SELECTED_FLAG 			= 8;
/** Flag for enablement button state */
protected static final int ENABLED_FLAG 			= 16;
/** Flag for rollover enablement button state */
protected static final int ROLLOVER_ENABLED_FLAG 	= 32;
/** Flag that can be used by subclasses to define more states */
protected static final int MAX_FLAG 				= ROLLOVER_ENABLED_FLAG;

private int state = ENABLED_FLAG;
private Object data;

/**
 * Action performed events are not fired until the mouse button is released. 
 */
public static final int DEFAULT_FIRING_BEHAVIOR = 0;

/**
 * Action performed events fire repeatedly until the mouse button is released.
 */
public static final int REPEAT_FIRING_BEHAVIOR  = 1;

/**
 * The name of the action associated with this button.
 */
protected String actionName;

/**
 * The ButtonGroup this button belongs to (if any).
 */
protected ButtonGroup group = null;

private EventListenerList listeners = new EventListenerList();

/**
 * Listens to button state transitions and fires action performed events based on the
 * desired behavior ({@link #DEFAULT_FIRING_BEHAVIOR} or {@link #REPEAT_FIRING_BEHAVIOR}).
 */
protected ButtonStateTransitionListener firingBehavior; {
	installFiringBehavior();
}

/**
 * Registers the given listener as an ActionListener.
 * 
 * @param listener The ActionListener to add
 * @since 2.0
 */
public void addActionListener(ActionListener listener) {
	if (listener == null)
		throw new IllegalArgumentException();
	listeners.addListener(ActionListener.class, listener);
}

/**
 * Registers the given listener as a ChangeListener.
 * 
 * @param listener The ChangeListener to add
 * @since 2.0
 */
public void addChangeListener(ChangeListener listener) {
	if (listener == null)
		throw new IllegalArgumentException();
	listeners.addListener(ChangeListener.class, listener);
}

/**
 * Registers the given listener as a ButtonStateTransitionListener.
 * 
 * @param listener The ButtonStateTransitionListener to add
 * @since 2.0
 */
public void addStateTransitionListener(ButtonStateTransitionListener listener) {
	if (listener == null)
		throw new IllegalArgumentException();
	listeners.addListener(ButtonStateTransitionListener.class, listener);
}

/**
 * Notifies any ActionListeners on this ButtonModel that an action has been performed.
 * 
 * @since 2.0
 */
protected void fireActionPerformed() {
	Iterator iter = listeners.getListeners(ActionListener.class);
	ActionEvent action = new ActionEvent(this);
	while (iter.hasNext())
		((ActionListener)iter.next()).
			actionPerformed(action);
}

/**
 * Notifies any listening ButtonStateTransitionListener that the pressed state of this
 * button has been cancelled.
 * 
 * @since 2.0
 */
protected void fireCanceled() {
	Iterator iter = listeners.getListeners(ButtonStateTransitionListener.class);
	while (iter.hasNext())
		((ButtonStateTransitionListener)iter.next()).
			canceled();
}

/**
 * Notifies any listening ButtonStateTransitionListener that this button has been pressed.
 * 
 * @since 2.0
 */
protected void firePressed() {
	Iterator iter = listeners.getListeners(ButtonStateTransitionListener.class);
	while (iter.hasNext())
		((ButtonStateTransitionListener)iter.next()).
			pressed();
}

/** 
 * Notifies any listening ButtonStateTransitionListener that this button has been
 * released.
 * 
 * @since 2.0
 */
protected void fireReleased() {
	Iterator iter = listeners.getListeners(ButtonStateTransitionListener.class);
	while (iter.hasNext())
		((ButtonStateTransitionListener)iter.next()).
			released();
}

/**
 * Notifies any listening ButtonStateTransitionListeners that this button has resumed
 * activity.
 * 
 * @since 2.0
 */
protected void fireResume() {
	Iterator iter = listeners.getListeners(ButtonStateTransitionListener.class);
	while (iter.hasNext())
		((ButtonStateTransitionListener)iter.next()).
			resume();
}

/**
 * Notifies any listening ChangeListeners that this button's state has changed.
 * 
 * @param property The name of the property that changed
 * @since 2.0
 */
protected void fireStateChanged(String property) {
	Iterator iter = listeners.getListeners(ChangeListener.class);
	ChangeEvent change = new ChangeEvent(this, property);
	while (iter.hasNext())
		((ChangeListener)iter.next()).
			handleStateChanged(change);
}

/**
 * Notifies any listening ButtonStateTransitionListeners that this button has suspended
 * activity.
 * 
 * @since 2.0
 */
protected void fireSuspend() {
	Iterator iter = listeners.getListeners(ButtonStateTransitionListener.class);
	while (iter.hasNext())
		((ButtonStateTransitionListener)iter.next()).
			suspend();
}

boolean getFlag(int which) {
	return (state & which) != 0;
}

/**
 * Returns the group to which this model belongs.
 * 
 * @return The ButtonGroup to which this model belongs
 * @since 2.0
 */
public ButtonGroup getGroup() {
	return group;
}

/**
 * Returns an object representing user data.
 * 
 * @return User data
 * @since 2.0
 */
public Object getUserData() {
	return data;
}

/**
 * Sets the firing behavior for this button.
 * 
 * @since 2.0
 */
protected void installFiringBehavior() {
	setFiringBehavior(DEFAULT_FIRING_BEHAVIOR);
}

/**
 * Returns <code>true</code> if this button is armed. If a button is armed, it will fire
 * an ActionPerformed when released.
 * 
 * @return <code>true</code> if this button is armed
 * @since 2.0
 */
public boolean isArmed() {
	return (state & ARMED_FLAG) != 0;
}

/**
 * Returns <code>true</code> if this button is enabled.
 * 
 * @return <code>true</code> if this button is enabled
 * @since 2.0
 */
public boolean isEnabled() {
	return (state & ENABLED_FLAG) != 0;
}

/**
 * Returns <code>true</code> if the mouse is over this button.
 * 
 * @return <code>true</code> if the mouse is over this button
 * @since 2.0
 */
public boolean isMouseOver() {
	return (state & MOUSEOVER_FLAG) != 0;
}

/**
 * Returns <code>true</code> if this button is pressed.
 * 
 * @return <code>true</code> if this button is pressed
 * @since 2.0
 */
public boolean isPressed() {
	return (state & PRESSED_FLAG) != 0;
}

/**
 * Returns the selection state of this model. If this model belongs to any group, the
 * group is queried for selection state, else the flags are used.
 *
 * @return  <code>true</code> if this button is selected
 * @since 2.0
 */
public boolean isSelected() {
	if (group == null) {
		return (state & SELECTED_FLAG) != 0;
	} else {
		return group.isSelected(this);
	}
}

/**
 * Removes the given ActionListener.
 * 
 * @param listener The ActionListener to remove
 * @since 2.0
 */
public void removeActionListener(ActionListener listener) {
	listeners.removeListener(ActionListener.class, listener);
}

/**
 * Removes the given ChangeListener.
 * 
 * @param listener The ChangeListener to remove
 * @since 2.0
 */
public void removeChangeListener(ChangeListener listener) {
	listeners.removeListener(ChangeListener.class, listener);
}

/**
 * Removes the given ButtonStateTransitionListener.
 * 
 * @param listener The ButtonStateTransitionListener to remove 
 * @since 2.0
 */
public void removeStateTransitionListener(ButtonStateTransitionListener listener) {
	listeners.removeListener(ButtonStateTransitionListener.class, listener);
}

/**
 * Sets this button to be armed. If a button is armed, it will fire an ActionPerformed
 * when released.
 *
 *@param value The armed state
 * @since 2.0
 */
public void setArmed(boolean value) {
	if (isArmed() == value)
		return;
	if (!isEnabled())
		return;
	setFlag(ARMED_FLAG, value);
	fireStateChanged(ARMED_PROPERTY);
}

/**
 * Sets this button to be enabled.
 * 
 * @param value The enabled state
 * @since 2.0
 */
public void setEnabled(boolean value) {
	if (isEnabled() == value)
		return;
	if (!value) {
		setMouseOver(false);
		setArmed(false);
		setPressed(false);
	}
	setFlag(ENABLED_FLAG, value);
	fireStateChanged(ENABLED_PROPERTY);
}

/**
 * Sets the firing behavior for this button. {@link #DEFAULT_FIRING_BEHAVIOR} is the
 * default behavior, where action performed events are not fired until the mouse button is
 * released. {@link #REPEAT_FIRING_BEHAVIOR} causes action performed events to fire
 * repeatedly until the mouse button is released.
 * 
 * @param type The firing behavior type
 * @since 2.0
 *          
 */
public void setFiringBehavior(int type) {
	if (firingBehavior != null)
		removeStateTransitionListener(firingBehavior);
	switch (type) {
		case REPEAT_FIRING_BEHAVIOR:
			firingBehavior = new RepeatFiringBehavior();
			break;
		default:
			firingBehavior = new DefaultFiringBehavior();
	}
	addStateTransitionListener(firingBehavior);
}

void setFlag(int flag, boolean value) {
	if (value)
		state |= flag;
	else
		state &= ~flag;
}

/**
 * Sets the ButtonGroup to which this model belongs to. Adds this model as a listener to
 * the group.
 *
 * @param bg The group to which this model belongs.
 * @since 2.0
 */
public void setGroup(ButtonGroup bg) {
	if (group == bg)
		return;
	if (group != null)
		group.remove(this);
	group = bg;
	if (group != null)
		group.add(this);
}

/**
 * Sets the mouseover property of this button.
 * 
 * @param value The value the mouseover property will be set to
 * @since 2.0
 */
public void setMouseOver(boolean value) {
	if (isMouseOver() == value)
		return;
	if (isPressed())
		if (value)
			fireResume();
		else
			fireSuspend();
	setFlag(MOUSEOVER_FLAG, value);
	fireStateChanged(MOUSEOVER_PROPERTY);
}

/**
 * Sets the pressed property of this button.
 * 
 * @param value The value the pressed property will be set to
 * @since 2.0
 */
public void setPressed(boolean value) {
	if (isPressed() == value)
		return;
	setFlag(PRESSED_FLAG, value);
	if (value)
		firePressed();
	else {
		if (isArmed())
			fireReleased();
		else
			fireCanceled();
	}
	fireStateChanged(PRESSED_PROPERTY);
}

/**
 * Sets this button to be selected.
 * 
 * @param value The value the selected property will be set to
 * @since 2.0
 */
public void setSelected(boolean value) {
	if (group == null) {
		if (isSelected() == value)
			return;
	} else {
		group.setSelected(this, value);
		if (getFlag(SELECTED_FLAG) == isSelected())
			return;
	}
	setFlag(SELECTED_FLAG, value);
	fireStateChanged(SELECTED_PROPERTY);
}

/**
 * Sets user data.
 * 
 * @param data The user data
 * @since 2.0
 */
public void setUserData(Object data) {
	this.data = data;
}

class DefaultFiringBehavior
	extends ButtonStateTransitionListener
{
	public void released() {
		fireActionPerformed();
	}
}

class RepeatFiringBehavior
	extends ButtonStateTransitionListener
{
	protected static final int
		INITIAL_DELAY = 250,
		STEP_DELAY = 40;
	
	protected int
		stepDelay = INITIAL_DELAY,
		initialDelay = STEP_DELAY;
	
	protected Timer timer;
	
	public void pressed() {
		fireActionPerformed();
		if (!isEnabled())
			return;
		
		timer = new Timer();
		TimerTask runAction = new Task(timer);

		timer.scheduleAtFixedRate(runAction, INITIAL_DELAY, STEP_DELAY);
	}

	public void canceled() {
		suspend();
	}	
	public void released() {
		suspend();
	}
	
	public void resume() {
		timer = new Timer();
		
		TimerTask runAction = new Task(timer);
		
		timer.scheduleAtFixedRate(runAction, STEP_DELAY, STEP_DELAY);
	}
	
	public void suspend() {
		if (timer == null) return;
		timer.cancel();
		timer = null;
	}
}

class Task 
	extends TimerTask {
	
	private Timer timer;
	
	public Task(Timer timer) {
		this.timer = timer;
	}
	
	public void run() {
		org.eclipse.swt.widgets.Display.getDefault().syncExec(new Runnable() {
			public void run() {
				if (!isEnabled())
					timer.cancel();
				fireActionPerformed();
			}
		});
	}
}

}
