/*
 * 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.api.common;

import java.awt.Component;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.pushingpixels.flamingo.internal.ui.common.BasicCommandButtonStripUI;
import org.pushingpixels.flamingo.internal.ui.common.CommandButtonStripUI;

/**
 * Button strip component. Provides visual appearance of a strip. The buttons in
 * the strip are either drawn horizontally with no horizontal space between them
 * or drawn vertically with no vertical space between them.
 * 
 * @author Kirill Grouchnikov
 */
public class JCommandButtonStrip extends JComponent {
	/**
	 * The UI class ID string.
	 */
	public static final String uiClassID = "CommandButtonStripUI";

	/**
	 * Element state for the buttons in this button strip. Default state is
	 * {@link CommandButtonDisplayState#SMALL}.
	 */
	protected CommandButtonDisplayState displayState;

	/**
	 * Scale factor for horizontal gaps.
	 * 
	 * @see #setVGapScaleFactor(double)
	 */
	protected double hgapScaleFactor;

	/**
	 * Scale factor for vertical gaps.
	 * 
	 * @see #setVGapScaleFactor(double)
	 */
	protected double vgapScaleFactor;

	/**
	 * Button strip orientation.
	 * 
	 * @author Kirill Grouchnikov
	 */
	public enum StripOrientation {
		/**
		 * Horizontal strip orientation.
		 */
		HORIZONTAL,

		/**
		 * Vertical strip orientation.
		 */
		VERTICAL
	}

	/**
	 * Orientation of <code>this</code> strip.
	 * 
	 * @see #getOrientation()
	 */
	private StripOrientation orientation;

	/**
	 * Creates an empty horizontally-oriented strip.
	 */
	public JCommandButtonStrip() {
		this(StripOrientation.HORIZONTAL);
	}

	/**
	 * Creates an empty strip.
	 * 
	 * @param orientation
	 *            Orientation for this strip.
	 */
	public JCommandButtonStrip(StripOrientation orientation) {
		this.orientation = orientation;
		this.displayState = CommandButtonDisplayState.SMALL;
		switch (orientation) {
		case HORIZONTAL:
			this.hgapScaleFactor = 0.75;
			this.vgapScaleFactor = 1.0;
			break;
		case VERTICAL:
			this.hgapScaleFactor = 1.0;
			this.vgapScaleFactor = 0.75;
			break;
		}
		this.setOpaque(false);
		updateUI();
	}

	/**
	 * Sets the display state for the buttons in this button strip. This method
	 * must be called <em>before</em> adding the first command button. The
	 * default state is {@link CommandButtonDisplayState#SMALL}.
	 * 
	 * @param elementState
	 *            New element state for the buttons in this button strip.
	 */
	public void setDisplayState(CommandButtonDisplayState elementState) {
		if (this.getComponentCount() > 0) {
			throw new IllegalStateException(
					"Can't call this method after buttons have been already added");
		}
		this.displayState = elementState;
	}

	/**
	 * Sets the horizontal gap scale factor for the buttons in this button
	 * strip. This method must be called <em>before</em> adding the first
	 * command button.
	 * 
	 * <p>
	 * The default horizontal gap scale factor for horizontally oriented strips
	 * is 0.75. The default horizontal gap scale factor for vertically oriented
	 * strips is 1.0.
	 * </p>
	 * 
	 * @param hgapScaleFactor
	 *            New horizontal gap scale factor for the buttons in this button
	 *            strip.
	 * @see #setVGapScaleFactor(double)
	 */
	public void setHGapScaleFactor(double hgapScaleFactor) {
		if (this.getComponentCount() > 0) {
			throw new IllegalStateException(
					"Can't call this method after buttons have been already added");
		}
		this.hgapScaleFactor = hgapScaleFactor;
	}

