/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (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.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * 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.
 */

package org.netbeans.modules.masterfs;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import javax.swing.filechooser.FileSystemView;
import org.netbeans.modules.masterfs.filebasedfs.FileBasedFileSystem;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileSystem;
import org.openide.util.Utilities;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.actions.SystemAction;
import org.openide.util.actions.CallableSystemAction;

/**
 * Provides special delegates, that are virtual: delegates for windows drives,
 * root for Windows and also root for Unix.
 *
 * @author Radek Matous
 */
final class SpecialDelegates {
    private static boolean isUnixRootResolved = false;
    private static FileSystem fs = new WinSpecialFs();
    private static final FileSystemView fsv = FileSystemView.getFileSystemView();

    private SpecialDelegates() {
    }

    /**
     * This method should be called only when resource resPath can't be found
     * in filesystems,that are placed in mount table (MoutTable). Then if this method
     * returns null, then delegate doesn't exists and MasterFileObject for
     * this delegate shouldn't be provided.
     *
     * Mounts LocalFileSystem for root on unix or for windows drive, if not mounted yet
     *
     * @param resPath requested resource name
     * @return returns delegates for windows drives, roots or null
     */
    static FileObject get(final ResourcePath resPath) {
        FileObject retVal;
        if ((Utilities.isWindows () || (Utilities.getOperatingSystem () == Utilities.OS_OS2)))
            retVal = getForWindows(resPath);
        else if (Utilities.getOperatingSystem() == Utilities.OS_VMS)
            retVal = getForVMS(resPath);
        else {
            retVal = getForUnix(resPath);
        }
        // here retVal may be null - not all resources must exist
        return retVal;
    }

    /**
     * Mounts LocalFileSystem for root on unix, if not mounted yet.
     * @param resPath requested resource name
     * @return returns delegates for windows drives, roots or null
     */
    private static FileObject getForUnix(final ResourcePath resPath) {
        FileObject retVal = null;
        if (!isUnixRootResolved) {
            mountUnixRoot();
            retVal = MountTable.getDefault().resolveBestDelegate(resPath.getNormalizedPath());
            isUnixRootResolved = true;
        }
        return retVal;
    }

    /**
     * Mounts LocalFileSystem for root on VMS, if not mounted yet.
     * @param resPath requested resource name
     * @return returns delegates for windows drives, roots or null
     */
    private static FileObject getForVMS(final ResourcePath resPath) {
        FileObject retVal = null;
        mountVMSRoot(resPath);
        retVal = MountTable.getDefault().resolveBestDelegate(resPath.getNormalizedPath());
        return retVal;
    }

    private static FileObject getForWindows(final ResourcePath resPath) {
        FileObject retVal = null;
        if (resPath.isRoot())
            retVal = WinRootVirtual.getDefault();
        else if (isWinDrive(resPath))
            retVal = WindowsDriveProxy.getDefault(resPath);
        else {
            WindowsDriveProxy winDrive = WindowsDriveProxy.getDefault(resPath);
            if (winDrive != null && !winDrive.isMounted()) {
                winDrive.lfs = mountWinDrive(winDrive.getResource());
                // try again after mount
                retVal = MountTable.getDefault().resolveBestDelegate(resPath.getNormalizedPath());
            }
        }
        return retVal;
    }

    // use only if Utilities.isWindows returns true
    private static boolean isWinDrive(final ResourcePath resPath) {
        boolean root = resPath.getParent().isRoot();
        if (root) {
           root = (checkValidWindowsDrive (resPath.getFile()) != null); 
        }
        return root;
    }

    private static FileSystem mountWinDrive(final ResourcePath mountPointPath) {
        FileSystem retVal = null;
        final String rootPath = mountPointPath.getNormalizedPath().substring(1) + "/";
        final File root = checkValidWindowsDrive(new File(rootPath));
        if (root != null) {
            retVal = mountLocalFileSystem(root, rootPath);
        }
        return retVal;
    }

    private static void mountUnixRoot() {
        final String rootPath = ResourcePath.getRoot().getNormalizedPath();
        final File root = new File(rootPath);
        if (root.exists())
            mountLocalFileSystem(root, rootPath);
    }
    
    /** mount the local root directory for OpenVMS platform
     * @param mountPointPath root directory to mount
     */
    private static void mountVMSRoot(final ResourcePath mountPointPath) {
        //On OpenVMS platform, root is not allowed, so instead supply
        //a mount point.
        //
        final String rootPath = mountPointPath.getNormalizedPath();
        final File root = new File(rootPath);
        if (root.exists())
            mountLocalFileSystem(root, rootPath);
        
    }

    private static FileSystem mountLocalFileSystem(final File root, final String rootPath) {
        FileSystem retVal;
        //retVal = new LocalFileSystem();
        try {
            //retVal.setRootDirectory(root);
            //retVal = ExLocalFileSystem.getInstance(root);            
            retVal = FileBasedFileSystem.getInstance(root);
            MountTable.getDefault().mount(rootPath, retVal);
        } catch (IOException e) {
            retVal = null;
        }
        return retVal;
    }

    static File checkValidWindowsDrive(final File root) {
        File retVal = null;
        if (fsv != null) {
            /** filters floppy drives*/
            retVal = ((isRoot(root) || (fsv.isFileSystemRoot(root))) && !fsv.isFloppyDrive(root)) ? root : null;
            
            /** filters empty CDROM drives*/            
            if (retVal != null && !retVal.exists()) {
                retVal = null;  
            }
        } else {
            retVal = isRoot(root) ? root : null;
        }

        return retVal;
    }

    private static boolean isRoot(final File root) {
        boolean retval = false;        
        if (root.getParentFile() == null) {
            File[] allFiles = File.listRoots();
            for (int i = 0; i < allFiles.length; i++) {
                File windowsDrive = allFiles[i];
                if (windowsDrive.equals(root)) {
                    retval = true;
                    break;
                }
            }
        }
        return retval;
    }

    private static File[] listRoots() {
        File[] all = File.listRoots();

        if ((Utilities.isWindows () || (Utilities.getOperatingSystem () == Utilities.OS_OS2)) && fsv != null) {
            Set roots = new LinkedHashSet();
            
            for (int i = 0; i < all.length; i++) {
                File file = checkValidWindowsDrive(all[i]);
                if (file != null) {
                    roots.add(file);
                }            
            }        
            all = new File[roots.size()];
            roots.toArray(all);
        }
        return all;
    }

    /**
     *
     */
    private static class WinRootVirtual extends InvalidDummy {
        /** generated Serialized Version UID */
        static final long serialVersionUID = -1244651321879256809L;
        transient private static WinRootVirtual instance = null;
        transient private static ArrayList delegs = null;
        //transient private File root;
        transient private Set listenerList;

        private static WinRootVirtual getDefault() {
            synchronized (WinRootVirtual.class) {
                if (instance == null)
                    instance = new WinRootVirtual(new ResourcePath(""));
            }
            return instance;
        }

        private WinRootVirtual(final ResourcePath resPath) {
            super(resPath);
        }

        public void addFileChangeListener(FileChangeListener fcl) {
            getListenerList().add(fcl);
        }

        public void removeFileChangeListener(FileChangeListener fcl) {
            getListenerList().remove(fcl);
        }

        public FileSystem getFileSystem() throws FileStateInvalidException {
            return fs;
        }

        public boolean isRoot() {
            return true;
        }

        public boolean isFolder() {
            return true;
        }

        public boolean isData() {
            return false;
        }

        public void refresh(final boolean expected) {
            List oldFos = Arrays.asList(getChildren());
            delegs = null;
            List newFos = Arrays.asList(getChildren());
            Set removeSet = new HashSet(oldFos);
            Set addSet = new HashSet(newFos);

            removeSet.removeAll(newFos);
            addSet.removeAll(oldFos);


            for (Iterator iterator = removeSet.iterator(); iterator.hasNext();) {
                FileObject fo = (FileObject) iterator.next();
                FileEvent fe = new FileEvent(this, fo);
                //TODO: here should be fired also from deleted file: fo.fireFileDel..
                fireFileDeletedEvent(Collections.enumeration(getListenerList()), fe);
            }

            for (Iterator iterator = addSet.iterator(); iterator.hasNext();) {
                FileObject fo = (FileObject) iterator.next();

                FileEvent fe = new FileEvent(this, fo);
                if (fo.isFolder())
                    fireFileFolderCreatedEvent(Collections.enumeration(getListenerList()), fe);
                else
                    fireFileDataCreatedEvent(Collections.enumeration(getListenerList()), fe);
            }
        }

        public FileObject[] getChildren() {
            synchronized (WinRootVirtual.class) {
                if (delegs == null) {
                    List roots = Arrays.asList(listRoots());
                    delegs = new ArrayList();
                    for (int i = 0; i < roots.size(); i++) {
                        File root = (File) roots.get(i);
                        ResourcePath resName = getResource().getChild(root.getAbsolutePath());
                        WindowsDriveProxy winDrive = WindowsDriveProxy.getDefault(resName);
                        if (winDrive != null) {
                            delegs.add(winDrive); // getOrCreateRoot
                        }
                    }
                }
            }

            FileObject[] retVal = new FileObject[delegs.size()];
            delegs.toArray(retVal);
            return retVal;
        }

        public boolean isValid() {
            return true;
        }

        public FileObject getFileObject(String name, String ext) {
            FileObject retVal = null;
            FileObject[] chlds = getChildren();
            for (int i = 0; i < chlds.length; i++) {
                FileObject chld = chlds[i];
                if (chld.getName().equals(name) && chld.getExt().equals(ext)) {
                    retVal = chld;
                    break;
                }
            }
            return retVal;
        }

        Set getListenerList() {
            synchronized (WindowsDriveProxy.class) {
                if (listenerList == null) {
                    listenerList = Collections.synchronizedSet(new HashSet());
                }
            }
            return listenerList;
        }
    }

    /**
     *
     */
    private static final class WindowsDriveProxy extends WinRootVirtual {
        /** generated Serialized Version UID */
        static final long serialVersionUID = -1244651321879256718L;
        transient private FileObject delegate;
        transient private boolean isValid = true;

        transient private static final Map instances = new WeakHashMap();
        transient private FileSystem lfs;


        private static WindowsDriveProxy getDefault(ResourcePath resPath) {
            WindowsDriveProxy in;
            String winDrivePath = getWindowsDriveSubstr(resPath.getNormalizedPath());
            if (winDrivePath == null) return null;

            resPath = new ResourcePath(winDrivePath);
            synchronized (WindowsDriveProxy.class) {
                in = (WindowsDriveProxy) instances.get(resPath);
                if (in == null) {
                    in = new WindowsDriveProxy(resPath);
                    instances.put(in.getResource(), in);
                }
            }
            return in;
        }


        private static String getWindowsDriveSubstr(String resPath) {
            int idx = resPath.indexOf(':');
            return (idx == -1) ? null : resPath.substring(0, idx + 1);
        }


        private WindowsDriveProxy(ResourcePath resPath) {
            super(resPath);
        }

        public boolean isRoot() {
            return false;
        }

        public boolean isFolder() {
            return true;
        }

        public boolean isData() {
            return false;
        }

        public boolean isValid() {
            return isValid;
        }

        public FileObject[] getChildren() {
            FileObject deleg = getDelegate(true);
            return (deleg != null) ? deleg.getChildren() : new FileObject [] {};
        }

        public FileObject getFileObject(String name, String ext) {
            FileObject deleg = getDelegate(true);
            return (deleg != null) ? deleg.getFileObject(name, ext) : null;
        }

        public FileObject getParent() {
            return WinRootVirtual.getDefault();
        }

        private FileObject getDelegate() {
            return getDelegate(false);
        }

        private FileObject getDelegate(boolean create) {
            if (delegate == null && create /*&& !isMounted()*/) {

                delegate = new InvalidDummy(getResource());
                lfs = mountWinDrive(getResource());
                delegate = MountTable.getDefault().resolveBestDelegate(getResource().getNormalizedPath());
                if (delegate != null) {
                    synchronized (getListenerList()) {
                        Iterator it = getListenerList().iterator();
                        while (it.hasNext()) {
                            FileChangeListener fcl = (FileChangeListener) it.next();
                            delegate.addFileChangeListener(fcl);
                            it.remove();

                        }
                    }
                } else {
                    isValid = false;
                }
            }
            return delegate;
        }

        public FileSystem getFileSystem() throws FileStateInvalidException {
            return (lfs != null) ? lfs : fs;
        }


        public FileObject createFolder(String name) throws IOException {
            return (getDelegate() != null) ? getDelegate().createFolder(name) :
                    super.createFolder(name);
        }

        public FileObject createData(String name, String ext) throws IOException {
            return (getDelegate() != null) ? getDelegate().createData(name) :
                    super.createData(name);
        }


        public Object getAttribute(String attrName) {
            return (getDelegate() != null) ? getDelegate().getAttribute(attrName) :
                    super.getAttribute(attrName);
        }

        public void setAttribute(String attrName, Object value) throws IOException {
            if (getDelegate() != null)
                getDelegate().setAttribute(attrName, value);
            else
                super.setAttribute(attrName, value);
        }

        public Enumeration getAttributes() {
            return (getDelegate() != null) ? getDelegate().getAttributes() :
                    super.getAttributes();
        }

        public void addFileChangeListener(FileChangeListener fcl) {
            if (getDelegate() != null)
                getDelegate().addFileChangeListener(fcl);
            else
                super.addFileChangeListener(fcl);
        }

        public void removeFileChangeListener(FileChangeListener fcl) {
            if (getDelegate() != null)
                getDelegate().removeFileChangeListener(fcl);
            else
                super.removeFileChangeListener(fcl);
        }

        private boolean isMounted() {
            return lfs != null;
        }
    }

    private static final class WinSpecialFs extends FileSystem {
        public boolean isReadOnly() {
            return false;
        }

        public FileObject getRoot() {
            return WinRootVirtual.getDefault();
        }

        public FileObject findResource(String name) {
            ResourcePath resPath = new ResourcePath(name);
            if (resPath.isRoot()) return getRoot();
            return WindowsDriveProxy.getDefault(resPath);
        }

        public SystemAction[] getActions() {
            return new SystemAction[] {};
        }
        
        public SystemAction[] getActions(Set/*<FileObject>*/ foSet) {
            return getActions();
        }

        public String getDisplayName() {
            return null;
        }
    }

}
