/* 
 * This file is part of the Echo Web Application Framework (hereinafter "Echo").
 * Copyright (C) 2002-2005 NextApp, Inc.
 *
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 */

package nextapp.echo2.app.list;

import java.util.EventListener;

import nextapp.echo2.app.Border;
import nextapp.echo2.app.Color;
import nextapp.echo2.app.Component;
import nextapp.echo2.app.Extent;
import nextapp.echo2.app.Font;
import nextapp.echo2.app.Insets;
import nextapp.echo2.app.event.ActionEvent;
import nextapp.echo2.app.event.ActionListener;
import nextapp.echo2.app.event.ChangeEvent;
import nextapp.echo2.app.event.ChangeListener;
import nextapp.echo2.app.event.ListDataEvent;
import nextapp.echo2.app.event.ListDataListener;

/**
 * An abstract base class for list components.
 */
public abstract class AbstractListComponent extends Component {

    public static final String INPUT_ACTION = "action";

    public static final String PROPERTY_ACTION_COMMAND = "actionCommand";
    
    public static final String ACTION_LISTENERS_CHANGED_PROPERTY = "actionListeners";
    public static final String LIST_DATA_CHANGED_PROPERTY = "listData";
    public static final String LIST_MODEL_CHANGED_PROPERTY = "listModel";
    public static final String LIST_CELL_RENDERER_CHANGED_PROPERTY = "listCellRenderer";
    public static final String SELECTION_MODEL_CHANGED_PROPERTY = "listSelectionModel";
    public static final String SELECTION_CHANGED_PROPERTY = "listSelectionChanged";

    public static final String PROPERTY_BORDER = "border";
    public static final String PROPERTY_DISABLED_BACKGROUND = "disabledBackground";
    public static final String PROPERTY_DISABLED_BORDER = "disabledBorder";
    public static final String PROPERTY_DISABLED_FONT = "disabledFont";
    public static final String PROPERTY_DISABLED_FOREGROUND = "disabledForeground";
    public static final String PROPERTY_HEIGHT = "height";
    public static final String PROPERTY_INSETS = "insets";
    public static final String PROPERTY_ROLLOVER_BACKGROUND = "rolloverBackground";
    public static final String PROPERTY_ROLLOVER_ENABLED = "rolloverEnabled";
    public static final String PROPERTY_ROLLOVER_FONT = "rolloverFont";
    public static final String PROPERTY_ROLLOVER_FOREGROUND = "rolloverForeground";
    public static final String PROPERTY_TOOL_TIP_TEXT = "toolTipText";
    public static final String PROPERTY_WIDTH = "width";

    public static final DefaultListCellRenderer DEFAULT_LIST_CELL_RENDERER = new DefaultListCellRenderer();

    /**
     * Local handler for list selection events.
     */
    private ChangeListener changeHandler = new ChangeListener() {

        /**
         * @see nextapp.echo2.app.event.ChangeListener#stateChanged(nextapp.echo2.app.event.ChangeEvent)
         */
        public void stateChanged(ChangeEvent e) {
            firePropertyChange(SELECTION_CHANGED_PROPERTY, null, null);
        }
    };

    /**
     * Local handler of <code>ListDataEvent</code>s.
     */
    private ListDataListener listDataHandler = new ListDataListener() { 
    
        /**
         * @see nextapp.echo2.app.event.ListDataListener#contentsChanged(nextapp.echo2.app.event.ListDataEvent)
         */
        public void contentsChanged(ListDataEvent e) {
            firePropertyChange(LIST_DATA_CHANGED_PROPERTY, null, null);
        }

        /**
         * @see nextapp.echo2.app.event.ListDataListener#intervalAdded(nextapp.echo2.app.event.ListDataEvent)
         */
        public void intervalAdded(ListDataEvent e) {
            firePropertyChange(LIST_DATA_CHANGED_PROPERTY, null, null);
        }

        /**
         * @see nextapp.echo2.app.event.ListDataListener#intervalRemoved(nextapp.echo2.app.event.ListDataEvent)
         */
        public void intervalRemoved(ListDataEvent e) {
            firePropertyChange(LIST_DATA_CHANGED_PROPERTY, null, null);
        }
    };
    
