/*
* Copyright 2003-2010 Tufts University  Licensed under the
 * Educational Community 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.osedu.org/licenses/ECL-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.
 */

/*
 * Cabinet.java
 *
 * Created on September 17, 2003, 10:04 AM
 *
 *  The software contained in this file is copyright 2003 by Mark J. Norton, all rights reserved.
 */

package tufts.oki.localFiling;
import java.io.*;
import java.util.*;
import tufts.oki.shared.*;
import osid.OsidException;

/**
 *  LocalCabinet corresponds to a directory in a local filing system.  Root cabinets are
 *  treated specially in that they have a rootBase.  The rootBase is the path to the root
 *  of the filing system.  Usually this is empty or contains the drive letter, if a PC.
 *
 * @author  Mark Norton
 * @author  Scott Fraize
 *
 */
public class LocalCabinet extends LocalCabinetEntry implements osid.filing.Cabinet
{
    private static final boolean UseCache = false;
    private static final Map<String,LocalCabinet> Cache;

    static { Cache = UseCache ? new java.util.concurrent.ConcurrentHashMap() : null; }

    //private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LocalCabinet.class);
    
    /* parent is inherited from Cabinet Entry.  */
    private final SortedSet children;
    //private tufts.oki.shared.Properties properties = null;
    private volatile boolean initialized = false;    //  True if expanded to include entries.
    private boolean open = false;           //  Indicates open or closed status for UI. 
    private final File dir;                //  The local directory being modeled.
    private final String rootBase;         //  Set if this is a root.

    private final String _cacheKey;

    public static LocalCabinet instance(String path, osid.shared.Agent owner, osid.filing.Cabinet parent) {
        if (UseCache) {
            LocalCabinet cabinet = Cache.get(path);
            if (cabinet == null) {
                cabinet = new LocalCabinet(path, owner, parent);
                Cache.put(path, cabinet);
            } else {
                if (Log.isDebugEnabled()) Log.debug("instance cache: " + cabinet);
            }
            return cabinet;
        } else
            return new LocalCabinet(path, owner, parent);
    }

    public static LocalCabinet instance(File file, osid.shared.Agent owner, osid.filing.Cabinet parent) {
        return instance(file.getPath(), owner, parent);
    }
    
    public String toString() {
        String name = "<unknown>";
        try {
            name = getDisplayName();
        } catch (Throwable t) {
            name = t.toString();
        }
        if (rootBase == null) {
            return String.format("LocalCabinet@%07x[%s](%s) n=%02d",
                                 System.identityHashCode(this),
                                 _cacheKey,
                                 name,
                                 children == null ? -1 : children.size()
                                 );
        } else {
            return String.format("LocalCabinet@%07x{%s} n=%02d",
                                 System.identityHashCode(this),
                                 rootBase,
                                 children == null ? -1 : children.size()
                                 );
        }
    }

    private void debug(String fmt, Object... args) {
        if (Log.isDebugEnabled())
            Log.debug(this + ": " + String.format(fmt, args));
    }
    
     /**
      * Initializes the feilds interited from CabinetEntry and adds a vector of children.
      * Creates a Properties object to hold properties associated with this cabinet.
      *
      * @author Mark Norton
      *
      */
    public LocalCabinet(String displayName, osid.shared.Agent agentOwner, osid.filing.Cabinet parent){
        super (displayName, agentOwner, parent);

        _cacheKey = displayName;

        children = new TreeSet(new LocalCabinetEntryComparator());
        //FilingCabinetType type = new FilingCabinetType();
        //properties = new Properties(type);

        //  Create a File for this cabinet.  Treat roots specially.
        dir = new File (displayName);
        //System.out.println ("LocalCabinet creator - display name: " + dir.getName());
        if (parent == null) {
            rootBase = dir.getPath();
            updateDisplayName (rootBase);
        } else {
            updateDisplayName(dir.getName());
            rootBase = null;
        }

        if (tufts.vue.DEBUG.IO && Log.isDebugEnabled()) {
            if (parent != null)
                Log.debug("CREATED in " + parent + ": " + this);
            else
                Log.debug("CREATED " + this);
        }
    }
    
    @Override
    public final boolean isCabinet() {
        return true;
    }
    
