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

import java.awt.EventQueue;
import java.beans.PropertyVetoException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
import org.netbeans.api.queries.VisibilityQuery;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionRegistration;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.loaders.DataShadow;
import org.openide.nodes.Node;
import org.openide.util.ContextAwareAction;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.actions.NodeAction;
import org.openide.windows.WindowManager;

/** List of all actions available for Favorites module.
* @author   Jaroslav Tulach
*/
public final class Actions extends Object {
    
    /** Used to keep current dir from JFileChooser for Add to Favorites action
     * on root node. */
    private static File currentDir = null;

    private static final Logger LOG = Logger.getLogger(Actions.class.getName());

    private Actions () {
        // noinstances
    }
    
    @ActionID(id = "org.netbeans.modules.favorites.Add", category = "Window")
    @ActionRegistration(displayName = "#ACT_Add", lazy=false)
    @ActionReference(position = 300, path = "UI/ToolActions/Files")
    public static ContextAwareAction add() { return Add.getDefault(); }

    public static Action addOnFavoritesNode () { return AddOnFavoritesNode.getDefault(); }

    @ActionID(id = "org.netbeans.modules.favorites.Remove", category = "Window")
    @ActionRegistration(displayName = "#ACT_Remove")
    public static Action remove () { return Remove.getDefault(); }

    @ActionID(id = "org.netbeans.modules.favorites.Select", category = "Window/SelectDocumentNode")
    @ActionRegistration(displayName = "#ACT_Select_Main_Menu", lazy=false)
    @ActionReference(position = 2800, name = "org-netbeans-modules-favorites-SelectInFavorites", path = "Menu/GoTo")
    public static ContextAwareAction select () { return Select.getDefault(); }
    
    /** An action which selects activated nodes in the Explorer's tab.
    * @author   Dusan Balek
    */
    private static class Select extends NodeAction {
        private static final Select SELECT = new Select ();
        
        public static ContextAwareAction getDefault () {
            return SELECT;
        }
        
        private Select () {
            super();
            putValue("noIconInMenu", Boolean.TRUE); //NOI18N
        }
        
        @Override
        protected void performAction(final Node[] activatedNodes) {
            final Tab proj = Tab.findDefault();
            Tab.RP.post(new Runnable() {
                @Override
                public void run() {
                    proj.doSelectNode(activatedNodes[0].getCookie(DataObject.class));
                }
            });
        }

        @Override
        protected boolean enable(Node[] activatedNodes) {
            if (activatedNodes.length != 1) {
                return false;
            }
            DataObject dobj = activatedNodes[0].getCookie(DataObject.class);
            if (dobj == null) {
                return false;
            }
            return true;
            /*return Tab.findDefault().containsNode(dobj);*/
          }

        @Override
        public String getName() {
            return NbBundle.getMessage(Select.class, "ACT_Select_Main_Menu"); // NOI18N
        }

        @Override
        protected String iconResource() {
            return "org/netbeans/modules/favorites/resources/actionView.png"; // NOI18N
        }

        @Override
        public HelpCtx getHelpCtx() {
            return null;
        }

        @Override
        protected boolean asynchronous() {
            return false;
        }

    } // end of Select

    /** Removes root link from favorites
    * @author   Jaroslav Tulach
    */
    private static class Remove extends NodeAction {
        static final long serialVersionUID =-6471281373153172312L;
        /** generated Serialized Version UID */
        //  static final long serialVersionUID = -5280204757097896304L;
        
        private static final Remove REMOVE = new Remove ();
        
        public static Action getDefault () {
            return REMOVE;
        }
        
        /** Enabled only if the current project is ProjectDataObject.
        */
        @Override
        public boolean enable (Node[] arr) {
            if ((arr == null) || (arr.length == 0)) return false;

            for (int i = 0; i < arr.length; i++) {
                DataObject shad = arr[i].getCookie(DataObject.class);
                //Disable when node is not shadow in Favorites folder.
                if (shad == null || shad.getFolder() != FavoritesNode.getFolder()) {
                    return false;
                }
            }
            return true;
        }

