/*
 * Copyright (c) 2002-2011 JGoodies Karsten Lentzsch. 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 JGoodies Karsten Lentzsch 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 com.jgoodies.binding.adapter;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.ListSelectionModel;
import javax.swing.event.EventListenerList;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import com.jgoodies.binding.value.ValueModel;

/**
 * A {@link ListSelectionModel} implementation that has the list index
 * bound to a {@link ValueModel}. Therefore this class supports only
 * the {@code SINGLE_SELECTION} mode where only one list index
 * can be selected at a time.  In this mode the setSelectionInterval
 * and addSelectionInterval methods are equivalent, and only
 * the second index argument (the "lead index") is used.<p>
 *
 * <strong>Example:</strong><pre>
 * SelectionInList selectionInList = new SelectionInList(...);
 * JList list = new JList();
 * list.setModel(selectionInList);
 * list.setSelectionModel(new SingleListSelectionAdapter(
 *               selectionInList.getSelectionIndexHolder()));
 * </pre>
 *
 * @author  Karsten Lentzsch
 * @version $Revision: 1.12 $
 *
 * @see     ValueModel
 * @see     javax.swing.JList
 * @see     javax.swing.JTable
 */
public final class SingleListSelectionAdapter implements ListSelectionModel {

    private static final int MIN = -1;
    private static final int MAX = Integer.MAX_VALUE;

    private int firstAdjustedIndex = MAX;
    private int lastAdjustedIndex  = MIN;
    private int firstChangedIndex  = MAX;
    private int lastChangedIndex   = MIN;

    /**
     * Refers to the selection index holder.
     */
    private final ValueModel selectionIndexHolder;

    /**
     * Indicates if the selection is undergoing a series of changes.
     */
    private boolean valueIsAdjusting;

    /**
     * Holds the List of ListSelectionListener.
     *
     * @see #addListSelectionListener(ListSelectionListener)
     * @see #removeListSelectionListener(ListSelectionListener)
     */
    private final EventListenerList listenerList = new EventListenerList();


    // Instance Creation ****************************************************

    /**
     * Constructs a {@code SingleListSelectionAdapter} with
     * the given selection index holder.
     *
     * @param selectionIndexHolder   holds the selection index
     */
    public SingleListSelectionAdapter(ValueModel selectionIndexHolder) {
        this.selectionIndexHolder = selectionIndexHolder;
        this.selectionIndexHolder
                .addValueChangeListener(new SelectionIndexChangeHandler());
    }


    // Accessing the Selection Index ****************************************

    /**
     * Returns the selection index.
     *
     * @return the selection index
     */
    private int getSelectionIndex() {
        Object value = selectionIndexHolder.getValue();
        return value == null ? MIN : ((Integer) value).intValue();
    }


    /**
     * Sets a new selection index. Does nothing if it is the same as before.
     * If this represents a change to the current selection then
     * notify each ListSelectionListener.
     *
     * @param newSelectionIndex   the selection index to be set
     */
    private void setSelectionIndex(int newSelectionIndex) {
        setSelectionIndex(getSelectionIndex(), newSelectionIndex);
    }


    private void setSelectionIndex(int oldSelectionIndex, int newSelectionIndex) {
        if (oldSelectionIndex == newSelectionIndex) { return; }
        markAsDirty(oldSelectionIndex);
        markAsDirty(newSelectionIndex);
        selectionIndexHolder.setValue(Integer.valueOf(newSelectionIndex));
        fireValueChanged();
    }


    // ListSelectionModel Implementation ************************************

    /**
     * Sets the selection index to {@code index1}. Since this model
     * supports only a single selection index, the index0 is ignored.
     * This is the behavior the {@code DefaultListSelectionModel}
     * uses in single selection mode.<p>
     *
     * If this represents a change to the current selection, then
     * notify each ListSelectionListener. Note that index0 doesn't have
     * to be less than or equal to index1.
     *
     * @param index0 one end of the interval.
     * @param index1 other end of the interval
     * @see #addListSelectionListener(ListSelectionListener)
     */
    public void setSelectionInterval(int index0, int index1) {
        if (index0 == -1 || index1 == -1) { return; }
        setSelectionIndex(index1);
    }


