/*
 * 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.project.ui;

import java.awt.Image;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Action;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.api.queries.VisibilityQuery;
import org.netbeans.modules.java.api.common.ant.UpdateHelper;
import org.netbeans.modules.java.api.common.project.ui.ProjectUISupport;
import org.netbeans.modules.web.api.webmodule.WebProjectConstants;
import org.netbeans.modules.web.project.WebProject;
import org.netbeans.modules.web.project.ui.customizer.WebProjectProperties;
import org.netbeans.spi.project.support.ant.PropertyEvaluator;
import org.netbeans.spi.project.ui.support.CommonProjectActions;
import org.netbeans.spi.project.ui.support.NodeFactory;
import org.netbeans.spi.project.ui.support.NodeList;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.actions.FileSystemAction;
import org.openide.actions.FindAction;
import org.openide.actions.PasteAction;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.ChangeableDataFilter;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.util.ChangeSupport;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
import org.openide.util.actions.SystemAction;
import org.openide.util.datatransfer.PasteType;

/**
 *
 * @author mkleint
 */
@NodeFactory.Registration(projectType="org-netbeans-modules-web-project",position=100)
public final class DocBaseNodeFactory implements NodeFactory {
    
    /** Creates a new instance of LibrariesNodeFactory */
    public DocBaseNodeFactory() {
    }

    private static RequestProcessor RP = new RequestProcessor();
    
    public NodeList createNodes(Project p) {
        WebProject project = p.getLookup().lookup(WebProject.class);
        assert project != null;
        return new DocBaseNodeList(project);
    }

    private static class DocBaseNodeList implements NodeList<String>, PropertyChangeListener {
        private static final String DOC_BASE = "docBase"; //NOI18N
        private static final String WEB_INF = "webInf"; //NOI18N

        private final WebProject project;
        private final ChangeSupport changeSupport = new ChangeSupport(this);

        private final PropertyEvaluator evaluator;
        private final UpdateHelper helper;
        
        private SourceGroup webDocRoot;
        
        DocBaseNodeList(WebProject proj) {
            project = proj;
            evaluator = project.evaluator();
            helper = project.getUpdateHelper();
            Sources s = project.getLookup().lookup(Sources.class);
            assert s != null;
//            assert s.getSourceGroups(WebProjectConstants.TYPE_DOC_ROOT).length > 0;

            if(s.getSourceGroups(WebProjectConstants.TYPE_DOC_ROOT).length > 0)
            {
                webDocRoot = s.getSourceGroups(WebProjectConstants.TYPE_DOC_ROOT)[0];
            }
            else
            {
                String name = ProjectUtils.getInformation( proj ).getDisplayName();

                NotifyDescriptor nd = new NotifyDescriptor.Message(NbBundle.getMessage(DocBaseNodeList.class, "LBL_No_Source_Groups_Found", name), NotifyDescriptor.INFORMATION_MESSAGE);
                DialogDisplayer.getDefault().notify(nd);
            }
        }
        
        public List<String> keys() {
            FolderHolder nodeFolders = getNodeFolders();
            List<String> result = new ArrayList<String>();
            result.add(DOC_BASE + getFolderPath(nodeFolders.getWebDocBaseDir()));
            if (!nodeFolders.hasCorrectStructure()) {
                result.add(WEB_INF + getFolderPath(nodeFolders.getWebInfDir()));
            }
            return result;
        }

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

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

        public Node node(String key) {
            FolderHolder nodeFolders = getNodeFolders();
            if (key.startsWith(DOC_BASE)) {
                FileObject webDocBaseDir = nodeFolders.getWebDocBaseDir();
                DataFolder webFolder = getFolder(webDocBaseDir);
                if (webFolder != null) {
                    return new DocBaseNode(webFolder, project, new VisibilityQueryDataFilter(webDocRoot));
                }
                return null;
            } else if (key.startsWith(WEB_INF)) {
                if (nodeFolders.hasCorrectStructure()) {
                    return null;
                }
                FileObject webInfDir = nodeFolders.getWebInfDir();
                DataFolder webInfFolder = getFolder(webInfDir);
                if (webInfFolder != null) {
                    return new WebInfNode(webInfFolder, project, new VisibilityQueryDataFilter(null));
                }
                return null;
            }
            assert false: "No node for key: " + key; // NOI18N
            return null;
        }