   /**
    *   Add the entry to the list of children.  
    *
    *   @author Mark Norton
    */
    public void add(osid.filing.CabinetEntry entry, java.lang.String name) throws osid.filing.FilingException {
        //System.out.println ("add - new display name: " + name);
        /* Update the display name in entry with name.  Does this make sense?  */
        entry.updateDisplayName (name);
        
        /*  Add the element to the Vector array.  */
        //Log.debug(this + " add " + entry + " [" + name + "]");
        children.add(entry);
    }

   /**
    *   Add the entry to the list of children.  
    *
    *   @author Mark Norton
    */
    public void add(osid.filing.CabinetEntry entry) throws osid.filing.FilingException {  
        /*  Add the element to the Vector array.  */
        children.add(entry);
    }

    /*  
     *  Prints a list of the cabinet entries to stdout.  Largely used for debugging purposes.
     *  This method is not part of the OSID interface definition for Cabinet.
     *
     *  @author Mark Norton
     */
    public void list () throws osid.filing.FilingException {
        if (children == null) {
            System.out.println ("The Cabinet is empty.");
        }
        int len = children.size();
 //       for (int i = 0; i < len; i++) {
        Iterator i = children.iterator();
        while(i.hasNext()) {
            LocalCabinetEntry entry = (LocalCabinetEntry) i.next();
            if (entry instanceof LocalCabinet)
                System.out.println ("Cabinet " + i + ":  " + entry.getDisplayName());
            else if (entry instanceof LocalByteStore)
                System.out.println ("Byte Store " + i + ":  " + entry.getDisplayName());
        }
  //      }
            
    }
    
    /**  
     *  Create a new ByteStore, add it to this Cabinet.
     *
     *  @author Mark Norton
     *
     *  @return A new ByteStore with the name provided and this cabinet as parent.
     */
    public osid.filing.ByteStore createByteStore(String name) {
        
        //System.out.println ("createByteStore - name: " + name);

        osid.filing.ByteStore bs = null;
        try {
             bs = new LocalByteStore(name, this);
             //if (Log.isDebugEnabled()) Log.debug("CREATED " + bs);
             this.add (bs);
        }
        catch (osid.OsidException ex) {
        }
        return bs;
    }
    
    /*  
     *  The oldByteStore is copied into the new ByteStore.
     *
     *  @author Mark Norton
     *
     *  @return A new ByteStore with the name provided and this cabinet as parent.
     */
    public osid.filing.ByteStore copyByteStore(String name, osid.filing.ByteStore oldByteStore) throws osid.filing.FilingException {

        osid.filing.ByteStore bs = (osid.filing.ByteStore) createByteStore (name);
        byte[] byte_store = ((LocalByteStore)oldByteStore).getBytes();
        byte[] new_store = (byte[])byte_store.clone();
        bs.write (new_store);
        return bs;
    }
    
    /**
     *  Currently unimplemented.  A lot depends on the eventual use of the filing
     *  system implemented by these classes.  Not all systems will have a restriction
     *  on available and used byes.
     *
     *  @author Mark Norton
     *
     *  @return The number of bytes available in this cabinet.
     */
    public long getAvailableBytes() throws osid.filing.FilingException {
         throw new osid.filing.FilingException (osid.filing.FilingException.UNIMPLEMENTED);
    }
        
    /**
     *  Currently unimplemented.  A lot depends on the eventual use of the filing
     *  system implemented by these classes.  Not all systems will have a restriction
     *  on available and used byes.
     *  <p>
     *  This could be implemented as the sum of of the sizes of the ByteStores in this
     *  directory, but its not clear what use that would be.
     *
     *  @author Mark Norton
     *
     *  @return The number of bytes used in this cabinet.
     */
    public long getUsedBytes() throws osid.filing.FilingException {
        throw new osid.filing.FilingException (osid.filing.FilingException.UNIMPLEMENTED);
    }
    
    /**
     *  Create a new cabinet entry with 
     *  the agentOwner of this new cabinet as this owner of this Cabinet.
     *
     *  @author Mark Norton
     *
     *  @return A new cabinet with the given displayName.
     */
    public osid.filing.Cabinet createCabinet(String displayName) throws osid.filing.FilingException {
        //System.out.println ("createCabinet - name: " + displayName);

        try {
             osid.shared.Agent agentOwner = super.getCabinetEntryAgent();
             LocalCabinet entry = instance(displayName, agentOwner, this);
             //LocalCabinet entry = new LocalCabinet(displayName, agentOwner, this);
             //entry.updateDisplayName(displayName);
            
            /*  Add the element to the Vector array.  */
            children.add(entry);
            return (osid.filing.Cabinet) entry;
         }
         catch (OsidException e1) {
             throw new osid.filing.FilingException (osid.filing.FilingException.OPERATION_FAILED);
             //throw e1;
         }
    }
    
