/*
 * 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.*;
import org.netbeans.modules.masterfs.providers.*;
import org.netbeans.modules.masterfs.providers.AnnotationProvider;
import org.openide.filesystems.*;
import org.openide.util.NbBundle;
import org.openide.util.actions.SystemAction;
import org.openide.util.Utilities;

import java.awt.*;
import java.beans.PropertyVetoException;
import java.io.File;
import java.io.Serializable;
import java.util.*;
import java.util.regex.Pattern;

/**
 * Implementation of FileSystem, that allows to host filesystems, that can be mounted
 * and unmounted.
 *
 * @author  Radek Matous
 */
final public class MasterFileSystem extends FileSystem implements FileStatusListener {
    /** generated Serialized Version UID */
    private static final long serialVersionUID = -97134851800761145L;
    //transient ArrayList rootChilds;
    transient private static MasterFileSystem instance;
    transient private final StatusImpl status = new StatusImpl();
    private static final Pattern lt = Pattern.compile("<");  // NOI18N
    private static final Pattern gt = Pattern.compile(">");  // NOI18N    

    static MasterFileSystem getDefault() {
        boolean init = false;
        synchronized (MasterFileSystem.class) {
            if (instance == null)  {
                instance = new MasterFileSystem();
                init = true;                
            }
            
        }
        if (init) {
            ProviderCall.init();
        }
        return instance;
    }
    
    public static MasterFileSystem settingsFactory(FileObject inst) {
        return getDefault();
    }

    private MasterFileSystem() {
        init();
    }

    private Object writeReplace () {
        return new Replace ();
    }

    private void init() {
        try {
            setSystemName(getClass().getName()); //NOI18N
        } catch (PropertyVetoException pvx) {
            ///nobody can`t have registered VetoableChangeListener, so no exception
            // can`t be fired
        }
        registerFileStatusListener();
    }

    private void registerFileStatusListener() {
        Enumeration en = MountTable.getDefault().geAllFileSystems();
        while (en.hasMoreElements()) {
            FileSystem fs = (FileSystem) en.nextElement();
            fs.addNotify();
            fs.addFileStatusListener(this);
        }
    }

    public String getDisplayName() {
        return NbBundle.getMessage(MasterFileSystem.class, "LBL_this_computer");
    }

    // XXX should be a MasterFileSystemBeanInfo that hides this property (hidden=true)!
    public boolean isReadOnly() {
        return false;
    }

    public FileObject getRoot() {
        return Cache.getDefault().getOrCreate(ResourcePath.getRoot());
    }
    
    public FileObject findResource(String name) {
        ResourcePath resPath = new ResourcePath (name);
        FileObject retVal = Cache.getDefault().get(resPath);
        if (Utilities.getOperatingSystem() == Utilities.OS_VMS) {
            if (retVal == null) retVal = getStepByStepVMS(resPath);
        } else {
            if (retVal == null) retVal = getStepByStep(resPath);
        }
        return retVal;
    }

    /**
     * May look strange. BUT: Every call of getFileObject tries to mount
     * filesystem, that may be requested to provide delegates.
     */
    private FileObject getStepByStep(ResourcePath resPath) {
        FileObject retVal = getRoot();
        Enumeration elems = resPath.getElements();
        while (elems.hasMoreElements()) {
            String nameExt = (String)elems.nextElement();
            retVal = retVal.getFileObject(nameExt);
           if (retVal == null) return null;
        }
        return retVal;
    }

    /** Returns the root for OpenVMS platform given the path
     * @param name the path to search root from
     * @return the root
     */
    private static String findVMSRoot(String name) {
         if (name.length() > 0) {
             StringTokenizer stok = new StringTokenizer(name, "/");
             String rootName = "";
             while (stok.hasMoreTokens()) {
                 rootName += "/" + stok.nextToken();
                 if (new File(rootName).exists()) {
                    return rootName;
                 }
             }
         } 
         return null;
    }
    
    /** Returns the root for OpenVMS platform. The root is in the form
     *  of "/<device name>".
     *  @param name name of the file specification to extract the root from
     *  @return the FileObject instance representing the root directory
     */
     private static FileObject getRootForVMS(String name) {
         return Cache.getDefault().getOrCreate(new ResourcePath(name));   
    }
    
