/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.gui.swing.spinner;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.FocusTraversalPolicy;
import java.awt.Font;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.io.Serializable;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.ComboBoxEditor;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JSpinner;
import javax.swing.MutableComboBoxModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;


/** JComboBox extension that uses a JSpinner as editor component.
 * It is assumed that all items in the list part and the selected item
 * are valid values for the JSpinner's model.
 *
 * For the moment, do not exchange 'model' or 'editor'.
 * @since 2.0
 */

// TODO: In 1.4, ENTER does not close the popup because the formatted
//       text field swallows it
// TODO: Exchange things (tricky)
public class JaxoSpinnerComboBox extends JComboBox {
    private static final long serialVersionUID = 7526471155622776147L;

    // allow ENTER to press the default button if nothing has changed
    private static final boolean ALLOW_DEFAULT_BUTTON = true;
    private static Action disabledAction =
        new AbstractAction() {
            private static final long serialVersionUID = 7526471155622776147L;
            public void actionPerformed(final ActionEvent e) {
                // disabled
            }

            @Override
            public boolean isEnabled() {
                return false;
            }
        };
    private JSpinner spinner;

    /** With the given spinner as editor component, and an empty list part.
     * @param s JSpinner
     */
    public JaxoSpinnerComboBox(final JSpinner s) {
        this(s, Collections.EMPTY_LIST);
    }

    /** With the given spinner as editor component, and the given list part.
     * @param s JSpinner
     * @param l List
     */
    public JaxoSpinnerComboBox(final JSpinner s, final List<?> l) {
        super();
        spinner = s;

        setEditable(true);

        setItems(l);
        setSelectedItem(s.getValue());

        setEditor(new SpinnerEditor(s));

        getActionMap().put("enterPressed",
            new AbstractAction() {
                private static final long serialVersionUID = 752647115562277L;
                {
                    // recognize real events by actionCommand -
                    // editor events are also routed here (should not)
                    putValue(ACTION_COMMAND_KEY, "FIX-enterPressed");
                }

                public void actionPerformed(final ActionEvent e) {
                    final boolean realEnter =
                        getValue(ACTION_COMMAND_KEY).equals(e.getActionCommand());

                    if (!realEnter) {
                        return;
                    }

                    if (ALLOW_DEFAULT_BUTTON) {
                        final ActionMap m = getActionMap().getParent();

                        if (m != null) {
                            final Action a = m.get("enterPressed");

                            if (a != null) {
                                a.actionPerformed(e);
                            }
                        }
                    } else {
                        setPopupVisible(false);
                    }
                }
            });
    }

    /** {@inheritDoc} */
    @Override
    public void firePopupMenuWillBecomeVisible() {
        setActionsEnabled(false);

        super.firePopupMenuWillBecomeVisible();
    }

    /** {@inheritDoc} */
    @Override
    public void firePopupMenuWillBecomeInvisible() {
        setActionsEnabled(true);

        super.firePopupMenuWillBecomeInvisible();
    }

    /** Set the items contained in the (popup) list.
     *  Requires a MutableComboBoxModel as model.
     * @param value List
     */
    public final void setItems(final List<?> value) {
        final MutableComboBoxModel model = (MutableComboBoxModel) getModel();

        if (model instanceof DefaultComboBoxModel) {
            ((DefaultComboBoxModel) model).removeAllElements();
        } else {
            for (int i = model.getSize() - 1; i >= 0; --i) {
                model.removeElement(model.getElementAt(i));
            }
        }

        for (final Iterator<?> i = value.iterator(); i.hasNext();) {
            model.addElement(i.next());
        }
    }

    /** {@inheritDoc} */
    @Override
    public void removeAllItems() {
        setItems(Collections.EMPTY_LIST);
    }

    // disable actions so that they are handled by the popup
    private void setActionsEnabled(final boolean value) {
        if (value) {
            spinner.getActionMap().remove("increment");
            spinner.getActionMap().remove("decrement");
        } else {
            spinner.getActionMap().put("increment", disabledAction);
            spinner.getActionMap().put("decrement", disabledAction);
        }
    }

    private static class SpinnerEditor implements ComboBoxEditor,
        ChangeListener, Serializable {
        private static final long serialVersionUID = 7526471155622776147L;

        private final EventListenerList listeners;
        private final JComponent root;
        private final JSpinner spinner;
        private boolean setting;

        SpinnerEditor(final JSpinner s) {
            listeners = new EventListenerList();

            spinner = s;

            spinner.setFocusable(false);

            spinner.addChangeListener(this);

            // Intermediate component, used to fix the
            // severely broken JComboBox focus handling
            root =
                new JComponent() {
                        private static final long serialVersionUID = 7526471557L;
                        {
                            setFocusable(false);
                            setLayout(new BorderLayout());
                            add(spinner);
                            super.setFont(spinner.getFont());
                            setBackground(Color.white);
                            setForeground(Color.black);
                            setBorder(BorderFactory.createEmptyBorder());
                        }

                        @Override
                        public void setFont(final Font value) {
                            super.setFont(value);
                            spinner.setFont(value);
                        }

                        @Override
                        public void setEnabled(final boolean value) {
                            super.setEnabled(value);
                            spinner.setEnabled(value);
                        }

                        private Component getFocusTarget() {
                            Container p = this;

                            while (p != null) {
                                final FocusTraversalPolicy t =
                                    p.getFocusTraversalPolicy();

                                if (t != null) {
                                    final Component c = t.getDefaultComponent(this);

                                    if (!isAncestorOf(c)) {
                                        break;
                                    }

                                    return c;
                                }
                                p = p.getParent();
                            }

                            return this; // default, will not be granted
                        }

                        @Override
                        public boolean isFocusable() {
                            return false;
                        }

                        @Override
                        public void requestFocus() {
                            if (containsFocus()) {
                                return;
                            }

                            getFocusTarget().requestFocus();
                        }

                        @Override
                        public boolean requestFocusInWindow() {
                            if (containsFocus()) {
                                return false;
                            }

                            return getFocusTarget().requestFocusInWindow();
                        }

                        private boolean containsFocus() {
                            Component c =
                                KeyboardFocusManager.getCurrentKeyboardFocusManager()
                                                    .getFocusOwner();
                            while (c != null) {
                                if (c == this) {
                                    return true;
                                }

                                c = c.getParent();
                            }

                            return false;
                        }
                    };
        }

        public void addActionListener(final ActionListener l) {
            listeners.add(ActionListener.class, l);
        }

        public void removeActionListener(final ActionListener l) {
            listeners.remove(ActionListener.class, l);
        }

        protected void fireActionPerformed() {
            ActionEvent e = null;

            final Object[] pairs = listeners.getListenerList();

            for (int i = pairs.length - 2; i >= 0; i -= 2) {
                if (pairs[i] == ActionListener.class) {
                    if (e == null) {
                        e = new ActionEvent(this,
                                ActionEvent.ACTION_PERFORMED, "");
                    }
                    ((ActionListener) pairs[i + 1]).actionPerformed(e);
                }
            }
        }

        public Component getEditorComponent() {
            return root;
        }

        public void setItem(final Object value) {
            setting = true;
            try {
                spinner.setValue(value);
            } finally {
                setting = false;
            }
        }

        public Object getItem() {
            return spinner.getValue();
        }

        public void selectAll() {
            // nop
        }

        public void stateChanged(final ChangeEvent e) {
            if (!setting) {
                fireActionPerformed();
            }
        }
    }
}