   /**
    *   Creates an iterator which lists the entries in this cabinet. A check is made to
    *   see if this cabinet was previously opened.  If not, it is opened and initialized
    *   with the entries contained in it.
    *
    *   @author Mark Norton
    *
    *   @return Return an iterator for the entries in this cabinet.    
    */
    public synchronized osid.filing.CabinetEntryIterator entries() throws osid.filing.FilingException {

        if (!initialized)
            loadChildren();
        
        return new LocalCabinetEntryIterator(children);
    }

    private void loadChildren() throws osid.filing.FilingException
    {
        //tufts.Util.printStackTrace("ENTRIES " + this + "; " + getDisplayName());
        debug("initializing");
        //  Initialize the directory by getting all entries contained in it.
        String[] files = null;

        //System.out.println ("Open Directory: " + this.cwd.getDisplayName());
        files = dir.list();
        if (files == null)
            throw new osid.filing.FilingException(osid.filing.FilingException.NOT_A_CABINET + ": no files");
        debug("listed, n=%d", files.length);

        String rootBase = getRootBase();
        String path = rootBase + getFullName();

        //System.out.println ("openDirectory - path name: " + path);
        for (int i = 0; i < files.length; i++) {
            //File temp = new File (cwd.getPath(), files[i]);
            File temp = new File (rootBase+getFullName(), files[i]);

            //System.out.println ("openDirectory - new file: " + rootBase + getFullName() + files[i]);

            String absolute = null;
            if (isRootCabinet())
                absolute = path + temp.getName();
            else
                absolute = path + separator() + temp.getName();

            if (temp.isDirectory()) {

                //System.out.println ("\tDir " + i + ": " + temp.getName() + "\t" + absolute);
                //cwd.createCabinet (temp.getName());
                createCabinet (absolute);
            }
            else if (temp.isFile()) {
                //System.out.println ("\tFile " + i + ": " + temp.getName() + "\t" + absolute);
                //cwd.createByteStore (temp.getName());
                createByteStore (absolute);
            }

            //  Unknown cases are ignored.
        }

        //  Set flags.
        open = true;
        initialized = true;
        debug("initialized");
        //new Throwable("HERE").printStackTrace();
    }
    
    
    /**
     *  Get a cabinet entry given its name.
     *
     *  @author Mark Norton
     *
     *  @return The cabinet entry with the desired display name.  Throws ITEM_DOES_NOT_EXIST if name is unknown.
     */
    public osid.filing.CabinetEntry getCabinetEntryByName(String name) throws osid.filing.FilingException {
         Iterator i = children.iterator();
        while(i.hasNext()) {
//        for (int i = 0; i < children.size(); i++) {
            LocalCabinetEntry entry = (LocalCabinetEntry) i.next();
            //System.out.println ("getCabinetEntryByName - scan: " + entry.getDisplayName());
            if (name.compareTo (entry.getDisplayName()) == 0) {
                return (osid.filing.CabinetEntry) entry;
            }
        }
        /*  ITEM_DOES_NOT_EXIST is not exactly the right sentiment for not finding the entry.  */
        throw new osid.filing.FilingException (osid.filing.FilingException.ITEM_DOES_NOT_EXIST);
    }
    
    /**
     *  Get a cabinet entry given its identifier.  Throws ITEM_DOES_NOT_EXIST if Id is unknown.
     *
     *  @author Mark Norton
     *
     *  @return The cabinet entry corresponding to the identifier passed.
     */
    public osid.filing.CabinetEntry getCabinetEntryById(osid.shared.Id id) throws osid.filing.FilingException {
 Iterator i = children.iterator();
        while(i.hasNext()) {
     //   for (int i = 0; i < children.size(); i++) {
            LocalCabinetEntry entry = (LocalCabinetEntry) i.next();
            try {
                if (id.isEqual (entry.getId())) {
                    return (osid.filing.CabinetEntry) entry;
                }
            }
            catch (osid.shared.SharedException ex) {
                /*  Not exactly sure what could go wrong with an Id comparison,
                 *  but the compiler insists on catching this exception.
                 *  This will fall through to ITEM_DOES_NOT_EXIST exception throw.
                 */
            }
        }
        /*  ITEM_DOES_NOT_EXIST is not exactly the right sentiment for not finding the entry.  */
        throw new osid.filing.FilingException (osid.filing.FilingException.ITEM_DOES_NOT_EXIST);
    }
    
