/*
 * 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.netbeans.core.windows;

import java.util.Collection;
import java.util.Iterator;
import org.openide.nodes.Node;
import org.openide.util.WeakSet;
import org.openide.windows.TopComponent;

import javax.swing.*;
import java.awt.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/** Implementstion of registry of top components. This implementation
 * receives information about top component changes from the window
 * manager implementation, to which is listening to.
 *
 * @author Peter Zavadsky
 */
@org.openide.util.lookup.ServiceProvider(service=org.openide.windows.TopComponent.Registry.class)
public final class RegistryImpl extends Object implements TopComponent.Registry {
    
    // fields
    /** Activated top component */
    private WeakReference<TopComponent> activatedTopComponent = new WeakReference<TopComponent>(null);
    /** Previouly activated top component */
    private WeakReference<TopComponent> previousActivated;
    /** Set of opened TopComponents */
    private final Set<TopComponent> openSet = new WeakSet<TopComponent>(30);
    /** Currently selected nodes. */
    private Node[] currentNodes;
    /** Last non-null value of current nodes. (If null -> it means they are
     * not initialized and weren't fired yet. */
    private Node[] activatedNodes;
    /** PropertyChange support */
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
    
    /** Debugging flag. */
    private static final boolean DEBUG = Debug.isLoggable(RegistryImpl.class);

    /** Creates new RegistryImpl */
    public RegistryImpl() {
    }
    
    /** Get all opened componets in the system.
     *
     * @return immutable set of {@link TopComponent}s
     */
    public synchronized Set<TopComponent> getOpened() {
        return new SyncSet();
    }
    
    /** Get the currently selected element.
     * @return the selected top component, or <CODE>null</CODE> if there is none
     */
    public TopComponent getActivated() {
        return activatedTopComponent.get();
    }
    
    /** Getter for the currently selected nodes.
     * @return array of nodes or null if no component activated. */
    public Node[] getCurrentNodes() {
        return currentNodes;
    }
    
    /** Getter for the lastly activated nodes. Comparing
     * to previous method it always remembers the selected nodes
     * of the last component that had ones.
     *
     * @return array of nodes (not null)
     */
    public Node[] getActivatedNodes() {
        return activatedNodes == null ? new Node[0] : activatedNodes;
    }
    
    /** Add a property change listener.
     * @param l the listener to add
     */
    public void addPropertyChangeListener(PropertyChangeListener l) {
        support.addPropertyChangeListener(l);
    }
    
