/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.openide.explorer.view;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.TreePath;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.netbeans.junit.NbTestCase;
import org.openide.explorer.ExplorerManager;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.RequestProcessor;

/**
 * Tests for class BeanTreeViewTest
 */
public class BeanTreeViewTest extends NbTestCase {
    
    public static Test suite() {
        return GraphicsEnvironment.isHeadless() ? new TestSuite() : new TestSuite(BeanTreeViewTest.class);
    }

    private static final int NO_OF_NODES = 3;
    static {
        System.setProperty("netbeans.debug.heap", "no wait");
    }
    
    
    public BeanTreeViewTest(String name) {
        super(name);
    }

    public void testOnlyChildRemoveCausesSelectionOfParent() throws Throwable {
        ExplorerManager em = doChildRemovalTest("one", "one");
        final List<Node> arr = Arrays.asList(em.getSelectedNodes());
        assertEquals("One selected: " + arr, 1, arr.size());
        assertEquals("Root selected", em.getRootContext(), arr.get(0));
    }
    
    public void testFirstChildRemovalCausesSelectionOfSibling() throws Throwable {
        doChildRemovalTest("foo");
    }
    public void testSecondChildRemovalCausesSelectionOfSibling() throws Throwable {
        doChildRemovalTest("bar");
    }
    public void testThirdChildRemovalCausesSelectionOfSibling() throws Throwable {
        doChildRemovalTest("bla");
    }

    private static Object holder;
    
    private void doChildRemovalTest(final String name) throws Throwable {
        doChildRemovalTest(name, "foo", "bar", "bla");
    }
    private ExplorerManager doChildRemovalTest(final String name, final String... childrenNames) throws Throwable {

        class AWTTst implements Runnable {
            AbstractNode root = new AbstractNode(new Children.Array());
            Node[] children;
            {
                List<Node> arr = new ArrayList<Node>();
                for (String s : childrenNames) {
                    arr.add(createLeaf(s));
                }
                children = arr.toArray(new Node[0]);
            }
            Panel p;
            BeanTreeView btv;
            JFrame f;
            JTree tree;
            Node operateOn;

            private void initUI() {
                if (p == null) {
                    p = new Panel();
                    btv = new BeanTreeView();
                    f = new JFrame();
                    tree = btv.tree;
                    root.setName("test root");
                    root.getChildren().add(children);
                    p.getExplorerManager().setRootContext(root);
                    p.add(BorderLayout.CENTER, btv);
                    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    f.getContentPane().add(BorderLayout.CENTER, p);
                    f.setVisible(true);
                }
            }

            @Override
            public void run() {

                initUI();

                for (int i = 0;; i++) {
                    if (name.equals(children[i].getName())) {
                        // this should select a sibling of the removed node
                        operateOn = children[i];
                        break;
                    }
                }

                try {
                    p.getExplorerManager().setSelectedNodes(new Node[]{operateOn});
                } catch (PropertyVetoException e) {
                    fail("Unexpected PropertyVetoException from ExplorerManager.setSelectedNodes()");
                }

                TreePath[] paths = tree.getSelectionPaths();
                assertNotNull("Before removal: one node should be selected, but there are none.", paths);
                assertEquals("Before removal: one node should be selected, but there are none.", 1, paths.length);
                assertEquals("Before removal: one node should be selected, but there are none.", operateOn, Visualizer.findNode(paths[0].getLastPathComponent()));
                assertEquals("Before removal: one node should be selected, but there are none.", operateOn, Visualizer.findNode(tree.getAnchorSelectionPath().getLastPathComponent()));

                // this should select a sibling of the removed node
                root.getChildren().remove(new Node[]{operateOn});
                assertNotNull("After removal: one node should be selected, but there are none.", tree.getSelectionPath());
                children = null;
            }

            public void tryGc() {
                WeakReference<Node> wref = new WeakReference<Node>(operateOn);
                operateOn = null;
                assertGC("Node should be released.", wref);    
            }
        }
        AWTTst awt = new AWTTst();
        holder = awt;
        try {
            SwingUtilities.invokeAndWait(awt);
        } catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
        awt.tryGc();
        return awt.p.getExplorerManager();
    }
    
    public void testOnlyChildDestroyedCausesSelectionOfParent () throws Throwable {
        // node.destroy called on the last selected node of the root node
        ExplorerManager em = doChildDestroyTest ("one", Collections.singleton("one"), "one");
        final List<Node> arr = Arrays.asList(em.getSelectedNodes());
        assertEquals("One selected: " + arr, 1, arr.size());
        assertEquals("Root selected", em.getRootContext(), arr.get(0));
    }
    
