/* Glazed Lists                                                 (c) 2003-2006 */
/* http://publicobject.com/glazedlists/                      publicobject.com,*/
/*                                                     O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.swing;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.swing.DefaultListSelectionModel;
import javax.swing.JList;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.DelayList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.ThreadRecorderEventList;
import ca.odell.glazedlists.TreeList;
import ca.odell.glazedlists.impl.testing.GlazedListsTests;
import ca.odell.glazedlists.matchers.Matchers;

/**
 * This test verifies that the EventSelectionModel works.
 *
 * @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
 */
public class EventSelectionModelTest extends SwingTestCase {

    /**
     * Tests that selection survives a sorting.
     */
    public void guiTestSort() {
        EventList<Comparable> list = new BasicEventList<Comparable>();
        SortedList<Comparable> sorted = new SortedList<Comparable>(list, null);
        EventSelectionModel<Comparable> eventSelectionModel = new EventSelectionModel<Comparable>(sorted);

        // populate the list
        list.addAll(GlazedListsTests.delimitedStringToList("E C F B A D"));

        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getSelected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getTogglingSelected());
        assertEquals(list, eventSelectionModel.getDeselected());
        assertEquals(list, eventSelectionModel.getTogglingDeselected());

        // select the vowels
        eventSelectionModel.addSelectionInterval(0, 0);
        eventSelectionModel.addSelectionInterval(4, 4);
        assertEquals(GlazedListsTests.delimitedStringToList("E A"), eventSelectionModel.getSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("E A"), eventSelectionModel.getTogglingSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("C F B D"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("C F B D"), eventSelectionModel.getTogglingDeselected());

        // flip the list
        sorted.setComparator(GlazedLists.comparableComparator());
        assertEquals(GlazedListsTests.delimitedStringToList("A E"), eventSelectionModel.getSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("A E"), eventSelectionModel.getTogglingSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("B C D F"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("B C D F"), eventSelectionModel.getTogglingDeselected());

        // flip the list again
        sorted.setComparator(GlazedLists.reverseComparator());
        assertEquals(GlazedListsTests.delimitedStringToList("E A"), eventSelectionModel.getSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("E A"), eventSelectionModel.getTogglingSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("F D C B"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("F D C B"), eventSelectionModel.getTogglingDeselected());
    }

    /**
     * Verifies that the selected index is cleared when the selection is cleared.
     */
    public void guiTestClear() {
        EventList<String> list = new BasicEventList<String>();
        EventSelectionModel eventSelectionModel = new EventSelectionModel<String>(list);

        // populate the list
        list.addAll(GlazedListsTests.delimitedStringToList("A B C D E F"));

        // make a selection
        eventSelectionModel.addSelectionInterval(1, 4);

        // test the selection
        assertEquals(list.subList(1, 5), eventSelectionModel.getSelected());
        assertEquals(list.subList(1, 5), eventSelectionModel.getTogglingSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("A F"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("A F"), eventSelectionModel.getTogglingDeselected());

        // clear the selection
        eventSelectionModel.clearSelection();

        // test the selection
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getSelected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getTogglingSelected());
        assertEquals(-1, eventSelectionModel.getMinSelectionIndex());
        assertEquals(-1, eventSelectionModel.getMaxSelectionIndex());
        assertEquals(true, eventSelectionModel.isSelectionEmpty());
        assertEquals(list, eventSelectionModel.getDeselected());
        assertEquals(list, eventSelectionModel.getTogglingDeselected());
    }

    /**
     * Tests the lists {@link EventSelectionModel#getTogglingSelected()} and
     * {@link EventSelectionModel#getTogglingDeselected()} for programmatic selection control.
     */
    public void guiTestToggleSelection() {
        EventList<String> list = new BasicEventList<String>();
        EventSelectionModel<String> eventSelectionModel = new EventSelectionModel<String>(list);
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getSelected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getTogglingSelected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getDeselected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getTogglingDeselected());

        // populate the list
        list.addAll(GlazedListsTests.delimitedStringToList("A B C D E F"));
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getSelected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getTogglingSelected());
        assertEquals(list, eventSelectionModel.getDeselected());
        assertEquals(list, eventSelectionModel.getTogglingDeselected());

        // remove on TogglingDeselected selects
        eventSelectionModel.getTogglingDeselected().remove("A");
        eventSelectionModel.getTogglingDeselected().remove(1);
        eventSelectionModel.getTogglingDeselected().removeAll(GlazedListsTests.delimitedStringToList("F D"));
        assertEquals(GlazedListsTests.delimitedStringToList("B E"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("B E"), eventSelectionModel.getTogglingDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("A C D F"), eventSelectionModel.getSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("A C D F"), eventSelectionModel.getTogglingSelected());

        // add on TogglingDeselected deselects
        eventSelectionModel.getTogglingDeselected().add("F");
        eventSelectionModel.getTogglingDeselected().addAll(GlazedListsTests.delimitedStringToList("C D"));
        assertEquals(GlazedListsTests.delimitedStringToList("B C D E F"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("B C D E F"), eventSelectionModel.getTogglingDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("A"), eventSelectionModel.getSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("A"), eventSelectionModel.getTogglingSelected());

        // add on TogglingSelected selects
        eventSelectionModel.getTogglingSelected().add("F");
        eventSelectionModel.getTogglingSelected().addAll(GlazedListsTests.delimitedStringToList("C D"));
        assertEquals(GlazedListsTests.delimitedStringToList("B E"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("B E"), eventSelectionModel.getTogglingDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("A C D F"), eventSelectionModel.getSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("A C D F"), eventSelectionModel.getTogglingSelected());

        // remove on TogglingSelected deselects
        eventSelectionModel.getTogglingSelected().remove("A");
        eventSelectionModel.getTogglingSelected().remove(1);
        eventSelectionModel.getTogglingSelected().removeAll(GlazedListsTests.delimitedStringToList("F"));
        assertEquals(GlazedListsTests.delimitedStringToList("A B D E F"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("A B D E F"), eventSelectionModel.getTogglingDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("C"), eventSelectionModel.getSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("C"), eventSelectionModel.getTogglingSelected());

        // remove on source list
        list.remove("C");
        list.removeAll(GlazedListsTests.delimitedStringToList("B E"));
        assertEquals(GlazedListsTests.delimitedStringToList("A D F"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("A D F"), eventSelectionModel.getTogglingDeselected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getSelected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getTogglingSelected());

        // add on source list
        list.add("E");
        list.addAll(GlazedListsTests.delimitedStringToList("C B"));
        assertEquals(GlazedListsTests.delimitedStringToList("A D F E C B"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("A D F E C B"), eventSelectionModel.getTogglingDeselected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getSelected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getTogglingSelected());

        // clear on TogglingDeselected selects all deselected
        eventSelectionModel.getTogglingDeselected().clear();
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getDeselected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getTogglingDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("A D F E C B"), eventSelectionModel.getSelected());
        assertEquals(GlazedListsTests.delimitedStringToList("A D F E C B"), eventSelectionModel.getTogglingSelected());

        // clear on TogglingSelected deselects all selected
        eventSelectionModel.getTogglingSelected().clear();
        assertEquals(GlazedListsTests.delimitedStringToList("A D F E C B"), eventSelectionModel.getDeselected());
        assertEquals(GlazedListsTests.delimitedStringToList("A D F E C B"), eventSelectionModel.getTogglingDeselected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getSelected());
        assertEquals(Collections.EMPTY_LIST, eventSelectionModel.getTogglingSelected());
    }

    /**
     * Tests a problem where the {@link EventSelectionModel} fails to fire events
     *
     * This test was contributed by: Sergey Bogatyrjov
     */
    public void guiTestSelectionModel() {
        EventList<Object> source = GlazedLists.eventListOf(new Object[] {"one", "two", "three"});
        FilterList<Object> filtered = new FilterList<Object>(source, Matchers.trueMatcher());

        // create selection model
        EventSelectionModel<Object> model = new EventSelectionModel<Object>(filtered);
        ListSelectionChangeCounter counter = new ListSelectionChangeCounter();
        model.addListSelectionListener(counter);

        // select the 1th
        model.setSelectionInterval(1, 1);
        assertEquals(1, counter.getCountAndReset());

        // clear the filter
        filtered.setMatcher(Matchers.falseMatcher());
        assertEquals(1, counter.getCountAndReset());

        // unclear the filter
        filtered.setMatcher(Matchers.trueMatcher());
        assertEquals(0, counter.getCountAndReset());

        // select the 0th
        model.setSelectionInterval(0, 0);
        assertEquals(1, counter.getCountAndReset());

        // clear the filter
        filtered.setMatcher(Matchers.falseMatcher());
        assertEquals(1, counter.getCountAndReset());
    }

    public void guiTestConstructorLocking() throws InterruptedException {
        // create a list which will record our multithreaded interactions with a list
        final ThreadRecorderEventList<Integer> atomicList = new ThreadRecorderEventList<Integer>(new BasicEventList<Integer>());

        // start a thread which adds new Integers every 50 ms
        final Thread writerThread = new Thread(GlazedListsTests.createJerkyAddRunnable(atomicList, null, 2000, 50), "WriterThread");
        writerThread.start();

        // make sure the writerThread has started writing
        Thread.sleep(200);

        // create a list whose get() method pauses for 50 ms before returning the value
        final EventList<Integer> delayList = new DelayList<Integer>(atomicList, 50);

        // the test: creating the EventSelectionModel should be atomic and pause the writerThread while it initializes its internal state
        new EventSelectionModel<Integer>(delayList);

        // wait until the writerThread finishes before asserting the recorded state
        writerThread.join();

        // correct locking should have produced a thread log like: WriterThread* AWT-EventQueue-0* WriterThread*
        // correct locking should have produced a read/write pattern like: W...W R...R W...W
        assertEquals(3, atomicList.getReadWriteBlockCount());
    }

    /**
     * If EventList changes are relegated to indexes AFTER the maxSelectionIndex
     * then no ListSelectionEvent needs to be fired. This test method verifies
     * that the expected number of ListSelectionEvents are produced when
     * inserting and removing at all locations relative to the range of list selections.
     */
    public void guiTestFireOnlyNecessaryEvents() {
        EventList<String> source = GlazedLists.eventListOf(new String[] {"Albert", "Alex", "Aaron", "Brian", "Bruce"});

        // create selection model
        EventSelectionModel<String> model = new EventSelectionModel<String>(source);
        ListSelectionChangeCounter counter = new ListSelectionChangeCounter();
        model.addListSelectionListener(counter);

        // select 2nd element (should produce 1 ListSelectionEvent)
        model.setSelectionInterval(1, 3);
        assertEquals(1, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(3, model.getMaxSelectionIndex());

        // inserts and removes before the minSelectionIndex shift the existing selections and thus produce ListSelectionEvents
        source.add(0, "Bart");
        assertEquals(1, counter.getCountAndReset());
        assertEquals(2, model.getMinSelectionIndex());
        assertEquals(4, model.getMaxSelectionIndex());
        source.remove(0);
        assertEquals(1, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(3, model.getMaxSelectionIndex());

        // inserts and removes on the minSelectionIndex shift the existing selections and thus produce ListSelectionEvents
        source.add(1, "Bart");
        assertEquals(1, counter.getCountAndReset());
        assertEquals(2, model.getMinSelectionIndex());
        assertEquals(4, model.getMaxSelectionIndex());
        source.remove(1);
        assertEquals(1, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(3, model.getMaxSelectionIndex());

        // inserts and removes between the minSelectionIndex and maxSelectionIndex change existing selections and thus produce ListSelectionEvents
        source.add(2, "Bart");
        assertEquals(1, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(4, model.getMaxSelectionIndex());
        source.remove(2);
        assertEquals(1, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(3, model.getMaxSelectionIndex());

        // inserts and removes on the maxSelectionIndex change the existing selections and thus produce ListSelectionEvents
        source.add(3, "Bart");
        assertEquals(1, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(4, model.getMaxSelectionIndex());
        source.remove(3);
        assertEquals(1, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(3, model.getMaxSelectionIndex());

        // inserts and removes after the maxSelectionIndex do not produce ListSelectionEvents
        source.add(4, "Bart");
        assertEquals(0, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(3, model.getMaxSelectionIndex());
        source.remove(4);
        assertEquals(0, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(3, model.getMaxSelectionIndex());

        // inserts and removes after the maxSelectionIndex do not produce ListSelectionEvents
        source.add(5, "Bart");
        assertEquals(0, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(3, model.getMaxSelectionIndex());
        source.remove(5);
        assertEquals(0, counter.getCountAndReset());
        assertEquals(1, model.getMinSelectionIndex());
        assertEquals(3, model.getMaxSelectionIndex());
    }

    public void guiTestModelChangesProducingSelectionModelEvents() {
        EventList<String> source = GlazedLists.eventListOf(new String[] {"Albert", "Alex", "Aaron", "Brian", "Bruce"});

        // create EventListModel (data model)
        EventListModel<String> model = new EventListModel<String>(source);

        // create EventSelectionModel (our selection model)
        EventSelectionModel<String> eventSelectionModel = new EventSelectionModel<String>(source);
        ListSelectionChangeCounter eventSelectionModelCounter = new ListSelectionChangeCounter();
        eventSelectionModel.addListSelectionListener(eventSelectionModelCounter);

        // create DefaultListSelectionModel (SUN's selection model)
        DefaultListSelectionModel defaultListSelectionModel = new DefaultListSelectionModel();
        ListSelectionChangeCounter defaultListSelectionModelCounter = new ListSelectionChangeCounter();
        defaultListSelectionModel.addListSelectionListener(defaultListSelectionModelCounter);

        // create two different JLists (one with EventSelectionModel and one with DefaultListSelectionModel) that share the same data model
        JList eventList = new JList(model);
        eventList.setSelectionModel(eventSelectionModel);

        JList defaultList = new JList(model);
        defaultList.setSelectionModel(defaultListSelectionModel);

        // select the first element in both selection models
        eventSelectionModel.setSelectionInterval(0, 0);
        defaultListSelectionModel.setSelectionInterval(0, 0);

        // verify that each selection model broadcasted the selection event
        assertEquals(1, defaultListSelectionModelCounter.getCountAndReset());
        assertEquals(1, eventSelectionModelCounter.getCountAndReset());

        // change an element in the model which is selected in the selection models
        source.set(0, "Bart");

        // selection should not have changed in either selection model
        assertEquals(0, defaultListSelectionModel.getMinSelectionIndex());
        assertEquals(0, defaultListSelectionModel.getMaxSelectionIndex());
        assertEquals(0, defaultListSelectionModel.getLeadSelectionIndex());
        assertEquals(0, defaultListSelectionModel.getAnchorSelectionIndex());

        assertEquals(0, eventSelectionModel.getMinSelectionIndex());
        assertEquals(0, eventSelectionModel.getMaxSelectionIndex());
        assertEquals(0, eventSelectionModel.getLeadSelectionIndex());
        assertEquals(0, eventSelectionModel.getAnchorSelectionIndex());

        // verify that neither DefaultListSelectionModel nor EventSelectionModel broadcasted a needless event for this model change
        assertEquals(0, defaultListSelectionModelCounter.getCountAndReset());
        assertEquals(0, eventSelectionModelCounter.getCountAndReset());
    }

    public void guiTestDeleteSelectedRows_FixMe() {
        EventList<String> source = GlazedLists.eventListOf(new String[] {"one", "two", "three"});

        // create selection model
        EventSelectionModel<String> model = new EventSelectionModel<String>(source);
        ListSelectionChangeCounter counter = new ListSelectionChangeCounter();
        model.addListSelectionListener(counter);

        // select all elements (should produce 1 ListSelectionEvent)
        model.setSelectionInterval(0, 2);
        assertEquals(source, model.getSelected());
        assertEquals(1, counter.getCountAndReset());

        // remove all selected elements (should produce 1 ListSelectionEvent)
        model.getSelected().clear();
        assertEquals(Collections.EMPTY_LIST, source);
        assertEquals(Collections.EMPTY_LIST, model.getSelected());
        // todo fix this defect (this is the functionality that Bruce Alspaugh is looking for)
        assertEquals(1, counter.getCountAndReset());
    }

    /**
     * Tests, that a selection is preserved when the source is a TreeList and an update event
     * happens for the selected element.
     */
    public void guiTestSelectionOnTreeListUpdate_FixMe() {
        final EventList<String> source = GlazedLists.eventListOf(new String[] {"zero", "one", "two", "three"});

        final EventList<String> sourceProxy = GlazedListsSwing.swingThreadProxyList(source);
        final TreeList<String> treeList = new TreeList<String>(sourceProxy, new StringFormat(),
                TreeList.NODES_START_EXPANDED);
        final EventSelectionModel<String> selModel = new EventSelectionModel<String>(treeList);
        selModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        selModel.setSelectionInterval(2, 2);
        assertEquals(Arrays.asList(new String[] {"two"}), selModel.getSelected());
        source.set(2, source.get(2));
        assertEquals(Arrays.asList(new String[] {"two"}), selModel.getSelected());
    }

    /** Simple Format for TreeList testing. */
    private static class StringFormat implements TreeList.Format<String> {
        public boolean allowsChildren(String element) {
            return element.equals("zero");
        }
        public Comparator<? extends String> getComparator(int depth) {
            return null;
        }
        public void getPath(List<String> path, String element) {
             if (!element.equals("zero")) {
                 getPath(path, "zero");
             }
             path.add(element);
        }
    }
    /**
     * Counts the number of ListSelectionEvents fired.
     */
    private static class ListSelectionChangeCounter implements ListSelectionListener {
        private int count = 0;
        public void valueChanged(ListSelectionEvent e) {
            count++;
        }
        public int getCountAndReset() {
            int result = count;
            count = 0;
            return result;
        }
    }
}