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

import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.opengroupware.jope.appserver.associations.WOKeyPathPatternAssociation;
import org.opengroupware.jope.appserver.associations.WOOgnlAssociation;
import org.opengroupware.jope.appserver.associations.WORegExAssociation;
import org.opengroupware.jope.appserver.core.WOAssociation;
import org.opengroupware.jope.appserver.core.WOComponent;
import org.opengroupware.jope.appserver.core.WODynamicElement;
import org.opengroupware.jope.appserver.core.WOElement;
import org.opengroupware.jope.appserver.core.WOResourceURLAssociation;
import org.opengroupware.jope.appserver.elements.WOCompoundElement;
import org.opengroupware.jope.appserver.elements.WOGenericElement;
import org.opengroupware.jope.appserver.elements.WOStaticHTMLElement;
import org.opengroupware.jope.foundation.NSClassLookupContext;
import org.opengroupware.jope.foundation.NSJavaRuntime;
import org.opengroupware.jope.foundation.NSPropertyListParser;

/**
 * WOWrapperTemplateBuilder
 * <p>
 * This class implements a parser for so called 'wrapper templates', which
 * in turn are WebObjects style templates composed of an HTML template plus
 * a .wod file.
 * <p>
 * Supported binding prefixes:
 * <pre>
 *   var    - WOKeyPathAssociation
 *   const  - WOValueAssociation
 *   rsrc   - WOResourceURLAssociation (lookup URL for a given resource name)
 *   varpat - WOKeyPathPatternAssociation
 *   ognl   - WOOgnlAssociation
 *   plist  - parse value as plist, then create a WOValueAssociation
 *   regex  - WORegExAssociation</pre>
 * <p>
 * WOHTMLParser vs WOWrapperTemplateBuilder
 * <p>
 * The WOHTMLParser just parses the HTML and asks the WOWrapperTemplateBuilder
 * to build the actual WOElements.
 * <p>
 * THREAD: this class is not threadsafe and uses ivars for temporary storage.
 * <pre>
 * TODO: WOWrapperTemplateBuilder is not a good name for this class because it
 *       has nothing to do with wrappers. The actual resolution of the wrapper
 *       is done in the WOResourceManager / WOComponentDefinition.
 *       This class receives the already-looked-up URLs for the .wod and .html
 *       templates.
 *       Possibly we could also move this class into the WOTemplateBuilder?
 * </pre>
 */