    /**
     * May look strange. BUT: Every call of getFileObject tries to mount
     * filesystem, that may be requested to provide delegates.
     */
    private static FileObject getStepByStepVMS(ResourcePath resPath) {
        //On OpenVMS platform, the root is not "/"
        //
        String root = findVMSRoot(resPath.getNormalizedPath());
        if (root == null)
            return null;
        
        FileObject retVal = getRootForVMS(root);
        //Get the part of the path that begins after the root
        //
        ResourcePath subPath = new ResourcePath(
            resPath.getNormalizedPath().substring(root.length()));
        Enumeration elems = subPath.getElements();
       
        while (elems.hasMoreElements()) {
            String nameExt = (String)elems.nextElement();
            //"000000" needs to be filtered out on OpenVMS platform
            //
            if (nameExt.equals("000000"))
                continue;
            retVal = retVal.getFileObject(nameExt);
            if (retVal == null) return null;
        }
        return retVal;
    }

    
    public void addNotify() {
    }

    public void removeNotify() {
        Cache.getDefault().clear();
    }


    public void refresh(boolean expected) {
        Enumeration en = MountTable.getDefault().geAllFileSystems();
        while (en.hasMoreElements()) {
            FileSystem fs = (FileSystem) en.nextElement();
            fs.refresh(expected);
        }
    }


    public SystemAction[] getActions() {
        return getEmptyActions();
    }

    private SystemAction[] getEmptyActions() {
        return new SystemAction[]{};
    }

    public SystemAction[] getActions(java.util.Set foSet) {
        SystemAction[] some = status.getActions (foSet);
        if (some != null) {
            return some;
        }
        
        SyncSection.getDefault().enterSection();
        try {
            //check if all fileobjects come from the same filesystem
            MasterFileObject hfo = getUniqueMasterFileObject(foSet);
            if (hfo == null) return getEmptyActions();

            FileSystem firstFs = getDelegateFileSystem(hfo.getDelegate().get());
            FileSystem secondFs = getDelegateFileSystem(hfo.getDelegate().getPrefered());
            if (firstFs == null) return getEmptyActions();

            if (secondFs != firstFs ) {
                return mergeActionsFromBothDelegates(foSet, firstFs, secondFs);
            }

            return firstFs.getActions(Utils.transformToDelegates(foSet, false));
        } finally {
            SyncSection.getDefault().finishSection();
        }
    }

    private static FileSystem getDelegateFileSystem(FileObject foDel) {
        if (foDel == null) return null;
        FileSystem fsDel = null;
        try {
            fsDel = foDel.getFileSystem();
        } catch (FileStateInvalidException e) {
            return null;
        }
        return fsDel;
    }

    private static MasterFileObject getUniqueMasterFileObject(Set hfoSet) {
        MasterFileObject retVal = null;
        FileSystem lastFs = null;
        for (Iterator it = hfoSet.iterator(); it.hasNext();) {
            Object o = it.next();
            if (!(o instanceof MasterFileObject)) return null;

            retVal = (MasterFileObject) o;
            FileObject deleg = retVal.getDelegate().get();
            if (deleg == null) continue;
            FileSystem fs = getDelegateFileSystem(deleg);
            if (fs == null) continue;

            if (lastFs != null && lastFs != fs) return null;
            lastFs = fs;
        }
        return retVal;
    }

    private SystemAction[] mergeActionsFromBothDelegates(Set hfoSet, FileSystem firstFs, FileSystem secondFs) {
        Set mergedActions = new HashSet(hfoSet.size());

        Set firstSet = Utils.transformToDelegates(hfoSet, false);
        SystemAction[] firstActions = firstFs.getActions(firstSet);
        mergedActions.addAll(Arrays.asList(firstActions));

        Set secondSet = Utils.transformToDelegates(hfoSet, true);
        SystemAction[] secondActions = secondFs.getActions(secondSet);
        mergedActions.addAll(Arrays.asList(secondActions));

        return (SystemAction[]) mergedActions.toArray(new SystemAction[mergedActions.size()]);
    }