    /**
     * Sets the selection interval using the given indices.<p>
     *
     * If this represents a change to the current selection, then
     * notify each ListSelectionListener. Note that index0 doesn't have
     * to be less than or equal to index1.
     *
     * @param index0 one end of the interval.
     * @param index1 other end of the interval
     * @see #addListSelectionListener(ListSelectionListener)
     */
    public void addSelectionInterval(int index0, int index1) {
        setSelectionInterval(index0, index1);
    }


    /**
     * Clears the selection if it is equals to index0. Since this model
     * supports only a single selection index, the index1 can be ignored
     * for the selection.<p>
     *
     * If this represents a change to the current selection, then
     * notify each ListSelectionListener. Note that index0 doesn't have
     * to be less than or equal to index1.
     *
     * @param index0 one end of the interval.
     * @param index1 other end of the interval
     * @see #addListSelectionListener(ListSelectionListener)
     */
    public void removeSelectionInterval(int index0, int index1) {
        if (index0 == -1 || index1 == -1) { return; }
        int max = Math.max(index0, index1);
        int min = Math.min(index0, index1);
        if (min <= getSelectionIndex() && getSelectionIndex() <= max) {
            clearSelection();
        }
    }


    /**
     * Returns the selection index.
     *
     * @return the selection index
     * @see #getMinSelectionIndex()
     * @see #setSelectionInterval(int, int)
     * @see #addSelectionInterval(int, int)
     */
    public int getMinSelectionIndex() {
        return getSelectionIndex();
    }


    /**
     * Returns the selection index.
     *
     * @return the selection index
     * @see #getMinSelectionIndex()
     * @see #setSelectionInterval(int, int)
     * @see #addSelectionInterval(int, int)
     */
    public int getMaxSelectionIndex() {
        return getSelectionIndex();
    }


    /**
     * Checks and answers if the given index is selected or not.
     *
     * @param index   the index to be checked
     * @return true if selected, false if deselected
     * @see javax.swing.ListSelectionModel#isSelectedIndex(int)
     */
    public boolean isSelectedIndex(int index) {
        return index < 0 ? false : index == getSelectionIndex();
    }


    /**
     * Returns the selection index.
     *
     * @return the selection index
     * @see #getAnchorSelectionIndex()
     * @see #setSelectionInterval(int, int)
     * @see #addSelectionInterval(int, int)
     */
    public int getAnchorSelectionIndex() {
        return getSelectionIndex();
    }


    /**
     * Sets the selection index.
     *
     * @param newSelectionIndex   the new selection index
     * @see #getLeadSelectionIndex()
     */
    public void setAnchorSelectionIndex(int newSelectionIndex) {
        setSelectionIndex(newSelectionIndex);
    }


    /**
     * Returns the selection index.
     *
     * @return the selection index
     * @see #getAnchorSelectionIndex()
     * @see #setSelectionInterval(int, int)
     * @see #addSelectionInterval(int, int)
     */
    public int getLeadSelectionIndex() {
        return getSelectionIndex();
    }


    /**
     * Sets the selection index.
     *
     * @param newSelectionIndex   the new selection index
     * @see #getLeadSelectionIndex()
     */
    public void setLeadSelectionIndex(int newSelectionIndex) {
        setSelectionIndex(newSelectionIndex);
    }


    /**
     * Changes the selection to have no index selected.  If this represents
     * a change to the current selection then notify each ListSelectionListener.
     *
     * @see #addListSelectionListener(ListSelectionListener)
     */
    public void clearSelection() {
        setSelectionIndex(-1);
    }


    /**
     * Returns true if no index is selected.
     *
     * @return true if no index is selected
     */
    public boolean isSelectionEmpty() {
        return getSelectionIndex() == -1;
    }


    /**
     * Inserts length indices beginning before/after index. If the value
     * This method is typically called to synchronize the selection model
     * with a corresponding change in the data model.
     *
     * @param index   the index to start the insertion
     * @param length  the length of the inserted interval
     * @param before  true to insert before the start index
     */
    public void insertIndexInterval(int index, int length, boolean before) {
        /*
         * The first new index will appear at insMinIndex and the last one will
         * appear at insMaxIndex
         */
        if (isSelectionEmpty()) {
            return;
        }

        int insMinIndex = before ? index : index + 1;
        int selectionIndex = getSelectionIndex();
        // If the added elements are after the index; do nothing.
        if (selectionIndex >= insMinIndex) {
            setSelectionIndex(selectionIndex + length);
        }
    }