public class WOWrapperTemplateBuilder extends WOTemplateBuilder
  implements WODParserHandler, WOTemplateParserHandler
{
  protected WOTemplate           iTemplate = null;
  protected Map                  wodEntries = null;
  protected NSClassLookupContext classLookup = null;
  
  /* builder */

  @Override
  public WOTemplate buildTemplate
    (URL _template, URL _wod, NSClassLookupContext _classLookup)
  {
    boolean isDebugOn = log.isDebugEnabled();
    
    if (isDebugOn) log.debug("parsing wrapper template ...");
    
    this.classLookup = _classLookup;
    
    /* parse wod file */
    
    if (_wod != null) {
      WODParser wodParser = new WODParser();
      wodParser.setHandler(this);
      this.wodEntries = (Map)wodParser.parse(_wod);
      if (this.wodEntries == null) {
        Exception e = wodParser.lastException();
        if (e != null)
          log.error("could not parse WOD file: " + e.getMessage(), e);
      }
      wodParser.reset();
      if (isDebugOn) log.debug("  parsed wod: " + this.wodEntries);
    }
    else
      if (isDebugOn) log.debug("  no wod to parse.");
    
    /* parse HTML file */
    
    WOTemplateParser htmlParser = this.instantiateTemplateParser(_template);
    htmlParser.setHandler(this);
    
    this.iTemplate = new WOTemplate(null /* URL */, null /* root */);
    List<WOElement> elements = htmlParser.parseHTMLData(_template);
    
    /* reset temporary state */
    
    this.wodEntries  = null;
    this.classLookup = null;
    
    /* process results */
    
    if (elements == null) {
      if (htmlParser.lastException() != null)
        log.error("could not parse HTML file", htmlParser.lastException());
      return null;
    }
    if (elements.size() == 0) {
      /* got no result? */
      log.warn("parsed no element from the HTML file?");
      return null;
    }
    if (isDebugOn) log.debug("  parsed elements: " + elements);
    
    /* build template */
    
    if (elements.size() == 1)
      this.iTemplate.setRootElement(elements.get(0));
    else
      this.iTemplate.setRootElement(new WOCompoundElement(elements));

    WOTemplate template = this.iTemplate;
    this.iTemplate = null;
    if (isDebugOn) log.debug("  parsed template: " + template);
    return template;
  }
  
  public WOTemplateParser instantiateTemplateParser(URL _template) {
    // TODO: rather use the WOParser API discovered in Wonder?
    return new WOHTMLParser();
  }
  
  /* WOD parser */

  public boolean willParseDeclarationData(WODParser _p, char[] _data) {
    return true;
  }
  public void failedParsingDeclarationData
    (WODParser _p, char[] _data, Exception _error)
  {
  }
  public void finishedParsingDeclarationData
    (WODParser _p, char[] _data, Map _decls)
  {
  }

  public WOAssociation makeAssociationWithKeyPath(WODParser _p, String _kp) {
    return WOAssociation.associationWithKeyPath(_kp);
  }

  public WOAssociation makeAssociationWithValue(WODParser _p, Object _value) {
    return WOAssociation.associationWithValue(_value);
  }

  public Object makeDefinitionForComponentNamed
    (WODParser _p, String _compname, Map _entry, String _clsname)
  {
    return new WODFileEntry(_compname, _clsname, _entry);
  }

  
  /* HTML parser callback */
  
  public static WOAssociation buildAssociation
    (String _prefix, String _name, String _value)
  {
    WOAssociation assoc;
    
    // TODO: make the mapping dynamic (use a common registry with WOx)
    if (_prefix.equals("var"))
      assoc = WOAssociation.associationWithKeyPath(_value);
    else if (_prefix.equals("const"))
      assoc = WOAssociation.associationWithValue(_value);
    else if (_prefix.equals("rsrc"))
      assoc = new WOResourceURLAssociation(_value);
    else if (_prefix.equals("varpat"))
      assoc = new WOKeyPathPatternAssociation(_value);
    else if (_prefix.equals("ognl"))
      assoc = new WOOgnlAssociation(_value);
    else if (_prefix.equals("regex"))
      assoc = new WORegExAssociation(_value);
    else if (_prefix.equals("plist")) {
      /* Allow arrays like this: list="(a,b,c)",
       * required because we can't specify plists in .html
       * template attributes. (we might want to change that?)
       */
      NSPropertyListParser parser = new NSPropertyListParser();
      Object v = parser.parse(_value);
      if (v == null) {
        log.warn("could not parse plist value of association: " + _value,
                 parser.lastException());
        assoc = null;
      }
      else
        assoc = WOAssociation.associationWithValue(v);
    }
    else
      assoc = WOAssociation.associationWithValue(_value);
    
    return assoc;
  }
  
  public static Map<String, WOAssociation> buildAssociationsForTagAttributes
    (Map<String, String> _attrs)
  {
    if (_attrs == null) {
      /* we return an empty hash-map for no attributes, because its valid to
       * have none but the elements always expect a (non-null) association
       * hashmap.
       */
      return new HashMap<String, WOAssociation>(0);
    }
    
    Map<String,WOAssociation> assocs = 
      new HashMap<String,WOAssociation>(_attrs.size());
    
    for (String k: _attrs.keySet()) {
      WOAssociation assoc;
      int           pm;
      String        value;
      
      value = _attrs.get(k);
      pm = k.indexOf(':');
      if (pm == -1)
        assoc = WOAssociation.associationWithValue(value);
      else {
        String prefix = k.substring(0, pm);
        k = k.substring(pm + 1);
        
        assoc = buildAssociation(prefix, k, value);
      }
      
      if (assoc != null)
        assocs.put(k, assoc);
    }
    return assocs;
  }
  
  protected static Class[] dynElemCtorSignature = {
    String.class,   /* element name */
    Map.class,      /* associations */
    WOElement.class /* template     */
  };
  
  /**
   * This method constructs a WODynamicElement for the given name. It will first
   * check for an entry with the name in the wod mapping table and otherwise
   * attempt to lookup the name as a class. If that also fails some fallbacks
   * kick in, that is element name aliases (<#if>) and automatic generic
   * elements (<#li>).
   * <p>
   * If the name represents a component, a WOChildComponentReference object
   * will get constructed (not the component itself, components are allocated
   * on demand).
   */
  @SuppressWarnings("unchecked")
  public WOElement dynamicElementWithName
    (String _name, Map<String, String> _attrs, List<WOElement> _children)
  {
    WODFileEntry entry = null;
    Class     cls      = null;
    WOElement content  = null;
    Map       assocs   = null;

    if (_name == null) {
      log().warn("parsed element has no name, attrs: " + _attrs);
      return new WOStaticHTMLElement("[Unnamed dynelement]");
    }
    
    if (this.wodEntries != null)
      entry = (WODFileEntry)this.wodEntries.get(_name);
    if (entry == null) {
      /*
       * Derive element from tag name, eg:
       * 
       *   <#WOString var:value="abc" const:escapeHTML="1"/>
       * 
       * This will attempt to find the class 'WOString'. If it can't find the
       * class, it checks for aliases and HTML tags (generic elements).
       */
      // TODO: I suppose we could also try WOxElemBuilder's!
      boolean addElementName = false;
      
      if ((cls = this.classLookup.lookupClass(_name)) == null) {
        /* Could not resolve tagname as a class, check for aliases and dynamic
         * HTML tags, like
         *   <#li var:style="current" var:+style="isCurrent" />
         */
        String clsName = elementNameAliasMap.get(_name);
        if (clsName != null) {
          cls = this.classLookup.lookupClass(clsName);
          if (cls == null) {
            log().warn("could not resolve name alias class: " + _name);
            return new WOStaticHTMLElement("[Missing element: " + _name + "]");
          }
          
          /* Note: WOGenericContainer inherits from WOGenericElement */
          addElementName = WOGenericElement.class.isAssignableFrom(cls);
        }
        else {
          log().info("did not find element in .wod file: " + _name);
          return new WOStaticHTMLElement("[Missing element: " + _name + "]");
        }
      }
      
      assocs = buildAssociationsForTagAttributes(_attrs);
      if (addElementName)
        assocs.put("elementName", WOAssociation.associationWithValue(_name));
    }
    else {
      cls = this.classLookup.lookupClass(entry.componentClassName);
      if (cls == null) {
        log().debug("did not find class for element in .wod file: " + _name);
        return new WOStaticHTMLElement
          ("[Missing dynelement: " + _name + " / " + 
           entry.componentClassName + "]");      
      }
      
      /* Note: its important that we copy the associations since an element can
       *       be used twice! (and we clear the Map during element init)
       */
      assocs = new HashMap(entry.associations);
      
      /* merge attributes of the tag (eg <#MyStyle color="red" />) */
      if (_attrs != null && _attrs.size() > 0) {
        Map<String, WOAssociation> tagAttrAssocs =
          buildAssociationsForTagAttributes(_attrs);
        if (tagAttrAssocs != null)
          assocs.putAll(tagAttrAssocs);
      }
    }
    if (assocs != null)
      assocs.remove("NAME");
    
    /* narrow down the children */
    
    if (_children != null) {
      if (_children.size() == 0)
        ;
      else if (_children.size() == 1)
        content = _children.get(0);
      else
        content = new WOCompoundElement(_children);
    }
    
    /* create element */
    
    WOElement element = null;
    
    if (WOComponent.class.isAssignableFrom(cls)) {
      /*
       * Note: we cannot use cls.getName (the fully qualified component name),
       *       this will make the template lookup fail because it won't use
       *       the proper resource manager (the first will succeed because
       *       the name is fully qualified).
       */
      String cname = this.iTemplate.addSubcomponent
        (entry != null ? entry.componentClassName : _name, assocs);
      element = new WOChildComponentReference(cname, content);
    }
    else {
      element = (WOElement)NSJavaRuntime.NSAllocateObject
        (cls, dynElemCtorSignature, new Object[] { 
            _name, assocs, content
        });
      
      // TODO: maybe we need to remove 'name' or so
      if (assocs != null && element != null) {
        if (assocs.size() > 0 && element instanceof WODynamicElement)
          ((WODynamicElement)element).setExtraAttributes(assocs);
      }
    }
    return element;
  }
  
  public boolean willParseHTMLData(WOTemplateParser _p, char[] _data) {
    return true;
  }
  public void failedParsingHTMLData
    (WOTemplateParser _p, char[] _data, Exception _error)
  {
  }
  public void finishedParsingHTMLData
    (WOTemplateParser _p, char[] _data, List<WOElement> _topLevel)
  {
  }
  
  /* list of aliases */
  
  private static final String[] commonElementAliases = {
    "if",        "WOConditional",
    "for",       "WORepetition",
    "get",       "WOString",
    "put",       "WOCopyValue",
    
    /* list of HTML tags (which can be made dynamic with a # in front */
    "html",      "WOGenericContainer",
    "head",      "WOGenericContainer",
    "body",      "WOGenericContainer",
    "title",     "WOGenericContainer",
    "link",      "WOGenericElement",
    "a",         "WOGenericContainer",
    "img",       "WOGenericElement",
    "ul",        "WOGenericContainer",
    "ol",        "WOGenericContainer",
    "li",        "WOGenericContainer",
    "table",     "WOGenericContainer",
    "tr",        "WOGenericContainer",
    "th",        "WOGenericContainer",
    "td",        "WOGenericContainer",
    "br",        "WOGenericElement",
    "hr",        "WOGenericElement",
    "input",     "WOGenericElement",
    "select",    "WOGenericContainer",
    "option",    "WOGenericContainer",
    "form",      "WOGenericContainer",
    "font",      "WOGenericContainer",
    "div",       "WOGenericContainer",
    "span",      "WOGenericContainer",
    "frame",     "WOGenericContainer",
    "frameset",  "WOGenericContainer",
    "script",    "WOGenericContainer",
    "applet",    "WOGenericContainer",
    "param",     "WOGenericElement",
    "blink",     "WOGenericContainer", /* ;-) */
    
    /* WML stuff */
    "wml",       "WOGenericContainer",
    "card",      "WOGenericContainer",
    "do",        "WOGenericContainer",
    "go",        "WOGenericContainer",
    "anchor",    "WOGenericContainer",
    "postfield", "WOGenericContainer"
  };
  
  private static Map<String, String> elementNameAliasMap;
  
  static {
    elementNameAliasMap = new HashMap<String, String>(32);
    
    for (int i = 0; i < commonElementAliases.length; i += 2) {
      String key = commonElementAliases[i];
      if (elementNameAliasMap.containsKey(key))
        log.error("duplicate alias: " + key);
      else
        elementNameAliasMap.put(key, commonElementAliases[i + 1]);
    }
  }
}
