/*
 * $Id$
 *
 * Copyright (c) 1996, 2009, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.javatest.tool;

import javax.accessibility.Accessible;
import javax.accessibility.AccessibleContext;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JToolBar;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.lang.reflect.Array;

/**
 * A component that displays an editable list of items.
 */
public class EditableList extends JComponent implements Accessible {
    // only initialize this if required
    private static UIFactory defaultUIF;
    /**
     * The factory used to create the GUI elements of the component.
     */
    protected final UIFactory uif;
    /**
     * The list model that contains the elements of the list.
     */
    protected DefaultListModel<Object> listModel;
    private JList<Object> list;
    private JButton addBtn;
    private JButton removeBtn;
    private JButton upBtn;
    private JButton downBtn;
    private Listener listener = new Listener();
    private Renderer renderer = new Renderer();
    private boolean duplicatesAllowed;

    /**
     * Create an empty component, using a standard UIFactory for this class,
     * and using resources beginning with "list.".
     */
    public EditableList() {
        this(new UIFactory(EditableList.class, null), "list");
    }

    /**
     * Create an empty component, using a specified UIFactory and resource prefix.
     *
     * @param uif   The UIFactory used to construct the component
     * @param uiKey The prefix for any UI resources that may be required
     */
    public EditableList(UIFactory uif, String uiKey) {
        this.uif = uif;
        setLayout(new BorderLayout());
        listModel = new DefaultListModel<>(); // need to force the type of model
        listModel.addListDataListener(listener);
        list = uif.createList(uiKey, listModel);
        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        list.setCellRenderer(renderer);

        list.addListSelectionListener(listener);
        add(new JScrollPane(list), BorderLayout.CENTER);

        JToolBar bar = new JToolBar(JToolBar.VERTICAL);
        bar.setFloatable(false);
        bar.add(addBtn = createButton(uiKey + ".add"));
        bar.add(removeBtn = createButton(uiKey + ".remove"));
        bar.add(upBtn = createButton(uiKey + ".up"));
        bar.add(downBtn = createButton(uiKey + ".down"));
        add(bar, BorderLayout.EAST);

        updateButtons();
        setBorder(BorderFactory.createEtchedBorder());
    }

    private static UIFactory getDefaultUIF() {
        // use EditableList.class instead of "this" to get correct i18n
        if (defaultUIF == null) {
            defaultUIF = new UIFactory(EditableList.class, null);  // no help required
        }
        return defaultUIF;
    }

    /**
     * Get the accessible context for this pane.
     *
     * @return the accessible context for this pane
     */
    @Override
    public AccessibleContext getAccessibleContext() {
        if (accessibleContext == null) {
            accessibleContext = new AccessibleJComponent() {
            };
        }
        return accessibleContext;
    }

    /**
     * Set whether or not the list can be edited by the user.
     *
     * @param b if true, the component can be edited by the user;
     *          if false, it cannot
     */
    @Override
    public void setEnabled(boolean b) {
        super.setEnabled(b);
        list.setEnabled(b);
        updateButtons();
    }

    /**
     * Remove all entries from the list.
     */
    public void clear() {
        listModel.clear();
    }

    /**
     * Get the items currently in the list.
     *
     * @return an array containing the items currently in the list
     * @see #setItems
     */
    public Object[] getItems() {
        return listModel.toArray();
    }

    /**
     * Set the items in the list. Any previous items are removed first.
     *
     * @param items the array of items to be put in the list.
     * @see #getItems
     */
    public void setItems(Object... items) {
        listModel.clear();
        if (items != null) {
            for (Object item : items) {
                listModel.addElement(item);
            }
        }
    }

    /**
     * Get the items currently in the list, in an array of a specific type.
     *
     * @param c the component type of the array to be returned
     * @return an array containing the items currently in the list
     * @see #setItems
     */
    public Object[] getItems(Class<?> c) {
        Object[] items = (Object[]) Array.newInstance(c, listModel.size());
        listModel.copyInto(items);
        return items;
    }

    /**
     * Get the tool tip text that appears on the list.
     * (Separate tool tip text will appear on the buttons to manipulate the list.)
     *
     * @return the tool tip text that appears on the list
     * @see #setToolTipText
     */
    @Override
    public String getToolTipText() {
        return list.getToolTipText();
    }