    /**
     *  Return the properties associated with this cabinet as a HashMap.  This is better
     *  implmented using the expanded Properties objects defined elsewhere.
     *
     *  @author Mark Norton
     *
     *  @return The property set of the Properties object associated with this cabinet as a Map.
     */
    public java.util.Map getProperties() throws osid.filing.FilingException {
        throw new UnsupportedOperationException(getClass() + ".getProperties");
//         // Properties found unsupported (uninitiailized) -- would throw NPE -- SMF 2007-10-10
//         HashMap map = properties.getPropertySet();
//         return (Map) map;
    }
    
    /**
     *  Get the root cabinet of this cabinet.
     *
     *  @author Mark Norton
     *
     *  @return The root cabinet of this cabinet by searching up the parent links.
     */
    public osid.filing.Cabinet getRootCabinet() {
        LocalCabinet cab = this;
        while (cab.getParent() != null)
            cab = (LocalCabinet)cab.getParent();
        return cab;
    }
    
    /**
     *  If this cabinet is a root, return rootBase.  Otherwise find the root
     *  and return rootBase.
     *
     *  @author Mark Norton
     */
    public String getRootBase() {
        if (super.getParent() == null)
            return rootBase;
        else {
            LocalCabinet root = (LocalCabinet) getRootCabinet();
            return root.getRootBase();
        }
    }
    
    /**
     *  Currently, this always returns true, since cabinets are always defined to be
     *  listable.  This could be made a property instead.
     *
     *  @author Mark Norton
     *
     *  @return True if this cabinet is listable.
     */
    public boolean isListable() throws osid.filing.FilingException {
        return true;
    }
    
    /**
     *  Currently, this always returns true, since cabinets are defined to be managable
     *  in this implementation.  It could be made a property instead.
     *
     *  @author Mark Norton
     *
     *  @return True if this cabinet is managable.
     */
    public boolean isManageable() throws osid.filing.FilingException {
        return true;
    }
    
    /**
     *  Return true if this is a root cabinet, ie., its parent is null.
     *
     *  @author Mark Norton
     *
     *  @return True if this is a root cabinet (parent == null).
     */
    public boolean isRootCabinet() throws osid.filing.FilingException {
        return (this.getParent() == null);
    }
    
    /**
     *  Remove the entry indicated from the children of this cabinet.  The entry is
     *  identified by comparing the Ids of the entry passed to the Id of the children
     *  present.  This assumes that such Ids are globally unique.
     *  <p>
     *  No error is thrown if entry is not present.
     *
     *  @author Mark Norton
     */
    public void remove(osid.filing.CabinetEntry entry) throws osid.filing.FilingException {

        osid.shared.Id entry_id = entry.getId();
         Iterator i = children.iterator();
        while(i.hasNext()) {
//        for (int i = 0; i < children.size(); i++) {
            LocalCabinetEntry ent = (LocalCabinetEntry)i.next();
            try {
                if (entry_id.isEqual (ent.getId())) {
                    children.remove (entry);
                }
            }
            catch (osid.shared.SharedException ex) {
                /*  Unlikely that isEqual() will throw a SharedException.  */
            }
        }
    }
    
    /**
     *  Check to see if the cabinet is empty.  This is largely used in remove and delete operations.
     *  Note:  This method is not part of the osid.filing interface definitions.
     *
     *  @author Mark Norton
     *
     *  @return True if this cabinet has no children of any kind.
     */
    public boolean isEmpty() {
        if (children == null)
            return true;
        if (children.size() == 0)
            return true;
        else
            return false;
    }
    
    /**
     *  Return the number of children this cabinent has.
     *  <p>
     *  This is an extension to osid.filing.Cabinet to support classes which imlement
     *  javax.swing.tree.TreeModel.
     *
     *  @author Mark Norton
     *
     *  @return the number of children in this cabinet.
     */
    public int getChildCount () {
        return children.size();
    }

