/*
 * $Id: JXRadioGroup.java,v 1.11 2009/05/25 04:09:03 kschaefe Exp $
 *
 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import javax.swing.AbstractButton;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.ButtonModel;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.event.EventListenerList;

/**
 * <p>
 * {@code JXRadioGroup} is a group of radio buttons that functions as a unit. It
 * is similar in concept to a {@link JComboBox} in functionality, but can offer
 * a better presentation for a small number of choices. {@code JXRadioGroup}
 * should be used in preference to {@code JComboBox} when the number of choices
 * is small (less than six) or the choices are verbose.
 * </p>
 * <p>
 * Notes:
 * <ol>
 * <li>Enabling and disabling the JXRadioGroup will enable/disable all of the
 * child buttons inside the JXRadioGroup.</li>
 * <li>
 * If the generic type parameter of JXRadioGroup is a subclass of
 * {@link AbstractButton}, then the buttons will be added "as is" to the
 * container. If the generic type is anything else, buttons will be created as
 * {@link JRadioButton} objects, and the button text will be set by calling
 * toString() on the value object.</li>
 * <li>
 * Alternatively, if you want to configure the buttons individually, construct
 * the JXRadioGroup normally, and then call {@link #getChildButton(int)} or
 * {@link #getChildButton(Object)} and configure the buttons.</li>
 * </ol>
 * </p>
 * <p>
 * TODO back with a model (possibly reuse of extend {@link ComboBoxModel}
 * </p>
 * 
 * @author Amy Fowler
 * @author Noel Grandin
 * @version 1.0
 */
public class JXRadioGroup<T> extends JPanel {

    private static final long serialVersionUID = 3257285842266567986L;

    private ButtonGroup buttonGroup;

    private final List<T> values = new ArrayList<T>();

    private ActionSelectionListener actionHandler;

    private List<ActionListener> actionListeners;

    /**
     * Create a default JXRadioGroup with a default layout axis of {@link BoxLayout#X_AXIS}.
     */
    public JXRadioGroup() {
        setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
        buttonGroup = new ButtonGroup();
    }
    
    /**
     * Create a default JXRadioGroup with a default layout axis of {@link BoxLayout#X_AXIS}.
     * 
     * @param radioValues the list of values used to create the group.
     */
    public JXRadioGroup(T[] radioValues) {
        this();
        for (int i = 0; i < radioValues.length; i++) {
            add(radioValues[i]);
        }
    }
    
    /**
     * Convenience factory method.
     * Reduces code clutter when dealing with generics.
     * 
     * @param radioValues the list of values used to create the group.
     */
    public static <T> JXRadioGroup<T> create(T[] radioValues)
    {
        return new JXRadioGroup<T>(radioValues);
    }

    /**
     * Set the layout axis of the radio group.
     * 
     * @param axis values from {@link BoxLayout}.
     */
    public void setLayoutAxis(int axis)
    {
        setLayout(new BoxLayout(this, axis));
    }

    /**
     * Sets the values backing this group. This replaces the current set of
     * values with the new set.
     * 
     * @param radioValues
     *            the new backing values for this group
     */
    public void setValues(T[] radioValues) {
        clearAll();
        for (int i = 0; i < radioValues.length; i++) {
            add(radioValues[i]);
        }
    }

    private void clearAll() {
        values.clear();
        buttonGroup = new ButtonGroup();
        // remove all the child components
        removeAll();
    }

    /**
     * You can use this method to manually add your own AbstractButton objects, provided you declared
     * the class as <code>JXRadioGroup&lt;JRadioButton&gt;</code>.
     */
    public void add(T radioValue) {
        if (values.contains(radioValue))
        {
            throw new IllegalArgumentException("cannot add the same value twice " + radioValue);
        }
        if (radioValue instanceof AbstractButton) {
            values.add(radioValue);
            addButton((AbstractButton) radioValue);
        } else {
            values.add(radioValue);
            // Note: the "quote + object" trick here allows null values
            addButton(new JRadioButton(""+radioValue));
        }
    }

    private void addButton(AbstractButton button) {
        buttonGroup.add(button);
        super.add(button);
        if (actionHandler == null) {
            actionHandler = new ActionSelectionListener();
        }
        button.addActionListener(actionHandler);
        button.addItemListener(actionHandler);
    }

    private class ActionSelectionListener implements ActionListener, ItemListener
    {
        public void actionPerformed(ActionEvent e) {
            fireActionEvent(e);
        }

