
package uk.co.wingpath.gui;

import java.util.*;
import java.util.List;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import uk.co.wingpath.event.*;
import uk.co.wingpath.event.Event;
import uk.co.wingpath.util.*;


/**
* This class implements <code>WComponent</code> using a <code>JComboBox</code>.
*/
public class WComboBox<T>
    extends WAbstractComponent<T>
{
    private final JComboBox comboBox;
    private final T [] values;
    private T value;
    private int lastIndex;
    private int alignment = SwingConstants.LEFT;

    /**
    * Constructs a <code>WComboBox</code>.
    * @param label text for the associated label, or <code>null</code> if
    * there is no associated label.
    * @param vals the values corresponding to each selection. These
    * are the possible values that may be returned by <code>getValue</code>.
    * The first value will be the initially selected value.
    * A {@code null} value may be used to indicate a non-selectable item.
    * @param items the items to be displayed for each selection.
    * Each item may be a String, an Icon, or a JSeparator.
    */
    public WComboBox (String label, final T [] vals, Object [] items)
    {
        Event.checkIsEventDispatchThread ();
        if (items.length != vals.length)
        {
            throw new IllegalArgumentException (
                "vals & items have different sizes");
        }
        comboBox = new JComboBox ();
        comboBox.setVerifyInputWhenFocusTarget (true);
        initialize (comboBox, label);
        values = vals.clone ();
        value = values.length == 0 ? null : values [0];

        for (Object item : items)
            comboBox.addItem (item);

        lastIndex = 0;

        comboBox.addActionListener (new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    int index = comboBox.getSelectedIndex ();
                    T v = index < 0 ? null : values [index];
                    if (v == null && index >= 0)
                    {
                        // Item is non-selectable (separator or heading).
                        // Search backwards or forwards for a selectable item.
                        // If the user is stepping through the items using
                        // the keyboard, we have to search for a selectable
                        // in the direction that the user is stepping.
                        int i = index;
                        if (i <= lastIndex)
                        {
                            while (i >= 0 && values [i] == null)
                                i--;
                            if (i < 0)
                                i = index;
                        }

                        while (i < values.length && values [i] == null)
                            i++;
                        if (i < values.length)
                        {
                            // Found a selectable item.
                            comboBox.setSelectedIndex (i);
                            index = i;
                            v = values [index];
                        }
                    }
                    lastIndex = index;
                    if (!(v == null ? value == null : v.equals (value)))
                    {
                        value = v;
                        fireValueChanged (false);
                    }
                }
            });

        final ListCellRenderer renderer = comboBox.getRenderer ();
        comboBox.setRenderer (
            new ListCellRenderer ()
            {
                public Component getListCellRendererComponent (JList list,
                    Object value, int index, boolean isSelected,
                    boolean cellHasFocus)
                {
                    if (value instanceof JSeparator)
                    {
                        return (JSeparator) value;
                    }
                    Component component =
                        renderer.getListCellRendererComponent (
                            list, value, index, isSelected, cellHasFocus);
                    if (component instanceof JLabel)
                        ((JLabel) component).setHorizontalAlignment (alignment);
                    return component;
                }
            });
    }

    /**
    * Sets the maximum number of rows to be displayed.
    * If the number of items is greater than the maximum number of rows,
    * a scrollbar is used.
    * Scrolling may be inhibited by passing 0 for the number of rows.
    * @param rows the maximum number of rows, or 0 to inhibit scrolling.
    */
    public void setMaximumRowCount (int rows)
    {
        if (rows == 0)
            rows = values.length;
        comboBox.setMaximumRowCount (rows);
    }

    /**
     * Sets the horizontal alignment of the selection text.
     * Valid parameter values are:
     * <ul>
     * <li><code>SwingConstants.LEFT</code>
     * <li><code>SwingConstants.CENTER</code>
     * <li><code>SwingConstants.RIGHT</code>
     * <li><code>SwingConstants.LEADING</code>
     * <li><code>SwingConstants.TRAILING</code>
     * </ul>
     * @param alignment the required alignment
     */
    @Override
    public void setAlignment (int alignment)
    {
        Event.checkIsEventDispatchThread ();
        this.alignment = alignment;
    }

    /**
    * Registers text to be displayed as tooltips for individual selections.
    * An individual tooltip may be {@code null} if no tooltip is wanted
    * for that selection.
    * @param tooltips tooltip text array, in same order as the values that were
    * supplied to the <code>WComboBox</code> constructor.
    */
    @Override
    public void setToolTipText (String [] tooltips)
    {
        Event.checkIsEventDispatchThread ();
        Gui.setToolTipText (comboBox, tooltips);
    }

    /**
    * Gets the value of the component. This will be null if no item is
    * selected.
    * @return the value of the component.
    */
    public T getValue ()
    {
        return value;
    }

    /**
    * Sets the value of the component. If the value is null, no item will
    * be selected.
    * <p>This method does NOT fire a value event.
    * @param value the new value for the component.
    */
    public void setValue (T value)
    {
        Event.checkIsEventDispatchThread ();

        if (value == null)
        {
            // The order of the next two statements is important - we don't
            // want to fire a ValueEvent.
            this.value = null;
            comboBox.setSelectedIndex (-1);
            return;
        }

        for (int i = 0 ; i < values.length ; i++)
        {
            if (value.equals (values [i]))
            {
                // The order of the next two statements is important - we don't
                // want to fire a ValueEvent.
                this.value = value;
                comboBox.setSelectedIndex (i);
                return;
            }
        }

        throw new IllegalArgumentException ("Unrecognized value: " + value);
    }

    @Override
    public void setBackground (Color bg)
    {
        if (bg == Gui.COLOUR_BACKGROUND_NORMAL)
            bg = Gui.COLOUR_BACKGROUND_PANEL;
        comboBox.setBackground (bg);
    }
}

