/*
 * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *  
 *     * 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.
 *  
 *     * Neither the name of Business Objects 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.
 */


/*
 * IntellicutList.java
 * Creation date: Dec 10th 2002
 * By: Ken Wong
 */
package org.openquark.gems.client;

import java.awt.Container;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;

import javax.swing.JList;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;

import org.openquark.gems.client.IntellicutListModel.FilterLevel;
import org.openquark.gems.client.IntellicutListModelAdapter.IntellicutListEntry;
import org.openquark.gems.client.IntellicutManager.IntellicutMode;


/**
 * A list that encapsulates all the details associated with populating
 * a list with the unqualified names of gems. 
 * @author Ken Wong
 * @author Frank Worsley
 */
public class IntellicutList extends JList {

    private static final long serialVersionUID = -8515575227984050475L;

    /** The last list index that was visible when the state was saved. */
    private int savedVisibleIndex = -1;

    /** The last list entry that was visible when the state was saved. */
    private IntellicutListEntry savedVisibleEntry = null;
    
    /** The list entry that was selected when the state was saved. */
    private IntellicutListEntry savedSelectedEntry = null;

    /** Whether or not this list should be drawn transparently. */
    private boolean transparent = false;

    /**
     * Constructor for IntellicutList.
     */
    public IntellicutList(IntellicutMode mode) {
        setName("MatchingGemsList");
        setCellRenderer(new IntellicutListRenderer(mode));
        setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    }

    /**
     * Enables the list to be transparent. If the list is transparent non selected list items
     * will let the components below them show through. Selected list items will draw a blue
     * semi-transparent tint as their background. 
     * @param value whether or not the list should be transparent
     */
    void setTransparent(boolean value) {
        this.transparent = value;
        updateUI();
    }
    
    /**
     * @return whether or not the intellicut list is transparent
     */
    public boolean isTransparent() {
        return transparent;
    }
    
    /**
     * Saves the currently selected list item and the scroll position of the
     * list so that the list state can be restored later.
     */
    private void saveListState() {

        IntellicutListModel listModel = (IntellicutListModel) getModel();
        
        savedVisibleEntry = null;
        savedSelectedEntry = null;
        savedVisibleIndex = getLastVisibleIndex();
        int selectedIndex = getSelectedIndex();
            
        try {
            savedVisibleEntry = listModel.get(savedVisibleIndex);
            savedSelectedEntry = listModel.get(selectedIndex);

            // We only want to make the selected entry visible again after refreshing
            // the list if it was visible before.
            if (selectedIndex > getLastVisibleIndex() || selectedIndex < getFirstVisibleIndex()) {
                savedSelectedEntry = null;
            }
                
        } catch (Exception ex) {
            // This might throw a NullPointerException or ArrayIndexOutOfBoundsException
            // if the model is not initialized. In that case there is no index we can restore. 
        }
        
    }
    
    /**
     * Restores the previously saved list state as closely as possible.
     */
    private void restoreListState() {

        IntellicutListModel listModel = (IntellicutListModel) getModel();

        int newSelectedIndex = listModel.indexOf(savedSelectedEntry);
        int newVisibleIndex = listModel.indexOf(savedVisibleEntry);
        if (newVisibleIndex == -1) {
            newVisibleIndex = savedVisibleIndex;
        }

        // We have to add one cell height, otherwise the cell will be just
        // outside of the visible range of the list. ensureIndexIsVisible doesn't
        // work here since we want the index to appear at the very bottom.
        Point newLocation = indexToLocation(newVisibleIndex);
        if (newLocation != null) {
            newLocation.y += getCellBounds(0, 0).height;
            scrollRectToVisible(new Rectangle(newLocation));
        }
        if (newSelectedIndex != -1) {
            setSelectedIndex(newSelectedIndex);
            ensureIndexIsVisible(newSelectedIndex);
        } else {
            setSelectedIndex(0);
        }
    }
    
    /**
     * Refreshes the list while remembering the selected item and scroll position
     * as closely as possible. Simply refreshing the list model will not remember
     * the list state.
     * @param prefix the new prefix for values in the list
     */
    public void refreshList(String prefix) {

        IntellicutListModel listModel = (IntellicutListModel) getModel();
        
        saveListState();
        listModel.refreshList(prefix);
        restoreListState();
    }
    
    /**
     * Finds the JViewport this list is embedded in.
     * @return the viewport or null if there is no viewport
     */
    private JViewport getViewport() {

        Container parent = getParent();
        while (parent != null && !(parent instanceof JViewport)) {
            parent = parent.getParent ();
        }
        
        return (JViewport) parent;
    }
    
    /**
     * Scrolls the given index as close as possible to the top of the list's viewport.
     * @param index index of the item to scroll to the top
     */
    void scrollToTop(int index) {
        
        Point location = indexToLocation(index);
        
        if (location == null) {
            return;
        }
        
        JViewport viewport = getViewport();
        
        // Scroll this element as close to the top as possible.
        if (viewport.getViewSize().height - location.y < viewport.getSize().height) {
            location.y -= viewport.getSize().height - viewport.getViewSize().height + location.y;
        }
        
        viewport.setViewPosition(location);
    }

    /**
     * Sets the current gem filtering level.
     * 
     * The list is automatically refreshed after doing this. This method also saves 
     * the list's displayed state as opposed to just setting the filter level which 
     * will not remember the list state.
     * @param filterLevel the new level to filter at
     */
    void setFilterLevel(FilterLevel filterLevel) {
        
        if (filterLevel == null) {
            throw new NullPointerException("filterLevel must not be null in IntellicutList.setFilterLevel");
        }
        
        IntellicutListModel listModel = (IntellicutListModel) getModel();

        saveListState();
        listModel.setFilterLevel(filterLevel);
        restoreListState();
    }
        
    /**
     * Returns the user selected IntellicutListEntry
     * @return IntellicutListEntry
     */
    IntellicutListEntry getSelected(){
        return getModel().getSize() > 0 ? (IntellicutListEntry) getSelectedValue() : null;
    }

    /**
     * We want tooltips to be displayed to the right of an item.
     * If there is no item at the coordinates it returns null.
     * @param e the mouse event for which to determine the location
     * @return the tooltip location for the list item at the coordinates of the mouse event
     */
    @Override
    public Point getToolTipLocation(MouseEvent e) {
        
        int index = locationToIndex(e.getPoint());
        
        if (index == -1) {
            return null;
        }
        
        Rectangle cellBounds = getCellBounds(index, index);

        // take off 50 and add 5 for good looks
        return new Point (cellBounds.x + cellBounds.width - 50, cellBounds.y + 5);        
    }

    /**
     * @param e the MouseEvent for which to get the tooltip text
     * @return the tooltip text for the list item at the coordinates or null if there is no
     * item at the coordinates
     */
    @Override
    public String getToolTipText(MouseEvent e) {
        
        int index = locationToIndex(e.getPoint());
        
        if (index == -1) {
            return null;
        }
        
        IntellicutListEntry listEntry = (IntellicutListEntry) getModel().getElementAt(index);
        
        if (listEntry == null) {
            return null;
        }

        IntellicutListModel listModel = (IntellicutListModel) getModel();
        
        return listModel.getAdapter().getToolTipTextForEntry(listEntry, this);
    }
}
