/* class JList
 *
 * Copyright (C) 2003  R M Pitman
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 * Modified Jul 15, 2003 by Tadpole Computer, Inc.
 * Modifications Copyright 2003 by Tadpole Computer, Inc.
 *
 * Modifications are hereby licensed to all parties at no charge under
 * the same terms as the original.
 *
 * Fixed case where visible row count was larger that the total number
 * of rows in the model.
 */

package charvax.swing;

import charva.awt.*;
import charva.awt.event.KeyEvent;
import charva.awt.event.MouseEvent;
import charva.awt.event.ScrollEvent;
import charva.awt.event.ScrollListener;
import charvax.swing.event.ListSelectionListener;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Vector;

/**
 * A component that allows the user to select one or more objects from a
 * list.<p>
 * <p/>
 * The JList does not provide scrolling capability. The
 * JList is normally inserted into a JScrollPane to provide scrolling.<p>
 */
public class JList
        extends JComponent
        implements Scrollable {

    /**
     * Constructs a JList with 5 rows, 10 columns wide.
     */
    public JList() {
        setModel(new DefaultListModel());
    }

    /**
     * Construct a JList that displays the elements in the specified
     * non-null model.
     */
    public JList(ListModel model_) {
        setModel(model_);
    }

    /**
     * Construct a JList containing the items in the specified array.
     */
    public JList(Object[] items_) {
        setListData(items_);
    }

    /**
     * Construct a JList containing the items in the specified Vector.
     */
    public JList(Vector items_) {
        setListData(items_);
    }

    /**
     * Constructs a ListModel from an array of Objects and then applies
     * setModel to it.
     */
    public void setListData(Object[] listData_) {
        DefaultListModel model = new DefaultListModel();

        for (int i = 0; i < listData_.length; i++) {
            model.addElement(listData_[i]);
        }
        setModel(model);
    }

    /**
     * Constructs a ListModel from a Vector and then applies setModel to it.
     */
    public void setListData(Vector listData_) {
        Enumeration e = listData_.elements();
        DefaultListModel model = new DefaultListModel();

        while (e.hasMoreElements()) {
            Object item = e.nextElement();
            model.addElement(item);
        }
        setModel(model);
    }

    /**
     * Sets the model that represents the "contents" of the list, and
     * clears the selection.
     */
    public void setModel(ListModel model_) {
        _listModel = model_;
        _selectionModel.clearSelection();
    }

    /**
     * Returns the data model that holds the list of items displayed by this
     * JList.
     */
    public ListModel getModel() {
        return _listModel;
    }

    /**
     * Set the maximum number of rows that can be displayed at a time
     * by the JScrollPane which contains this JList.
     */
    public void setVisibleRowCount(int rows_) {
        _visibleRows = rows_;
    }

    public int getVisibleRowCount() {
        return _visibleRows;
    }

    /**
     * Set the number of columns INSIDE the list.
     */
    public void setColumns(int cols_) {
        _columns = cols_;
    }

    public Dimension getSize() {
        return new Dimension(getWidth(), getHeight());
    }

    public int getWidth() {
        Insets insets = super.getInsets();
        return _columns + insets.left + insets.right;
    }

    public int getHeight() {
        Insets insets = super.getInsets();
        return _listModel.getSize() + insets.top + insets.bottom;
    }

    /**
     * Returns the size of the viewport needed to display visibleRows rows.
     */
    public Dimension getPreferredScrollableViewportSize() {
        return new Dimension(getWidth(), getVisibleRowCount());
    }

    /**
     * Sets the selection model of the JList to an implementation
     * of the ListSelectionModel interface.
     */
    public void setSelectionModel(ListSelectionModel model_) {
        _selectionModel = model_;
    }

    /**
     * Returns the list's implementation of ListSelectionModel.
     */
    public ListSelectionModel getSelectionModel() {
        return _selectionModel;
    }

    /**
     * Register an ListSelectionListener object for this component.
     * The listener is notified each time a change to the selection occurs.
     */
    public void addListSelectionListener(ListSelectionListener il_) {
        _selectionModel.addListSelectionListener(il_);
    }

    /**
     * Remove the specified ListSelectionListener from the list of listeners
     * that will be notified when the selection changes.
     */
    public void removeListSelectionListener(ListSelectionListener listener_) {
        _selectionModel.removeListSelectionListener(listener_);
    }

    /**
     * Get the first selected index, or -1 if there is no selected index.
     */
    public int getSelectedIndex() {
        return _selectionModel.getMinSelectionIndex();
    }

    /**
     * Returns an array of the selected indices. The indices are
     * sorted in increasing index order.
     */
    public int[] getSelectedIndices() {
        ArrayList objects = new ArrayList();
        if (!_selectionModel.isSelectionEmpty()) {
            int first = _selectionModel.getMinSelectionIndex();
            int last = _selectionModel.getMaxSelectionIndex();
            for (int i = first; i <= last; i++) {
                if (_selectionModel.isSelectedIndex(i))
                    objects.add(new Integer(i));
            }
        }

        int[] values = new int[objects.size()];
        for (int i = 0; i < values.length; i++) {
            values[i] = ((Integer) objects.get(i)).intValue();
        }
        return values;
    }

    /**
     * Get the first selected item on this list, or <code>null</code>
     * if the selection is empty.
     */
    public Object getSelectedValue() {

        /* Return null if there are no selected items.
         */
        int index = _selectionModel.getMinSelectionIndex();
        if (index == -1)
            return null;

        return _listModel.getElementAt(index);
    }

    /**
     * Returns an array of the selected values. The objects are
     * sorted in increasing index order.
     */
    public Object[] getSelectedValues() {
        ArrayList objects = new ArrayList();
        if (!_selectionModel.isSelectionEmpty()) {
            int first = _selectionModel.getMinSelectionIndex();
            int last = _selectionModel.getMaxSelectionIndex();
            for (int i = first; i <= last; i++) {
                if (_selectionModel.isSelectedIndex(i))
                    objects.add(_listModel.getElementAt(i));
            }
        }
        return objects.toArray();
    }

    /**
     * Make the specified item visible (by scrolling the list up or down).
     * This method does not do anything unless the JList is in a JViewport.
     * Note that the list is not redrawn by this method; the redrawing is
     * done by the JScrollPane (that is registered as a ScrollListener).
     */
    public synchronized void ensureIndexIsVisible(int index_) {
        if (!(getParent() instanceof JViewport))
            return;

        if (index_ < 0)
            index_ = 0;
        else if (index_ > _listModel.getSize() - 1) {
            index_ = _listModel.getSize() - 1;
        }

        // It seems reasonable to assume that the "current row" should
        // be set to the index that is being made visible.
        _currentRow = index_;

        Toolkit term = Toolkit.getDefaultToolkit();
        EventQueue evtqueue = term.getSystemEventQueue();

        // First scroll the list DOWN so that index 0 is visible.
        evtqueue.postEvent(new ScrollEvent(this, ScrollEvent.DOWN,
                new Point(0, 0)));

        // Then (if necessary) scroll it UP so that the specified index
        // is not below the bottom of the viewport.
        evtqueue.postEvent(new ScrollEvent(this, ScrollEvent.UP,
                new Point(0, index_)));
    }

    /**
     * Select the item at the specified index. Note that this method
     * does not redraw the JList.
     */
    public void setSelectedIndex(int index_) {
        _selectionModel.setSelectionInterval(index_, index_);
    }

    /**
     * Sets the selection to be the set union between the current
     * selection and the specified interval between index0_ and index1_
     * (inclusive).
     */
    public void addSelectionInterval(int index0_, int index1_) {
        _selectionModel.addSelectionInterval(index0_, index1_);
    }

    /**
     * Sets the selection to be the set difference between the current
     * selection and the specified interval between index0_ and index1_
     * (inclusive).
     */
    public void removeSelectionInterval(int index0_, int index1_) {
        _selectionModel.removeSelectionInterval(index0_, index1_);
    }

    /**
     * Clears the selection. After this <code>isSelectionEmpty()</code>
     * will return true.
     */
    public void clearSelection() {
        _selectionModel.clearSelection();
    }

    /**
     * Returns the lowest selected item index.
     */
    public int getMinSelectionIndex() {
        return _selectionModel.getMinSelectionIndex();
    }

    /**
     * Returns the highest selected item index.
     */
    public int getMaxSelectionIndex() {
        return _selectionModel.getMaxSelectionIndex();
    }

    /**
     * Sets the flag that determines whether this list allows multiple
     * selections.
     *
     * @param mode_ the selection mode. Allowed values are:<p>
     *              <ul>
     *              <li> ListSelectionModel.SINGLE_SELECTION. Only one list index
     *              can be selected at a time.
     *              <li> ListSelectionModel.SINGLE_INTERVAL_SELECTION.
     *              <li> ListSelectionModel.MULTIPLE_INTERVAL_SELECTION. Any number
     *              of list items can be selected simultaneously.
     *              </ul>
     */
    public void setSelectionMode(int mode_) {
        _selectionModel.setSelectionMode(mode_);
    }

    /**
     * Determines whether this list allows multiple selections.
     */
    public int getSelectionMode() {
        return _selectionModel.getSelectionMode();
    }

    /**
     * Determines if the specified item in this scrolling list is selected.
     */
    public boolean isIndexSelected(int index_) {
        return _selectionModel.isSelectedIndex(index_);
    }

    /**
     * Called by LayoutManager.
     */
    public Dimension minimumSize() {

        /* Calculate the minimum number of columns that will contain all the
         * items in the list.
         */
        _columns = 1;
        for (int i = 0; i < _listModel.getSize(); i++) {
            String c = _listModel.getElementAt(i).toString();
            if (c.length() > _columns)
                _columns = c.length();
        }

        /* Take into account the border inherited from the JComponent
         * superclass.
         */
        Insets insets = super.getInsets();
        return new Dimension(_columns + insets.left + insets.right,
                _visibleRows + insets.top + insets.bottom);
    }

    public void requestFocus() {
        /* Generate the FOCUS_GAINED event.
         */
        super.requestFocus();

        /* Get the absolute origin of this component
         */
        Point origin = getLocationOnScreen();
        Insets insets = super.getInsets();
        origin.translate(insets.left, insets.top);
        Toolkit.getDefaultToolkit().setCursor(origin.addOffset(0, _currentRow));
    }

    /**
     * Draws this component. Overrides draw() in Component.
     */
    public void draw() {

        /* This situation can occur if the ListModel has been changed.
         */
        if (_currentRow >= _listModel.getSize()) {
            if (_listModel.getSize() == 0)
                _currentRow = 0;
            else
                _currentRow = _listModel.getSize() - 1;
        }

        /* Draw the border if it exists
         */
        super.draw();

        /* Get the absolute origin of this component.
         */
        Point origin = getLocationOnScreen();
        Insets insets = super.getInsets();
        origin.translate(insets.left, insets.top);

        Toolkit term = Toolkit.getDefaultToolkit();

        int colorpair = getCursesColor();
        int attribute;

        StringBuffer blanks = new StringBuffer();
        for (int j = 0; j < _columns; j++)
            blanks.append(' ');

        for (int i = 0; i < _listModel.getSize(); i++) {

            term.setCursor(origin.x, origin.y + i);

            if (isIndexSelected(i))
                attribute = Toolkit.A_REVERSE;
            else
                attribute = 0;

            if (i == _currentRow)
                attribute += Toolkit.A_BOLD;

            String item = _listModel.getElementAt(i).toString();

            StringBuffer buffer = new StringBuffer(item);
            for (int k = item.length(); k < _columns; k++)
                buffer.append(' ');

            term.addString(buffer.toString(), attribute, colorpair);
        }	// end FOR loop

        for (int i = _listModel.getSize(); i < _visibleRows; i++) {
            term.setCursor(origin.x, origin.y + i);
            term.addString(blanks.toString(), 0, colorpair);
        }
    }

    public void processKeyEvent(KeyEvent ke_) {
        /* First call all KeyListener objects that may have been registered
         * for this component.
         */
        super.processKeyEvent(ke_);

        /* Check if any of the KeyListeners consumed the KeyEvent.
         */
        if (ke_.isConsumed())
            return;

        Toolkit term = Toolkit.getDefaultToolkit();
        EventQueue evtqueue = term.getSystemEventQueue();
        int key = ke_.getKeyCode();
        switch (key) {
            case '\t':
                getParent().nextFocus();
                return;

            case KeyEvent.VK_BACK_TAB:
                getParent().previousFocus();
                return;

            case KeyEvent.VK_DOWN:
                /* If we are already at the bottom of the list, ignore
                 * this keystroke.
                 */
                if (_currentRow >= _listModel.getSize() - 1)
                    return;

                evtqueue.postEvent(new ScrollEvent(this, ScrollEvent.UP,
                        new Point(0, ++_currentRow)));
                break;

            case KeyEvent.VK_PAGE_DOWN:
                _currentRow += _visibleRows;
                if (_currentRow >= _listModel.getSize())
                    _currentRow = _listModel.getSize() - 1;

                evtqueue.postEvent(new ScrollEvent(this, ScrollEvent.UP,
                        new Point(0, _currentRow)));
                break;

            case KeyEvent.VK_END:
                _currentRow = _listModel.getSize() - 1;
                evtqueue.postEvent(new ScrollEvent(this, ScrollEvent.UP, new Point(0, _currentRow)));
                break;

            case KeyEvent.VK_UP:
                /* If we are already at the top of the list, ignore
                 * this keystroke.
                 */
                if (_currentRow < 1)
                    return;

                evtqueue.postEvent(new ScrollEvent(this, ScrollEvent.DOWN, new Point(0, --_currentRow)));
                break;

            case KeyEvent.VK_PAGE_UP:
                _currentRow -= _visibleRows;
                if (_currentRow < 0)
                    _currentRow = 0;

                evtqueue.postEvent(new ScrollEvent(this, ScrollEvent.DOWN,
                        new Point(0, _currentRow)));
                break;

            case KeyEvent.VK_ENTER:
                _doSelect();
                break;

            case KeyEvent.VK_HOME:
                _currentRow = 0;
                break;
        }

        if ((getParent() instanceof JViewport) == false) {
            draw();
            requestFocus();
            super.requestSync();
        }
    }

    private void _doSelect() {
        /* Pressing ENTER or double-clicking on a row selects/deselects
         * the current row. If the list is empty, ignore the keystroke.
         */
        if (_listModel.getSize() == 0)
            return;

        if (isIndexSelected(_currentRow))
            removeSelectionInterval(_currentRow, _currentRow);
        else {
            int mode = getSelectionMode();
            if (mode == ListSelectionModel.SINGLE_SELECTION)
                setSelectedIndex(_currentRow);
            else
                addSelectionInterval(_currentRow, _currentRow);
        }
        repaint();
    }

    public void processMouseEvent(MouseEvent e_) {
        super.processMouseEvent(e_);

        if (e_.getButton() == MouseEvent.BUTTON1 &&
                e_.getModifiers() == MouseEvent.MOUSE_CLICKED &&
                this.isFocusTraversable()) {

            if (e_.getClickCount() == 1) {
                int y = e_.getY();
                Point origin = getLocationOnScreen();
                Container parent = getParent();
                if (parent instanceof JViewport) {
                    _currentRow = y - origin.y;
                    repaint();
                }
            } else
                _doSelect();
        }
    }

    /**
     * Register a ScrollListener object for this JList.
     */
    public void addScrollListener(ScrollListener sl_) {
        if (_scrollListeners == null)
            _scrollListeners = new Vector();
        _scrollListeners.add(sl_);
    }

    /**
     * Remove a ScrollListener object that is registered for this JList.
     */
    public void removeScrollListener(ScrollListener sl_) {
        if (_scrollListeners == null)
            return;
        _scrollListeners.remove(sl_);
    }

    /**
     * Process scroll events generated by this JList.
     */
    public void processScrollEvent(ScrollEvent e_) {
        if (_scrollListeners != null) {
            for (Enumeration e = _scrollListeners.elements();
                 e.hasMoreElements();) {

                ScrollListener sl = (ScrollListener) e.nextElement();
                sl.scroll(e_);
            }
        }
    }

    /**
     * Outputs a textual description of this component to stderr.
     */
    public void debug(int level_) {
        for (int i = 0; i < level_; i++)
            System.err.print("    ");
        System.err.println("JList origin=" + _origin +
                " size=" + minimumSize());
    }

    //================================================================
    // INSTANCE VARIABLES

    private int _visibleRows = 5;
    private int _columns = 10;

    /**
     * Offset (from start of list) of the item under the cursor (i.e. the
     * item that will be selected/deselected if the user presses ENTER)
     */
    protected int _currentRow = 0;

    /**
     * The ListSelectionModel used by this JList.
     */
    protected ListSelectionModel _selectionModel =
            new DefaultListSelectionModel();

    /**
     * The ListModel that holds the items that are displayed by
     * this JList.
     */
    protected ListModel _listModel;

    /**
     * A list of ScrollListeners registered for this JList.
     */
    private Vector _scrollListeners = null;

}
