/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */

package org.netbeans.modules.db.util;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.AbstractListModel;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.SwingUtilities;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

/**
 * This is an utility class for filling combo boxes with some data (usually
 * some items). The combo box has a "New item" item allowing
 * the user to invoke the adding of new items to the combo box. The client of
 * this class should provide a {@link DataComboBoxModel} and call
 * the {@link #connect} method.
 *
 * @author Andrei Badea
 */
public final class DataComboBoxSupport {
    private final DataComboBoxModel dataModel;
    private final boolean allowAdding;

    private Object previousItem = null;
    private Object previousNonSpecialItem = null;

    private boolean performingNewItemAction = false;

    /**
     * Serves as the new item. Not private because used in tests.
     */
    final Object NEW_ITEM = new Object() {
        @Override
        public String toString() {
            return dataModel.getNewItemDisplayName();
        }
    };

    /** Not private because used in tests. */
    DataComboBoxSupport(JComboBox comboBox, DataComboBoxModel dataModel, boolean allowAdding) {
        this.dataModel = dataModel;
        this.allowAdding = allowAdding;
        
        comboBox.setEditable(false);

        comboBox.setModel(new ItemComboBoxModel());

        comboBox.setRenderer(new ItemListCellRenderer());
        comboBox.addActionListener(new ItemActionListener());
        comboBox.addPopupMenuListener(new ItemPopupMenuListener());
    }

    /**
     * Connects a combo box with the specified combo box model.
     */
    public static void connect(JComboBox comboBox, DataComboBoxModel dataModel) {
        connect(comboBox, dataModel, true);
    }

    /**
     * Connects a combo box with the specified combo box model.
     */
    @SuppressWarnings("ResultOfObjectAllocationIgnored")
    public static void connect(JComboBox comboBox, DataComboBoxModel dataModel, boolean allowAdding) {
        new DataComboBoxSupport(comboBox, dataModel, allowAdding);
    }

    private boolean isSpecialItem(Object item) {
        return item == NEW_ITEM;
    }

    private void setPreviousNonSpecialItem(JComboBox comboBox) {
        if (comboBox.getSelectedItem() == NEW_ITEM) {
            // no new item added
            comboBox.setSelectedItem(previousNonSpecialItem);
        }
    }

    private class ItemComboBoxModel extends AbstractListModel implements ComboBoxModel, ListDataListener {

        // XXX intervalAdded() and intervalRemoved() are not implemented,
        // but it is enough for the connection and drivers combo boxes

        @SuppressWarnings("LeakingThisInConstructor")
        public ItemComboBoxModel() {
            getDelegate().addListDataListener(this);
        }

        @Override
        public Object getElementAt(int index) {
            if (allowAdding) {
                if (getSize() == 1) {
                    // there is just NEW_ITEM
                    if (index == 0) {
                        return NEW_ITEM;
                    } else {
                        throw new IllegalStateException("Index out of bounds: " + index); // NOI18N
                    }
                }

                // there are the delegate items and NEW_ITEM
                if (index >= 0 && index < getDelegate().getSize()) {
                    return getDelegate().getElementAt(index);
                } else if (index == getSize() - 1) {
                    return NEW_ITEM;
                } else {
                    throw new IllegalStateException("Index out of bounds: " + index); // NOI18N
                }
            } else {
                // there are no other items than those of the delegate
                return getDelegate().getElementAt(index);
            }
        }

        @Override
        public int getSize() {
            // 1 = NEW_ITEM
            if (allowAdding) {
                return getDelegate().getSize() == 0 ? 1 : getDelegate().getSize() + 1;
            } else {
                return getDelegate().getSize();
            }
        }

        @Override
        public void setSelectedItem(Object anItem) {
            previousItem = getDelegate().getSelectedItem();

            if (!isSpecialItem(previousItem)) {
                previousNonSpecialItem = previousItem;
            }

            getDelegate().setSelectedItem(anItem);
        }

        @Override
        public Object getSelectedItem() {
            return getDelegate().getSelectedItem();
        }

        public Object getPreviousItem() {
            return previousItem;
        }

        private ComboBoxModel getDelegate() {
            return dataModel.getListModel();
        }

        private int getItemIndex(Object item) {
            if (item == null) {
                return -1;
            }
            for (int i = 0; i < getSize(); i++ ) {
                if (getElementAt(i).equals(item)) {
                    return i;
                }
            }
            return -1;
        }

        @Override
        public void intervalRemoved(ListDataEvent e) {
            throw new UnsupportedOperationException("This is currently not supported.");
        }

        @Override
        public void intervalAdded(ListDataEvent e) {
            throw new UnsupportedOperationException("This is currently not supported.");
        }

        @Override
        public void contentsChanged(ListDataEvent e) {
            fireContentsChanged(this, 0, getSize());
        }
    }

    private class ItemListCellRenderer extends DefaultListCellRenderer {

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {

            Component component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            JLabel label = (JLabel)component;

            if (value != null && !isSpecialItem(value)) {
                String displayName = dataModel.getItemDisplayName(value);
                label.setText(dataModel.getItemDisplayName(value));
                label.setToolTipText(dataModel.getItemTooltipText(value));
            } else if (value != null) {
                label.setText(value.toString());
                label.setToolTipText(null);
            }

            return label;
        }
    }

    private final class ItemActionListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            final JComboBox comboBox = (JComboBox)e.getSource();

            Object selectedItem = comboBox.getSelectedItem();
            if (selectedItem == NEW_ITEM) {
                performingNewItemAction = true;
                try {
                    comboBox.setPopupVisible(false);
                    dataModel.newItemActionPerformed();
                } finally {
                    performingNewItemAction = false;
                }

                setPreviousNonSpecialItem(comboBox);
                // we (or maybe the client) have just selected an item inside an actionPerformed event,
                // which will not send another actionPerformed event for the new item. 
                // We need to make sure all listeners get an event for the new item,
                // thus...
                final Object newSelectedItem = comboBox.getSelectedItem();
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        comboBox.setSelectedItem(newSelectedItem);
                    }
                });
            }
        }
    }

    private final class ItemPopupMenuListener implements PopupMenuListener {

        @Override
        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
        }

        @Override
        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            if (!performingNewItemAction) {
                setPreviousNonSpecialItem((JComboBox)e.getSource());
            }
        }

        @Override
        public void popupMenuCanceled(PopupMenuEvent e) {
            // without the check the previous non-special item would be displayed
            // while calling DataComboBoxModel.newItemActionPerformed() 
            // instead of NEW_ITEM, but this is unwanted. Same for
            // popupMenuWillBecomeImvisible().
            if (!performingNewItemAction) {
                setPreviousNonSpecialItem((JComboBox)e.getSource());
            }
        }
    }
}