    /**
     * Remove the indices in the interval index0,index1 (inclusive) from
     * the selection model.  This is typically called to sync the selection
     * model width a corresponding change in the data model.<p>
     *
     * Clears the selection if it is in the specified interval.
     *
     * @param index0 the first index to remove from the selection
     * @param index1 the last index to remove from the selection
     * @throws IndexOutOfBoundsException if either {@code index0}
     *    or {@code index1} are less than -1
     * @see ListSelectionModel#removeSelectionInterval(int, int)
     * @see #setSelectionInterval(int, int)
     * @see #addSelectionInterval(int, int)
     * @see #addListSelectionListener(ListSelectionListener)
     */
    public void removeIndexInterval(int index0, int index1) {
        if (index0 < -1 || index1 < -1) {
            throw new IndexOutOfBoundsException(
                "Both indices must be greater or equals to -1.");
        }
        if (isSelectionEmpty()) {
            return;
        }
        int lower = Math.min(index0, index1);
        int upper = Math.max(index0, index1);
        int selectionIndex = getSelectionIndex();

        if (lower <= selectionIndex && selectionIndex <= upper) {
            clearSelection();
        } else if (upper < selectionIndex) {
            int translated = selectionIndex - (upper - lower + 1);
            setSelectionInterval(translated, translated);
        }
    }


    /**
     * This property is true if upcoming changes to the value
     * of the model should be considered a single event. For example
     * if the model is being updated in response to a user drag,
     * the value of the valueIsAdjusting property will be set to true
     * when the drag is initiated and be set to false when
     * the drag is finished.  This property allows listeners to
     * to update only when a change has been finalized, rather
     * than always handling all of the intermediate values.
     *
     * @param newValueIsAdjusting  The new value of the property.
     * @see #getValueIsAdjusting()
     */
    public void setValueIsAdjusting(boolean newValueIsAdjusting) {
        boolean oldValueIsAdjusting = valueIsAdjusting;
        if (oldValueIsAdjusting == newValueIsAdjusting) { return; }
        valueIsAdjusting = newValueIsAdjusting;
        fireValueChanged(newValueIsAdjusting);
    }


    /**
     * Returns true if the value is undergoing a series of changes.
     * @return true if the value is currently adjusting
     * @see #setValueIsAdjusting(boolean)
     */
    public boolean getValueIsAdjusting() {
        return valueIsAdjusting;
    }


    /**
     * Sets the selection mode. Only {@code SINGLE_SELECTION} is allowed
     * in this implementation. Other modes are not supported and will throw
     * an {@code IllegalArgumentException}.<p>
     *
     * With {@code SINGLE_SELECTION} only one list index
     * can be selected at a time.  In this mode the setSelectionInterval
     * and addSelectionInterval methods are equivalent, and only
     * the second index argument (the "lead index") is used.
     *
     * @param selectionMode   the mode to be set
     * @see #getSelectionMode()
     * @see javax.swing.ListSelectionModel#setSelectionMode(int)
     */
    public void setSelectionMode(int selectionMode) {
        if (selectionMode != SINGLE_SELECTION) {
            throw new UnsupportedOperationException(
                "The SingleListSelectionAdapter must be used in single selection mode.");
        }
    }


    /**
     * Returns the fixed selection mode {@code SINGLE_SELECTION}.
     *
     * @return {@code SINGLE_SELECTION}
     * @see #setSelectionMode(int)
     */
    public int getSelectionMode() {
        return ListSelectionModel.SINGLE_SELECTION;
    }


    /**
     * Add a listener to the list that's notified each time a change
     * to the selection occurs.
     *
     * @param listener the ListSelectionListener
     * @see #removeListSelectionListener(ListSelectionListener)
     * @see #setSelectionInterval(int, int)
     * @see #addSelectionInterval(int, int)
     * @see #removeSelectionInterval(int, int)
     * @see #clearSelection()
     * @see #insertIndexInterval(int, int, boolean)
     * @see #removeIndexInterval(int, int)
     */
    public void addListSelectionListener(ListSelectionListener listener) {
        listenerList.add(ListSelectionListener.class, listener);
    }


    /**
     * Remove a listener from the list that's notified each time a
     * change to the selection occurs.
     *
     * @param listener the ListSelectionListener
     * @see #addListSelectionListener(ListSelectionListener)
     */
    public void removeListSelectionListener(ListSelectionListener listener) {
        listenerList.remove(ListSelectionListener.class, listener);
    }