    /**
     * Set the tool tip text that appears on the list.
     * (Separate tool tip text will appear on the buttons to manipulate the list.)
     *
     * @param tip the tool tip text to appear on the list
     * @see #getToolTipText
     */
    @Override
    public void setToolTipText(String tip) {
        list.setToolTipText(tip);
    }

    /**
     * Add a listener to be notified of events when the list data changes.
     *
     * @param l the listener to be notified
     * @see #removeListDataListener
     */
    public void addListDataListener(ListDataListener l) {
        listenerList.add(ListDataListener.class, l);
    }

    /**
     * Remove a listener that was previously added to be notified of
     * events when the list data changes.
     *
     * @param l the listener to be notified
     * @see #addListDataListener
     */
    public void removeListDataListener(ListDataListener l) {
        listenerList.remove(ListDataListener.class, l);
    }

    /**
     * Check whether or not duplicates should be allowed in the list.
     *
     * @return true if duplicates should be allowed, and false otherwise
     * @see #setDuplicatesAllowed
     */
    public boolean isDuplicatesAllowed() {
        return duplicatesAllowed;
    }

    /**
     * Specify whether or not duplicates should be allowed in the list.
     *
     * @param b true if duplicates should be allowed, and false otherwise
     * @see #isDuplicatesAllowed
     */
    public void setDuplicatesAllowed(boolean b) {
        duplicatesAllowed = b;
    }

    /**
     * Get the display value for an item in the list. By default, the display
     * value is the item itself.
     *
     * @param item the object for which to get the display value
     * @return the display value for the specified item
     */
    protected Object getDisplayValue(Object item) {
        return item;
    }

    /**
     * Invoked to get a new item to put in the list, when the user clicks
     * the "Add" button". The default is to show an input dialog to allow
     * the user to type in a new string. Subtypes may override this method
     * to provide other ways of specifying items to be added, such as a
     * file chooser.
     *
     * @return an object to be added to the list, or null if no object
     * to be added.
     */
    protected Object getNewItem() {
        return JOptionPane.showInputDialog(this, uif.getI18NString("list.add.txt"));
    }

    /**
     * Invoked to get a new item to replace an existing item in the list.
     * The default is to show an input dialog to allow
     * the user to type in a new string. Subtypes may override this method
     * to provide other ways of specifying items to be added, such as a
     * file chooser.
     *
     * @param oldItem the item to be replaced in the list
     * @return an object to replace the old item the list, or null if no
     * replacement should occur.
     */
    protected Object getNewItem(Object oldItem) {
        return JOptionPane.showInputDialog(this, uif.getI18NString("list.change.txt"), oldItem);
    }

