/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */

package org.netbeans.modules.project.ui;

import java.awt.Image;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectInformation;
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 static org.netbeans.modules.project.ui.Bundle.*;
import org.netbeans.spi.project.ui.LogicalViewProvider;
import org.netbeans.spi.project.ui.support.CommonProjectActions;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUIUtils;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.ChangeableDataFilter;
import org.openide.loaders.DataFilter;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.nodes.NodeEvent;
import org.openide.nodes.NodeListener;
import org.openide.nodes.NodeMemberEvent;
import org.openide.nodes.NodeNotFoundException;
import org.openide.nodes.NodeOp;
import org.openide.nodes.NodeReorderEvent;
import org.openide.util.ChangeSupport;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle.Messages;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ProxyLookup;

/**
 * Support for creating logical views.
 * @author Jesse Glick, Petr Hrebejk
 */
public class PhysicalView {

    private PhysicalView() {}

    private static final Logger LOG = Logger.getLogger(PhysicalView.class.getName());
    private static final RequestProcessor RP = new RequestProcessor(PhysicalView.class);

    private static final class GroupNodeInfo {
        public final boolean isProjectDir;
        public GroupNodeInfo(boolean isProjectDir) {
            this.isProjectDir = isProjectDir;
        }
    }
        
    public static boolean isProjectDirNode( Node n ) {
        GroupNodeInfo i = n.getLookup().lookup(GroupNodeInfo.class);
        return i != null && i.isProjectDir;
    }
    
    public static Node[] createNodesForProject( Project p ) {
        Sources s = ProjectUtils.getSources(p);
        SourceGroup[] groups = s.getSourceGroups(Sources.TYPE_GENERIC);
                
        FileObject projectDirectory = p.getProjectDirectory();
        ArrayList<Node> nodesList = new ArrayList<Node>( groups.length );
        
        for (SourceGroup group : groups) {
            if ("sharedlibraries".equals(group.getName())) { //NOI18N
                //HACK - ignore shared libs group in UI, it's only useful for version control commits.
                continue;
            }
            FileObject rootFolder = group.getRootFolder();
            if (!rootFolder.isValid() || !rootFolder.isFolder()) {
                continue;
            }
            nodesList.add(new ProjectIconNode(new GroupNode(p, group, projectDirectory.equals(rootFolder) || FileUtil.isParentOf(rootFolder, projectDirectory), DataFolder.findFolder(rootFolder)), true));
        }
        
        Node nodes[] = new Node[ nodesList.size() ];
        nodesList.toArray( nodes );
        return nodes;
    }
   
    static final class VisibilityQueryDataFilter implements ChangeListener, ChangeableDataFilter, DataFilter.FileBased {
        
        private final ChangeSupport changeSupport = new ChangeSupport( this );
        
        public VisibilityQueryDataFilter() {
            VisibilityQuery.getDefault().addChangeListener( this );
        }
                
        public @Override boolean acceptDataObject(DataObject obj) {
            return acceptFileObject(obj.getPrimaryFile());
        }
        
        public @Override void stateChanged(ChangeEvent e) {
            final Runnable r = new Runnable () {
                public @Override void run() {
                    changeSupport.fireChange();
                }
            };            
            SwingUtilities.invokeLater(r);            
        }        
    
        public @Override void addChangeListener(ChangeListener listener) {
            changeSupport.addChangeListener( listener );
        }        
                        
        public @Override void removeChangeListener(ChangeListener listener) {
            changeSupport.removeChangeListener( listener );
        }

        public @Override boolean acceptFileObject(FileObject fo) {
            return VisibilityQuery.getDefault().isVisible(fo);
        }
        
    }
    
    static final class GroupNode extends FilterNode implements PropertyChangeListener {
        
        private static final DataFilter VISIBILITY_QUERY_FILTER = new VisibilityQueryDataFilter();
        
        private ProjectInformation pi;
        private SourceGroup group;
        private boolean isProjectDir;
        private Boolean initialized;
        private final Node projectDelegateNode;

        public GroupNode(Project project, SourceGroup group, boolean isProjectDir, DataFolder dataFolder ) {
            super( dataFolder.getNodeDelegate(),
                   dataFolder.createNodeChildren( VISIBILITY_QUERY_FILTER ),                       
                   createLookup(project, group, dataFolder, isProjectDir));

            this.pi = ProjectUtils.getInformation( project );
            this.group = group;
            this.isProjectDir = isProjectDir;
            
            if(isProjectDir) {
                LogicalViewProvider lvp = project.getLookup().lookup(LogicalViewProvider.class);
                // used to retrieve e.g. actions in case of a folder representing a project,
                // so that a projects context menu is the same is in a logical view
                this.projectDelegateNode = lvp != null ? lvp.createLogicalView() : null;
            } else {
                this.projectDelegateNode = null;
            }
            
            pi.addPropertyChangeListener(WeakListeners.propertyChange(this, pi));
            group.addPropertyChangeListener( WeakListeners.propertyChange( this, group ) );
        }

