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

import org.openide.util.SharedClassObject;
import org.openide.util.actions.SystemAction;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;

import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.StringTokenizer;


/**
 * This convenience implementation does much of the hard work of
 * <code>FileSystem</code> and is generally more pleasant to create
 * subclasses of.
 *
 * <p>It caches information about the filesystem in memory and periodically refreshes its content. Many other
 * operations are performed in a safer manner so as to reuse experience of NetBeans developers; should be
 * substantially simpler to subclass than {@link FileSystem} itself.
 *
 * <p>Besides what is mentioned here, the <code>AbstractFileSystem</code> subclass be configurable as a JavaBean
 * (e.g. server address, prefix, whatever it needs).
 *
 * <p>First of all, you need not separately implement a {@link FileObject} - this is taken care of for you. You
 * need only provide some information about how essential actions should be carried out.
 *
 * <p>You should implement {@link #getDisplayName()} to set the display name.
 *
 * <p>You may cause the filesystem to automatically refresh itself at periodic intervals, in case it is not
 * possible to be notified of changes directly. To do so, call {@link #setRefreshTime(int)}, e.g. from the
 * constructor.
 *
 * <p>{@link #refreshRoot()} must be called if the filesystem supports changing of its root directory (as the
 * local filesystem does, for example).
 *
 * <p>Most of the meat of the implementation is provided by setting appropriate values for four protected
 * variables, which should be initialized in the constructor; each represents an implementation of a separate
 * interface handling one aspect of the filesystem.
 *
 * <h4>List member</h4>
 *
 * The variable {@link #list} should contain an implementation of {@link AbstractFileSystem.List}. This only
 * specifies a way of accessing the list of children in a folder.
 *
 * <h4>Info member</h4>
 *
 * {@link #info} contains an {@link AbstractFileSystem.Info} which provides basic file statistics.
 *
 * <p>It also specifies the raw implementation of two basic types of actions: getting input and output streams
 * for the file; and locking and unlocking it physically (optional and not to be confused with locking within
 * the IDE).
 *
 * <h4>Change member</h4>
 *
 * {@link #change} is an {@link AbstractFileSystem.Change} which provides the interface to create new folders
 * and files; delete them; and rename them (within the same directory).
 *
 * <h4>Attribute member</h4>
 *
 * {@link #attr} is an {@link AbstractFileSystem.Attr} allowing the IDE to read and write serializable
 * attributes (meta-information) to be associated with the file. Such attributes are not much used any more, but
 * they are occasionally.
 *
 * <p>There is a default implementation in {@link DefaultAttributes} which stores attributes in a file called
 * <code>.nbattrs</code> in each folder for which a file has some attributes, using XML augmented by Java
 * serialization, though the regular filesystems in the IDE now instead store all attributes globally in
 * <code>$userdir/var/attributes.xml</code>. If you do use <code>DefaultAttributes</code>, use it not only for
 * {@link #attr} but also for {@link #list} (passing in a separate private {@link AbstractFileSystem.List}
 * implementation to its constructor) in order to filter the <code>.nbattrs</code> files from view.
 */
public abstract class AbstractFileSystem extends FileSystem {
    /** generated Serialized Version UID */
    static final long serialVersionUID = -3345098214331282438L;

    /** system actions for this FS if it has refreshTime != 0 */
    private static SystemAction[] SYSTEM_ACTIONS;

    /** system actions for this FS */
    private static final SystemAction[] NO_SYSTEM_ACTIONS = new SystemAction[] {  };

    /** cached last value of Enumeration which holds resource name (enumeration like StringTokenizer)*/
    static transient private PathElements lastEnum;

    /** root object for the filesystem */
    private transient AbstractFileObject root;

    /** refresher */
    private transient RefreshRequest refresher;

    /** Provider of hierarchy of files. */
    protected List list;

    /** Methods for modification of files. */
    protected Change change;

    /** Methods for moving of files. This field can be left null if the filesystem
    * does not require special handling handling of FileObject.move and is satified
    * with the default implementation.
    */
    protected Transfer transfer;

    /** Methods for obtaining information about files. */
    protected Info info;

    /** Handling of attributes for files. */
    protected Attr attr;

    /**
     * Actually implements contract of FileSystem.refresh().
     */
    public void refresh(boolean expected) {
        Enumeration en = getAbstractRoot().existingSubFiles(true);

        while (en.hasMoreElements()) {
            FileObject fo = (FileObject) en.nextElement();
            fo.refresh(expected);
        }
    }

    /* Provides a name for the system that can be presented to the user.
    * @return user presentable name of the filesystem
    */
    public abstract String getDisplayName();