    private ListCellRenderer listCellRenderer = DEFAULT_LIST_CELL_RENDERER;
    private ListModel model;
    private ListSelectionModel selectionModel;
    
    /**
     * Creates a new <code>AbstractListComponent</code> with default models.
     */
    public AbstractListComponent() {
        this(null, null);
    }
    
    /**
     * Creates a new <code>AbstractListComponent</code> with the specified 
     * models.
     * 
     * @param model the list data model
     * @param selectionModel the selection model
     */
    public AbstractListComponent(ListModel model, ListSelectionModel selectionModel) {
        super();
        if (model == null) {
            model = new DefaultListModel();
        }
        if (selectionModel == null) {
            selectionModel = new DefaultListSelectionModel();
        }
        setModel(model);
        setSelectionModel(selectionModel);
    }
    
    /**
     * Adds an <code>ActionListener</code> to the list component.
     * The <code>ActionListener</code> will be invoked when the user
     * selects an item.
     * 
     * @param l the <code>ActionListener</code> to add
     */
    public void addActionListener(ActionListener l) {
        getEventListenerList().addListener(ActionListener.class, l);
        // Notification of action listener changes is provided due to 
        // existence of hasActionListeners() method. 
        firePropertyChange(ACTION_LISTENERS_CHANGED_PROPERTY, null, l);
    }

    /**
     * Fires an action event to all listeners.
     */
    private void fireActionEvent() {
        if (!hasEventListenerList()) {
            return;
        }
        EventListener[] listeners = getEventListenerList().getListeners(ActionListener.class);
        ActionEvent e = null;
        for (int i = 0; i < listeners.length; ++i) {
            if (e == null) {
                e = new ActionEvent(this, (String) getRenderProperty(PROPERTY_ACTION_COMMAND));
            } 
            ((ActionListener) listeners[i]).actionPerformed(e);
        }
    }
    
    /**
     * Returns the action command which will be provided in 
     * <code>ActionEvent</code>s fired by this 
     * <code>AbstractListComponent</code>.
     * 
     * @return the action command
     */
    public String getActionCommand() {
        return (String) getProperty(PROPERTY_ACTION_COMMAND);
    }
    
    /**
     * Returns the <code>Border</code> surrounding the list component.
     * 
     * @return the border
     */
    public Border getBorder() {
        return (Border) getProperty(PROPERTY_BORDER);
    }
    
    /**
     * Returns the <code>ListCellRenderer</code> used to render items.
     * 
     * @return the renderer
     */
    public ListCellRenderer getCellRenderer() {
        return listCellRenderer;
    }
    
    /**
     * Returns the background color displayed when the component is 
     * disabled.
     * 
     * @return the color
     */
    public Color getDisabledBackground() {
        return (Color) getProperty(PROPERTY_DISABLED_BACKGROUND);
    }

    /**
     * Returns the border displayed when the component is 
     * disabled.
     * 
     * @return the border
     */
    public Border getDisabledBorder() {
        return (Border) getProperty(PROPERTY_DISABLED_BORDER);
    }

    /**
     * Returns the font displayed when the component is 
     * disabled.
     * 
     * @return the font
     */
    public Font getDisabledFont() {
        return (Font) getProperty(PROPERTY_DISABLED_FONT);
    }

    /**
     * Returns the foreground color displayed when the component is 
     * disabled.
     * 
     * @return the color
     */
    public Color getDisabledForeground() {
        return (Color) getProperty(PROPERTY_DISABLED_FOREGROUND);
    }

    /**
     * Returns the height.
     * This property only supports <code>Extent</code>s with
     * fixed (i.e., not percent) units.
     * 
     * @return the height
     */
    public Extent getHeight() {
        return (Extent) getProperty(PROPERTY_HEIGHT);
    }
    
    /**
     * Returns the inset margin around between the list components border and content.
     * 
     * @return the inset margin
     */
    public Insets getInsets() {
        return (Insets) getProperty(PROPERTY_INSETS);
    }
    
    /**
     * Returns the model.
     * 
     * @return the model
     */
    public ListModel getModel() {
        return model;
    }
    
    /**
     * Returns the rollover background.
     * 
     * @return the rollover background
     */
    public Color getRolloverBackground() {
        return (Color) getProperty(PROPERTY_ROLLOVER_BACKGROUND);
    }
    