    /** Notifies listener about change in annotataion of a few files.
     * @param ev event describing the change
     */
    public void annotationChanged(final FileStatusEvent ev) {
        /** Ugly piece of code, that relies on impl. of FileStatusEvent and its
         * usage of Set of FileObjects. Adopted from TreeFS.
         */
        HashSet set = new HashSet(1) {
            public boolean contains(Object o) {
                if (o instanceof MasterFileObject) {
                    MasterFileObject fo = (MasterFileObject) o;
                    FileObject deleg = fo.getDelegate().get();
                    return deleg != null && ev.hasChanged(deleg);
                }
                return false;
            }
        };


        fireFileStatusChanged(new FileStatusEvent(
                this, set, ev.isIconChange(), ev.isNameChange()
        ));
    }


    public FileSystem.Status getStatus() {
        return status;
    }

    final void fireFileStatus (FileStatusEvent event) {
        fireFileStatusChanged(event);
    }

    
    static final class StatusImpl implements FileSystem.HtmlStatus,
    org.openide.util.LookupListener, org.openide.filesystems.FileStatusListener {
        /** result with providers */
        private org.openide.util.Lookup.Result annotationProviders;
        private Collection previousProviders;
        {
            annotationProviders = org.openide.util.Lookup.getDefault ().lookup (
                new org.openide.util.Lookup.Template (AnnotationProvider.class)
            );
            annotationProviders.addLookupListener (this);
            resultChanged (null);
        }

        public ProvidedExtensions getExtensions() {
            Collection c = (previousProviders != null) ?
                Collections.unmodifiableCollection(previousProviders) : Collections.EMPTY_LIST;
            return new ProvidedExtensionsProxy(c);
        }
        
        public void resultChanged (org.openide.util.LookupEvent ev) {
            java.util.Collection now = annotationProviders.allInstances ();
            java.util.Collection add;
            
            if (previousProviders != null) {
                add = new HashSet (now);
                add.removeAll (previousProviders);
                
                HashSet toRemove = new HashSet(previousProviders);
                toRemove.removeAll (now);
                java.util.Iterator it = toRemove.iterator ();
                while (it.hasNext ()) {
                    AnnotationProvider ap = (AnnotationProvider)it.next ();
                    ap.removeFileStatusListener (this);
                }
            
            } else {
                add = now;
            }

            
            
            java.util.Iterator it = add.iterator ();
            while (it.hasNext ()) {
                AnnotationProvider ap = (AnnotationProvider)it.next ();
                try {
                    ap.addFileStatusListener (this);
                } catch (java.util.TooManyListenersException ex) {
                    org.openide.ErrorManager.getDefault ().notify (ex);
                }
            }
            
            previousProviders = now;
        }

        public SystemAction[] getActions(java.util.Set foSet) {
            
            javax.swing.Action[] retVal = null;
            java.util.Iterator it = annotationProviders.allInstances ().iterator ();
            while (retVal == null && it.hasNext ()) {
                AnnotationProvider ap = (AnnotationProvider)it.next ();
                retVal = ap.actions (foSet);
            }
            if (retVal != null) {
                // right now we handle just SystemAction, it can be changed if necessary
                SystemAction[] ret = new SystemAction[retVal.length];
                for (int i = 0; i < retVal.length; i++) {
                    if (retVal[i] instanceof SystemAction) {
                        ret[i] = (SystemAction)retVal[i];
                    }
                }
                return ret;
            }
            return null;
        }
        
        public void annotationChanged (org.openide.filesystems.FileStatusEvent ev) {
            if (ev.getSource () != MasterFileSystem.getDefault ()) {
                throw new IllegalStateException ("The source must be master fs and not : " + ev.getSource ()); // NOI18N
            }
            MasterFileSystem.getDefault ().fireFileStatusChanged (ev);
        }
        
        private FileSystem getDelegateFileSystem (Set files) {
            FileSystem retVal = null;
           Iterator it = files.iterator();
           if (it.hasNext())  {
                MasterFileObject mfo = (MasterFileObject)it.next();
                retVal = mfo.getDelegateFileSystem();
                
           }
           return retVal;
        }
        
        public Image annotateIcon(Image icon, int iconType, Set files) {
            Image retVal = null;            
            
            Iterator it = annotationProviders.allInstances ().iterator ();
            while (retVal == null && it.hasNext ()) {
                AnnotationProvider ap = (AnnotationProvider)it.next ();
                retVal = ap.annotateIcon (icon, iconType, files);
            }
            if (retVal != null) {
                return retVal;
            }
            
            
            retVal = icon;
            FileSystem fs = getDelegateFileSystem(files);                        
            if (fs != null) {
                Set transformedSet = new LazySet (files);
                retVal = fs.getStatus().annotateIcon(icon, iconType, transformedSet);                                
            }
            
            return retVal;
        }

        public String annotateName(String name, Set files) {
            String retVal = null;
            Iterator it = annotationProviders.allInstances ().iterator ();
            while (retVal == null && it.hasNext ()) {
                AnnotationProvider ap = (AnnotationProvider)it.next ();
                retVal = ap.annotateName (name, files);
            }
            if (retVal != null) {
                return retVal;
            }
            retVal = name;
            
            Set transformedSet = new LazySet (files);
            FileSystem fs = getDelegateFileSystem(files);                        
            if (fs != null) {
                retVal = fs.getStatus().annotateName(name, transformedSet);
            }
            return retVal;
        }

        
        public String annotateNameHtml(String name, Set files) {
            name = htmlEncode(name);
            String retVal = null;
            Iterator it = annotationProviders.allInstances ().iterator ();
            while (retVal == null && it.hasNext ()) {
                AnnotationProvider ap = (AnnotationProvider)it.next ();
                retVal = ap.annotateNameHtml (name, files);
            }
            if (retVal == null) {
                retVal = name;
                
                FileSystem fs = getDelegateFileSystem(files);
                if (fs != null) {
                    if (fs != null && fs.getStatus() instanceof FileSystem.HtmlStatus) {
                        Set transformedSet = new LazySet(files);
                        retVal = ((FileSystem.HtmlStatus) fs.getStatus()).annotateNameHtml(name, transformedSet);
                    }
                }
            }
            return retVal;
        }
    }
    