    /* Getter for root folder in the filesystem.
    *
    * @return root folder of whole filesystem
    */
    public FileObject getRoot() {
        return getAbstractRoot();
    }

    /* Finds file when its name is provided.
    *
    * @param aPackage package name where each package is separated by a dot
    * @param name name of the file (without dots) or <CODE>null</CODE> if
    *    one want to obtain name of package and not file in it
    * @param ext extension of the file or <CODE>null</CODE> if one needs
    *    package and not file name
    *
    * @warning when one of name or ext is <CODE>null</CODE> then name and
    *    ext should be ignored and scan should look only for a package
    *
    * @return FileObject that represents file with given name or
    *   <CODE>null</CODE> if the file does not exist
    */
    public FileObject find(String aPackage, String name, String ext) {
        // create enumeration of name to look for
        StringTokenizer st = new StringTokenizer(aPackage, "."); // NOI18N

        if ((name == null) || (ext == null)) {
            // search for folder, return the object only if it is folder
            FileObject fo = getAbstractRoot().find(st);

            return ((fo != null) && fo.isFolder()) ? fo : null;
        } else {
            Enumeration en = org.openide.util.Enumerations.concat(
                    st, org.openide.util.Enumerations.singleton(name + '.' + ext)
                );

            // tries to find it (can return null)
            return getAbstractRoot().find(en);
        }
    }

    /* Finds file when its resource name is given.
    * The name has the usual format for the {@link ClassLoader#getResource(String)}
    * method. So it may consist of "package1/package2/filename.ext".
    * If there is no package, it may consist only of "filename.ext".
    *
    * @param name resource name
    *
    * @return FileObject that represents file with given name or
    *   <CODE>null</CODE> if the file does not exist
    */
    public FileObject findResource(String name) {
        if (name.length() == 0) {
            return getAbstractRoot();
        }

        /**Next piece of code is preformance enhancement; lastEnum = last value cache;
         PathElements is StringTokenizer wrapper that caches individual elements*/
        PathElements local = lastEnum;

        if ((local == null) || !local.getOriginalName().equals(name)) {
            local = new PathElements(name);
            lastEnum = local;
        }

        return getAbstractRoot().find(local.getEnumeration());
    }

    /** Creates Reference. In FileSystem, which subclasses AbstractFileSystem, you can overload method
    * createReference(FileObject fo) to achieve another type of Reference (weak, strong etc.)
    * @param fo is FileObject. It`s reference yourequire to get.
    * @return Reference to FileObject
    */
    protected Reference createReference(FileObject fo) {
        return (new WeakReference(fo));
    }

    /** This method allows to find Reference to resourceName
    * @param resourceName is name of resource
    * @return Reference to resourceName
    */
    protected final Reference findReference(String resourceName) {
        if (resourceName.length() == 0) {
            return null;
        } else {
            StringTokenizer tok = new StringTokenizer(resourceName, "/"); // NOI18N

            return getAbstractRoot().findRefIfExists(tok);
        }
    }

    /*
    * @return true if RefreshAction should be enabled
    */
    boolean isEnabledRefreshFolder() {
        return (refresher != null);
    }

    /* Action for this filesystem.
    *
    * @return refresh action
    */
    public SystemAction[] getActions() {
        if (!isEnabledRefreshFolder()) {
            return NO_SYSTEM_ACTIONS;
        } else {
            if (SYSTEM_ACTIONS == null) {
                try {
                    ClassLoader l = (ClassLoader) org.openide.util.Lookup.getDefault().lookup(ClassLoader.class);

                    if (l == null) {
                        l = getClass().getClassLoader();
                    }

                    Class c = Class.forName("org.openide.actions.FileSystemRefreshAction", true, l); // NOI18N
                    SystemAction ra = (SystemAction) SharedClassObject.findObject(c, true);

                    // initialize the SYSTEM_ACTIONS
                    SYSTEM_ACTIONS = new SystemAction[] { ra };
                } catch (Exception ex) {
                    // ok, we are probably running in standalone mode and
                    // classes needed to initialize the RefreshAction are
                    // not available
                    SYSTEM_ACTIONS = NO_SYSTEM_ACTIONS;
                }
            }

            return SYSTEM_ACTIONS;
        }
    }

    /** Set the number of milliseconds between automatic
    * refreshes of the directory structure.
    *
    * @param ms number of milliseconds between two refreshes; if <code><= 0</code> then refreshing is disabled
    */
    protected synchronized final void setRefreshTime(int ms) {
        if (refresher != null) {
            refresher.stop();
        }

        if ((ms <= 0) || (System.getProperty("netbeans.debug.heap") != null)) {
            refresher = null;
        } else {
            refresher = new RefreshRequest(this, ms);
        }
    }