    /**
     * Returns the rollover font.
     * 
     * @return the rollover font
     */
    public Font getRolloverFont() {
        return (Font) getProperty(PROPERTY_ROLLOVER_FONT);
    }
    
    /**
     * Returns the rollover foreground.
     * 
     * @return the rollover foreground
     */
    public Color getRolloverForeground() {
        return (Color) getProperty(PROPERTY_ROLLOVER_FOREGROUND);
    }
    
    /**
     * Returns the selection model.
     * 
     * @return the selection model
     */
    public ListSelectionModel getSelectionModel() {
        return selectionModel;
    }
    
    /**
     * Returns the tool tip text (displayed when the mouse cursor is hovered 
     * over the component).
     * 
     * @return the tool tip text
     */
    public String getToolTipText() {
        return (String) getProperty(PROPERTY_TOOL_TIP_TEXT);
    }
    
    /**
     * Returns the width.
     * This property supports <code>Extent</code>s with
     * fixed or percentile units.
     * 
     * @return the width
     */
    public Extent getWidth() {
        return (Extent) getProperty(PROPERTY_WIDTH);
    }
    
    /**
     * Determines the any <code>ActionListener</code>s are registered.
     * 
     * @return true if any action listeners are registered
     */
    public boolean hasActionListeners() {
        return hasEventListenerList() && getEventListenerList().getListenerCount(ActionListener.class) != 0;
    }
    
    /**
     * Determines if rollover effects are enabled.
     * 
     * @return true if rollover effects are enabled
     */
    public boolean isRolloverEnabled() {
        Boolean value = (Boolean) getProperty(PROPERTY_ROLLOVER_ENABLED);
        return value == null ? false : value.booleanValue();
    }

    /**
     * This component does not support children.
     *
     * @see nextapp.echo2.app.Component#isValidChild(nextapp.echo2.app.Component)
     */
    public boolean isValidChild(Component child) {
        return false;
    }

    /**
     * @see nextapp.echo2.app.Component#processInput(java.lang.String, java.lang.Object)
     */
    public void processInput(String inputName, Object inputValue) {
        super.processInput(inputName, inputValue);
        
        if (SELECTION_CHANGED_PROPERTY.equals(inputName)) {
            int[] selectedIndices = (int[]) inputValue;
            ListSelectionModel selectionModel = getSelectionModel();
            selectionModel.clearSelection();
            for (int i = 0; i < selectedIndices.length; ++i) {
                selectionModel.setSelectedIndex(selectedIndices[i], true);
            }
        } else if (INPUT_ACTION.equals(inputName)) {
            fireActionEvent();
        }
    }
    
    /**
     * Removes an <code>ActionListener</code> from the list component.
     * 
     * @param l the <code>ActionListener</code> to remove
     */
    public void removeActionListener(ActionListener l) {
        if (!hasEventListenerList()) {
            return;
        }
        getEventListenerList().removeListener(ActionListener.class, l);
        // Notification of action listener changes is provided due to 
        // existence of hasActionListeners() method. 
        firePropertyChange(ACTION_LISTENERS_CHANGED_PROPERTY, l, null);
    }
    
    /**
     * Sets the action command which will be provided in
     * <code>ActionEvent</code>s fired by this 
     * <code>AbstractListComponent</code>.
     * 
     * @param newValue the new action command
     */
    public void setActionCommand(String newValue) {
        setProperty(PROPERTY_ACTION_COMMAND, newValue);
    }
    
    /**
     * Sets the <code>Border</code> surrounding the list component.
     * 
     * @param newValue the new <code>Border</code>
     */
    public void setBorder(Border newValue) {
        setProperty(PROPERTY_BORDER, newValue);
    }
    
    /**
     * Sets the renderer for items.
     * The renderer may not be null (use <code>DEFAULT_LIST_CELL_RENDERER</code>
     * for default behavior).
     * 
     * @param newValue the new renderer
     */
    public void setCellRenderer(ListCellRenderer newValue) {
        if (newValue == null) {
            throw new IllegalArgumentException("Cell Renderer may not be null.");
        }
        ListCellRenderer oldValue = listCellRenderer;
        listCellRenderer = newValue;
        firePropertyChange(LIST_CELL_RENDERER_CHANGED_PROPERTY, oldValue, newValue);
    }
    