    private JButton createButton(String uiKey) {
        JButton b = uif.createButton(uiKey);
        // set max size so button can grow within toolbar
        b.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));
        b.addActionListener(listener);
        // should be in uif.createButton?
        b.setMnemonic(uif.getI18NString(uiKey + ".mne").charAt(0));
        return b;
    }

    private void insertItem() {
        Object newItem = getNewItem();

        if (!duplicatesAllowed && listModel.contains(newItem)) {
            showDuplicateError(newItem);
            return;
        }

        if (newItem != null) {
            if (list.isSelectionEmpty()) {
                listModel.addElement(newItem);
            } else {
                listModel.add(1 + list.getSelectedIndex(), newItem);
            }
            list.setSelectedValue(newItem, true);
        }
    }

    private void removeSelectedItem() {
        if (!list.isSelectionEmpty()) {
            listModel.remove(list.getSelectedIndex());
        }
    }

    private void moveSelectedItemUp() {
        if (!list.isSelectionEmpty()) {
            int i = list.getSelectedIndex();
            if (i > 0) {
                swap(i, i - 1);
                list.setSelectedIndex(i - 1);
            }
        }
    }

    private void moveSelectedItemDown() {
        if (!list.isSelectionEmpty()) {
            int i = list.getSelectedIndex();
            if (i + 1 < listModel.size()) {
                swap(i, i + 1);
                list.setSelectedIndex(i + 1);
            }
        }
    }

    private void editItem(int index) {
        Object newItem = getNewItem(listModel.getElementAt(index));

        if (!duplicatesAllowed && listModel.contains(newItem)) {
            showDuplicateError(newItem);
            return;
        }

        if (newItem != null) {
            listModel.set(index, newItem);
        }
    }

    private void showDuplicateError(Object item) {
        String text = uif.getI18NString("list.duplicate.text",
                getDisplayValue(item));

        String title = uif.getI18NString("list.duplicate.title");

        JOptionPane.showMessageDialog(this,
                text,
                title,
                JOptionPane.INFORMATION_MESSAGE);
    }

    private void updateButtons() {
        boolean enabled = isEnabled();
        addBtn.setEnabled(enabled);
        if (list.isSelectionEmpty() || !enabled) {
            removeBtn.setEnabled(false);
            upBtn.setEnabled(false);
            downBtn.setEnabled(false);
        } else {
            removeBtn.setEnabled(true);
            int i = list.getSelectedIndex();
            upBtn.setEnabled(i > 0);
            downBtn.setEnabled(i + 1 < listModel.size());
        }
    }

    private void swap(int i1, int i2) {
        Object o1 = listModel.get(i1);
        Object o2 = listModel.get(i2);
        listModel.set(i1, o2);
        listModel.set(i2, o1);
    }

    private class Renderer
            extends DefaultListCellRenderer {
        @Override
        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            return super.getListCellRendererComponent(list,
                    getDisplayValue(value),
                    index,
                    isSelected,
                    cellHasFocus);
        }
    }

    private class Listener
            implements ActionListener, ListDataListener, ListSelectionListener, MouseListener {
        // ActionListener events, for buttons
        @Override
        public void actionPerformed(ActionEvent e) {
            Object src = e.getSource();
            if (src == addBtn) {
                insertItem();
            } else if (src == removeBtn) {
                removeSelectedItem();
            } else if (src == upBtn) {
                moveSelectedItemUp();
            } else if (src == downBtn) {
                moveSelectedItemDown();
            }

            updateButtons();
        }

        // ListSelect events, to update buttons depending on list selection
        @Override
        public void valueChanged(ListSelectionEvent e) {
            updateButtons();
        }

        // MouseListener, to react to double click in list
        @Override
        public void mouseClicked(MouseEvent e) {
            if (e.getClickCount() == 2) {
                int index = list.locationToIndex(e.getPoint());
                if (index != -1) {
                    editItem(index);
                }
            }
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }

        @Override
        public void mousePressed(MouseEvent e) {
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }

        // ListData events, to redispatch to client, with EditableList.this as the source
        @Override
        public void contentsChanged(ListDataEvent e) {
            ListDataEvent e2 = null;
            Object[] listeners = listenerList.getListenerList();
            for (int i = listeners.length - 2; i >= 0; i -= 2) {
                if (listeners[i] == ListDataListener.class) {
                    if (e2 == null) {
                        e2 = new ListDataEvent(EditableList.this, e.getType(), e.getIndex0(), e.getIndex1());
                    }
                    ((ListDataListener) listeners[i + 1]).contentsChanged(e2);
                }
            }
        }

        @Override
        public void intervalAdded(ListDataEvent e) {
            ListDataEvent e2 = null;
            Object[] listeners = listenerList.getListenerList();
            for (int i = listeners.length - 2; i >= 0; i -= 2) {
                if (listeners[i] == ListDataListener.class) {
                    if (e2 == null) {
                        e2 = new ListDataEvent(EditableList.this, e.getType(), e.getIndex0(), e.getIndex1());
                    }
                    ((ListDataListener) listeners[i + 1]).intervalAdded(e2);
                }
            }
        }

        @Override
        public void intervalRemoved(ListDataEvent e) {
            ListDataEvent e2 = null;
            Object[] listeners = listenerList.getListenerList();
            for (int i = listeners.length - 2; i >= 0; i -= 2) {
                if (listeners[i] == ListDataListener.class) {
                    if (e2 == null) {
                        e2 = new ListDataEvent(EditableList.this, e.getType(), e.getIndex0(), e.getIndex1());
                    }
                    ((ListDataListener) listeners[i + 1]).intervalRemoved(e2);
                }
            }
        }
    }
}