    /** Get the number of milliseconds between automatic
    * refreshes of the directory structure.
    * By default, automatic refreshing is disabled.
    * @return the number of milliseconds, or <code>0</code> if refreshing is disabled
    */
    protected final int getRefreshTime() {
        RefreshRequest r = refresher;

        return (r == null) ? 0 : r.getRefreshTime();
    }

    /** Instruct the filesystem
    * that the root should change.
    * A fresh root is created. Subclasses that support root changes should use this.
    *
    * @return the new root
    */
    final synchronized AbstractFileObject refreshRootImpl() {
        if (root != null) {
            root.validFlag = false;
        }

        root = createFileObject(null, ""); // NOI18N

        return root;
    }

    /** Instruct the filesystem
    * that the root should change.
    * A fresh root is created. Subclasses that support root changes should use this.
    *
    * @return the new root
    */
    protected final FileObject refreshRoot() {
        return refreshRootImpl();
    }

    /** @deprecated Just for backward compatibility, renamed during runtime to refreshRoot
     * and made available publicly
     */
    private AbstractFileObject r3fr3shRoot() {
        return refreshRootImpl();
    }

    /** Allows subclasses to fire that a change occured in a
    * file or folder. The change can be "expected" when it is
    * a result of an user action and the user knows that such
    * change should occur.
    *
    * @param name resource name of the file where the change occured
    * @param expected true if the user initiated change and expects it
    */
    protected final void refreshResource(String name, boolean expected) {
        AbstractFileObject fo = (AbstractFileObject) findResourceIfExists(name);

        if (fo != null) {
            // refresh and behave like the changes is expected
            fo.refresh(null, null, true, expected);
        }
    }

    /**
     * For the FileObject specified as parameter, returns the recursive enumeration
     * of existing children fileobjects (both folders and data). It doesn't create
     * any new FileObject instances. Direct children are at the begining of the enumeration.
     * @param fo the starting point for the recursive fileobject search
     * @return enumeration of currently existing fileobjects.
     */
    protected final Enumeration existingFileObjects(FileObject fo) {
        class OnlyValidAndDeep implements org.openide.util.Enumerations.Processor {
            public Object process(Object obj, Collection toAdd) {
                if (obj instanceof Reference) {
                    obj = ((Reference) obj).get();
                }

                AbstractFileObject file = (AbstractFileObject) obj;

                if (file != null) {
                    FileObject[] arr = file.subfiles();

                    // make the array weak
                    for (int i = 0; i < arr.length; i++) {
                        toAdd.add(new WeakReference(arr[i]));
                    }

                    return file.isValid() ? file : null;
                }

                return null;
            }
        }

        return org.openide.util.Enumerations.removeNulls(
            org.openide.util.Enumerations.queue(org.openide.util.Enumerations.singleton(fo), new OnlyValidAndDeep())
        );
    }

    /**
    * @return if value of lastModified should be cached
    */
    boolean isLastModifiedCacheEnabled() {
        return true;
    }

    /* Finds file when its resource name is given.
    * The name has the usual format for the {@link ClassLoader#getResource(String)}
    * method. So it may consist of "package1/package2/filename.ext".
    * If there is no package, it may consist only of "filename.ext".
    *
    * @param name resource name
    *
    * @return FileObject that represents file with given name or
    *   <CODE>null</CODE> if the file does not exist
    */
    private FileObject findResourceIfExists(String name) {
        if (name.length() == 0) {
            return getAbstractRoot();
        } else {
            StringTokenizer tok = new StringTokenizer(name, "/"); // NOI18N

            return getAbstractRoot().findIfExists(tok);
        }
    }

    /** Hooking method to allow MultiFileSystem to be informed when a new
    * file object is created. This is the only method that creates AbstractFileObjects.
    *
    * @param parent parent object
    * @param name of the object
    */
    AbstractFileObject createFileObject(AbstractFileObject parent, String name) {
        return new AbstractFileObject(this, parent, name);
    }

    /**
     * Creates root object for the fs.
     */
    final AbstractFileObject getAbstractRoot() {
        synchronized (this) {
            if (root == null) {
                return refreshRootImpl();
            }
        }

        return root;
    }