        public void addNotify() {
            evaluator.addPropertyChangeListener(this);
        }

        public void removeNotify() {
            evaluator.removePropertyChangeListener(this);
        }

        public void propertyChange(PropertyChangeEvent evt) {
            // The caller holds ProjectManager.mutex() read lock
            
            // #197171 - changeSupport used to fire change in AWT thread which
            // does not work in following scenario: when project properties has
            // been changed and are being saved the write lock on ProjectManager
            // can be hold for long time. And that prevents getNodeFolders()
            // method in this class from being executed (in response to change). Therefore I posted
            // below fireChange to background thread rather then AWT thread:
            RP.post(new Runnable() {
                public void run() {
                    changeSupport.fireChange();
                }
            });
        }
        
        private DataFolder getFolder(FileObject folder) {
            if (folder != null) {
                return DataFolder.findFolder(folder);
            }
            return null;
        }
        
        // # 114402
        private String getFolderPath(FileObject folder) {
            if (folder == null) {
                return "";
            }
            return folder.getPath();
        }
        
        private FileObject getFileObject(String propName) {
            String foName = evaluator.getProperty(propName);
            if (foName == null) {
                return null;
            }
            FileObject fo = helper.getAntProjectHelper().resolveFileObject(foName);
            // when the project is deleted externally, the sources change could
            // trigger a call to thid method before the project directory is 
            // notified about the deletion - invalid FileObject-s could be returned
            return fo != null && fo.isValid() ? fo : null;
        }
        
        private FolderHolder getNodeFolders() {
            return ProjectManager.mutex().readAccess(new Mutex.Action<FolderHolder>() {
                public FolderHolder run() {
                    FileObject webDocBaseDir = getFileObject(WebProjectProperties.WEB_DOCBASE_DIR);
                    FileObject webInf = getFileObject(WebProjectProperties.WEBINF_DIR);
                    
                    return new FolderHolder(webDocBaseDir, webInf);
                }
            });
        }
        
        private static final class FolderHolder {
            private final FileObject webDocBaseDir;
            private final FileObject webInfDir;

            public FolderHolder(FileObject webDocBaseDir, FileObject webInfDir) {
                this.webDocBaseDir = webDocBaseDir;
                this.webInfDir = webInfDir;
            }

            public FileObject getWebDocBaseDir() {
                return webDocBaseDir;
            }

            public FileObject getWebInfDir() {
                return webInfDir;
            }
            
            /**
             * Return <code>true</code> if <tt>WEB-INF<tt> folder
             * is located inside <tt>web</tt> folder.
             * Return <code>false</code> if any of these folders
             * is <code>null</code>.
             */
            public boolean hasCorrectStructure() {
                if (webDocBaseDir == null
                        || webInfDir == null) {
                    return false;
                }
                return FileUtil.isParentOf(webDocBaseDir, webInfDir);
            }
        }
    }
    
    private static final class DocBaseNode extends BaseNode {
        
        DocBaseNode (DataFolder folder, WebProject project, VisibilityQueryDataFilter filter) {
            super(folder, project, filter);
        }
        
        @Override
        public String getDisplayName () {
            return NbBundle.getMessage(DocBaseNodeFactory.class, "LBL_Node_DocBase"); //NOI18N
        }
    }

    private static final class WebInfNode extends BaseNode {
        
        WebInfNode (DataFolder folder, WebProject project, VisibilityQueryDataFilter filter) {
            super (folder, project, filter);
        }
        
        @Override
        public String getDisplayName() {
            return NbBundle.getMessage(DocBaseNodeFactory.class, "LBL_Node_WebInf"); //NOI18N
        }
    }