        public void itemStateChanged(ItemEvent e) {
            fireActionEvent(null);
        }
    }

    /**
     * Gets the currently selected button.
     * 
     * @return the currently selected button
     * @see #getSelectedValue()
     */
    public AbstractButton getSelectedButton() {
        final ButtonModel selectedModel = buttonGroup.getSelection();
        final AbstractButton children[] = getButtonComponents();
        for (int i = 0; i < children.length; i++) {
            AbstractButton button = children[i];
            if (button.getModel() == selectedModel) {
                return button;
            }
        }
        return null;
    }

    private AbstractButton[] getButtonComponents() {
        final Component[] children = getComponents();
        final List<AbstractButton> buttons = new ArrayList<AbstractButton>();
        for (int i = 0; i < children.length; i++) {
            if (children[i] instanceof AbstractButton) {
                buttons.add((AbstractButton) children[i]);
            }
        }
        return buttons.toArray(new AbstractButton[buttons.size()]);
    }

    private int getSelectedIndex() {
        final ButtonModel selectedModel = buttonGroup.getSelection();
        final Component children[] = getButtonComponents();
        for (int i = 0; i < children.length; i++) {
            AbstractButton button = (AbstractButton) children[i];
            if (button.getModel() == selectedModel) {
                return i;
            }
        }
        return -1;
    }

    /**
     * The currently selected value.
     * 
     * @return the current value
     */
    public T getSelectedValue() {
        final int index = getSelectedIndex();
        return (index < 0 || index >= values.size()) ? null : values.get(index);
    }

    /**
     * Selects the supplied value.
     * 
     * @param value
     *            the value to select
     */
    public void setSelectedValue(T value) {
        final int index = values.indexOf(value);
        AbstractButton button = getButtonComponents()[index];
        button.setSelected(true);
    }
    
    /**
     * Retrieve the child button by index.
     */
    public AbstractButton getChildButton(int index) {
        return getButtonComponents()[index];
    }

    /**
     * Retrieve the child button that represents this value.
     */
    public AbstractButton getChildButton(T value) {
        final int index = values.indexOf(value);
        return getButtonComponents()[index];
    }

    /**
     * Get the number of child buttons.
     */
    public int getChildButtonCount() {
        return getButtonComponents().length;
    }
    
    /** 
     * Adds an <code>ActionListener</code>. 
     * <p>
     * The <code>ActionListener</code> will receive an <code>ActionEvent</code>
     * when a selection has been made.
     *
     * @param l  the <code>ActionListener</code> that is to be notified
     * @see #setSelectedItem
     */
    public void addActionListener(ActionListener l) {
        if (actionListeners == null) {
            actionListeners = new ArrayList<ActionListener>();
        }
        actionListeners.add(l);
    }

    /**
     * Removes an <code>ActionListener</code>.
     * 
     * @param l
     *            the <code>ActionListener</code> to remove
     */
    public void removeActionListener(ActionListener l) {
        if (actionListeners != null) {
            actionListeners.remove(l);
        }
    }

    /**
     * Returns an array of all the <code>ActionListener</code>s added
     * to this JRadioGroup with addActionListener().
     *
     * @return all of the <code>ActionListener</code>s added or an empty
     *         array if no listeners have been added
     */
    public ActionListener[] getActionListeners() {
        if (actionListeners != null) {
            return actionListeners.toArray(new ActionListener[0]);
        }
        return new ActionListener[0];
    }

    /**
     * Notifies all listeners that have registered interest for notification on
     * this event type.
     * 
     * @param e
     *            the event to pass to the listeners
     * @see EventListenerList
     */
    protected void fireActionEvent(ActionEvent e) {
        if (actionListeners != null) {
            for (int i = 0; i < actionListeners.size(); i++) {
                ActionListener l = actionListeners.get(i);
                l.actionPerformed(e);
            }
        }
    }

    /**
     * Enable/disable all of the child buttons
     * 
     * @see JComponent#setEnabled(boolean)
     */
    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        for (Enumeration<AbstractButton> en = buttonGroup.getElements(); en.hasMoreElements();) {
            final AbstractButton button = en.nextElement();
            /* We don't want to enable a button where the action does not
             * permit it. */
            if (enabled && button.getAction() != null
                    && !button.getAction().isEnabled()) {
                // do nothing
            } else {
                button.setEnabled(enabled);
            }
        }
    }

}