    /** Writes the common fields and the state of refresher.
    */
    private void writeObject(ObjectOutputStream oos) throws IOException {
        ObjectOutputStream.PutField fields = oos.putFields();

        fields.put("change", change); // NOI18N
        fields.put("info", info); // NOI18N
        fields.put("attr", attr); // NOI18N
        fields.put("list", list); // NOI18N
        fields.put("transfer", transfer); // NOI18N
        oos.writeFields();

        oos.writeInt(getRefreshTime());
    }

    /** Reads common fields and state of refresher.
    */
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField fields = ois.readFields();

        Object o1 = readImpl("change", fields); // change // NOI18N
        Object o2 = readImpl("info", fields); // info // NOI18N
        Object o3 = readImpl("attr", fields); // attr // NOI18N
        Object o4 = readImpl("list", fields); // list // NOI18N
        Object o5 = readImpl("transfer", fields); // transfer // NOI18N

        change = (Change) o1;
        info = (Info) o2;
        attr = (Attr) o3;
        list = (List) o4;
        transfer = (Transfer) o5;

        setRefreshTime(ois.readInt());
    }

    //
    // Backward compatibility methods
    //

    /** Reads object from input stream, if it is
    * LocalFileSystem or JarFileSystem then replaces the object
    * by its Impl.
    */
    static Object readImpl(String name, ObjectInputStream.GetField fields)
    throws ClassNotFoundException, IOException {
        Object o = fields.get(name, null);

        if (o instanceof LocalFileSystem) {
            return new LocalFileSystem.Impl((LocalFileSystem) o);
        } else if (o instanceof JarFileSystem) {
            return new JarFileSystem.Impl((JarFileSystem) o);
        }

        return o;
    }

    /**
     * This method is called from AbstractFileObject.isVirtual. Tests if file
     * really exists or is missing. Some operation on it may be restricted if returns true.
     * @param name of the file
     * @return  true indicates that the file is missing.
     * @since 1.9
     */
    protected boolean checkVirtual(String name) {
        return false;
    }

    /** Tests if this file can be written to.
     * @param name resource name
     * @return true if this file can be written, false if not.
     * @since 3.31
     */
    protected boolean canWrite(String name) {
        AbstractFileObject afo = (AbstractFileObject) this.findResource(name);

        return (afo != null) ? afo.superCanWrite() : false;
    }

    /** Tests if this file can be read.
     * @param name resource name
     * @return true if this file can be read, false if not.
     * @since 3.31
     */
    protected boolean canRead(String name) {
        AbstractFileObject afo = (AbstractFileObject) this.findResource(name);

        return (afo != null) ? afo.superCanRead() : false;
    }

    /** Mark the file as being important or unimportant.
    * @param name the file to mark
    * @param important true indicates that file is important, false conversely
    * file is unimportant.
    * @since 1.9
    */
    protected void markImportant(String name, boolean important) {
        if (!important && (info != null)) {
            info.markUnimportant(name);
        }
    }

    /** Provides access to the hierarchy of resources.
    */
    public interface List extends Serializable {
        /** @deprecated Only public by accident. */

        /* public static final */ long serialVersionUID = -6242105832891012528L;

        /** Get a list of children files for a given folder.
        *
        * @param f the folder, by name; e.g. <code>top/next/afterthat</code>
        * @return a list of children of the folder, as <code>file.ext</code> (no path)
        *   the array can contain <code>null</code> values that will be ignored
        */
        public String[] children(String f);
    }

    /** Controls modification of files.
    */
    public interface Change extends Serializable {
        /** @deprecated Only public by accident. */

        /* public static final */ long serialVersionUID = -5841597109944924596L;

        /** Create new folder.
        * @param name full name of new folder, e.g. <code>topfolder/newfolder</code>
        * @throws IOException if the operation fails
        */
        public void createFolder(String name) throws IOException;

        /** Create new data file.
        *
        * @param name full name of the file, e.g. <code>path/from/root/filename.ext</code>
        *
        * @exception IOException if the file cannot be created (e.g. already exists)
        */
        public void createData(String name) throws IOException;

        /** Rename a file.
        *
        * @param oldName old name of the file; fully qualified
        * @param newName new name of the file; fully qualified
        * @throws IOException if it could not be renamed
        */
        public void rename(String oldName, String newName)
        throws IOException;

        /** Delete a file.
        *
        * @param name name of file; fully qualified
        * @exception IOException if the file could not be deleted
        */
        public void delete(String name) throws IOException;
    }

    /** Controls on moving of files. This is additional interface to
    * allow filesystem that require special handling of move to implement
    * it in different way then is the default one.
    */
    public interface Transfer extends Serializable {
        /** @deprecated Only public by accident. */

        /* public static final */ long serialVersionUID = -8945397853892302838L;

        /** Move a file.
        *
        * @param name of the file on current filesystem
        * @param target move implementation
        * @param targetName of target file
        * @exception IOException if the move fails
        * @return false if the method is not able to handle the request and
        *    default implementation should be used instead
        */
        public boolean move(String name, Transfer target, String targetName)
        throws IOException;

        /** Copy a file.
        *
        * @param name of the file on current filesystem
        * @param target target transfer implementation
        * @param targetName name of target file
        * @exception IOException if the copy fails
        * @return false if the method is not able to handle the request and
        *    default implementation should be used instead
        */
        public boolean copy(String name, Transfer target, String targetName)
        throws IOException;
    }

    /** Information about files.
    */
    public interface Info extends Serializable {
        /** @deprecated Only public by accident. */

        /* public static final */ long serialVersionUID = -2438286177948307985L;

        /**
        * Get last modification time.
        * @param name the file to test
        * @return the date of last modification
        */
        public Date lastModified(String name);

        /** Test if the file is a folder or contains data.
        * @param name name of the file
        * @return <code>true</code> if the file is folder, <code>false</code> if it is data
        */
        public boolean folder(String name);

        /** Test whether this file can be written to or not.
        * @param name the file to test
        * @return <CODE>true</CODE> if the file is read-only
        */
        public boolean readOnly(String name);

        /** Get the MIME type of the file. If filesystem has no special support
        * for MIME types then can simply return null. FileSystem can register
        * MIME types for a well-known extensions: FileUtil.setMIMEType(String ext, String mimeType)
        * or together with filesystem supply some resolvers subclassed from MIMEResolver.
        *
        * @param name the file to test
        * @return the MIME type textual representation (e.g. <code>"text/plain"</code>)
        * or null if no special support for recognizing MIME is implemented.
         */
        public String mimeType(String name);

        /** Get the size of the file.
        *
        * @param name the file to test
        * @return the size of the file in bytes, or zero if the file does not contain data (does not
        *  exist or is a folder).
        */
        public long size(String name);

        /** Get input stream.
        *
        * @param name the file to test
        * @return an input stream to read the contents of this file
        * @exception FileNotFoundException if the file does not exist or is invalid
        */
        public InputStream inputStream(String name) throws FileNotFoundException;

        /** Get output stream.
        *
        * @param name the file to test
        * @return output stream to overwrite the contents of this file
        * @exception IOException if an error occurs (the file is invalid, etc.)
        */
        public OutputStream outputStream(String name) throws IOException;

        /** Lock the file.
        * May do nothing if the underlying storage does not support locking.
        * This does not affect locking using {@link FileLock} within the IDE, however.
        * @param name name of the file
        * @throws FileAlreadyLockedException if the file is already locked
        */
        public void lock(String name) throws IOException;

        /** Unlock the file.
        * @param name name of the file
        */
        public void unlock(String name);

        /** Mark the file as being unimportant.
         * If not called, the file is assumed to be important.
         * <em>Deprecated</em> and new implementations need not do anything.
         * @param name the file to mark
         */
        public void markUnimportant(String name);
    }

    /** Handle attributes of files.
    */
    public interface Attr extends Serializable {
        /** @deprecated Only public by accident. */

        /* public static final */ long serialVersionUID = 5978845941846736946L;

        /** Get the file attribute with the specified name.
        * @param name the file
        * @param attrName name of the attribute
        * @return appropriate (serializable) value or <CODE>null</CODE> if the attribute is unset (or could not be properly restored for some reason)
        */
        public Object readAttribute(String name, String attrName);

        /** Set the file attribute with the specified name.
        * @param name the file
        * @param attrName name of the attribute
        * @param value new value or <code>null</code> to clear the attribute. Must be serializable, although particular filesystems may or may not use serialization to store attribute values.
        * @exception IOException if the attribute cannot be set. If serialization is used to store it, this may in fact be a subclass such as {@link NotSerializableException}.
        */
        public void writeAttribute(String name, String attrName, Object value)
        throws IOException;

        /** Get all file attribute names for the file.
        * @param name the file
        * @return enumeration of keys (as strings)
        */
        public Enumeration attributes(String name);

        /** Called when a file is renamed, to appropriately update its attributes.
        * @param oldName old name of the file
        * @param newName new name of the file
        */
        public void renameAttributes(String oldName, String newName);

        /** Called when a file is deleted, to also delete its attributes.
        *
        * @param name name of the file
        */
        public void deleteAttributes(String name);
    }
}
