/*
 * 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 org.netbeans.modules.masterfs.providers.MountSupport;
import org.netbeans.modules.masterfs.filebasedfs.FileBasedFileSystem;
import org.netbeans.modules.masterfs.filebasedfs.fileobjects.WriteLockUtils;
import org.openide.filesystems.*;

import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * Encapsulates access to mount table, which can be considered as set
 * of pairs <mountPoint, FileSystem>. Method mount adds new pair and
 * method unmount removes one. Every MasterFileObject delegates its functionality
 * and  FileSystems, that are held in mout table, provide such delgates.
 * Method resolveBestDelegate is responsible for finding the best delegate to
 * requested resource path. There may be possible to find more than one delagate to
 * requested path. So, there also exist method resolveSecondDelegate.
 *
 *
 * @author Radek Matous
 */
final class MountTable implements InternalMountSupport {
    /** maps <String rootName, FileSystem fs>*/
    final private static Map res2fsMap = Collections.synchronizedMap(new HashMap());
    final private static MountTable instance = new MountTable();
    final static MountSupport mountSupport = APIAccess.DEFAULT.createMountSupport(instance);

    /***
     * @return one shared instance of MountTable
     */
    static MountTable getDefault() {
        return instance;
    }

    private MountTable() {
    }

    /**
     * Mounts new pair <mountPointPath, mount>.
     * @param mountPointPath resource path associated with mounted filesystem mount
     * @param mount mounted filesystem
     * @throws IOException is thrown if mount can't be mounted (e.g.: if already mounted)
     */
    public void mount(final String mountPointPath, final FileSystem mount) throws IOException {
        /*if (!(mount instanceof LocalFileSystem || mount instanceof JarFileSystem))
            return;*/
        final boolean syncNeeded = !(mount instanceof  ExLocalFileSystem);
        
        final String normName = ResourcePath.getNormalizedPath(mountPointPath);

        MasterFileSystem.getDefault().runAtomicAction(new FileSystem.AtomicAction() {
            public void run() throws IOException {
                if (syncNeeded) {
                    SyncSection.getDefault().enterExclusiveSection();
                    try {
                        mountIt(normName, mount, mountPointPath);
                    } finally {
                        SyncSection.getDefault().finishExclusiveSection();
                    }
                } else {
                    mountIt(normName, mount, mountPointPath);                    
                }
            }
        });

        mount.addFileStatusListener(MasterFileSystem.getDefault());
        mount.addNotify();
    }

    private void mountIt(final String normName, final FileSystem mount, final String mountPointPath) throws IOException {
        FileSystem oldFs = (FileSystem) res2fsMap.put(normName, mount);
        if (oldFs != null) handleAlreadyMounted(oldFs, mount, normName);
        else refreshAfterMount(getMountEnumeration(mountPointPath));
    }

    /**
     * Unmounts already mounted filesystem mount.
     * @param mount filesystem requested to unmount
     * @throws IOException is thrown if fs2Mount can't be unmounted
     * (e.g.: haven't been mounted yet)
     */
    public void unmount(final FileSystem mount) throws IOException {
        MasterFileSystem.getDefault().runAtomicAction(new FileSystem.AtomicAction() {
            public void run() throws IOException {
                SyncSection.getDefault().enterExclusiveSection();
                try {
                    if (!removeFileSystem(mount)) {
                        String errMsg = Utils.formatString("EXC_CannotUnMount",
                                new Object[]{mount.getDisplayName()});
                        throw new IOException(errMsg);
                    }
                } finally {
                    SyncSection.getDefault().finishExclusiveSection();
                }
            }
        });

        mount.removeFileStatusListener(MasterFileSystem.getDefault());
        mount.removeNotify();
    }

    /**
     * Tries to find the best delegate to requested resourcePath.
     * @param resourcePath
     * @return the best delegate or null if delegate doesn't exist
     */
    FileObject resolveBestDelegate(final String resourcePath) {
        return resolveDelegate(resourcePath, Delegate.BEST_DELEGATE);
    }

