/*
  Copyright (C) 2007 Helge Hess

  This file is part of JOPE.

  JOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  JOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/
package org.opengroupware.jope.ofs;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.opengroupware.jope.appserver.core.WOResourceManager;
import org.opengroupware.jope.appserver.publisher.IJoContext;
import org.opengroupware.jope.appserver.publisher.IJoObject;
import org.opengroupware.jope.appserver.publisher.JoClass;
import org.opengroupware.jope.appserver.publisher.JoContainerResourceManager;
import org.opengroupware.jope.eocontrol.EODataSource;
import org.opengroupware.jope.ofs.fs.IOFSFileInfo;
import org.opengroupware.jope.ofs.fs.IOFSFileManager;

/**
 * OFSFolder
 * <p>
 * TBD: document
 */
public class OFSFolder extends OFSBaseObject
  implements IJoFolderish, IOFSLifecycleObject
{
  protected static ConcurrentHashMap<IOFSFileInfo, OFSFileContainerChildInfo>
    pathToChildInfo =
      new ConcurrentHashMap<IOFSFileInfo, OFSFileContainerChildInfo>(64);
  
  protected OFSFileContainerChildInfo childInfo;
  protected Map<String, Object> cacheNameToObject;
  
  protected WOResourceManager resourceManager;
  protected IJoContext context; /* required for the RM? */

  
  public Object awakeFromRestoration
    (OFSRestorationFactory _factory, Object _container,
     IOFSFileManager _fm, IOFSFileInfo _file,
     IJoContext _ctx)
  {
    // TBD: check whether we can remove the context from the controller. Its
    //      currently required by the resource manager, I think during the
    //      lookup phase (not 100% sure).
    this.context = _ctx;
    return this;
  }
  
  /* directory contents */
  
  public OFSFileContainerChildInfo childInfo() {
    if (this.childInfo == null) {
      IOFSFileInfo info = this.fileInfo();
      if (info == null) return null;
      
      /* check cache */

      if ((this.childInfo = pathToChildInfo.get(info)) != null) {
        // Hm, this does not seem to speedup the operation, even though we get
        // a good hitrate? Maybe the kernel cache is sufficient or the File
        // does some caching?
        long currentTimestamp = info.lastModified();
        if (currentTimestamp != this.childInfo.timestamp()) {
          // no gain in removing the old info? Will be overridden below
          this.childInfo = null;
        }
      }
      
      /* fetch item if cache was empty or item got changed */
      
      if (this.childInfo == null) {
        this.childInfo = OFSFileContainerChildInfo
          .infoForFile(this.fileManager(), this.fileInfo());
        if (this.childInfo != null) {
          this.childInfo.load(); /* ensure a threadsafe state */
          
          pathToChildInfo.put(info, this.childInfo);
        }
      }
    }
    return this.childInfo;
  }

  
  /* container */
  
  public boolean isFolderish() {
    return true; /* not strictly necessary, but this is static info anyways */
  }

  
  /* contents */
  
  protected static final String[] emptyStringArray = new String[0];
  
  protected String[] collectIds(boolean _directories) {
    OFSFileContainerChildInfo ci = this.childInfo();
    if (ci == null) return null;
    
    String[] fileNames = ci.fileNames();
    int      len       = fileNames.length;
    if (len == 0)
      return emptyStringArray;
    
    List<String> ids = new ArrayList<String>(8);
    for (int i = 0; i < len; i++) {
      IOFSFileInfo info =
        this.fileManager.fileInfoForPath(this.storagePath, fileNames[i]);
      if (info.isDirectory() == _directories)
        ids.add(ci.ids[i]);
    }
    len = ids.size();
    return len == 0 ? emptyStringArray : ids.toArray(new String[len]);
  }
  
  public String[] toOneRelationshipKeys() {
    return this.collectIds(false /* files */);
  }
  public String[] toManyRelationshipKeys() {
    return this.collectIds(true /* directories */);
  }
  
  public String[] objectIds() {
    OFSFileContainerChildInfo ci = this.childInfo();
    if (ci == null) return null;
    
    return ci != null ? ci.ids() : null;
  }
  
  /* IJoFolderish */
  
  /**
   * The default implementation returns an OFSFolderDataSource focused on this
   * object.
   * 
   * @return a datasource representing the contents of this folder
   */
  public EODataSource folderDataSource(IJoContext _ctx) {
    return new OFSFolderDataSource(this, _ctx);
  }
  
  
  /* stored keys */
  
  /**
   * Derives an object-id from the given filename. The default implementation
   * just cuts of everything after the first dot (all extensions). This is
   * invoked by lookupStoredName().
   */
  public String idFromName(String _name, IJoContext _ctx) {
    if (_name == null)
      return null;
    
    int idx = _name.indexOf('.');
    if (idx == -1) return _name;
    
    return _name.substring(0, idx);
  }
  
  /**
   * This method first locates the IOFSFileInfo for the given name in the
   * folder. It then uses the OFSRestorationFactory derived from the context
   * to reconstruct the child object.
   * <p>
   * This object does no caching of the resulting object. All caching is done
   * by the lookupName() method.
   * <p>
   * The method is called by lookupName(), you usually don't call it manually. 
   * 
   * @param _name - name of the object to lookup
   * @param _ctx  - the context to perform the operation in
   * @return a freshly created object, or an Exception/null on error
   */
  public Object lookupStoredName(String _name, IJoContext _ctx) {
    boolean debugOn = log.isDebugEnabled();
    
    /* first turn lookup name into lookup id (aka: cut off extension */
    
    String lookupId = this.idFromName(_name, _ctx);
    if (debugOn) log.debug("lookupStoredName(" + _name + ") => id=" + lookupId);
    
    /* lookup File object for given id */
    
    OFSFileContainerChildInfo ci = this.childInfo();
    if (ci == null) {
      if (debugOn) log.debug("did not find childinfo of container: " + this);
      return null;
    }
    else if (debugOn)
      log.debug("  childinfo: " + ci);
    
    String[] files = ci.fileNames();
    int      len   = files.length;
    if (len == 0) {
      if (debugOn)
        log.debug("childinfo of container returned no filenames: " + this);
      return null;
    }
    if (debugOn) log.debug("  number of files: " + files.length);
    
    String lfile = null;
    for (int i = 0; i < len; i++) {
      if (debugOn) log.debug("    check[" + i + "]: " + ci.fileIds[i]);
      if (lookupId.equals(ci.fileIds[i])) {
        lfile = files[i];
        
        // TODO: DEBUG
        if (!files[i].startsWith(lookupId)) {
          log.error("FOUND " + lookupId + " as " + lfile);
          
          for (int j = 0; j < len; j++) { 
            log.error("  id: " + ci.fileIds[j]);
            log.error("  =>: " + ci.fileNames[j]);
          }
        }
        break;
      }
    }
    if (lfile == null) {
      if (debugOn) log.debug("did not find file for id: " + lookupId);
      return null;
    }
    
    IOFSFileInfo linfo =
      this.fileManager.fileInfoForPath(this.storagePath, lfile);

    if (debugOn)
      log.debug("found file for id=" + lookupId + " => " + lfile + ": " +linfo);
    
    /* find factory using the context */
    
    OFSRestorationFactory factory =
      OFSRestorationFactory.restorationFactoryInContext(_ctx);
    if (factory == null) {
      if (debugOn) log.debug("did not find OFS restoration factory!");
      return null;
    }
    
    /* attempt to restore object */
    Object o = factory.restoreObjectFromFileInContext
      (this, this.fileManager, linfo, _ctx);
    
    if (debugOn) {
      if (o != null)
        log.debug("restored OFS object: " + o);
      else
        log.debug("could not restore file: " + linfo);
    }
    return o;
  }
  
  /* IJoObject */
  
  /**
   * Lookup the given name in this object. This works by first checking the
   * JoClass of the object and then calling lookupStoredName() to discover an
   * object on-disk.
   * <p>
   * This method maintains a cache of restored disk objects.
   * 
   * @param _name - name of the object to lookup
   * @param _ctx  - the context to perform the operation in
   * @return a freshly created object, or an Exception/null on error
   */
  @Override
  public Object lookupName(String _name, IJoContext _ctx, boolean _acquire) {
    boolean debugOn = log.isDebugEnabled();
    
    /* first check cache */
    
    if (this.cacheNameToObject != null) {
      Object o = this.cacheNameToObject.get(_name);
      if (o != null) {
        if (debugOn) log.debug("cache hit[" + _name + "]: " + o);
        return o;
      }
      if (debugOn) log.debug("cache miss[" + _name + "].");
    }
    else if (debugOn)
      log.debug("no child cache in container.");
    
    /* lookup using JoClass */
    
    JoClass cls = this.joClassInContext(_ctx);
    if (cls != null) {
      Object o = cls.lookupName(this, _name, _ctx);
      if (o != null) return o;
    }
    
    /* check children */
    
    OFSFileContainerChildInfo ci = this.childInfo();
    if (ci != null && ci.hasKey(_name)) {
      Object o = this.lookupStoredName(_name, _ctx);
      if (o != null) {
        if (this.cacheNameToObject != null)
          this.cacheNameToObject.put(_name, o);
        
        return o;
      }
    }
    else if (log.isDebugEnabled()) {
      if (ci != null)
        log.debug("container misses key " + _name + " in: " + ci);
      else
        log.debug("container has no child info: " + this);
    }
    
    /* if we shall acquire, continue at parent */
    
    if (_acquire && this.container != null)
      return ((IJoObject)this.container).lookupName(_name, _ctx, true /* aq */);
    
    return null;
  }
  
  
  /* resource manager */
  
  /**
   * Lookup and cache a resource manager for the folder.
   * 
   * @return a WOResourceManager instance
   */
  public WOResourceManager resourceManager() {
    if (this.resourceManager != null)
      return this.resourceManager;
    
    WOResourceManager parentRM =
      JoContainerResourceManager.lookupResourceManager
        (this.container(), this.context);
    
    this.resourceManager = 
      new JoContainerResourceManager(this, parentRM, this.context);
    return this.resourceManager;
  }
  
  
  /* description */

  @Override
  public void appendAttributesToDescription(StringBuilder _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.childInfo != null)
      _d.append(" has-childinfo");
  }
}