        /** Human presentable name of the action. This should be
        * presented as an item in a menu.
        * @return the name of the action
        */
        @Override
        public String getName() {
            return NbBundle.getMessage (
                    Actions.class, "ACT_Remove"); // NOI18N
        }

        /** Help context where to find more about the action.
        * @return the help context for this action
        */
        @Override
        public HelpCtx getHelpCtx() {
            return new HelpCtx(Remove.class);
        }

        /**
        * Removes the links.
        *
        * @param arr gives array of actually activated nodes.
        */
        @Override
        protected void performAction (Node[] arr) {
            for (int i = 0; i < arr.length; i++) {
                DataObject shad = arr[i].getCookie(DataObject.class);

                if (shad != null && shad.getFolder() == FavoritesNode.getFolder()) {
                    try {
                        shad.delete();
                    } catch (IOException ex) {
                        LOG.log(Level.WARNING, null, ex);
                    }
                }
            }
        }

        @Override
        protected boolean asynchronous() {
            return false;
        }

    } // end of Remove
    

    /** Adds something to favorites. Made public so it can be referenced
    * directly from manifest.
    *
    * @author   Jaroslav Tulach
    */
    public static class Add extends NodeAction {
        static final long serialVersionUID =-6471281373153172312L;
        /** generated Serialized Version UID */
        //  static final long serialVersionUID = -5280204757097896304L;
        private static final Add ADD = new Add ();
        
        public static ContextAwareAction getDefault() {
            return ADD;
        }
        
        private Add () {
            putValue("noIconInMenu", Boolean.TRUE); // NOI18N
        }
        
        /** Enabled only if the current project is ProjectDataObject.
        */
        @Override
        public boolean enable (Node[] arr) {
            if ((arr == null) || (arr.length == 0)) return false;
            if (arr.length == 1 && arr[0] instanceof FavoritesNode) return true;
                
            

            for (int i = 0; i < arr.length; i++) {
                DataObject dataObject = arr[i].getCookie(DataObject.class);
                if (! isAllowed(dataObject))
                    return false;
            }
            return true;
        }
        
        /** Human presentable name of the action. This should be
        * presented as an item in a menu.
        * @return the name of the action
        */
        @Override
        public String getName() {
            return NbBundle.getMessage (
                    Actions.class, "ACT_Add"); // NOI18N
        }

        /** Help context where to find more about the action.
        * @return the help context for this action
        */
        @Override
        public HelpCtx getHelpCtx() {
            return new HelpCtx(Add.class);
        }

        /**
        * Standard perform action extended by actually activated nodes.
        *
        * @param activatedNodes gives array of actually activated nodes.
        */
        @Override
        protected void performAction (final Node[] activatedNodes) {
            try {
                final List<DataObject> toShadows;
                if (activatedNodes.length == 1 && activatedNodes[0] instanceof FavoritesNode) {
                    // show JFileChooser
                    FileObject fo = chooseFileObject();
                    if (fo == null || !VisibilityQuery.getDefault().isVisible(fo)) return;
                    toShadows = Collections.singletonList(DataObject.find(fo));
                } else {
                    toShadows = new ArrayList<DataObject>();
                    for (Node node : activatedNodes) {
                        DataObject obj = node.getCookie(DataObject.class);
                        if (obj != null)
                            toShadows.add(obj);
                    }
                }
                Tab.RP.post(new Runnable () {
                    @Override
                    public void run() {
                        addToFavorites(toShadows);
                    }
                });
            } catch (DataObjectNotFoundException e) {
                LOG.log(Level.INFO, null, e);
            }
        }
        