    /**
     * Tries to find the second possible delegate to requested resourcePath.
     * There may be  possible to find more than one delagate to requested path.
     * If the second one is not found, then the best one is returned.
     * @param resourcePath
     * @return the second possible delegate if found or best delegate or null
     * (in mentioned order)
     */
    FileObject resolveSecondDelegate(final String resourcePath) {
        return resolveDelegate(resourcePath, Delegate.SECOND_DELEGATE);
    }

    void unmount(final String mountPointPath) throws IOException {
        final FileSystem fs = getMountedFileSystem(mountPointPath);
        if (fs != null)
            MountTable.getDefault().unmount(fs);
    }

    /**
     * Gets all mounted filesystems
     * @return enumeration of mounted filesystems
     */
    Enumeration geAllFileSystems() {
        final ArrayList qE = new ArrayList ();
        final Collection values = res2fsMap.values();
        //SyncSection.getDefault().enterExclusiveSection();
        SyncSection.getDefault().enterSection();
        try {
            synchronized (res2fsMap) {
                for (Iterator it = values.iterator(); it.hasNext();) {
                    FileSystem fs = (FileSystem) it.next();
                    if (fs != null)
                        qE.add (fs);
                }
            }
        } finally {
            //SyncSection.getDefault().finishExclusiveSection();
            SyncSection.getDefault().finishSection();
        }
        return Collections.enumeration (qE);
    }

    /**
     * Gets mount point, if mount is mounted. Else null is returned.
     *
     * @param mount filesystem of which mount point is looked for
     * @return mount point or null */
    boolean isMounted(final FileSystem mount) {
        ResourcePath resource = findResourcePath(mount);
        FileSystem fsTest = (FileSystem) res2fsMap.get(resource.getNormalizedPath());
        return (mount == fsTest) ? true : false;
    }

    private ResourcePath findResourcePath(final FileSystem fs) {
        final File f = FileUtil.toFile(fs.getRoot());
        return Utils.getResource(f);
    }

    private FileObject resolveDelegate(String resourcePath, final int delegateLevel) {
        FileSystem fs;
        String bestMountPoint = "";

        final LinkedList retValList = new LinkedList();
        resourcePath = ResourcePath.getNormalizedPath(resourcePath);

        if (WriteLockUtils.hasActiveLockFileSigns(resourcePath)) {
            File f = new ResourcePath(resourcePath).getFile();
            if (WriteLockUtils.hasActiveLockFileSigns(f.getAbsolutePath())) {
                return null;
            }
        }

        synchronized (res2fsMap) {
            final List sortedList = new ArrayList(res2fsMap.keySet());
            Collections.sort(sortedList);

            for (Iterator it = sortedList.iterator(); it.hasNext();) {
                String mountPoint = (String) it.next();
                if (resourcePath.startsWith(mountPoint) &&
                        mountPoint.length() > bestMountPoint.length()) {
                    boolean isMountPoint = mountPoint.length() == resourcePath.length() || 
                            resourcePath.charAt(mountPoint.length()) == '/';
                    if (isMountPoint || new ResourcePath (mountPoint).isRoot()) {
                        bestMountPoint = mountPoint;
                        retValList.addLast(bestMountPoint);                        
                    }
                }
            }
            if (retValList.size() == 0) return null;

            int idx = retValList.size() - (delegateLevel + 1);
            if (idx < 0) idx = 0;

            bestMountPoint = (String) retValList.get(idx);
            fs = (FileSystem) res2fsMap.get(bestMountPoint);
        }
        FileObject retVal;        
        if (fs instanceof FileBasedFileSystem) {
            File f = new ResourcePath (resourcePath).getFile();
            retVal = fs.findResource(f.getAbsolutePath());            
        } else {
            retVal = fs.findResource(resourcePath.substring(bestMountPoint.length()));    
        }
        

        return retVal;
    }