    /**
     *  Check to see if this cabinet is open.
     *
     *  @author Mark Norton
     *
     *  @return true if the cabinet is open.
     */
    public boolean isOpen () {
        return this.open;
    }
    
    /**
     *  Set the open flag to the value given.
     *
     *  @author Mark Norton
     */
    public void setOpen (boolean flag) {
        this.open = flag;
    }
    
    /**
     *  Check to see if this cabinet is initialized.
     *
     *  @author Mark Norton
     *
     *  @return true if the cabinet is initialized.
     */
    public boolean isInitialized() {
        return this.initialized;
    }
    
    /**
     *  Set the initialized flag to the value given.
     *
     *  @author Mark Norton
     */
    public void setInitialized(boolean flag) {
        this.initialized = flag;
    }
    
    /**
     *  Get the file path to this cabinet entry.  The path name is built by walking
     *  the parent references all the way back to the root.
     *
     *  @author Mark Norton
     */
    public String xxgetPath () {
        return dir.getPath();
    }
    public String xxgetPath2() {
        Vector nodes = new Vector(100);
        String path = null;
        
        //  Collect the entry nodes between here and root.
        try {
            //nodes.add (getDisplayName());
            LocalCabinet ptr = this;
            while (ptr.getParent() != null) {
                nodes.add (ptr.getDisplayName());
                ptr = (LocalCabinet)ptr.getParent();
            }
            nodes.add ("/");  // add the root.
        }
        catch (osid.filing.FilingException ex1) {}
        
        //  Build the path name string.
        path = (String) nodes.elementAt(nodes.size()-1);
        for (int i = nodes.size()-2; i != -1; i--) {
            path += (String) nodes.elementAt(i) + "/";
        }
        
        return path;
    }

    /**
     *  Get the full file name for this entry, including path to local root.
     *  <p>
     *  Warning!  This name cannot be used to open local files, since it does
     *  not include rootBase.  The absolute name can be created by concatenating
     *  rootBase and getFullName().
     *
     *  @author Mark Norton
     */
    public String getFullName() {
        ArrayList nodes = new ArrayList(100);
        String path = null;
        //String sep = Character.toString(this.pathSeparatorChar());
        String sep = this.separator();
        
        //  Collect the entry nodes between here and root.
        try {
            //  Check for the root node case.
            if (this.getParent() == null) {
                return sep;
            }
            
            LocalCabinet ptr = this;
            while (ptr.getParent() != null) {
                nodes.add (0, ptr.getDisplayName());
                ptr = (LocalCabinet)ptr.getParent();
            }
            //nodes.add ("/");  // add the root.
        }
        catch (osid.filing.FilingException ex1) {}
        
        //  Build the path name string.
        path = sep + (String) nodes.get(0);
        for (int i = 1; i < nodes.size(); i++) {
            path += sep + (String) nodes.get(i);
        }
        
        return path;
    }    
    
    /**
     *  Get the File object associated with this cabinet entry.
     *
     *  @author Mark Norton
     */
    public File getFile() {
        return dir;
    }
    
    /**
     *  Return a URL string for this LocalByteStore.
     */
    public String getUrl() {
        try {
            return getFile().toURL().toString();
        } catch (Throwable t) {
            Log.debug("failed to create URL from file: " + getFile(), t);
            return getFile().toString();
        }
        //String fn = getFile().getAbsolutePath();
        //return"file://" + fn;
    }
    /**
     *  Get the string character that separates path nodes.
     */
    public String separator () {
        LocalCabinet root = null;

        root = (LocalCabinet) getRootCabinet();
        File file = root.getFile();
        return file.separator;
    }
    
    /**
     *  Get the char character that separates path nodes.
     */
    public char separatorChar () {
        LocalCabinet root = (LocalCabinet) getRootCabinet();
        File file = root.getFile();
        return file.separatorChar;
    }

    /**
     *  Rename the file corresponding to this byte store and update it's display
     *  name to the new name.
     *
     *  @author Mark Norton
     */
    public void rename (String absolute, String newName) throws osid.filing.FilingException {
        File dst = new File (absolute);
        dir.renameTo (dst);
        updateDisplayName (newName);
    }
}