        /**
         * 
         * @return FileObject or null if FileChooser dialog is cancelled
         */ 
        private static FileObject chooseFileObject() {
            FileObject retVal = null;
            File chooserSelection = null;
            JFileChooser chooser = new JFileChooser ();
            chooser.setFileSelectionMode( JFileChooser.FILES_AND_DIRECTORIES );
            chooser.setDialogTitle(NbBundle.getBundle(Actions.class).getString ("CTL_DialogTitle"));
            chooser.setApproveButtonText(NbBundle.getBundle(Actions.class).getString ("CTL_ApproveButtonText"));
            if (currentDir != null) {
                chooser.setCurrentDirectory(currentDir);
            }
            int option = chooser.showOpenDialog( WindowManager.getDefault().getMainWindow() ); // Show the chooser
            if ( option == JFileChooser.APPROVE_OPTION ) {                    
                chooserSelection = chooser.getSelectedFile();
                File selectedFile = FileUtil.normalizeFile(chooserSelection);
                //Workaround for JDK bug #5075580 (filed also in IZ as #46882)
                if (!selectedFile.exists()) {
                    if ((selectedFile.getParentFile() != null) && selectedFile.getParentFile().exists()) {
                        if (selectedFile.getName().equals(selectedFile.getParentFile().getName())) {
                            selectedFile = selectedFile.getParentFile();
                        }
                    }
                }
                //#50482: Check if selected file exists eg. user can enter any file name to text box.
                //#144985: Create new File because of inconsistence in File.exists (JDK bug 6751997)
                if (!new File(selectedFile, "").exists()) {
                    String message = NbBundle.getMessage(Actions.class,"ERR_FileDoesNotExist",selectedFile.getPath());
                    String title = NbBundle.getMessage(Actions.class,"ERR_FileDoesNotExistDlgTitle");
                    DialogDisplayer.getDefault().notify
                    (new NotifyDescriptor(message,title,NotifyDescriptor.DEFAULT_OPTION,
                    NotifyDescriptor.INFORMATION_MESSAGE, new Object[] { NotifyDescriptor.CLOSED_OPTION },
                    NotifyDescriptor.OK_OPTION));
                } else {
                    retVal = FileUtil.toFileObject(selectedFile);
                    assert retVal != null;
                }
            }
            currentDir = chooser.getCurrentDirectory();
            return retVal;
        }
        
        public static void selectAfterAddition(final DataObject createdDO) {
            final Tab projectsTab = Tab.findDefault();
            projectsTab.open();
            projectsTab.requestActive();
            //Try to locate newly added node and select it
            if (createdDO != null) {
                Tab.RP.post(new Runnable() {
                    @Override
                    public void run () {
                        Node [] nodes = projectsTab.getExplorerManager().getRootContext().getChildren().getNodes(true);
                        final Node [] toSelect = new Node[1];
                        boolean setSelected = false;
                        for (int i = 0; i < nodes.length; i++) {
                            if (createdDO.getName().equals(nodes[i].getName())) {
                                toSelect[0] = nodes[i];
                                setSelected = true;
                                break;
                            }
                        }
                        if (setSelected) {
                            SwingUtilities.invokeLater(new Runnable () {
                                @Override
                                public void run() {
                                    try {
                                        projectsTab.getExplorerManager().setExploredContextAndSelection(toSelect[0],toSelect);
                                    } catch (PropertyVetoException ex) {
                                        //Nothing to do
                                    }
                                }
                            });
                        }
                    }
                });
            }
        }

        static DataObject createShadows(final DataFolder favourities, final List<DataObject> dos, final List<DataObject> listAdd) {
            DataObject createdDO = null;
            for (DataObject obj : dos) {
                try {
                    DataShadow added = findShadow(favourities, obj);
                    if (added != null) {
                        if (createdDO == null) {
                            createdDO = added;
                        }
                    } else {
                        if (createdDO == null) {
                            // Select only first node in array added to favorites
                            createdDO = obj.createShadow(favourities);
                            listAdd.add(createdDO);
                        } else {
                            listAdd.add(obj.createShadow(favourities));
                        }
                    }
                } catch (IOException ex) {
                    LOG.log(Level.WARNING, null, ex);
                }
            }
            return createdDO;
        }