    private static void refreshAfterMount(final Enumeration changeList) {
        final Set toReset = new LinkedHashSet();
        while (changeList.hasMoreElements()) {
            MasterFileObject hfo = (MasterFileObject) changeList.nextElement();
            refreshAfterMount(hfo);
            toReset.add(hfo);
        }
        resetAfterMount(toReset);
        MasterFileSystem masterFs = MasterFileSystem.getDefault();
        masterFs.fireFileStatus(new FileStatusEvent(masterFs,toReset, true, true ));  
    }

    static void refreshAfterMount(MasterFileObject hfo) {
        //if (true) return;
        FileObject oldDelegate = hfo.getDelegate().get();
        if (oldDelegate == null) return;
        FileObject newDelegate = MountTable.getDefault().resolveBestDelegate(hfo.getResource().getNormalizedPath());
        if (oldDelegate == newDelegate) return;
        MasterFileObject.refreshAfterMount(newDelegate, oldDelegate, hfo);
    }

    private static void resetAfterMount(final Set toReset) {
        Iterator it = toReset.iterator();
        while (it.hasNext()) {
            MasterFileObject hfo = (MasterFileObject) it.next();
            hfo.getDelegate().reset(hfo.getResource());
        }
    }

    static void renameCachedFileObjects(final String oldName, final String newName) {
        final Enumeration en = Cache.getDefault().getAll();
        while (en.hasMoreElements()) {
            MasterFileObject fo = (MasterFileObject) en.nextElement();
            String oldResPath = fo.getResource().getNormalizedPath();
            if (oldResPath.startsWith(newName)) {
                continue;
            }
            if (oldResPath.startsWith(oldName)) {
                ResourcePath newResPath = new ResourcePath(newName + oldResPath.substring(oldName.length()));
                fo.setResource(newResPath);
                Cache.getDefault().replace(oldResPath, fo);
            }
        }
    }

    private Enumeration getMountEnumeration(final String mountPointPath) {
        class Mnt implements org.openide.util.Enumerations.Processor {
            public Object process (Object o, Collection ignore) {
                final MasterFileObject hfo = (MasterFileObject) o;
                String normalizedMountPoint = ResourcePath.getNormalizedPath(mountPointPath);                
                String normalizedResourcePath = hfo.getResource().getNormalizedPath();
                
                if (normalizedResourcePath.startsWith(normalizedMountPoint) && hfo.isValid()) {
                    return o;
                }
                return null;
            }
        }
        return org.openide.util.Enumerations.filter (
            Cache.getDefault().getAll(), 
            new Mnt ()
        );
    }

    private static Enumeration getUnMountEnumeration(final FileSystem fs2Umount) {
        class UnMnt implements org.openide.util.Enumerations.Processor {
            public Object process (Object o, Collection ignore) {
                final MasterFileObject hfo = (MasterFileObject) o;
                if (hfo != null && hfo.isValid()) {
                    FileSystem delgFs = hfo.getDelegateFileSystem();
                    if (delgFs == fs2Umount)
                        return o;

                }
                return null;
            }
        }
        return org.openide.util.Enumerations.filter (
            Cache.getDefault().getAll(), 
            new UnMnt ()
        );
    }

    FileSystem getMountedFileSystem(String resName) {
        resName = ResourcePath.getNormalizedPath(resName);
        final FileSystem fsTest = (FileSystem) res2fsMap.get(resName);
        return fsTest;
    }

    private void handleAlreadyMounted(FileSystem originalFs, final FileSystem newFs, final String name) throws IOException {
        /** already registered another filesystem. First must be unregistered*/
        res2fsMap.put(name, originalFs);
/*
        Object[] params = new Object[]{newFs.getDisplayName(), name, originalFs.getDisplayName()};
        IOException iex = new IOException(Utils.formatString("EXC_AlreadyMounted", params));
        iex.printStackTrace();
        throw iex;
*/
    }

    private boolean removeFileSystem(final FileSystem fs) {
        Iterator it = res2fsMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            FileSystem fsValue = (FileSystem) entry.getValue();
            if (fsValue == fs) {
                it.remove();
                refreshAfterMount(getUnMountEnumeration(fs));
                return true;
            }
        }
        return false;
    }

}
