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

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

import org.opengroupware.jope.appserver.core.WOAssociation;
import org.opengroupware.jope.appserver.core.WODynamicElement;
import org.opengroupware.jope.appserver.core.WOElement;
import org.opengroupware.jope.appserver.core.WOResourceManager;
import org.opengroupware.jope.appserver.elements.WOCompoundElement;
import org.opengroupware.jope.appserver.elements.WOGenericContainer;
import org.opengroupware.jope.appserver.elements.WOGenericElement;
import org.opengroupware.jope.appserver.elements.WOHyperlink;
import org.opengroupware.jope.appserver.elements.WOStaticHTMLElement;
import org.opengroupware.jope.foundation.NSJavaRuntime;

/**
 * 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.
 *
 * <h4>Supported binding prefixes</h4>
 * <pre>
 *   const  - WOValueAssociation
 *   jo     - JoPathAssociation
 *   label  - WOLabelAssociation
 *   ognl   - WOOgnlAssociation
 *   plist  - parse value as plist, then create a WOValueAssociation
 *   q      - WOQualifierAssociation (evaluate the given qualifier)
 *   regex  - WORegExAssociation
 *   rsrc   - WOResourceURLAssociation (lookup URL for a given resource name)
 *   var    - WOKeyPathAssociation
 *   varpat - WOKeyPathPatternAssociation
 * </pre>
 *
 * <h4>Shortcuts</h4>
 * <pre>
 *   &lt;#a&gt;         - WOHyperlink
 *   &lt;#for&gt;       - WORepetition
 *   &lt;#get&gt;       - WOString
 *   &lt;#if&gt;        - WOConditional
 *   &lt;#put&gt;       - WOCopyValue
 *   &lt;#submit&gt;    - WOSubmitButton
 *   &lt;#table-for&gt; - WOTableRepetition
 * </pre>
 *
 * <h4>WOHTMLParser vs WOWrapperTemplateBuilder</h4>
 * <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;
  protected Map               wodEntries;
  protected WOResourceManager resourceManager;
  // Note: we only use lookupDynamicElementClass() of the resource manager
  // TBD: consolidate that in an interface?

  /* builder */

  @Override
  public WOTemplate buildTemplate
    (URL _template, URL _wod, WOResourceManager _resourceManager)
  {
    boolean isDebugOn = log.isDebugEnabled();

    if (isDebugOn) log.debug("parsing wrapper template ...");

    this.resourceManager = _resourceManager;

    /* 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.resourceManager = 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 _kp != null ? WOAssociation.associationWithKeyPath(_kp) : null;
  }

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

  /**
   * Called by the WODParser once it has parsed the data of a WOD entry
   * like:<pre>
   *   Frame: MyFrame {
   *     title = "Welcome to Hola";
   *   }</pre>
   * The parser stores the result of this method in a Map under the _cname
   * (<code>Frame</code>). This Map is queried after the .wod has been
   * parsed. The parser does not care about the type of the object being
   * returned, it just stores it.
   * 
   * @param _p       - the parser
   * @param _cname   - the name of the element (<code>Frame</code>)
   * @param _entry   - the Map containing the bindings (String-WOAssociation)
   * @param _clsname - the name of the component (<code>MyFrame</code>)
   * @return a WODFileEntry object representing the WOD entry
   */
  public Object makeDefinitionForComponentNamed
    (WODParser _p, String _compname, Map _entry, String _clsname)
  {
    return new WODFileEntry(_compname, _clsname, _entry);
  }


  /* HTML parser callback */

  /**
   * This method builds a
   *   <code>Map&lt;String, WOAssociation&gt;</code>
   * from a
   *   <code>Map&lt;String, String&gt;</code>.
   * 
   * It scans the key for a colon (<code>:</code>). If it does not find one,
   * it creates a value-association, if not, it calls the
   *   <code>WOAssociation.associationForPrefix()</code>
   * to determine an appropriate WOAssociation for the prefix.
   * <p>
   * Example:<pre>
   *   {
   *     var:list = "persons";
   *     var:item = "person";
   *     count = 5;
   *   }</pre>
   * is mapped to:<pre>
   *   {
   *     list  = [WOKeyPathAssociation keypath="persons"];
   *     item  = [WOKeyPathAssociation keypath="person"];
   *     count = [WOValueAssociation value=5];
   *   }</pre>
   * 
   * @param _attrs - a Map&lt;String,String&gt; as parsed from HTML
   * @return a Map&lt;String, WOAssociation&gt;
   */
  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 = WOAssociation.associationForPrefix(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;

      assocs = buildAssociationsForTagAttributes(_attrs);

      if ((cls = this.resourceManager.lookupDynamicElementClass(_name))==null) {
        /* Could not resolve tagname as a WODynamicElement class, check for
         * aliases and dynamic HTML tags, like
         * 
         *   <#li var:style="current" var:+style="isCurrent" />
         * 
         * Note: we only check for dynamic element classes! The _name could
         *       still be the name of a WOComponent class!
         */
        String clsName;

        if (_name.equals("a")) {
          if (assocs.containsKey("action") ||
              assocs.containsKey("actionClass") ||
              assocs.containsKey("@action") ||
              assocs.containsKey("pageName") ||
              assocs.containsKey("disabled"))
            cls = WOHyperlink.class;
          else {
            cls = WOGenericContainer.class;
            addElementName = true;
          }
        }
        else if ((clsName = elementNameAliasMap.get(_name)) != null) {
          cls = this.resourceManager.lookupDynamicElementClass(clsName);
          if (cls == null) {
            log().error("could not resolve name alias class: " + _name);
            return new WOStaticHTMLElement("[Missing element: " + _name + "]");
          }

          /* Note: WOGenericContainer inherits from WOGenericElement */
          addElementName = WOGenericElement.class.isAssignableFrom(cls);
        }
        /* ELSE: probably a WOComponent. We do not resolve WOComponent classes
         *       during template parsing (anymore).
        else {
          log().info("did not find element in .wod file: " + _name);
          return new WOStaticHTMLElement("[Missing element in wod: "+_name+"]");
        }
        */
      }

      if (addElementName)
        assocs.put("elementName", WOAssociation.associationWithValue(_name));
    }
    else {
      cls = this.resourceManager.lookupDynamicElementClass
        (entry.componentClassName);
      /* if this returns null, its most likely a WOComponent
      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 (cls == null /* || 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).
       * => and in the new code we don't resolve the WOComponent class
       *    anyways.
       */
      String cname = this.iTemplate.addSubcomponent
        (entry != null ? entry.componentClassName : _name, assocs);
      element = new WOChildComponentReference(cname, content);
    }
    else /* if (WOElement.class.isAssignableFrom(cls)) */ {
      /* all classes we find are dyn elems */
      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);
      }
    }
    /*
    else {
      log.error("non-WOElement in template: " +
          cls.getSimpleName() + " (" + cls + ")");
    }
    */
    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 = {
    "a",         "WOHyperlink",
    "if",        "WOConditional",
    "for",       "WORepetition",
    "table-for", "WOTableRepetition",
    "get",       "WOString",
    "put",       "WOCopyValue",
    "submit",    "WOSubmitButton",

    /* list of HTML tags (which can be made dynamic with a # in front */
    "html",      "WOGenericContainer",
    "head",      "WOGenericContainer",
    "body",      "WOGenericContainer",
    "title",     "WOGenericContainer",
    "link",      "WOGenericElement",
    "meta",      "WOGenericElement",
    "img",       "WOGenericElement",
    "ul",        "WOGenericContainer",
    "ol",        "WOGenericContainer",
    "li",        "WOGenericContainer",
    "table",     "WOGenericContainer",
    "tr",        "WOGenericContainer",
    "th",        "WOGenericContainer",
    "td",        "WOGenericContainer",
    "label",     "WOGenericContainer",
    "br",        "WOGenericElement",
    "hr",        "WOGenericElement",
    "input",     "WOGenericElement",
    "select",    "WOGenericContainer",
    "option",    "WOGenericContainer",
    "form",      "WOGenericContainer",
    "textarea",  "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]);
    }
  }
}
