/*
  Copyright (C) 2006 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.appserver.core;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.appserver.elements.WOHTMLDynamicElement;
import org.opengroupware.jope.foundation.NSJavaRuntime;
import org.opengroupware.jope.foundation.NSObject;
import org.opengroupware.jope.foundation.NSPropertyListParser;
import org.opengroupware.jope.foundation.NSClassLookupContext;

/*
 * WOResourceManager
 * 
 * Manages access to resources associated with WOApplication. In the context
 * of Java this is mostly based upon the resource mechanism supported by
 * Class.getResource().
 * 
 * THREAD: TODO
 */

/*
  Component Discovery and Page Creation

    All WO code uses either directly or indirectly the WOResourceManager's
    -pageWithName:languages: method to instantiate WO components.

    This methods works in three steps:
      
      1. discovery of files associated with the component
      
      2. creation of a proper WOComponentDefinition, which is some kind
         of 'blueprint' or 'class' for components
      
      3. component instantiation using the definition
    
    All the instantiation/setup work is done by a component definition, the
    resource manager is only responsible for managing those 'blueprint'
    resources.

    If you want to customize component creation, you can supply your
    own WOComponentDefinition in a subclass of WOResourceManager by
    overriding:
      - (WOComponentDefinition *)definitionForComponent:(id)_name
        inFramework:(NSString *)_frameworkName
        languages:(NSArray *)_languages
*/
public abstract class WOResourceManager extends NSObject
  implements NSClassLookupContext
{
  protected static final Log log = LogFactory.getLog("WOResourceManager");

  /* keep this at null if you do not want to cache ... */
  protected Map<Object,WOComponentDefinition> componentDefinitions;
  
  public WOResourceManager(boolean _enableCaching) {
    if (_enableCaching)
      this.componentDefinitions = new HashMap<Object,WOComponentDefinition>(32);
  }
  
  /* templates */
  
  public WOComponent pageWithName(String _name, WOContext _ctx) {
    if (log.isDebugEnabled())
      log.debug("pageWithName(" + _name + ", " + _ctx + ")");
    
    WOComponentDefinition cdef;
    
    cdef = this._definitionForComponent
             (_name, _ctx.languages(), _ctx.application().resourceManager());
    if (cdef == null) {
      if (log.isDebugEnabled())
        log.debug("  found no cdef for component: " + _name);
      return null;
    }
    
    return cdef.instantiateComponent(this, _ctx);
  }
  
  public WOElement templateWithName
    (String _name, List<String> _langs, NSClassLookupContext _clsctx)
  {
    WOComponentDefinition cdef;
    
    if ((cdef = this._definitionForComponent(_name, _langs, _clsctx)) == null)
      return null;
    
    return cdef.template();
  }
  
  /* component definitions */
  
  protected WOComponentDefinition _definitionForComponent
    (String _name, List<String> _langs, NSClassLookupContext _clsctx)
  {
    /* 
     * First check whether the cdef is cached, otherwise create a new one.

     * This method is used by the higher level methods and just implements the
     * cache control.
     * The definition itself is created by definitionForComponent:languages:.
     */
    WOComponentDefinition cdef;
    String[] langs = _langs.toArray(new String[0]);
    
    /* look into cache */
  
    cdef = this._cachedDefinitionForComponent(_name, langs);
    if (cdef != null) {
      if (cdef == null) // TODO: add some kind of 'empty' marker
        /* component does not exist */
        return null;
    
      cdef.touch();
      return cdef;
    }
    
    /* not cached, create a definition */
  
    cdef = this.definitionForComponent(_name, langs, _clsctx);

    /* cache created definition */
  
    return this._cacheDefinitionForComponent(_name, langs, cdef);
  }
  
  protected WOComponentDefinition definitionForComponent
    (String _name, String[] _langs, NSClassLookupContext _clsctx)
  {
    /*
     * Note: a 'package component' is a component which has its own package,
     *       eg: org.opengroupware.HomePage with subelements 'HomePage.class',
     *       'HomePage.html' and 'HomePage.wod'.
     */
    // TODO: complete me
    URL     templateData = null;
    String  type         = "WOx";
    String  rsrcName     = _name != null ? _name.replace('.', '/') : null;
    boolean debugOn      = log.isDebugEnabled();
    
    if (debugOn) log.debug("make cdef for component: " + _name);
    
    Class cls = this.lookupClass(_name);
    if (cls == null) { /* we do not serve this class */
      if (debugOn)
        log.debug("rm does not serve the class, check for templates: " + _name);
      
      /* check whether its a component w/o a class */
      templateData = this.urlForResourceNamed(rsrcName + ".wox", _langs);
      if (templateData == null) {
        type = "WOWrapper";
        templateData = this.urlForResourceNamed(rsrcName + ".html", _langs);
      }
      
      if (templateData == null)
        return null; /* did not find a template */
      
      if (debugOn) log.debug("  found a class-less component: " + _name);
      cls = WOComponent.class; // TODO: we might want to use a different class
    }
    if (debugOn) log.debug("  comp class: " + cls);
    
    /* this is a bit hackish ;-), but well, ... */
    boolean isPackageComponent = false;
    String className = cls.getName();
    if (className.endsWith("." + _name + ".Component"))
      isPackageComponent = true;
    else if (className.endsWith("." + _name + "." + _name))
      isPackageComponent = true;
    
    if (debugOn) {
      if (isPackageComponent)
        log.debug("  found a package component: " + className);
      else
        log.debug("  component is not a pkg one: " + _name + "/" +  className);
    }
        
    /* def */
    
    WOComponentDefinition cdef = new WOComponentDefinition(_name, cls);
    
    /* find template */
    
    URL wodData = null;
    
    if (!isPackageComponent) {
      if (templateData == null)
        templateData = this.urlForResourceNamed(rsrcName + ".wox", _langs);
      if (templateData == null) {
        templateData = this.urlForResourceNamed(rsrcName + ".html", _langs);
        type = "WOWrapper";
      }
      
      if ("WOWrapper".equals(type))
        wodData = this.urlForResourceNamed(rsrcName + ".wod", _langs);
    }
    else {
      // Note: we directly access the class resources. Not sure yet whether
      //       this is a good idea or whether we should instantiate a new
      //       WOClassResourceManager for resource lookup?
      //       This resource manager could also be set as the components
      //       resource manager?
      // TODO: localization?
      templateData = cls.getResource(_name + ".wox");
      if (templateData == null)
        templateData = cls.getResource("Component.wox");
      
      if (templateData == null) {
        type = "WOWrapper";
        templateData = cls.getResource(_name + ".html");
        wodData      = cls.getResource(_name + ".wod");
        
        if (debugOn)
          log.debug("in " + cls + " lookup " + _name + ".html: " +templateData);
        
        if (templateData == null)
          templateData = cls.getResource("Component.html");
        if (wodData == null)
          wodData = cls.getResource("Component.wod");
      }
    }
    
    if (templateData == null) {
      if (debugOn) log.debug("component has no template: " + _name);
      return cdef;
    }
    
    /* load it */
    
    if (!cdef.load(type, templateData, wodData, _clsctx)) {
      log.error("failed to load template.");
      return null;
    }
    return cdef;
  }
  
  /* component definition cache */
  
  protected String genCacheKey(String _name, String[] _langs) {
    // TODO: improve ...
    /* Note: using arrays as keys didn't work properly? */
    StringBuffer sb = new StringBuffer(_langs.length * 8 + _name.length());
    
    sb.append(_name);
    
    for (int i = 0; i < _langs.length; i++) {
      sb.append(':');
      sb.append(_langs[i]);
    }
    return sb.toString();
  }
  
  protected WOComponentDefinition _cachedDefinitionForComponent
    (String _name, String[] _langs)
  {
    if (this.componentDefinitions == null) /* caching disabled */
      return null;
    
    synchronized (this.componentDefinitions) {
      if (_langs == null)
        return this.componentDefinitions.get(_name);
      if (_langs.length == 0)
        return this.componentDefinitions.get(_name);
    }
    
    String cacheKey = this.genCacheKey(_name, _langs);
    synchronized (this.componentDefinitions) {
      return this.componentDefinitions.get(cacheKey);
    }
  }
  
  protected static boolean didWarnOnCaching = false;
  
  protected WOComponentDefinition _cacheDefinitionForComponent
    (String _name, String[] _langs, WOComponentDefinition _cdef)
  {
    if (this.componentDefinitions == null) { /* caching disabled */
      if (!didWarnOnCaching) {
        log.warn("component caching is disabled!");
        didWarnOnCaching = true;
      }
      return _cdef;
    }
    
    boolean isDebugOn = log.isDebugEnabled();
    
    synchronized (this.componentDefinitions) {
      if (_langs == null) {
        if (isDebugOn) log.debug("cache cdef w/o langs: " + _name);
        this.componentDefinitions.put(_name, _cdef);
        return _cdef;
      }
      if (_langs.length == 0) {
        if (isDebugOn) log.debug("cache cdef w/o langs: " + _name);
        this.componentDefinitions.put(_name, _cdef);
        return _cdef;
      }
    }
    
    String cacheKey = this.genCacheKey(_name, _langs);
    synchronized(this.componentDefinitions) {
      if (isDebugOn) log.debug("cache cdef w/ langs: " + cacheKey);
      this.componentDefinitions.put(cacheKey, _cdef);
    }
    return _cdef;
  }
  
  /* resources */
  
  public URL urlForResourceNamed(String _name, String[] _ls) {
    return null;
  }
  public InputStream inputStreamForResourceNamed(String _name, String[] _ls) {
    URL url = this.urlForResourceNamed(_name, _ls);
    if (url == null) return null;
    try {
      return url.openStream();
    }
    catch (IOException e) {
      log.info("could not open URL to get stream: " + url);
      return null;
    }
  }
  
  public byte[] bytesForResourceNamed(String _name, String[] _langs) {
    InputStream in = this.inputStreamForResourceNamed(_name, _langs);
    if (in == null)
      return null;
    
    return NSPropertyListParser.loadContentFromStream(in);
  }
  
  public String urlForResourceNamed(String _name, String _fwname,
                                    List<String> _langs, WOContext _ctx)
  {
    // TODO: crappy way to detect whether a resource is available
    InputStream in = this.inputStreamForResourceNamed
                            ("www/" + _name,
                             _langs != null 
                             ? _langs.toArray(new String[0]) : null);
    if (in == null)
      return null;
    
    return _ctx.urlWithRequestHandlerKey("wr", _name, null);
  }
  
  /* strings */
  
  static public Locale localeForLanguages(List<String> _langs) {
    if (_langs == null)
      return Locale.US;
    if (_langs.size() == 0)
      return Locale.US;

    return localeForLanguages(_langs.toArray(new String[0]));
  }
  static public Locale localeForLanguages(String[] _langs) {
    if (_langs == null)
      return Locale.US;
    if (_langs.length == 0)
      return Locale.US;
    
    String s   = _langs[0];
    int    idx = s.indexOf('-');
    return idx == -1 
      ? new Locale(s)
      : new Locale(s.substring(0, idx), s.substring(idx + 1));
  }
  
  public ResourceBundle stringTableWithName(String _table, String _fwname, 
                                            String[] _langs)
  {
    return null;
  }
  
  public String stringForKey(String _key, String _table, String _default,
                             String _fwname, String[] _langs)
  {
    ResourceBundle rb = this.stringTableWithName(_table, _fwname, _langs);
    if (rb == null)
      return _default != null ? _default : _key;
    
    try {
      return rb.getString(_key);
    }
    catch (MissingResourceException e) {
      return _default;
    }
  }
  
  /* reflection */
  
  protected static String[] JOPELookupPath = {
    WOHTMLDynamicElement.class.getPackage().getName(),
    WOApplication.class.getPackage().getName()
  };
  
  public Class lookupClass(String _name) {
    // TODO: cache lookup results?
    Class cls;
    
    if ((cls = NSJavaRuntime.NSClassFromString(_name)) != null)
      return cls;
    
    /* then check package hierarchy of JOPE */
    cls = NSJavaRuntime.NSClassFromString(_name, JOPELookupPath);
    if (cls != null)
      return cls;    
    
    return null;
  }

  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
  }
}