        private boolean initialized() {
            synchronized (RP) {
                if (initialized != null) {
                    return initialized;
                } else {
                    initialized = false;
                    RP.post(new Runnable() {
                        @Override public void run() {
                            pi.getDisplayName();
                            synchronized (RP) {
                                initialized = true;
                            }
                            fireNameChange(null, null);
                            fireDisplayNameChange(null, null);
                        }
                    });
                    return false;
                }
            }
        }

        // XXX May need to change icons as well
        
        public @Override String getName() {
            if (isProjectDir && initialized()) {
                return pi.getName();
            }
            else {
                String n = group.getName();
                if (n == null) {
                    n = "???"; // NOI18N
                    LOG.log(Level.WARNING, "SourceGroup impl of type {0} specified a null getName(); this is illegal", group.getClass().getName());
                }
                return n;
            }
        }

        @Messages({"# {0} - display name of the group", "# {1} - display name of the project", "# {2} - original name of the folder", "FMT_PhysicalView_GroupName={1} - {0}"})
        public @Override String getDisplayName() {
            if ( isProjectDir ) {
                return initialized() ? pi.getDisplayName() : group.getDisplayName();
            }
            else {
                return FMT_PhysicalView_GroupName(group.getDisplayName(), pi.getDisplayName(), getOriginal().getDisplayName());
            }
        }

        @Messages({"HINT_project=Project in {0}", "HINT_group=Source folder in {0}"})
        public @Override String getShortDescription() {
            FileObject gdir = group.getRootFolder();
            String dir = FileUtil.getFileDisplayName(gdir);
            return isProjectDir ? HINT_project(dir) : HINT_group(dir);
        }

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

        @Override public Node.PropertySet[] getPropertySets() {
            return new Node.PropertySet[0];
        }

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

        public @Override boolean canCopy() {
            // At least for now.
            return false;
        }

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

        public @Override Action[] getActions(boolean context) {

            if ( context ) {
                return super.getActions( true );
            }
            else { 
                Action[] folderActions = super.getActions( false );
                Action[] projectActions;
                
                if ( isProjectDir ) {
                    if( projectDelegateNode != null ) {
                        projectActions = projectDelegateNode.getActions( false );
                    }
                    else {
                        // If this is project dir then the properties action 
                        // has to be replaced to invoke project customizer
                        projectActions = new Action[ folderActions.length ]; 
                        for ( int i = 0; i < folderActions.length; i++ ) {
                            if ( folderActions[i] instanceof org.openide.actions.PropertiesAction ) {
                                projectActions[i] = CommonProjectActions.customizeProjectAction();
                            }
                            else {
                                projectActions[i] = folderActions[i];
                            }
                        }
                    }
                }
                else {
                    projectActions = folderActions;
                }
                
                return projectActions;
            }                                            
        }

        // Private methods -------------------------------------------------    

        public @Override void propertyChange(final PropertyChangeEvent evt) {
            final Runnable r = new Runnable () {
                public @Override void run() {
                    String prop = evt.getPropertyName();
                    boolean ok = false;
                    if (prop == null || ProjectInformation.PROP_DISPLAY_NAME.equals(prop)) {
                        fireDisplayNameChange(null, null);
                        ok = true;
                    }
                    if (prop == null || ProjectInformation.PROP_NAME.equals(prop)) {
                        fireNameChange(null, null);
                        ok = true;
                    }

                    if (prop == null || ProjectInformation.PROP_ICON.equals(prop)) {
                        // OK, ignore
                        ok = true;
                    }

                    if (prop == null || "name".equals(prop) ) { // NOI18N
                        fireNameChange(null, null);
                        ok = true;
                    }

                    if (prop == null || "displayName".equals(prop) ) { // NOI18N
                        fireDisplayNameChange(null, null);
                        ok = true;
                    }

                    if (prop == null || "icon".equals(prop) ) { // NOI18N
                        // OK, ignore
                        ok = true;
                    }

                    if (prop == null || "rootFolder".equals(prop) ) { // NOI18N
                        // XXX Do something to children and lookup 
                        fireNameChange(null, null);
                        fireDisplayNameChange(null, null);
                        fireShortDescriptionChange(null, null);
                        ok = true;
                    }

                    if (prop == null || SourceGroup.PROP_CONTAINERSHIP.equals(prop)) {
                        // OK, ignore
                        ok = true;
                    }

                    if (!ok) {
                        assert false : "Attempt to fire an unsupported property change event from " + pi.getClass().getName() + ": " + prop;
                    }
                }
            };            
            SwingUtilities.invokeLater(r);            
        }
        
        private static Lookup createLookup(Project p, SourceGroup group, DataFolder dataFolder, boolean isProjectDir) {
            return new ProxyLookup(
                dataFolder.getNodeDelegate().getLookup(),
                Lookups.fixed(p, new PathFinder(group), new GroupNodeInfo(isProjectDir)),
                p.getLookup());
        }

    }