    public void testChildDestroyedMoreInSelection () throws Throwable {
        // two children in selection, should not select parent
        // the second child should be selected in the end
        ExplorerManager em = doChildDestroyTest ("one", Arrays.asList("one", "two"), "one", "two");
        final List<Node> arr = Arrays.asList(em.getSelectedNodes());
        assertEquals("One node selected: " + arr, 1, arr.size());
        assertEquals("second child selected", "two", arr.get(0).getName());
    }
    
    public void testChildDestroyedCausesNextChildSelected () throws Throwable {
        // first selected child is destroyed => selection should move to the next child
        ExplorerManager em = doChildDestroyTest ("one", Collections.singleton("one"), "one", "two");
        final List<Node> arr = Arrays.asList(em.getSelectedNodes());
        assertEquals("One selected: " + arr, 1, arr.size());
        assertEquals("second child selected", "two", arr.get(0).getName());
    }
    
    private ExplorerManager doChildDestroyTest (final String name, final Collection<String> toSelect,
            final String... childrenNames) throws Throwable {
        
        class AWTTst implements Runnable {
            Node[] children;
            {
                List<Node> arr = new ArrayList<Node>();
                for (String s : childrenNames) {
                    arr.add(createLeaf(s));
                }
                children = arr.toArray(new Node[0]);
            }
            AbstractNode root = new AbstractNode(new RefreshableChildren());
            Panel p;
            BeanTreeView btv;
            JFrame f;
            JTree tree;
            Node operateOn;
            
            private void initUI() {
                if (p == null) {
                    p = new Panel();
                    btv = new BeanTreeView();
                    f = new JFrame();
                    tree = btv.tree;
                    ((RefreshableChildren) root.getChildren()).refreshKeys(childrenNames);
                    root.setName("test root");
                    p.getExplorerManager().setRootContext(root);
                    p.add(BorderLayout.CENTER, btv);
                    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    f.getContentPane().add(BorderLayout.CENTER, p);
                    f.setVisible(true);
                }
            }

            // children must be Children.Keys(lazy)
            class RefreshableChildren extends Children.Keys<String> {

                public RefreshableChildren () {
                    super(true);
                }
                
                @Override
                protected Node[] createNodes (String key) {
                    Node n = null;
                    for (Node cand : children) {
                        if (cand.getName().equals(key)) {
                            n = cand;
                            break;
                        }
                    }
                    return new Node[] { n };
                }
                
                void refreshKeys (String[] keys) {
                    super.setKeys(keys);
                }
            }

            @Override
            public void run() {

                initUI();

                List<Node> selection = new ArrayList<Node>();
                
                for (int i = 0; i < children.length; i++) {
                    if (name.equals(children[i].getName())) {
                        // this should select a sibling of the removed node
                        operateOn = children[i];
                    }
                    if (toSelect.contains(children[i].getName())) {
                        selection.add(children[i]);
                    }
                }

                try {
                    p.getExplorerManager().setSelectedNodes(selection.toArray(new Node[selection.size()]));
                } catch (PropertyVetoException e) {
                    fail("Unexpected PropertyVetoException from ExplorerManager.setSelectedNodes()");
                }

                TreePath[] paths = tree.getSelectionPaths();
                assertNotNull("Before removal: one node should be selected, but there are none.", paths);
                assertEquals("Before removal: one node should be selected, but there are none.", selection.size(), paths.length);
                if (selection.size() == 1) {
                    assertEquals("Before removal: one node should be selected, but there are none.", operateOn, Visualizer.findNode(paths[0].getLastPathComponent()));
                    assertEquals("Before removal: one node should be selected, but there are none.", operateOn, Visualizer.findNode(tree.getAnchorSelectionPath().getLastPathComponent()));
                }

                try {
                    // destroy the node
                    operateOn.destroy();
                } catch (IOException ex) {
                    fail(ex.getMessage());
                }
                
                assertNotNull("After removal: one node should be selected, but there are none.", tree.getSelectionPath());
            }

            public void tryGc() throws InterruptedException, InvocationTargetException {
                // somewhere around here it's time to remove the destroyed node from children
                // the same as DataObject and FolderChildren work
                EventQueue.invokeAndWait(new Runnable() {
                    @Override
                    public void run () {
                        List<String> newKeys = new ArrayList<String>(Arrays.asList(childrenNames));
                        newKeys.remove(name);
                        ((RefreshableChildren) root.getChildren()).refreshKeys(newKeys.toArray(new String[newKeys.size()]));
                        children = null;
                    }
                });
                WeakReference<Node> wref = new WeakReference<Node>(operateOn);
                operateOn = null;
                RequestProcessor selectionProcessor;
                int delay;
                try {
                    Method getSelectionProcessorMethod = ExplorerManager.class.getDeclaredMethod("getSelectionProcessor");
                    getSelectionProcessorMethod.setAccessible(true);
                    selectionProcessor = (RequestProcessor) getSelectionProcessorMethod.invoke(null);
                    Field delayField = ExplorerManager.class.getDeclaredField("SELECTION_SYNC_DELAY");
                    delayField.setAccessible(true);
                    delay = (Integer) delayField.get(null);
                    
                    // Wait for the task in selectionProcessor to start up:
                    Class ticTacClass = Class.forName(RequestProcessor.class.getName()+"$TickTac");
                    Field tickField = ticTacClass.getDeclaredField("TICK");
                    tickField.setAccessible(true);
                    Object tick = tickField.get(null);
                    if (tick != null) {
                        // Waiting for the tick queue to become empty (scheduled tasks removed)
                        Field queueField = ticTacClass.getDeclaredField("queue");
                        queueField.setAccessible(true);
                        Queue queue = (Queue) queueField.get(tick);
                        while (hasOwnersOf(tick, queue, selectionProcessor)) {
                            //System.err.println("Waiting for queue "+Integer.toHexString(System.identityHashCode(queue))+" to become empty... peek = "+queue.peek()+" is empty = "+queue.isEmpty());
                            Thread.sleep(2*delay); // Wait for the queue with scheduled tasks to become empty
                        }
                    }
                } catch (IllegalAccessException ex) {
                    throw new InvocationTargetException(ex);
                } catch (NoSuchMethodException ex) {
                    throw new InvocationTargetException(ex);
                } catch (NoSuchFieldException ex) {
                    throw new InvocationTargetException(ex);
                } catch (ClassNotFoundException ex) {
                    throw new InvocationTargetException(ex);
                }
                
                // Wait for the task, removed from tick queue, to be attached to a processor...
                Thread.sleep(2*delay);  // No reliable way :-(
                // Wait for the task in selectionProcessor to finish
                selectionProcessor.awaitTermination(60, TimeUnit.SECONDS);
                assertGC("Node should be released.", wref);    
            }
            private boolean hasOwnersOf(Object tick, Queue q, RequestProcessor rp) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException, ClassNotFoundException {
                Class itemClass = Class.forName(RequestProcessor.class.getName()+"$Item");
                Field ownerField = itemClass.getDeclaredField("owner");
                ownerField.setAccessible(true);
                synchronized (tick) {
                    for (Object o : q) {
                        Object ownerRP = ownerField.get(o);
                        if (rp.equals(ownerRP)) {
                            return true;
                        }
                    }
                }
                return false;
            }
        }
        AWTTst awt = new AWTTst();
        holder = awt;
        try {
            SwingUtilities.invokeAndWait(awt);
        } catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
        awt.tryGc();
        return awt.p.getExplorerManager();
    }
    
