/*
 * File:     SplittedExplicativeJComboBoxPanel.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/* This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package mpi.util.gui;

import mpi.util.CVentry;
import mpi.util.DescriptedObject;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.InputEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import javax.swing.ComboBoxEditor;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.ListCellRenderer;
import javax.swing.ToolTipManager;


/**
 * SplittedExplicativeJComboBoxPanel is a Panel which splits a long Vector of similar Strings into
 * two depending JComboBoxes. The beginnings (before splitting char) of the strings are put in the
 * first ComboBox, the second ComboBox actually contains the full items, however displays only the
 * string-endings.  One might consider it as a primitive form of automatic completion.  The second
 * box is a modyfied ExplicativeComboBox, e.g. it contains the CVentries as Items and presents
 * ToolTips.  If an Item doesn't contain the split-character, it is automatically used in the
 * first ComboBox. If there are no corresponding items in the second, than it is ignored. If no
 * Item contains the split-character, all Items are shown in a single ExplicativeJComboBox.
 *
 * @author Alexander Klassmann
 * @version 2.0 24/10/2002
 */
public class SplittedExplicativeJComboBoxPanel extends JPanel
    implements ItemListener {
    /** Holds value of property DOCUMENT ME! */
    private final HashMap hash;

    /** Holds value of property DOCUMENT ME! */
    private final JComboBox categoryComboBox = new ExplicativeJComboBox();

    /** Holds value of property DOCUMENT ME! */
    private final char splitChar;
    private ComboBoxEditor comboBoxEditor;
    private JComboBox itemComboBox; // placeholder for one of the two above boxes
    private SteppedComboBox featureComboBox = null;
    private boolean itemBoxEditable;

    /**
     * DOCUMENT ME!
     *
     * @param items Vector of mpi.vocabs.CVentries
     * @param splitChar character which defines the position to where to split entries
     */
    public SplittedExplicativeJComboBoxPanel(List items, char splitChar) {
        this(items, splitChar, 50);
    }

    /**
     * DOCUMENT ME!
     *
     * @param items Vector of mpi.vocabs.CVentries
     * @param splitChar max number of entries in second panel
     * @param limit character which defines the position to where to split entries
     */
    public SplittedExplicativeJComboBoxPanel(List items, char splitChar,
        int limit) {
        this.splitChar = splitChar;
        setLayout(new BorderLayout());
        hash = splitItems(items, splitChar);

        //Use original vector, if no split items or all items belong to different categories
        Set set;

        if (hash.size() == items.size()) {
            set = (Set) new HashSet(items);
        } else {
            Iterator it = hash.keySet().iterator();

            while (it.hasNext()) {
                Object key = it.next();
                Vector vector = (Vector) hash.get(key);

                if (vector.size() > 2) {
                    vector.insertElementAt(new EmptyDescriptedObject(
                            "All Elements", key.toString() + "(*)"), 0);
                }
            }

            setVectorSizeLimit(hash, limit);
            set = (Set) new HashSet(hash.keySet()).clone();
        }

        //Adding the elements in sorted order to the categoryBox
        while (set.size() > 0) {
            Iterator it = set.iterator();
            Object object1 = it.next();

            while (it.hasNext()) {
                Object object2 = it.next();

                if (object1.toString().compareTo(object2.toString()) > 0) {
                    object1 = object2;
                }
            }

            categoryComboBox.addItem(object1);
            set.remove(object1);
        }

        categoryComboBox.setSelectedIndex(0);
        categoryComboBox.setMaximumRowCount(15); // larger than swing default

        if (hash.size() != items.size()) {
            featureComboBox = new SteppedComboBox();
            updateFeatureComboBox();
            add(categoryComboBox, BorderLayout.WEST);
            featureComboBox.setMaximumRowCount(15); //larger than swing default
            categoryComboBox.addItemListener(this);
            ToolTipManager.sharedInstance().registerComponent(featureComboBox);
            comboBoxEditor = new RestrictedComboBoxEditor(splitChar, ',', ')',
                    '*');
            featureComboBox.setEditor(comboBoxEditor);
            featureComboBox.setRenderer(new MyListCellRenderer('('));
            itemComboBox = featureComboBox;
        } else {
            itemComboBox = categoryComboBox;
        }

        add(itemComboBox, BorderLayout.CENTER);

        if (itemBoxEditable) {
            itemComboBox.setToolTipText("Enable editing with right mouse key");
        }

        itemComboBox.addItemListener(new ItemListener() {
                public void itemStateChanged(ItemEvent ie) {
                    if ((itemBoxEditable) &&
                            (ie.getStateChange() == ItemEvent.SELECTED) &&
                            (itemComboBox.getSelectedIndex() != -1)) {
                        itemComboBox.setEditable(false);
                        itemComboBox.setToolTipText(
                            "Enable editing with right mouse key");
                    }
                }
            });
        itemComboBox.getComponent(0).addMouseListener(new MouseAdapter() {
                public void mouseClicked(MouseEvent me) {
                    if ((itemBoxEditable) &&
                            (me.getModifiers() == InputEvent.BUTTON3_MASK)) {
                        itemComboBox.setEditable(true);
                        itemComboBox.setToolTipText(
                            "Use DELETE key to change entry");
                    }
                }
            });
    }

    /**
     * Sets the possibility of editing the Items in the ComboBox. However the ComboBox is turned
     * editable only after a right mouse click.
     *
     * @param b DOCUMENT ME!
     */
    public void setEditable(boolean b) {
        itemBoxEditable = b;
    }

    /**
     * DOCUMENT ME!
     *
     * @param o DOCUMENT ME!
     */
    public void setSelectedItem(Object o) {
        if (featureComboBox == null) {
            for (int i = 0; i < categoryComboBox.getItemCount(); i++) {
                if (categoryComboBox.getItemAt(i).toString().equals(o.toString())) {
                    categoryComboBox.setSelectedIndex(i);

                    break;
                }
            }
        } else {
            String s = o.toString();
            int catIndex = s.indexOf(splitChar);

            if (catIndex == -1) {
                System.out.println("String " + s + " not found!");

                return;
            }

            for (int i = 0; i < categoryComboBox.getItemCount(); i++) {
                if (categoryComboBox.getItemAt(i).toString().startsWith(s.substring(
                                0, catIndex))) {
                    categoryComboBox.setSelectedIndex(i);
                    updateFeatureComboBox();

                    if (s.indexOf('*') == -1) {
                        for (int j = 0; j < featureComboBox.getItemCount();
                                j++) {
                            if (featureComboBox.getItemAt(j).toString()
                                                   .endsWith(s.substring(
                                            catIndex))) {
                                featureComboBox.setSelectedIndex(j);

                                return;
                            }
                        }
                    } else {
                        featureComboBox.setEditable(true);
                        featureComboBox.setSelectedIndex(-1);

                        //remove additional brackets inserted for reg-ex search
                        int leftBracket = s.indexOf('[');
                        int rightBracket = s.indexOf(']');

                        if ((leftBracket == -1) || (rightBracket == -1)) {
                            comboBoxEditor.setItem(o);
                        } else {
                            comboBoxEditor.setItem(s.substring(0, leftBracket) +
                                s.substring(rightBracket + 1));
                        }

                        return;
                    }
                }
            }
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public Object getSelectedItem() {
        //this is short and it works, but it is still a logical bad work-around, 
        //as getSelectedItem never should return null
        return ((featureComboBox != null) &&
        (featureComboBox.getSelectedIndex() == -1)) ? comboBoxEditor.getItem()
                                                    : itemComboBox.getSelectedItem();
    }

    // implementation of java.awt.event.ItemListener interface 
    // only if featureBox exists, is catBox listener
    public void itemStateChanged(ItemEvent event) {
        if (event.getStateChange() == ItemEvent.SELECTED) {
            updateFeatureComboBox();
        }
    }

    /**
     * This method breaks up those vectors in the HashMap which exceed a given limit. For a given
     * key, it builds new (sub)keys,  each corresponding to a value vector with maximal 'limit'
     * elements.  It tries to split the original vector in peaces of equal size.
     *
     * @param hash hash with strings as keys and vectors as values
     * @param limit limit Maximum allowed size for each vector
     */
    private static void setVectorSizeLimit(HashMap hash, int limit) {
        for (Iterator it = ((Set) new HashSet(hash.keySet()).clone()).iterator();
                it.hasNext();) {
            Object key = it.next();
            Vector vector = (Vector) hash.get(key);
            float frac = vector.size() / (float) limit;

            if (frac > 1) {
                //adjust limit in order to make partitions equally big
                int alimit = (int) (((float) vector.size() / ((int) (frac +
                    0.99999))) + 0.99999);
                hash.remove(key);

                String first;
                String last;
                String previousFirst = "";

                for (int i = 0; i < frac; i++) {
                    int end = ((alimit * (i + 1)) < vector.size())
                        ? (alimit * (i + 1)) : vector.size();
                    Vector localVector = new Vector(vector.subList(alimit * i,
                                end));
                    first = localVector.firstElement().toString();
                    last = localVector.lastElement().toString();

                    //set into category box an abbreviation of the first element of the chunk
                    //those abbreviations should have the minimal number of letters necessary to 
                    //distinguish between them
                    if (i > 0) {
                        int diff = 1;

                        while (first.substring(0, diff).equals(previousFirst.substring(
                                        0, diff)))
                            diff++;

                        Object subKey = first.substring(0, diff) + "-" +
                            last.substring(diff - 1, diff);

                        //if key is CVentry create subkey with same CVentry-Value (i.e. Tooltiptext)
                        if (key instanceof CVentry) {
                            subKey = new CVentry((String) subKey,
                                    ((CVentry) key).getDescription());
                        }

                        hash.put(subKey, localVector);
                    } else {
                        hash.put(key, localVector);
                    }

                    previousFirst = first;
                }
            }
        }
    }

    /**
     * This method uses only the toString() method. The items can be arbitrary objects. It uses the
     * frontpart of the string (before the separating character) as key in a HashMap. Each item
     * with the same frontpart is put into a Vector belonging to that key.  If the item-string
     * doesn't contain the split character, the key "null" is used.
     *
     * @param items Vector with arbitrary items
     * @param splitChar splitChar Character that is used to split
     *
     * @return HashMap HashMap with frontparts as keys and Vectors of corresponding  items as
     *         values
     */
    private static HashMap splitItems(List items, char splitChar) {
        HashMap hash = new HashMap();
        Vector nonSplit = new Vector();

        for (int i = 0; i < items.size(); i++) {
            String cat = items.get(i).toString();
            int splitIndex = cat.indexOf(splitChar);

            if (splitIndex >= 0) {
                cat = cat.substring(0, splitIndex);

                if (hash.containsKey(cat)) {
                    ((Vector) hash.get(cat)).add(items.get(i));
                } else {
                    Vector vector = new Vector();
                    vector.add(items.get(i));
                    hash.put(cat, vector);
                }
            } else {
                nonSplit.add(items.get(i));
            }
        }

        for (int i = 0; i < nonSplit.size(); i++) {
            Object key = nonSplit.elementAt(i);
            String keyString = key.toString();

            //CGN has also "empty" DescriptedObjects, containing only the category name
            //and a general description of that category
            //the corresponding String-Keys are thus replaced by those DescriptedObjects
            if (hash.containsKey(keyString)) {
                Object value = hash.get(keyString);
                hash.remove(keyString);
                hash.put(key, value);
            } else {
                Vector vector = new Vector();
                vector.add(key);
                hash.put(key, vector);
            }
        }

        return hash;
    }

    private void updateFeatureComboBox() {
        if (featureComboBox.getItemCount() > 0) {
            featureComboBox.removeAllItems();
        }

        Vector vector = (Vector) hash.get(categoryComboBox.getSelectedItem());

        for (int i = 0; i < vector.size(); i++) {
            featureComboBox.addItem(vector.elementAt(i));
        }

        featureComboBox.setPopupWidth(featureComboBox.getPreferredSize().width);
    }

    /**
     * This renderer displays only the backpart of the string of CVentries
     */
    public class MyListCellRenderer implements ListCellRenderer {
        /** Holds value of property DOCUMENT ME! */
        private final JLabel label = new JLabel();

        /** Holds value of property DOCUMENT ME! */
        private final char splitChar;

        /**
         * Creates a new MyListCellRenderer instance
         *
         * @param splitChar DOCUMENT ME!
         */
        MyListCellRenderer(char splitChar) {
            this.splitChar = splitChar;
            setOpaque(true);
        }

        /**
         * DOCUMENT ME!
         *
         * @param list DOCUMENT ME!
         * @param value DOCUMENT ME!
         * @param index DOCUMENT ME!
         * @param isSelected DOCUMENT ME!
         * @param celHasFocus DOCUMENT ME!
         *
         * @return DOCUMENT ME!
         */
        public Component getListCellRendererComponent(JList list, Object value,
            int index, boolean isSelected, boolean celHasFocus) {
            if (isSelected) {
                label.setBackground(list.getSelectionBackground());
                label.setForeground(list.getSelectionForeground());
            } else {
                label.setBackground(list.getBackground());
                label.setForeground(list.getForeground());
            }

            if (value instanceof DescriptedObject) {
                DescriptedObject descrobj = (DescriptedObject) value;

                if (isSelected) {
                    list.setToolTipText(descrobj.getDescription());
                }

                String string = value.toString();
                int splitindex = string.indexOf(splitChar);
                string = (splitindex >= 0) ? string.substring(splitindex) : "";
                label.setText(string);

                return label;
            } else {
                return comboBoxEditor.getEditorComponent();
            }
        }
    }

    /**
     * DOCUMENT ME! $Id: SplittedExplicativeJComboBoxPanel.java,v 1.5 2005/05/24 16:42:51 klasal
     * Exp $
     *
     * @author $Author: klasal $
     * @version $Revision: 1.3 $
     */
    private class EmptyDescriptedObject implements DescriptedObject {
        /** Holds value of property DOCUMENT ME! */
        private final String description;

        /** Holds value of property DOCUMENT ME! */
        private final String string;

        /**
         * Creates a new EmptyDescriptedObject instance
         *
         * @param description DOCUMENT ME!
         * @param string DOCUMENT ME!
         */
        public EmptyDescriptedObject(String description, String string) {
            this.string = string;
            this.description = description;
        }

        /**
         * DOCUMENT ME!
         *
         * @return DOCUMENT ME!
         */
        public String getDescription() {
            return description;
        }

        /**
         * DOCUMENT ME!
         *
         * @return DOCUMENT ME!
         */
        public String toString() {
            return string;
        }
    }
}
