
package uk.co.wingpath.util;

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

/**
* This implementation of {@link Variable} is used to store a selection from
* a {@link ListModel}.
* <p>This class also implements {@link ComboBoxModel}.
* <p>Changes to the list are monitored using a {@link ListDataListener} to
* ensure that the selection is always valid. If the current selection is
* removed the list, either another item is selected (if {@code autoSelect} is
* {@code true} or the selection is set to {@code null} (if {@code autoSelect}
* is {@code false}.
* <p>A value of type <code>T</code> may stored in the variable using the
* {@link #setValue} method, and the value may be retrieved using the
* {@link #getValue} method.
* <p>A {@link ValueListener} may be added to a variable to monitor
* changes to the value of the variable. The
* {@link ValueListener#valueChanged valueChanged} method
* of the listener will be called if the {@link #setValue} method is used
* to change the value. The listener will NOT be called if the old and new
* values are the same, as determined by the <code>equals</code> method of
* type <code>T</code>.
* <p>It is permissible to store <code>null</code> as a value.
* <p>Instances of this class are thread-safe.
*/
public class SelectionVariable<T>
    implements Variable<T>, ComboBoxModel
{
    private final ListModel listModel;
    private final boolean autoSelect;
    private int lastSelectedIndex;
    private final ValueEventSource listeners;
    private final ListEventSource listListeners;
    private T value;

    /**
    * Constructs a {@code SelectionVariable} using the specified
    * {@link ListModel}.
    * @param listModel the list model.
    * @param autoSelect if {@code true} a new selection will be made if the
    * current selection is removed from the list. If {@code false}, the
    * selection will be set to {@code null}.
    */
    public SelectionVariable (ListModel listModel, boolean autoSelect)
    {
        this.listModel = listModel;
        this.autoSelect = autoSelect;
        value = null;
        lastSelectedIndex = -1;
        listListeners = new ListEventSource ();
        listeners = new ValueEventSource ();
        listModel.addListDataListener (
            new ListListener ()
            {
                public void listChanged (ListDataEvent e)
                {
                    checkSelection ();
                }
            });
        checkSelection ();
    }

    /**
    * Constructs a SelectionVariable that has a permanently empty list.
    */
    public SelectionVariable ()
    {
        this (
            new AbstractListModel ()
            {
                public int getSize ()
                {
                    return 0;
                }

                public Object getElementAt (int index)
                {
                    return null;
                }
            },
            true);
    }

    /**
    * Gets the list model.
    * @return the list model.
    */
    public ListModel getListModel ()
    {
        return listModel;
    }

    private boolean eq (T v1, T v2)
    {
        if (v1 == v2)
            return true;
        if (v1 == null || v2 == null)
            return false;
        return v1.equals (v2);
    }

    /**
    * Called from the listModel listener if the list contents may have changed.
    * Checks whether the selected item is still in the list, and selects
    * another item if it isn't.
    */
    private synchronized void checkSelection ()
    {
        int size = listModel.getSize ();
        if (lastSelectedIndex < 0)
            lastSelectedIndex = 0;
        if (lastSelectedIndex >= size)
            lastSelectedIndex = size - 1;
        @SuppressWarnings ("unchecked")
        T v = size == 0 ? null : (T) listModel.getElementAt (lastSelectedIndex);
        if (!eq (v, value))
        {
            // Selection no longer valid.
            value = autoSelect ? v : null;
            listeners.fireValueChanged (this);

            // Fire the special event required by JComboBox.
            // Swing Tutorial: "combo box models ... must fire
            // a list data event (a CONTENTS_CHANGED event) when
            // the selection changes".
            // JComboBox doesn't seem to care about the interval,
            // but other listeners might.
            listListeners.fireChanged (this, -1, -1);
        }
    }

    /**
    * Gets the value of the variable.
    * @return the value of the variable.
    */
    public synchronized T getValue ()
    {
        return value;
    }

    /**
    * Sets the value of the variable.
    * <p>If the new value is not equal to the old value, the
    * <code>valueChanged</code> method of each listener will be called.
    * @param value the new value.
    */
    public synchronized void setValue (T value)
    {
        if (!eq (value, this.value))
        {
            this.value = value;
            if (value == null)
            {
                lastSelectedIndex = -1;
            }
            else
            {
                for (lastSelectedIndex = listModel.getSize () -1 ;
                    lastSelectedIndex >= 0 ;
                    --lastSelectedIndex)
                {
                    if (listModel.getElementAt (lastSelectedIndex) == value)
                        break;
                }
            }
            listeners.fireValueChanged (this);

            // Fire the special event required by JComboBox.
            // Swing Tutorial: "combo box models ... must fire
            // a list data event (a CONTENTS_CHANGED event) when
            // the selection changes".
            // JComboBox doesn't seem to care about the interval,
            // but other listeners might.
            listListeners.fireChanged (this, -1, -1);
        }
    }

    /**
    * Adds the specified listener.
    * @param l the listener to be added.
    */
    public void addValueListener (ValueListener l)
    {
        listeners.addListener (l);
    }

    /**
    * Removes the specified listener.
    * @param l the listener to be removed.
    */
    public void removeValueListener (ValueListener l)
    {
        listeners.removeListener (l);
    }

    // ListModel methods.

    @Override
    public int getSize ()
    {
        return listModel.getSize ();
    }

    @Override
    public Object getElementAt (int index)
    {
        return listModel.getElementAt (index);
    }

    @Override
    public void addListDataListener (ListDataListener l)
    {
        listModel.addListDataListener (l);
        listListeners.addListener (l);
    }

    @Override
    public void removeListDataListener (ListDataListener l)
    {
        listModel.removeListDataListener (l);
        listListeners.removeListener (l);
    }

    // ComboBoxModel methods.

    @Override
    @SuppressWarnings ("unchecked")
    public void setSelectedItem (Object item)
    {
        setValue ((T) item);
    }

    @Override
    public synchronized T getSelectedItem ()
    {
        return value;
    }
}