    public void testVisibleVisNodesAreNotGCed() throws InterruptedException, Throwable {
        doTestVisibleVisNodesAreNotGCed(false);
    }
    public void testVisibleVisNodesAreNotGCedAfterCollapseExpand() throws InterruptedException, Throwable {
        doTestVisibleVisNodesAreNotGCed(true);
    }

    public void doTestVisibleVisNodesAreNotGCed(final boolean collapseAndExpand) throws InterruptedException, Throwable {
        class AWTTst implements Runnable {

            AbstractNode root = new AbstractNode(new Children.Array());
            Node[] children = {
                createLeaf("foo"),
                createLeaf("bar"),
                createLeaf("bla")
            };
            VisualizerNode[] visNodes;
            Panel p;
            BeanTreeView btv;
            JFrame f;
            JTree tree;

            private void initUI() {
                if (p == null) {
                    p = new Panel();
                    btv = new BeanTreeView();
                    f = new JFrame();
                    tree = btv.tree;
                    root.setName("test root");
                    root.getChildren().add(children);
                    p.getExplorerManager().setRootContext(root);
                    p.add(BorderLayout.CENTER, btv);
                    f.setDefaultCloseOperation(f.EXIT_ON_CLOSE);
                    f.getContentPane().add(BorderLayout.CENTER, p);
                    f.setVisible(true);
                }
            }

            public void run() {

                initUI();

                try {
                    p.getExplorerManager().setSelectedNodes(children);
                } catch (PropertyVetoException e) {
                    fail("Unexpected PropertyVetoException from ExplorerManager.setSelectedNodes()");
                }

                TreePath[] paths = tree.getSelectionPaths();
                assertEquals("3 nodes should be selected.", 3, paths.length);
                visNodes = new VisualizerNode[NO_OF_NODES];
                for (int i = 0; i < visNodes.length; i++) {
                    visNodes[i] = (VisualizerNode) paths[i].getLastPathComponent();
                }

                try {
                    p.getExplorerManager().setSelectedNodes(new Node[0]);
                } catch (PropertyVetoException e) {
                    fail("Unexpected PropertyVetoException from ExplorerManager.setSelectedNodes()");
                }

                paths = tree.getSelectionPaths();
                if (paths != null && paths.length == 0) {
                    paths = null;
                }
                assertNull("Nothing should be selected: " + Arrays.toString(paths), paths);
                
                if (collapseAndExpand) {
                    btv.collapseNode(root);
                    btv.expandNode(root);
                }
            }

            public void checkNotGc() {
                WeakReference<VisualizerNode> wref = new WeakReference<VisualizerNode>(visNodes[1]);
                visNodes = null;
                try {
                    EQFriendlyGC.assertGC("Node should be released.", wref);
                } catch (AssertionFailedError e) {
                    return;
                }
                fail("should not be GC");
            }
        }
        AWTTst awt = new AWTTst();
        holder = awt;
        try {
            SwingUtilities.invokeAndWait(awt);
        } catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
        awt.checkNotGc();
    }
    