    /**
     * Sets the background color displayed when the component is disabled.
     * 
     * @param newValue the new <code>Color</code>
     */
    public void setDisabledBackground(Color newValue) {
        setProperty(PROPERTY_DISABLED_BACKGROUND, newValue);
    }

    /**
     * Sets the border displayed when the component is disabled.
     * 
     * @param newValue the new border
     */
    public void setDisabledBorder(Border newValue) {
        setProperty(PROPERTY_DISABLED_BORDER, newValue);
    }

    /**
     * Sets the font displayed when the component is disabled.
     * 
     * @param newValue the new <code>Font</code>
     */
    public void setDisabledFont(Font newValue) {
        setProperty(PROPERTY_DISABLED_FONT, newValue);
    }

    /**
     * Sets the foreground color displayed when the component is disabled.
     * 
     * @param newValue the new <code>Color</code>
     */
    public void setDisabledForeground(Color newValue) {
        setProperty(PROPERTY_DISABLED_FOREGROUND, newValue);
    }

    /**
     * Sets the height.
     * This property only supports <code>Extent</code>s with
     * fixed (i.e., not percent) units.
     * 
     * @param newValue the new height
     */
    public void setHeight(Extent newValue) {
        setProperty(PROPERTY_HEIGHT, newValue);
    }
    
    /**
     * Sets the inset margin around between the list components border and content.
     * 
     * @param newValue the new inset margin
     */
    public void setInsets(Insets newValue) {
        setProperty(PROPERTY_INSETS, newValue);
    }
    
    /**
     * Sets the model.
     * The model may not be null.
     *
     * @param newValue the new model
     */
    public void setModel(ListModel newValue) {
        if (newValue == null) {
            throw new IllegalArgumentException("Model may not be null.");
        }
        ListModel oldValue = model;
        if (oldValue != null) {
            oldValue.removeListDataListener(listDataHandler);
        }
        newValue.addListDataListener(listDataHandler);
        model = newValue;
        firePropertyChange(LIST_MODEL_CHANGED_PROPERTY, oldValue, newValue);
    }
    
    /**
     * Sets the rollover background.
     * 
     * @param newValue the new rollover background
     */
    public void setRolloverBackground(Color newValue) {
        setProperty(PROPERTY_ROLLOVER_BACKGROUND, newValue);
    }
    
    /**
     * Sets whether rollover effects are enabled.
     * 
     * @param newValue the new rollover enabled state
     */
    public void setRolloverEnabled(boolean newValue) {
        setProperty(PROPERTY_ROLLOVER_ENABLED, new Boolean(newValue));
    }
    
    /**
     * Sets the rollover font.
     * 
     * @param newValue the new rollover font
     */
    public void setRolloverFont(Font newValue) {
        setProperty(PROPERTY_ROLLOVER_FONT, newValue);
    }
    
    /**
     * Sets the rollover foreground.
     * 
     * @param newValue the new rollover foreground
     */
    public void setRolloverForeground(Color newValue) {
        setProperty(PROPERTY_ROLLOVER_FOREGROUND, newValue);
    }
    
    /**
     * Sets the selection model.
     * The selection model may not be null.
     * 
     * @param newValue the new selection model
     */
    public void setSelectionModel(ListSelectionModel newValue) {
        if (newValue == null) {
            throw new IllegalArgumentException("Selection model may not be null.");
        }
        ListSelectionModel oldValue = selectionModel;
        if (oldValue != null) {
            oldValue.removeChangeListener(changeHandler);
        }
        newValue.addChangeListener(changeHandler);
        selectionModel = newValue;
        firePropertyChange(SELECTION_MODEL_CHANGED_PROPERTY, oldValue, newValue);
    }
    
    /**
     * Sets the tool tip text (displayed when the mouse cursor is hovered 
     * over the component).
     * 
     * @param newValue the new tool tip text
     */
    public void setToolTipText(String newValue) {
        setProperty(PROPERTY_TOOL_TIP_TEXT, newValue);
    }

    /**
     * Sets the width.
     * This property supports <code>Extent</code>s with
     * fixed or percentile units.
     *
     * @param newValue the new width 
     */
    public void setWidth(Extent newValue) {
        setProperty(PROPERTY_WIDTH, newValue);
    }
}