    static final class ProjectIconNode extends FilterNode implements NodeListener { // #194068
        private final boolean root;
        public ProjectIconNode(Node orig, boolean root) {
            super(orig, orig.isLeaf() ? Children.LEAF : new ProjectBadgingChildren(orig));
            this.root = root;
            setValue("VCS_PHYSICAL", Boolean.TRUE); //#159543 
            addNodeListener(this);
        }

        @Override
        protected NodeListener createNodeListener() {
            return new NodeAdapter(this) {
                @Override
                protected void propertyChange(FilterNode fn, PropertyChangeEvent ev) {
                    super.propertyChange(fn, ev);
                    if (Node.PROP_LEAF.equals(ev.getPropertyName())) {
                        Node orig = getOriginal();
                        setChildren(orig.isLeaf() ? Children.LEAF : new ProjectBadgingChildren(orig));
                    }
                }
            };
        }
        
        public @Override Image getIcon(int type) {
            return swap(super.getIcon(type), type);
        }
        public @Override Image getOpenedIcon(int type) {
            return swap(super.getOpenedIcon(type), type);
        }
        private Image swap(Image base, int type) {
            if (!root) { // do not use icon on root node in Files tab
                FileObject folder = getOriginal().getLookup().lookup(FileObject.class);
                if (folder != null && folder.isFolder()) {
                    ProjectManager.Result r = ProjectManager.getDefault().isProject2(folder);
                    if (r != null) {
                        Icon icon = r.getIcon();
                        
                        if (icon != null) {
                            Image img = ImageUtilities.icon2Image(icon);
                            try {
                                //#217008
                                DataFolder df = getOriginal().getLookup().lookup(DataFolder.class);
                                img = FileUIUtils.getImageDecorator(folder.getFileSystem()).annotateIcon(img, type, df.files());
                            } catch (FileStateInvalidException e) {
                                // no fs, do nothing
                            }
                            return img;
                        }
                    }
                }
            }
            return base;
        }
        public @Override String getShortDescription() {
            FileObject folder = getOriginal().getLookup().lookup(FileObject.class);
            if (folder != null && folder.isFolder()) {
                try {
                    Project p = ProjectManager.getDefault().findProject(folder);
                    if (p != null) {
                        return ProjectUtils.getInformation(p).getDisplayName();
                    }
                } catch (IOException x) {
                    LOG.log(Level.FINE, null, x);
                }
            }
            return super.getShortDescription();
        }

        @Override
        public void childrenAdded(NodeMemberEvent ev) {
        }

        @Override
        public void childrenRemoved(NodeMemberEvent ev) {
        }

        @Override
        public void childrenReordered(NodeReorderEvent ev) {
        }

        @Override
        public void nodeDestroyed(NodeEvent ev) {
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
        }
    }
    private static final class ProjectBadgingChildren extends FilterNode.Children {
        public ProjectBadgingChildren(Node orig) {
            super(orig);
        }
        protected @Override Node copyNode(Node orig) {
            return new ProjectIconNode(orig, false);
        }
    }
    
    public static class PathFinder {
        
        private SourceGroup group;
        
        public PathFinder( SourceGroup group ) {
            this.group = group;
        }
        
        public Node findPath( Node root, Object object ) {
                 
            if ( !( object instanceof FileObject ) ) {
                return null;
            }
            
            FileObject fo = (FileObject)object;        
            FileObject groupRoot = group.getRootFolder();
            if ( FileUtil.isParentOf( groupRoot, fo ) /* && group.contains( fo ) */ ) {
                // The group contains the object

                String relPath = FileUtil.getRelativePath( groupRoot, fo );
                
                ArrayList<String> path = new ArrayList<String>();
                StringTokenizer strtok = new StringTokenizer( relPath, "/" );
                while( strtok.hasMoreTokens() ) {
                   path.add( strtok.nextToken() );
                }
                
                if (path.size() > 0) {
                    path.remove(path.size() - 1);
                } else {
                    return null;
                }
                try {
                    //#75205
                    Node parent = NodeOp.findPath( root, Collections.enumeration( path ) );
                    if (parent != null) {
                        //not nice but there isn't a findNodes(name) method.
                        Node[] nds = parent.getChildren().getNodes(true);
                        for (int i = 0; i < nds.length; i++) {
                            FileObject dobj = nds[i].getLookup().lookup(FileObject.class);
                            if (dobj != null && fo.equals(dobj)) {
                                return nds[i];
                            }
                        }
                        String name = fo.getName();
                        try {
                            DataObject dobj = DataObject.find( fo );
                            name = dobj.getNodeDelegate().getName();
                        } catch (DataObjectNotFoundException ex) {
                        }
                        return parent.getChildren().findChild(name);
                    }
                }
                catch ( NodeNotFoundException e ) {
                    return null;
                }
            }   
            else if ( groupRoot.equals( fo ) ) {
                return root;
            }

            return null;
        }
                    
    }
    
}