    public void testSelectingRootDoesNotClearExploredContext() throws InterruptedException, Throwable {
        class AWTTst implements Runnable {

            AbstractNode root = new AbstractNode(new Children.Array());
            VisualizerNode visNode;
            Panel p;
            BeanTreeView btv;
            JFrame f;
            JTree tree;

            private void initUI() {
                if (p == null) {
                    p = new Panel();
                    btv = new BeanTreeView();
                    f = new JFrame();
                    tree = btv.tree;
                    root.setName("test root");
                    p.getExplorerManager().setRootContext(root);
                    p.add(BorderLayout.CENTER, btv);
                    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    f.getContentPane().add(BorderLayout.CENTER, p);
                    f.setVisible(true);
                }
            }

            @Override
            public void run() {
                initUI();
                try {
                    btv.selectionChanged(new Node[] { root }, p.getExplorerManager());
                } catch (PropertyVetoException ex) {
                    fail(ex.getMessage());
                }
                
                assertSame("Root is explored", root, p.getExplorerManager().getExploredContext());
            }
            
        }
        AWTTst awt = new AWTTst();
        holder = awt;
        try {
            SwingUtilities.invokeAndWait(awt);
        } catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }    
    
    public void testVisibleCollapsedNodesAreGCed() throws InterruptedException, Throwable {
        class AWTTst implements Runnable {

            AbstractNode root = new AbstractNode(new Children.Array());
            Node[] children = {
                createLeaf("foo"),
                createLeaf("bar"),
                createLeaf("bla")
            };
            VisualizerNode visNode;
            Panel p;
            BeanTreeView btv;
            JFrame f;
            JTree tree;

            private void initUI() {
                if (p == null) {
                    p = new Panel();
                    btv = new BeanTreeView();
                    f = new JFrame();
                    tree = btv.tree;
                    root.setName("test root");
                    root.getChildren().add(children);
                    p.getExplorerManager().setRootContext(root);
                    p.add(BorderLayout.CENTER, btv);
                    f.setDefaultCloseOperation(f.EXIT_ON_CLOSE);
                    f.getContentPane().add(BorderLayout.CENTER, p);
                    f.setVisible(true);
                }
            }

            public void run() {

                initUI();

                try {
                    p.getExplorerManager().setSelectedNodes(new Node[] {children[0]});
                } catch (PropertyVetoException e) {
                    fail("Unexpected PropertyVetoException from ExplorerManager.setSelectedNodes()");
                }

                TreePath[] paths = tree.getSelectionPaths();
                assertEquals("one node should be selected.", 1, paths.length);
                visNode = (VisualizerNode) paths[0].getLastPathComponent();

                try {
                    p.getExplorerManager().setSelectedNodes(new Node[0]);
                } catch (PropertyVetoException e) {
                    fail("Unexpected PropertyVetoException from ExplorerManager.setSelectedNodes()");
                }
                paths = tree.getSelectionPaths();
                if (paths != null && paths.length == 0) {
                    paths = null;
                }
                assertNull("Nothing should be selected: " + Arrays.toString(paths), paths);

                btv.collapseNode(children[0].getParentNode());
            }
            
            public void checkGc() {
                WeakReference<VisualizerNode> wref = new WeakReference<VisualizerNode>(visNode);
                visNode = null;
                assertGC("Collapsed - should be GCed.", wref);
            }            
        }
        AWTTst awt = new AWTTst();
        holder = awt;
        try {
            SwingUtilities.invokeAndWait(awt);
        } catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
        awt.checkGc();
    }    
    
    private static Node createLeaf(String name) {
        AbstractNode n = new AbstractNode(Children.LEAF);
        n.setName(name);
        return n;
    }
    
    private static class Panel extends JPanel
            implements ExplorerManager.Provider {
        private ExplorerManager em = new ExplorerManager();
        
        public ExplorerManager getExplorerManager() {
            return em;
        }
    }
}