    private abstract static class BaseNode extends FilterNode {
        private static Image WEB_PAGES_BADGE = ImageUtilities.loadImage( "org/netbeans/modules/web/project/ui/resources/webPagesBadge.gif" ); //NOI18N
        /**
         * The MIME type of Java files.
         */
        private static final String JAVA_MIME_TYPE = "text/x-java"; //NO18N
        private Action actions[];
        protected final WebProject project;
        
        BaseNode(final DataFolder folder, WebProject project, VisibilityQueryDataFilter filter) {
            super(folder.getNodeDelegate(), folder.createNodeChildren(filter));
            this.project = project;
        }

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

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

        private Node getDataFolderNodeDelegate() {
            return getLookup().lookup(DataFolder.class).getNodeDelegate();
        }

        private Image computeIcon(boolean opened, int type) {
            Image image;

            image = opened ? getDataFolderNodeDelegate().getOpenedIcon(type) : getDataFolderNodeDelegate().getIcon(type);
            image = ImageUtilities.mergeImages(image, WEB_PAGES_BADGE, 7, 7);

            return image;        
        }
        
        @Override
        public boolean canRename() {
            return false;
        }
        
        @Override
        public Action[] getActions(boolean context) {
            if (actions == null) {
                actions = new Action[9];
                actions[0] = CommonProjectActions.newFileAction();
                actions[1] = null;
                actions[2] = SystemAction.get(FindAction.class);
                actions[3] = null;
                actions[4] = SystemAction.get(PasteAction.class);
                actions[5] = null;
                actions[6] = SystemAction.get(FileSystemAction.class);
                actions[7] = null;
                actions[8] = ProjectUISupport.createPreselectPropertiesAction(project, "Sources", null); //NOI18N
            }
            return actions;
        }

        @Override
        public PasteType getDropType(Transferable t, int action, int index) {
            try {
                if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)){
                    Object data = t.getTransferData(DataFlavor.javaFileListFlavor);
                    if (data != null) {
                        List list = (List) data;
                        for (Object each : list) {
                            File f = FileUtil.normalizeFile((File) each);
                            FileObject file = FileUtil.toFileObject(f);
                            if (file != null && JAVA_MIME_TYPE.equals(file.getMIMEType())) { //NO18N
                                // don't allow java files, see #119968
                                return null;
                            }
                        }
                    }
                }
            } catch (UnsupportedFlavorException ex) {
                Exceptions.printStackTrace(ex);
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
            return super.getDropType(t, action, index);
        }
    }

    static final class VisibilityQueryDataFilter implements ChangeListener, ChangeableDataFilter, PropertyChangeListener {
        private static final long serialVersionUID = 1L;
        
        private final ChangeSupport changeSupport = new ChangeSupport(this);
        private final SourceGroup sourceGroup;
        
        public VisibilityQueryDataFilter(SourceGroup sourceGroup) {
            this.sourceGroup = sourceGroup;
            if (this.sourceGroup != null) {
                this.sourceGroup.addPropertyChangeListener(this);
            }
            VisibilityQuery defaultQuery = VisibilityQuery.getDefault();
            defaultQuery.addChangeListener(WeakListeners.change(this, defaultQuery));
        }
                
        public boolean acceptDataObject(DataObject obj) {                
            FileObject fo = obj.getPrimaryFile();                
            return (sourceGroup == null || sourceGroup.contains(fo)) && VisibilityQuery.getDefault().isVisible(fo);
        }
        
        public void stateChanged( ChangeEvent e) {            
            changeSupport.fireChange();
        }        
    
        public void addChangeListener( ChangeListener listener ) {
            changeSupport.addChangeListener(listener);
        }        
                        
        public void removeChangeListener( ChangeListener listener ) {
            changeSupport.removeChangeListener(listener);
        }

        public void propertyChange(PropertyChangeEvent arg0) {
            changeSupport.fireChange();
        }
        
    }

}