    private static String htmlEncode(String name) {
        String retval = name;
        if (retval.indexOf('<') != -1) {
            retval = lt.matcher(retval).replaceAll("&lt;"); // NOI18N
        }
        if (retval.indexOf('>') != -1) {
            retval = gt.matcher(retval).replaceAll("&gt;"); // NOI18N
        }
                
        return retval;
    }
    
    
    private static final class Replace implements Serializable {
        static final long serialVersionUID = 50485340814380L;
        
        public Object readResolve () {
            return MasterFileSystem.getDefault();
        }
    } // end of Replace
    
    final static class LazySet implements Set {
        private Set obj_files;
        private boolean initialized = false;
        private Iterator it = null;
        
        LazySet(Set obj_files) {
            this.obj_files = obj_files;
        }
        
        synchronized private void lazyInitialization() {
            if (!initialized) {
                Set transformedSet = Utils.transformSet(obj_files);                
                obj_files = transformedSet;
                initialized = true;
            }
        }
        
        public boolean add(Object o) {
            throw new UnsupportedOperationException ();
        }
        
        public boolean addAll(Collection c) {
            throw new UnsupportedOperationException ();
        }
        
        public void clear() {
            throw new UnsupportedOperationException ();
        }
        
        public boolean contains(Object o) {
            lazyInitialization();
            return obj_files.contains(o);
        }
        
        public boolean containsAll(Collection c) {
            lazyInitialization();
            return obj_files.containsAll(c);
        }
        
        public boolean isEmpty() {
            lazyInitialization();
            return obj_files.isEmpty();
        }
        
        public Iterator iterator() {            
            if (initialized) {
                it = obj_files.iterator();
            } else {
                it = new Iterator() {
                    private final Iterator originalIterator = obj_files.iterator();        

                    public void remove() {
                        throw new UnsupportedOperationException ();
                    }

                    public Object next() {
                        MasterFileObject mfo = (MasterFileObject)originalIterator.next();
                        return mfo.getDelegate().get (true);
                    }

                    public boolean hasNext() {
                        return originalIterator.hasNext();
                    }

                };                    
            }
            
            return it;
        }
        
        public boolean remove(Object o) {
            lazyInitialization();
            return obj_files.remove(o);
        }
        
        public boolean removeAll(Collection c) {
            lazyInitialization();
            return obj_files.removeAll(c);
        }
        
        public boolean retainAll(Collection c) {
            lazyInitialization();
            return obj_files.retainAll(c);
        }
        
        public int size() {
            lazyInitialization();
            return obj_files.size();
        }
        
        public Object[] toArray() {
            lazyInitialization();
            return obj_files.toArray();
        }
        
        public Object[] toArray(Object[] a) {
            lazyInitialization();
            return obj_files.toArray(a);
        }
    }    
}