    /** Remove a property change listener.
     * @param l the listener to remove
     */
    public void removePropertyChangeListener(PropertyChangeListener l) {
        support.removePropertyChangeListener(l);
    }

    
    //////////////////////////////////////////////////////
    /// notifications of changes from window manager >>>>>
    /** Called when a TopComponent is activated.
     *
     * @param ev TopComponentChangedEvent
     */
    void topComponentActivated(TopComponent tc) {
        if(activatedTopComponent.get() == tc
        && activatedNodes != null) { // When null it means were not inited yet.
            return;
        }
        
        final TopComponent old = activatedTopComponent.get();
        if (old != null && old.getActivatedNodes() != null) {
            previousActivated = new WeakReference<TopComponent>(old);
        }
        activatedTopComponent = new WeakReference<TopComponent>(tc);
        
/** PENDING:  Firing the change asynchronously improves perceived responsiveness
 considerably (toolbars are updated after the component repaints, so it appears
 to immediately become selected), but means that 
 for one EQ cycle the activated TopComponent will be out of sync with the 
 global node selection.  Needs testing. -Tim
 
 C.f. issue 42256 - most of the delay may be called by contention in 
     ProxyLookup, but this fix will have some responsiveness benefits 
     even if that is fixed
*/ 
        final TopComponent tmp = this.activatedTopComponent.get();
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                doFirePropertyChange(PROP_ACTIVATED, old, tmp);
            }
        });

        selectedNodesChanged(tmp, tmp == null ? null : tmp.getActivatedNodes());
    }
    
    
    
    /** Called when a TopComponent is opened. */
    public synchronized void topComponentOpened(TopComponent tc) {
        assert null != tc;
        if (openSet.contains(tc)) {
            return;
        }
        Set<TopComponent> old = new HashSet<TopComponent>(openSet);
        openSet.add(tc);
        doFirePropertyChange(PROP_TC_OPENED, null, tc);
        doFirePropertyChange(PROP_OPENED, old, new HashSet<TopComponent>(openSet));
    }
    
    /** Called when a TopComponent is closed. */
    public synchronized void topComponentClosed(TopComponent tc) {
        if (!openSet.contains(tc)) {
            return;
        }

        Set<TopComponent> old = new HashSet<TopComponent>(openSet);
        openSet.remove(tc);
        doFirePropertyChange(PROP_TC_CLOSED, null, tc);
        doFirePropertyChange(PROP_OPENED, old, new HashSet<TopComponent>(openSet));

        if (activatedNodes != null) {
            Node[] closedNodes = tc.getActivatedNodes();
            if (closedNodes != null && Arrays.equals(closedNodes, activatedNodes)) {
                // The component whose nodes were activated has been closed; cancel the selection.
                activatedNodes = null;
                doFirePropertyChange(PROP_ACTIVATED_NODES, closedNodes, null);
            }
        }
    }

    /**
     * Called during 'Reset Windows' to re-add document windows that the Reset
     * action does not close.
     * @param tc
     */
    public synchronized void addTopComponent( TopComponent tc ) {
        assert null != tc;
        openSet.add( tc );
    }
    
    /** Called when selected nodes changed. */
    public void selectedNodesChanged(TopComponent tc, Node[] newNodes) {
        Node[] oldNodes = currentNodes;
        
        //Fixed bug #8933 27 Mar 2001 by Marek Slama
        //Selected nodes event was processed for other than activated component.
        //Check if activatedTopComponent is the same as event source top component
        //If not ignore event
        if(tc != activatedTopComponent.get()
        && activatedNodes != null) { // When null it means were not inited yet.
            // #82319: update activated nodes from previously active TopComponent 
            // under special conditions
            if (!isProperPrevious(tc, newNodes)) {
                return;
            }
        }
        //End of bugfix #8933
        
        if(Arrays.equals(oldNodes, newNodes)
        && activatedNodes != null) { // When null it means were not inited yet.
            return;
        }

        currentNodes = newNodes == null ? null : newNodes.clone();
        // fire immediatelly only if window manager in proper state
        tryFireChanges(oldNodes, currentNodes);
    }
    
    /** Part of #82319 bugfix.
     * Returns true if given top component is the one previously selected
     * and conditions are met to update activated nodes from it.
     */
    private boolean isProperPrevious (TopComponent tc, Node[] newNodes) {
        if (previousActivated == null || newNodes == null) {
            return false;
        }
        
        TopComponent previousTC = previousActivated.get();
        if (previousTC == null || !previousTC.equals(tc)) {
            return false;
        }

        TopComponent tmp = activatedTopComponent.get();
        return tmp != null && tmp.getActivatedNodes() == null;
    }
    
    /// notifications of changes from window manager <<<<<
    //////////////////////////////////////////////////////

    /** Cancels the menu if it is not assigned to specified window.
     * @param window window that the menu should be checked against
     *    (if this window contains the menu, then the menu will not be closed)
     */
    /** Closes popup menu.
     */
    public static void cancelMenu(Window window) {
        MenuSelectionManager msm = MenuSelectionManager.defaultManager();
        MenuElement[] path = msm.getSelectedPath();
        
        for (int i = 0; i < path.length; i++) {
            //      if (newPath[i] != path[i]) return;
            java.awt.Window w = SwingUtilities.windowForComponent(
                path[i].getComponent()
            );
            
            // we must check for null because windowForComponent above can return null
            if ((w != null) && (w == window || w.getOwner() == window)) {
                // ok, this menu can stay
                return;
            }
            
        }
        if( path.length > 0 )
            msm.clearSelectedPath();
    }
    
    
    /** If window manager in proper state, fire selected and
     * activated node changes */
    private void tryFireChanges(Node[] oldNodes, Node[] newNodes) {
        doFirePropertyChange(PROP_CURRENT_NODES, oldNodes, newNodes);
        
        if(newNodes == null && activatedNodes == null) {
            // Ensure activated nodes are going to be fired first time in session for this case.
            newNodes = new Node[0];
        }
        
        if (newNodes != null) {
            oldNodes = activatedNodes;
            activatedNodes = newNodes;
            support.firePropertyChange(PROP_ACTIVATED_NODES, oldNodes, activatedNodes);
        }
    }
    
    
    private void doFirePropertyChange(final String propName,
    final Object oldValue, final Object newValue) {
        if(DEBUG) {
            debugLog(""); // NOI18N
            debugLog("Scheduling event firing: propName=" + propName); // NOI18N
            debugLog("\toldValue=" + (oldValue instanceof Object[] ? Arrays.asList((Object[])oldValue) : oldValue)); // NOI18N
            debugLog("\tnewValue=" + (newValue instanceof Object[] ? Arrays.asList((Object[])newValue) : newValue)); // NOI18N
        }
        // PENDING When #37529 finished, then uncomment the next row and move the 
        // checks of AWT thread away.
        //  WindowManagerImpl.assertEventDispatchThread();
        if(SwingUtilities.isEventDispatchThread()) {
            support.firePropertyChange(propName, oldValue, newValue);
        } else {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    support.firePropertyChange(propName, oldValue, newValue);
                }
            });
        }
    }
    
    void clear() {
        activatedTopComponent.clear();
        openSet.clear();
        currentNodes = null;
        activatedNodes = null;
    }
    
    private static void debugLog(String message) {
        Debug.log(RegistryImpl.class, message);
    }

    private final class SyncSet implements Set<TopComponent> {
        public int size() {
            synchronized (RegistryImpl.this) {
                return openSet.size();
            }
        }

        public boolean isEmpty() {
            synchronized (RegistryImpl.this) {
                return openSet.isEmpty();
            }
        }

        public boolean contains(Object o) {
            synchronized (RegistryImpl.this) {
                return openSet.contains(o);
            }
        }

        public Iterator<TopComponent> iterator() {
            synchronized (RegistryImpl.this) {
                return new HashSet<TopComponent>(openSet).iterator();
            }
        }

        public Object[] toArray() {
            synchronized (RegistryImpl.this) {
                return openSet.toArray();
            }
        }

        public <T> T[] toArray(T[] a) {
            synchronized (RegistryImpl.this) {
                return openSet.toArray(a);
            }
        }

        public boolean containsAll(Collection<?> c) {
            synchronized (RegistryImpl.this) {
                return openSet.containsAll(c);
            }
        }

        public boolean add(TopComponent e) {
            throw new UnsupportedOperationException();
        }

        public boolean remove(Object o) {
            throw new UnsupportedOperationException();
        }

        public boolean addAll(Collection<? extends TopComponent> c) {
            throw new UnsupportedOperationException();
        }

        public boolean retainAll(Collection<?> c) {
            throw new UnsupportedOperationException();
        }

        public boolean removeAll(Collection<?> c) {
            throw new UnsupportedOperationException();
        }

        public void clear() {
            throw new UnsupportedOperationException();
        }
    }
}