    /**
     * Returns an array of all the list selection listeners
     * registered on this {@code DefaultListSelectionModel}.
     *
     * @return all of this model's {@code ListSelectionListener}s
     *         or an empty
     *         array if no list selection listeners are currently registered
     *
     * @see #addListSelectionListener(ListSelectionListener)
     * @see #removeListSelectionListener(ListSelectionListener)
     */
    public ListSelectionListener[] getListSelectionListeners() {
        return listenerList.getListeners(ListSelectionListener.class);
    }


    // Change Handling and Listener Notification ****************************

    // Updates first and last change indices
    private void markAsDirty(int index) {
        if (index < 0) {
            return;
        }
        firstAdjustedIndex = Math.min(firstAdjustedIndex, index);
        lastAdjustedIndex  = Math.max(lastAdjustedIndex,  index);
    }


    /**
     * Notifies listeners that we have ended a series of adjustments.
     *
     * @param isAdjusting   true if there are multiple changes
     */
    private void fireValueChanged(boolean isAdjusting) {
        if (lastChangedIndex == MIN) { return; }

        /* Change the values before sending the event to the
         * listeners in case the event causes a listener to make
         * another change to the selection.
         */
        int oldFirstChangedIndex = firstChangedIndex;
        int oldLastChangedIndex  = lastChangedIndex;
        firstChangedIndex = MAX;
        lastChangedIndex  = MIN;
        fireValueChanged(oldFirstChangedIndex, oldLastChangedIndex, isAdjusting);
    }


    /**
     * Notifies {@code ListSelectionListeners} that the value
     * of the selection, in the closed interval {@code firstIndex},
     * {@code lastIndex}, has changed.
     *
     * @param firstIndex   the first index that has changed
     * @param lastIndex    the last index that has changed
     */
    private void fireValueChanged(int firstIndex, int lastIndex) {
        fireValueChanged(firstIndex, lastIndex, getValueIsAdjusting());
    }


    /**
     * Notifies all registered listeners that the selection has changed.
     *
     * @param firstIndex the first index in the interval
     * @param lastIndex the last index in the interval
     * @param isAdjusting true if this is the final change in a series of
     *      adjustments
     * @see EventListenerList
     */
    private void fireValueChanged(int firstIndex, int lastIndex,
            boolean isAdjusting) {
        Object[] listeners = listenerList.getListenerList();
        ListSelectionEvent e = null;
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == ListSelectionListener.class) {
                if (e == null) {
                    e = new ListSelectionEvent(this, firstIndex, lastIndex,
                            isAdjusting);
                }
                ((ListSelectionListener) listeners[i + 1]).valueChanged(e);
            }
        }
    }


    private void fireValueChanged() {
        if (lastAdjustedIndex == MIN) { return; }

        /* If getValueAdjusting() is true, (eg. during a drag opereration)
         * record the bounds of the changes so that, when the drag finishes (and
         * setValueAdjusting(false) is called) we can post a single event
         * with bounds covering all of these individual adjustments.
         */
        if (getValueIsAdjusting()) {
            firstChangedIndex = Math.min(firstChangedIndex, firstAdjustedIndex);
            lastChangedIndex  = Math.max(lastChangedIndex, lastAdjustedIndex);
        }

        /* Change the values before sending the event to the
         * listeners in case the event causes a listener to make
         * another change to the selection.
         */
        int oldFirstAdjustedIndex = firstAdjustedIndex;
        int oldLastAdjustedIndex  = lastAdjustedIndex;
        firstAdjustedIndex = MAX;
        lastAdjustedIndex  = MIN;
        fireValueChanged(oldFirstAdjustedIndex, oldLastAdjustedIndex);
    }

    // Helper Classes *******************************************************

    /**
     * Listens to changes of the selection index.
     */
    private final class SelectionIndexChangeHandler implements PropertyChangeListener {

        /**
         * The selection index has been changed.
         * Notifies all registered listeners about the change.
         *
         * @param evt   the property change event to be handled
         */
        public void propertyChange(PropertyChangeEvent evt) {
            Object oldValue = evt.getOldValue();
            Object newValue = evt.getNewValue();
            int oldIndex = oldValue == null
                ? MIN
                : ((Integer) oldValue).intValue();
            int newIndex = newValue == null
                ? MIN
                : ((Integer) newValue).intValue();
            setSelectionIndex(oldIndex, newIndex);
        }
    }

}