	/**
	 * Sets the vertical gap scale factor for the buttons in this button strip.
	 * This method must be called <em>before</em> adding the first command
	 * button.
	 * 
	 * <p>
	 * The default vertical gap scale factor for vertically oriented strips is
	 * 0.75. The default vertical gap scale factor for horizontally oriented
	 * strips is 1.0.
	 * </p>
	 * 
	 * @param vgapScaleFactor
	 *            New vertical gap scale factor for the buttons in this button
	 *            strip.
	 * @see #setHGapScaleFactor(double)
	 */
	public void setVGapScaleFactor(double vgapScaleFactor) {
		if (this.getComponentCount() > 0) {
			throw new IllegalStateException(
					"Can't call this method after buttons have been already added");
		}
		this.vgapScaleFactor = vgapScaleFactor;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.Container#add(java.awt.Component, java.lang.Object, int)
	 */
	@Override
	public void add(Component comp, Object constraints, int index) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.Container#add(java.awt.Component, java.lang.Object)
	 */
	@Override
	public void add(Component comp, Object constraints) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.Container#add(java.awt.Component, int)
	 */
	@Override
	public Component add(Component comp, int index) {
		if (!(comp instanceof AbstractCommandButton))
			throw new UnsupportedOperationException();
		this.configureCommandButton((AbstractCommandButton) comp);
		return super.add(comp, index);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.Container#add(java.awt.Component)
	 */
	@Override
	public Component add(Component comp) {
		if (!(comp instanceof AbstractCommandButton))
			throw new UnsupportedOperationException();
		try {
			this.configureCommandButton((AbstractCommandButton) comp);
			Component result = super.add(comp);
			return result;
		} finally {
			this.fireStateChanged();
		}
	}

	/**
	 * Configures the specified command button.
	 * 
	 * @param button
	 *            Command button to configure.
	 */
	private void configureCommandButton(AbstractCommandButton button) {
		button.setDisplayState(this.displayState);
		button.setHGapScaleFactor(this.hgapScaleFactor);
		button.setVGapScaleFactor(this.vgapScaleFactor);
		button.setFlat(false);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.Container#add(java.lang.String, java.awt.Component)
	 */
	@Override
	public Component add(String name, Component comp) {
		throw new UnsupportedOperationException();
	}

	/**
	 * Sets the new UI delegate.
	 * 
	 * @param ui
	 *            New UI delegate.
	 */
	public void setUI(CommandButtonStripUI ui) {
		super.setUI(ui);
	}

	/**
	 * Resets the UI property to a value from the current look and feel.
	 * 
	 * @see JComponent#updateUI
	 */
	@Override
	public void updateUI() {
		if (UIManager.get(getUIClassID()) != null) {
			setUI((CommandButtonStripUI) UIManager.getUI(this));
		} else {
			setUI(BasicCommandButtonStripUI.createUI(this));
		}
	}

	/**
	 * Returns the UI object which implements the L&F for this component.
	 * 
	 * @return a <code>ButtonStripUI</code> object
	 * @see #setUI
	 */
	public CommandButtonStripUI getUI() {
		return (CommandButtonStripUI) ui;
	}

	/**
	 * Returns the name of the UI class that implements the L&F for this
	 * component.
	 * 
	 * @return the string "ButtonStripUI"
	 * @see JComponent#getUIClassID
	 * @see UIDefaults#getUI
	 */
	@Override
	public String getUIClassID() {
		return uiClassID;
	}

	/**
	 * Returns the number of buttons in <code>this</code> strip.
	 * 
	 * @return Number of buttons in <code>this</code> strip.
	 * @see #getButton(int)
	 */
	public int getButtonCount() {
		return this.getComponentCount();
	}

	/**
	 * Returns the specified button component of <code>this</code> strip.
	 * 
	 * @param index
	 *            Button index.
	 * @return The matching button.
	 * @see #getButtonCount()
	 */
	public AbstractCommandButton getButton(int index) {
		return (AbstractCommandButton) this.getComponent(index);
	}

	/**
	 * Checks whether the specified button is the first button in
	 * <code>this</code> strip.
	 * 
	 * @param button
	 *            Button to check.
	 * @return <code>true</code> if the specified button is the first button in
	 *         <code>this</code> strip, <code>false</code> otherwise.
	 * @see #isLast(AbstractCommandButton)
	 */
	public boolean isFirst(AbstractCommandButton button) {
		return (button == this.getButton(0));
	}

	/**
	 * Checks whether the specified button is the last button in
	 * <code>this</code> strip.
	 * 
	 * @param button
	 *            Button to check.
	 * @return <code>true</code> if the specified button is the last button in
	 *         <code>this</code> strip, <code>false</code> otherwise.
	 * @see #isFirst(AbstractCommandButton)
	 */
	public boolean isLast(AbstractCommandButton button) {
		return (button == this.getButton(this.getButtonCount() - 1));
	}

	/**
	 * Returns the orientation of <code>this</code> strip.
	 * 
	 * @return Orientation of <code>this</code> strip.
	 */
	public StripOrientation getOrientation() {
		return orientation;
	}

	/**
	 * Adds the specified change listener to track changes to this command
	 * button strip.
	 * 
	 * @param l
	 *            Change listener to add.
	 * @see #removeChangeListener(ChangeListener)
	 */
	public void addChangeListener(ChangeListener l) {
		this.listenerList.add(ChangeListener.class, l);
	}

	/**
	 * Removes the specified change listener from tracking changes to this
	 * command button strip.
	 * 
	 * @param l
	 *            Change listener to remove.
	 * @see #addChangeListener(ChangeListener)
	 */
	public void removeChangeListener(ChangeListener l) {
		this.listenerList.remove(ChangeListener.class, l);
	}

	/**
	 * Notifies all registered listener that the state of this command button
	 * strip has changed.
	 */
	protected void fireStateChanged() {
		// Guaranteed to return a non-null array
		Object[] listeners = this.listenerList.getListenerList();
		// Process the listeners last to first, notifying
		// those that are interested in this event
		ChangeEvent event = new ChangeEvent(this);
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == ChangeListener.class) {
				((ChangeListener) listeners[i + 1]).stateChanged(event);
			}
		}
	}

}
