/*
 * 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.modules.web.clientproject.ui;

import java.awt.EventQueue;
import java.awt.Image;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.CharConversionException;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectInformation;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.Sources;
import org.netbeans.api.queries.VisibilityQuery;
import org.netbeans.modules.gsf.codecoverage.api.CoverageActionFactory;
import org.netbeans.modules.web.clientproject.ClientSideProject;
import org.netbeans.modules.web.clientproject.api.BadgeIcon;
import org.netbeans.modules.web.clientproject.api.build.BuildTools;
import org.netbeans.modules.web.clientproject.api.jstesting.JsTestingProvider;
import org.netbeans.modules.web.clientproject.api.jstesting.JsTestingProviders;
import org.netbeans.modules.web.clientproject.api.platform.PlatformProvider;
import org.netbeans.modules.web.clientproject.api.remotefiles.RemoteFilesNodeFactory;
import org.netbeans.modules.web.clientproject.env.Values;
import org.netbeans.modules.web.clientproject.spi.platform.ClientProjectEnhancedBrowserImplementation;
import org.netbeans.modules.web.clientproject.util.ClientSideProjectUtilities;
import org.netbeans.spi.project.ActionProvider;
import org.netbeans.spi.project.ui.LogicalViewProvider;
import org.netbeans.spi.project.ui.ProjectProblemsProvider;
import org.netbeans.spi.project.ui.support.CommonProjectActions;
import org.netbeans.spi.project.ui.support.NodeFactory;
import org.netbeans.spi.project.ui.support.NodeFactorySupport;
import org.netbeans.spi.project.ui.support.NodeList;
import org.netbeans.spi.queries.VisibilityQueryChangeEvent;
import org.openide.actions.FileSystemAction;
import org.openide.actions.FindAction;
import org.openide.actions.PasteAction;
import org.openide.actions.ToolsAction;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUIUtils;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.nodes.NodeNotFoundException;
import org.openide.nodes.NodeOp;
import org.openide.util.ChangeSupport;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
import org.openide.util.WeakSet;
import org.openide.util.actions.SystemAction;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;
import org.openide.util.lookup.ProxyLookup;
import org.openide.xml.XMLUtil;

@ActionReferences({
    @ActionReference(
        id=@ActionID(id="org.netbeans.modules.project.ui.problems.BrokenProjectActionFactory", category="Project"),
        position=2950,
        path="Projects/org-netbeans-modules-web-clientproject/Actions")
})
public class ClientSideProjectLogicalView implements LogicalViewProvider {

    static final Logger LOGGER = Logger.getLogger(ClientSideProjectLogicalView.class.getName());

    static final RequestProcessor RP = new RequestProcessor(ClientSideProjectLogicalView.class.getName(), 2);

    private final ClientSideProject project;

    public ClientSideProjectLogicalView(ClientSideProject project) {
        this.project = project;
    }

    @Override
    public Node createLogicalView() {
        return ClientSideProjectNode.createForProject(project);
    }

    @Override
    public Node findPath(Node root, Object target) {
        Project prj = root.getLookup().lookup(Project.class);
        if (prj == null) {
            return null;
        }

        FileObject fo;
        if (target instanceof FileObject) {
            fo = (FileObject) target;
        } else if (target instanceof DataObject) {
            fo = ((DataObject) target).getPrimaryFile();
        } else {
            // unsupported object
            return null;
        }
        // first check project
        Project owner = FileOwnerQuery.getOwner(fo);
        if (!prj.equals(owner)) {
            // another project
            return null;
        }

        // XXX later, use source roots here
        for (Node node : root.getChildren().getNodes(true)) {
            FileObject kid = node.getLookup().lookup(FileObject.class);
            if (kid == null) {
                continue;
            }
            if (kid == fo) {
                return node;
            } else if (FileUtil.isParentOf(kid, fo)) {
                Node found = findNode(node, kid, fo);
                if (found == null) {
                    return null;
                }
                if (hasObject(found, target)) {
                    return found;
                }
                // #252446 ?
                Node[] nodes = found.getParentNode().getChildren().getNodes(true);
                for (Node child : nodes) {
                    if (child.getName().equals(fo.getName())
                            && hasObject(child, fo)) {
                        return child;
                    }
                }
            }
        }
        return null;
    }


    @CheckForNull
    private static Node findNode(Node node, FileObject root, FileObject fo) {
        String relPath = FileUtil.getRelativePath(root, fo);

        // first, try to find the file without extension (more common case)
        String[] path = relPath.split("/"); // NOI18N
        path[path.length - 1] = fo.getName();
        Node found = findNode(node, path);
        if (found != null) {
            return found;
        }
        // file not found, try to search for the name with the extension
        path[path.length - 1] = fo.getNameExt();
        return findNode(node, path);
    }

    private static Node findNode(Node start, String[] path) {
        Node found = null;
        try {
            found = NodeOp.findPath(start, path);
        } catch (NodeNotFoundException ex) {
            // ignored
        }
        return found;
    }

    private boolean hasObject(Node node, Object obj) {
        if (obj == null) {
            return false;
        }
        FileObject fileObject = node.getLookup().lookup(FileObject.class);
        if (fileObject == null) {
            return false;
        }
        if (obj instanceof DataObject) {
            DataObject dataObject = node.getLookup().lookup(DataObject.class);
            if (dataObject == null) {
                return false;
            }
            if (dataObject.equals(obj)) {
                return true;
            }
            return hasObject(node, ((DataObject) obj).getPrimaryFile());
        } else if (obj instanceof FileObject) {
            return obj.equals(fileObject);
        }
        return false;
    }


/** This is the node you actually see in the project tab for the project */
    private static final class ClientSideProjectNode extends AbstractNode implements ChangeListener, PropertyChangeListener {

        @StaticResource
        private static final String HTML5_BADGE_ICON = "org/netbeans/modules/web/clientproject/ui/resources/html5-badge.png"; // NOI18N
        @StaticResource
        private static final String JS_LIBRARY_BADGE_ICON = "org/netbeans/modules/web/clientproject/ui/resources/js-library-badge.png"; // NOI18N
        @StaticResource
        private static final String PLACEHOLDER_BADGE_ICON = "org/netbeans/modules/web/clientproject/ui/resources/placeholder-badge.png"; // NOI18N
        private static final URL PLACEHOLDER_BADGE_URL = ClientSideProjectNode.class.getResource(PLACEHOLDER_BADGE_ICON);
        private static final String ICON_TOOLTIP = "<img src=\"%s\">&nbsp;%s"; // NOI18N

        private final ClientSideProject project;
        private final ProjectInformation projectInfo;
        private final Values evaluator;
        private final ProjectProblemsProvider problemsProvider;


        private ClientSideProjectNode(ClientSideProject project) {
            super(NodeFactorySupport.createCompositeChildren(project, "Projects/org-netbeans-modules-web-clientproject/Nodes"), createLookup(project));
            this.project = project;
            projectInfo = ProjectUtils.getInformation(project);
            evaluator = project.getEvaluator();
            problemsProvider = project.getLookup().lookup(ProjectProblemsProvider.class);
        }

        public static ClientSideProjectNode createForProject(ClientSideProject project) {
            ClientSideProjectNode rootNode = new ClientSideProjectNode(project);
            rootNode.addListeners();
            return rootNode;
        }

        private void addListeners() {
            evaluator.addPropertyChangeListener(WeakListeners.propertyChange(this, evaluator));
            projectInfo.addPropertyChangeListener(WeakListeners.propertyChange(this, projectInfo));
            problemsProvider.addPropertyChangeListener(WeakListeners.propertyChange(this, problemsProvider));
        }

        private static Lookup createLookup(ClientSideProject project) {
            final InstanceContent instanceContent = new InstanceContent();
            instanceContent.add(project);
            instanceContent.add(project, new InstanceContent.Convertor<ClientSideProject, FileObject>() {
                @Override
                public FileObject convert(ClientSideProject obj) {
                    return obj.getProjectDirectory();
                }

                @Override
                public Class<? extends FileObject> type(ClientSideProject obj) {
                    return FileObject.class;
                }

                @Override
                public String id(ClientSideProject obj) {
                    final FileObject fo = obj.getProjectDirectory();
                    return fo == null ? "" : fo.getPath();  // NOI18N
                }

                @Override
                public String displayName(ClientSideProject obj) {
                    return obj.toString();
                }

            });
            instanceContent.add(project, new InstanceContent.Convertor<ClientSideProject, DataObject>() {
                @Override
                public DataObject convert(ClientSideProject obj) {
                    try {
                        final FileObject fo = obj.getProjectDirectory();
                        return fo == null ? null : DataObject.find(fo);
                    } catch (DataObjectNotFoundException ex) {
                        LOGGER.log(Level.WARNING, null, ex);
                        return null;
                    }
                }

                @Override
                public Class<? extends DataObject> type(ClientSideProject obj) {
                    return DataObject.class;
                }

                @Override
                public String id(ClientSideProject obj) {
                    final FileObject fo = obj.getProjectDirectory();
                    return fo == null ? "" : fo.getPath();  // NOI18N
                }

                @Override
                public String displayName(ClientSideProject obj) {
                    return obj.toString();
                }

            });
            return new AbstractLookup(instanceContent);
        }

        @Override
        public Action[] getActions(boolean arg0) {
            List<Action> actions = new LinkedList<>(Arrays.asList(CommonProjectActions.forType("org-netbeans-modules-web-clientproject"))); // NOI18N
            addBuildActions(actions);
            addCodeCoverageAction(actions);
            return actions.toArray(new Action[actions.size()]);
        }

        @Override
        public Image getIcon(int type) {
            return annotateImage(ImageUtilities.loadImage(ClientSideProject.HTML5_PROJECT_ICON));
        }

        @Override
        public Image getOpenedIcon(int type) {
            return getIcon(type);
        }

        private Image annotateImage(Image image) {
            Image icon = image;
            boolean badged = false;
            // platform providers
            for (PlatformProvider provider : project.getPlatformProviders()) {
                BadgeIcon badgeIcon = provider.getBadgeIcon();
                if (badgeIcon != null) {
                    icon = ImageUtilities.addToolTipToImage(icon, String.format(ICON_TOOLTIP, badgeIcon.getUrl(), provider.getDisplayName()));
                    if (!badged) {
                        icon = ImageUtilities.mergeImages(icon, badgeIcon.getImage(), 0, 0);
                        badged = true;
                    }
                } else {
                    icon = ImageUtilities.addToolTipToImage(icon, String.format(ICON_TOOLTIP, PLACEHOLDER_BADGE_URL, provider.getDisplayName()));
                }
            }
            // project type, only if no platform
            if (!badged) {
                Image projectBadge = ImageUtilities.loadImage(project.isJsLibrary() ? JS_LIBRARY_BADGE_ICON : HTML5_BADGE_ICON);
                icon = ImageUtilities.mergeImages(icon, projectBadge, 0, 0);
            }
            return icon;
        }

        @Override
        public String getName() {
            // i would expect getName() here but see #222588
            return projectInfo.getDisplayName();
        }

        @Override
        public String getHtmlDisplayName() {
            String dispName = super.getDisplayName();
            try {
                dispName = XMLUtil.toElementContent(dispName);
            } catch (CharConversionException ex) {
                return dispName;
            }
            return ClientSideProjectUtilities.hasErrors(project)
                    ? "<font color=\"#" + Integer.toHexString(ClientSideProjectUtilities.getErrorForeground().getRGB() & 0xffffff) + "\">" + dispName + "</font>" // NOI18N
                    : null;
        }

        @NbBundle.Messages({
            "# {0} - project directory",
            "ClientSideProjectNode.project.description=HTML5 application in {0}",
            "# {0} - project directory",
            "ClientSideProjectNode.library.description=JavaScript library in {0}",
        })
        @Override
        public String getShortDescription() {
            String projectDirName = FileUtil.getFileDisplayName(project.getProjectDirectory());
            if (project.isJsLibrary()) {
                return Bundle.ClientSideProjectNode_library_description(projectDirName);
            }
            return Bundle.ClientSideProjectNode_project_description(projectDirName);
        }

        @Override
        public boolean canCopy() {
            return false;
        }

        @Override
        public boolean canCut() {
            return false;
        }

        @Override
        public boolean canDestroy() {
            return false;
        }

        @Override
        public boolean canRename() {
            return false;
        }

        @Override
        public void stateChanged(ChangeEvent e) {
            RP.post(new Runnable() {
                @Override
                public void run() {
                    fireIconChange();
                    fireOpenedIconChange();
                    fireDisplayNameChange(null, null);
                }
            });
        }

        @Override
        public void propertyChange(final PropertyChangeEvent evt) {
            RP.post(new Runnable() {
                @Override
                public void run() {
                    fireIconChange();
                    fireNameChange(null, null);
                    fireDisplayNameChange(null, null);
                }
            });
        }

        private void addBuildActions(List<Action> actions) {
            ClientProjectEnhancedBrowserImplementation cfg = project.getEnhancedBrowserImpl();
            if (cfg == null) {
                return;
            }
            ActionProvider actionProvider = cfg.getActionProvider();
            if (actionProvider == null) {
                return;
            }
            Set<String> supportedActions = new HashSet<>(Arrays.asList(actionProvider.getSupportedActions()));
            boolean hasBuildTools = BuildTools.getDefault().hasBuildTools(project);
            boolean buildSupported = hasBuildTools
                    || supportedActions.contains(ActionProvider.COMMAND_BUILD);
            boolean rebuildSupported = hasBuildTools
                    || supportedActions.contains(ActionProvider.COMMAND_REBUILD);
            boolean cleanSupported = hasBuildTools
                    || supportedActions.contains(ActionProvider.COMMAND_CLEAN);
            int index = 1; // right after New... action
            if (buildSupported
                    || rebuildSupported
                    || cleanSupported) {
                actions.add(index++, null);
            }
            if (buildSupported) {
                actions.add(index++, FileUtil.getConfigObject("Actions/Project/org-netbeans-modules-project-ui-BuildProject.instance", Action.class)); // NOI18N
            }
            if (rebuildSupported) {
                actions.add(index++, FileUtil.getConfigObject("Actions/Project/org-netbeans-modules-project-ui-RebuildProject.instance", Action.class)); // NOI18N
            }
            if (cleanSupported) {
                actions.add(index++, FileUtil.getConfigObject("Actions/Project/org-netbeans-modules-project-ui-CleanProject.instance", Action.class)); // NOI18N
            }
        }

        private void addCodeCoverageAction(List<Action> actions) {
            JsTestingProvider jsTestingProvider = project.getJsTestingProvider(false);
            if (jsTestingProvider == null
                    || !jsTestingProvider.isCoverageSupported(project)) {
                return;
            }
            int secondSeparatorIndex = actions.size();
            int separatorCount = 0;
            for (int i = 0; i < actions.size(); i++) {
                if (actions.get(i) == null) {
                    separatorCount++;
                }
                if (separatorCount == 2) {
                    secondSeparatorIndex = i;
                    break;
                }
            }
            actions.add(secondSeparatorIndex, CoverageActionFactory.createCollectorAction(null, null));
        }

    }

    private static enum BasicNodes {
        Sources,
        SiteRoot,
        SourcesAndSiteRoot,
        Tests,
        TestsSelenium,
    }

    // TODO: all three nodes are registered at the same time - could be refactored and
    //       broken into individual nodes if there is a need to insert nodes in between them
    @NodeFactory.Registration(projectType="org-netbeans-modules-web-clientproject",position=500)
    public static final class BaseHTML5ProjectNodeFactory implements NodeFactory {

        public BaseHTML5ProjectNodeFactory() {
        }

        public NodeList createNodes(Project p) {
            return new ClientProjectNodeList((ClientSideProject)p);
        }

    }

    @NodeFactory.Registration(projectType="org-netbeans-modules-web-clientproject",position=900)
    public static NodeFactory createRemoteFiles() {
        return RemoteFilesNodeFactory.createRemoteFilesNodeFactory();
    }

    @NodeFactory.Registration(projectType = "org-netbeans-modules-web-clientproject",position = 1000)
    public static NodeFactory createJsTestingProvidersNodes() {
        return JsTestingProviders.getDefault().createJsTestingProvidersNodeFactory();
    }

    private static final class ClientProjectNodeList implements NodeList<Key>, ChangeListener {

        private final ChangeSupport changeSupport = new ChangeSupport(this);
        private final ClientSideProject project;
        private final FileObject nbprojectFolder;
        final Sources projectSources;
        final ChangeListener changeListener;


        private ClientProjectNodeList(ClientSideProject p) {
            this.project = p;
            nbprojectFolder = p.getProjectDirectory().getFileObject("nbproject"); // NOI18N
            assert nbprojectFolder != null : "Folder nbproject must exist for project " + project.getName();
            projectSources = ProjectUtils.getSources(p);
            changeListener = WeakListeners.change(this, projectSources);
        }

        @Override
        public void addChangeListener(ChangeListener l) {
            changeSupport.addChangeListener(l);
        }

        @Override
        public void removeChangeListener(ChangeListener l) {
            changeSupport.removeChangeListener(l);
        }

        @Override
        public void addNotify() {
            RP.post(new Runnable() {
                @Override
                public void run() {
                    // #230378 - use weak listeners otherwise project is not garbage collected
                    // #237407 - do it in a background thread
                    projectSources.addChangeListener(changeListener);
                }
            });
        }

        @Override
        public void removeNotify() {
            // #230378 - weak listeners are used so in fact, no need to call "removeListener"
        }

        void fireChange() {
            changeSupport.fireChange();
        }

        @Override
        public void stateChanged(ChangeEvent e) {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    fireChange();
                }
            });
        }

        @Override
        public Node node(Key key) {
            BasicNodes node = key.getNode();
            switch (node) {
                case Sources:
                case SiteRoot:
                case SourcesAndSiteRoot:
                case Tests:
                case TestsSelenium:
                    return createNodeForFolder(key);
                default:
                    assert false : "Unknown node type: " + node;
                    return null;
            }
        }

        // #218736
        private List<File> getIgnoredFiles(BasicNodes basicNodes) {
            List<File> ignoredFiles = new ArrayList<>();
            FileObject buildFolder = project.getProjectDirectory().getFileObject("build"); // NOI18N
            switch (basicNodes) {
                case Sources:
                case SiteRoot:
                case SourcesAndSiteRoot:
                case Tests:
                case TestsSelenium:
                    addIgnoredFile(ignoredFiles, nbprojectFolder);
                    addIgnoredFile(ignoredFiles, buildFolder);
                    break;
                default:
                    throw new IllegalStateException("Unknown BasicNodes: " + basicNodes);
            }
            return ignoredFiles;
        }

        private void addIgnoredFile(List<File> ignoredFiles, FileObject fileObject) {
            if (fileObject == null) {
                return;
            }
            File file = FileUtil.toFile(fileObject);
            if (file != null) {
                ignoredFiles.add(file);
            }
        }

        private FileObject getRootForNode(BasicNodes node) {
            FileObject sources = project.getSourcesFolder();
            FileObject siteRoot = project.getSiteRootFolder();
            switch (node) {
                case Sources:
                    if (sources == null) {
                        return null;
                    }
                    if (sources.equals(siteRoot)) {
                        return null;
                    }
                    return sources;
                case SiteRoot:
                    if (siteRoot == null) {
                        return null;
                    }
                    if (siteRoot.equals(sources)) {
                        return null;
                    }
                    return siteRoot;
                case SourcesAndSiteRoot:
                    if (sources == null
                            || siteRoot == null) {
                        return null;
                    }
                    if (sources.equals(siteRoot)) {
                        return sources;
                    }
                    return null;
                case Tests:
                    return project.getTestsFolder(false);
                case TestsSelenium:
                    return project.getTestsSeleniumFolder(false);
                default:
                    assert false : "Unknown node: " + node;
                    return null;
            }
        }

        private Node createNodeForFolder(Key key) {
            BasicNodes node = key.getNode();
            FileObject root = key.getRoot();
            assert root != null;
            assert root.isValid() : root;
            DataFolder df = DataFolder.findFolder(root);
            return new FolderFilterNode(project, node, df.getNodeDelegate().cloneNode(), getIgnoredFiles(node));
        }

        @Override
        public List<Key> keys() {
            BasicNodes[] allNodes = BasicNodes.values();
            ArrayList<Key> keys = new ArrayList<>(allNodes.length);
            for (BasicNodes node : allNodes) {
                FileObject root = getRootForNode(node);
                if (root != null && root.isValid()) {
                    keys.add(new Key(node, root));
                }
            }
            return keys;
        }

    }

    /**
     * The purpose of this Key class is to be able to create several different
     * instances of BasicNodes.Sources node in order "refresh" the node if project
     * was reconfigured.
     */
    private static class Key {

        private final BasicNodes node;
        private final FileObject root;


        public Key(BasicNodes node, FileObject root) {
            assert node != null;
            assert root != null;
            this.node = node;
            this.root = root;
        }


        public BasicNodes getNode() {
            return node;
        }

        public FileObject getRoot() {
            return root;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 89 * hash + Objects.hashCode(this.node);
            hash = 89 * hash + Objects.hashCode(this.root);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Key other = (Key) obj;
            if (this.node != other.node) {
                return false;
            }
            if (!Objects.equals(this.root, other.root)) {
                return false;
            }
            return true;
        }

    }

    private static final class FolderFilterNode extends FilterNode {

        @StaticResource
        private static final String SOURCES_FILES_BADGE = "org/netbeans/modules/web/clientproject/ui/resources/sources-badge.gif"; // NOI18N
        @StaticResource
        private static final String SITE_ROOT_FILES_BADGE = "org/netbeans/modules/web/clientproject/ui/resources/siteroot-badge.gif"; // NOI18N

        private final BasicNodes nodeType;
        private final Node delegate;


        public FolderFilterNode(ClientSideProject project, BasicNodes nodeType, Node folderNode, List<File> ignoreList) {
            super(folderNode, folderNode.isLeaf() ? Children.LEAF :
                    new FolderFilterChildren(project, folderNode, ignoreList));
            this.nodeType = nodeType;
            delegate = folderNode;
        }

        @Override
        public Action[] getActions(boolean context) {
            List<Action> actions = new ArrayList<Action>();
            actions.add(CommonProjectActions.newFileAction());
            actions.add(null);
            actions.add(SystemAction.get(FindAction.class));
            actions.add(null);
            actions.add(SystemAction.get(FileSystemAction.class));
            actions.add(null);
            actions.add(SystemAction.get(PasteAction.class));
            actions.add(null);
            actions.add(SystemAction.get(ToolsAction.class));
            actions.add(null);
            actions.add(CommonProjectActions.customizeProjectAction());
            return actions.toArray(new Action[actions.size()]);
        }

        @Override
        public boolean canRename() {
            return false;
        }

        @Override
        public Image getIcon(int type) {
            return computeIcon(nodeType, false, type);
        }

        @Override
        public Image getOpenedIcon(int type) {
            return computeIcon(nodeType, true, type);
        }

        private Image computeIcon(BasicNodes node, boolean opened, int type) {
            Image image;
            String badge = null;
            switch (nodeType) {
                case Sources:
                case Tests:
                case TestsSelenium:
                    badge = SOURCES_FILES_BADGE;
                    break;
                case SiteRoot:
                case SourcesAndSiteRoot:
                    badge = SITE_ROOT_FILES_BADGE;
                    break;
                default:
                    assert false : "Unknown nodeType: " + nodeType;
            }

            image = opened ? delegate.getOpenedIcon(type) : delegate.getIcon(type);
            if (badge != null) {
                image = ImageUtilities.mergeImages(image, ImageUtilities.loadImage(badge, false), 7, 7);
            }

            return image;
        }

        @Override
        public String getDisplayName() {
            switch (nodeType) {
                case Sources:
                    return java.util.ResourceBundle.getBundle("org/netbeans/modules/web/clientproject/ui/Bundle").getString("SOURCES");
                case SiteRoot:
                    return java.util.ResourceBundle.getBundle("org/netbeans/modules/web/clientproject/ui/Bundle").getString("SITE_ROOT");
                case SourcesAndSiteRoot:
                    return java.util.ResourceBundle.getBundle("org/netbeans/modules/web/clientproject/ui/Bundle").getString("SOURCES_SITE_ROOT");
                case Tests:
                    return java.util.ResourceBundle.getBundle("org/netbeans/modules/web/clientproject/ui/Bundle").getString("UNIT_TESTS");
                case TestsSelenium:
                    return java.util.ResourceBundle.getBundle("org/netbeans/modules/web/clientproject/ui/Bundle").getString("SELENIUM_TESTS");
                default:
                    throw new AssertionError(nodeType.name());
            }
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 29 * hash + (this.nodeType != null ? this.nodeType.hashCode() : 0);
            hash = 29 * hash + Objects.hashCode(this.delegate);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final FolderFilterNode other = (FolderFilterNode) obj;
            if (this.nodeType != other.nodeType) {
                return false;
            }
            if (!Objects.equals(this.delegate, other.delegate)) {
                return false;
            }
            return true;
        }


    }

    private static class FolderFilterChildren extends FilterNode.Children implements ChangeListener {

        private final Set<File> ignoreList = new WeakSet<File>();
        private final ClientSideProject project;


        FolderFilterChildren(ClientSideProject project, Node n) {
            this(project, n, Collections.emptyList());
        }

        FolderFilterChildren(ClientSideProject project, Node n, List<File> ignoreList) {
            super(n);
            this.project = project;
            this.ignoreList.addAll(ignoreList);
        }

        @Override
        protected void addNotify() {
            super.addNotify();
            VisibilityQuery.getDefault().addChangeListener(WeakListeners.change(this, VisibilityQuery.getDefault()));
        }

        @Override
        protected Node[] createNodes(Node key) {
            FileObject fo = key.getLookup().lookup(FileObject.class);
            if (fo == null) {
                return super.createNodes(key);
            }
            File file = FileUtil.toFile(fo);
            if (file == null) {
                LOGGER.log(Level.WARNING, "No file found for fileobject {0}", fo);
                return super.createNodes(key);
            }
            if (!VisibilityQuery.getDefault().isVisible(fo)) {
                return new Node[0];
            }
            if (ignoreList.contains(file)) {
                return new Node[0];
            }
            return super.createNodes(key);
        }

        @Override
        protected Node copyNode(Node node) {
            FileObject fo = node.getLookup().lookup(FileObject.class);
            if (fo == null) {
                return super.copyNode(node);
            }
            if (fo.isFolder()) {
                return new FolderNode(project, node);
            }
            return super.copyNode(node);
        }

        @Override
        public void stateChanged(ChangeEvent event) {
            Node[] children = original.getChildren().getNodes();
            if (event instanceof VisibilityQueryChangeEvent) {
                Set<FileObject> fileObjects = new HashSet(Arrays.asList(((VisibilityQueryChangeEvent) event).getFileObjects()));
                for (Node child : children) {
                    if (fileObjects.contains(child.getLookup().lookup(FileObject.class))) {
                        refreshNodes(new Node[] {child});
                    }
                }
                return;
            }
            refreshNodes(children);
        }

        private void refreshNodes(Node[] nodes) {
            for (Node node : nodes) {
                refreshKey(node);
            }
        }

    }

    private static final class FolderNode extends FilterNode {

        private final ClientSideProject project;


        FolderNode(ClientSideProject project, Node original) {
            super(original, new FolderFilterChildren(project, original), new ProxyLookup(original.getLookup()));
            assert original != null;
            assert project != null;
            this.project = project;
        }

        @Override
        public Image getIcon(int type) {
            return getIcon(type, false);
        }

        @Override
        public Image getOpenedIcon(int type) {
            return getIcon(type, true);
        }

        private Image getIcon(int type, boolean opened) {
            FileObject folder = getOriginal().getLookup().lookup(FileObject.class);
            assert folder.isFolder() : folder;
            Project owner = FileOwnerQuery.getOwner(folder);
            Image originalIcon;
            if (owner != null
                    && !owner.equals(project)
                    && owner.getProjectDirectory().equals(folder)) {
                originalIcon = ImageUtilities.icon2Image(ProjectUtils.getInformation(owner).getIcon());
                try {
                    final Set<FileObject> clds = new HashSet<>();
                    Collections.addAll(clds, owner.getProjectDirectory().getChildren());
                    originalIcon = FileUIUtils.getImageDecorator(owner.getProjectDirectory().getFileSystem())
                            .annotateIcon(
                                    originalIcon,
                                    type,
                                    clds);
                } catch (FileStateInvalidException e) {
                    LOGGER.log(Level.INFO, null, e);
                }
            } else {
                originalIcon = opened ? super.getOpenedIcon(type) : super.getIcon(type);
            }
            return originalIcon;
        }

    }

}