        private static DataShadow findShadow (DataFolder f, DataObject dobj) {
            FileObject fo = dobj.getPrimaryFile();
            if (fo != null) {
                DataObject [] arr = f.getChildren();
                for (int i = 0; i < arr.length; i++) {
                    if (arr[i] instanceof DataShadow) {
                        DataShadow obj = (DataShadow) arr[i];
                        if (fo.equals(obj.getOriginal().getPrimaryFile())) {
                            return obj;
                        }
                    }
                }
            }
            return null;
        }

        public static void reorderAfterAddition(final DataFolder favourities, final DataObject[] children, final List<? extends DataObject> listAdd) {
            List<DataObject> listDest = new ArrayList<DataObject>();
            if (listAdd.size() > 0) {
                //Insert new nodes just before last (root) node
                DataObject root = null;
                //Find root
                for (int i = 0; i < children.length; i++) {
                    FileObject fo = children[i].getPrimaryFile();
                    if ("Favorites/Root.instance".equals(fo.getPath())) { //NOI18N
                        root = children[i];
                    }
                }
                if (root != null) {
                    for (int i = 0; i < children.length; i++) {
                        if (!root.equals(children[i])) {
                            listDest.add(children[i]);
                        }
                    }
                    listDest.addAll(listAdd);
                    listDest.add(root);
                } else {
                    listDest.addAll(Arrays.asList(children));
                    listDest.addAll(listAdd);
                }
                //Set desired order
                DataObject [] newOrder = listDest.toArray(new DataObject[listDest.size()]);
                try {
                    favourities.setOrder(newOrder);
                } catch (IOException ex) {
                    LOG.log(Level.WARNING, null, ex);
                }
            }
        }

        @Override
        protected boolean asynchronous() {
            return false;
        }

        static void addToFavorites(List<DataObject> toShadows) {
            assert !EventQueue.isDispatchThread();
            final DataFolder f = FavoritesNode.getFolder();
            final DataObject[] arr = f.getChildren();
            final List<DataObject> listAdd = new ArrayList<DataObject>();
            final DataObject toSelect = createShadows(f, toShadows, listAdd);
            //This is done to set desired order of nodes in view
            reorderAfterAddition(f, arr, listAdd);
            EventQueue.invokeLater(new Runnable () {
                @Override
                public void run () {
                    selectAfterAddition(toSelect);
                }
            });
        }

        static boolean isAllowed(DataObject dataObject) {
            //Action is disabled for root folder eg:"/" on Linux or "C:" on Win
            if (dataObject == null) {
                return false;
            }
            FileObject fo = dataObject.getPrimaryFile();
            if (fo != null) {
                //#63459: Do not enable action on internal object/URL.
                if (URLMapper.findURL(fo, URLMapper.EXTERNAL) == null) {
                    return false;
                }
                //Check if it is root.
                if (fo.isRoot()) {
                    //It is root: disable.
                    return false;
                }
            }

            // Fix #14740 disable action on SystemFileSystem.
            try {
                if (dataObject.getPrimaryFile().getFileSystem().isDefault()) {
                    return false;
                }
            } catch (FileStateInvalidException fsie) {
                return false;
            }
            return true;
        }

    } // end of Add
    /** Subclass of Add. Only its display name is different otherwise the same as Add.
    *
    * @author   Marek Slama
    */
    public static class AddOnFavoritesNode extends Add {
        static final long serialVersionUID =-6471284573153172312L;
        
        private static final AddOnFavoritesNode ADD_ON_FAVORITES_NODE = new AddOnFavoritesNode ();
        
        public static ContextAwareAction getDefault () {
            return ADD_ON_FAVORITES_NODE;
        }
        
        /** Human presentable name of the action. This should be
        * presented as an item in a menu.
        * @return the name of the action
        */
        @Override
        public String getName() {
            return NbBundle.getMessage (
                    Actions.class, "ACT_AddOnFavoritesNode"); // NOI18N
        }
    }
    
}
