/*
  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.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.opengroupware.jope.appserver.associations.WOKeyPathPatternAssociation;
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.WOStaticHTMLElement;
import org.opengroupware.jope.foundation.NSClassLookupContext;
import org.opengroupware.jope.foundation.NSJavaRuntime;
import org.opengroupware.jope.foundation.NSPropertyListParser;

/*
 * WOWrapperTemplateBuilder
 * 
 * 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.
 * 
 * THREAD: this class is not threadsafe and uses ivars for temporary storage.
 */

public class WOWrapperTemplateBuilder extends WOTemplateBuilder
  implements WODParserHandler, WOHTMLParserHandler
{
  protected WOTemplate           iTemplate = null;
  protected Map                  wodEntries = null;
  protected NSClassLookupContext classLookup = null;

  @Override
  public WOTemplate buildTemplate(URL _template, URL _wod,
                                  NSClassLookupContext _classLookup)
  {
    boolean isDebugOn = this.log.isDebugEnabled();
    
    if (isDebugOn) this.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)
          this.log.error("could not parse WOD file: " + e.getMessage(), e);
      }
      wodParser.reset();
      if (isDebugOn) this.log.debug("  parsed wod: " + this.wodEntries);
    }
    else
      if (isDebugOn) this.log.debug("  no wod to parse.");
    
    /* parse HTML file */
    
    WOHTMLParser htmlParser = new WOHTMLParser();
    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)
        this.log.error("could not parse HTML file", htmlParser.lastException());
      return null;
    }
    if (elements.size() == 0) {
      /* got no result? */
      this.log.warn("parsed no element from the HTML file?");
      return null;
    }
    if (isDebugOn) this.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) this.log.debug("  parsed template: " + template);
    return template;
  }
  
  /* 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 */
  
  protected Map buildAssociationsForTagAttributes(Map<String, String> _attrs) {
    if (_attrs == null)
      return null;
    
    Map<String,WOAssociation> assocs = 
      new HashMap<String,WOAssociation>(_attrs.size());
    
    for (String k: _attrs.keySet()) {
      WOAssociation assoc;
      String        value;
      int           pm;
      
      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);
        
        // TODO: make the mapping dynamic
        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("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);
          assoc = WOAssociation.associationWithValue(v);
        }
        else {
          assoc = WOAssociation.associationWithValue(value);
          this.log.warn("unexpected template key prefix: " + prefix);
        }
      }
      if (assoc != null)
        assocs.put(k, assoc);
    }
    return assocs;
  }
  
  protected static Class[] dynElemCtorSignature = {
    String.class,   /* element name */
    Map.class,      /* associations */
    WOElement.class /* template     */
  };
  
  @SuppressWarnings("unchecked")
  public WOElement dynamicElementWithName
    (String _name, Map<String, String> _attrs, List<WOElement> _children)
  {
    WODFileEntry entry = null;
    Class     cls;
    WOElement content = null;
    Map       assocs = null;
    
    if (this.wodEntries != null)
      entry = (WODFileEntry)this.wodEntries.get(_name);
    if (entry == null) {
      // TODO: derive element from attributes, eg:
      //   <#WOString var:value="abc" const:escapeHTML="1"/>
      // 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 dynamic HTML tags,
         * like <#li var:style="current" var:+style="isCurrent" />
         */
        if (htmlTagNamesList.contains(_name)) {
          if (_children != null && _children.size() > 0)
            cls = this.classLookup.lookupClass("WOGenericContainer");
          else
            cls = this.classLookup.lookupClass("WOGenericElement");
          addElementName = true;
        }
        else {
          this.log.debug("did not find element in .wod file: " + _name);
          return new WOStaticHTMLElement("[Missing element: " + _name + "]");
        }
      }
      
      assocs = this.buildAssociationsForTagAttributes(_attrs);
      if (addElementName)
        assocs.put("elementName", WOAssociation.associationWithValue(_name));
    }
    else {
      cls = this.classLookup.lookupClass(entry.componentClassName);
      if (cls == null) {
        this.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, String> tagAttrAssocs =
          this.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(WOHTMLParser _p, char[] _data) {
    return true;
  }
  public void failedParsingHTMLData
    (WOHTMLParser _p, char[] _data, Exception _error)
  {
  }
  public void finishedParsingHTMLData
    (WOHTMLParser _p, char[] _data, List<WOElement> _topLevel)
  {
  }
  
  /* list of HTML tags (which can be made dynamic with a # in front */
  
  public static final String[] htmlTagNames = {
    "html", "head", "body", "title",
    "link", "a", "img",
    "ul", "ol", "li",
    "table", "tr", "th", "td",
    "br", "hr",
    "input", "select", "option", "form",
    "font", "div", "span",
    "frame", "frameset",
    "script",
    "blink" /* ;-) */,
    
    /* WML stuff */
    "wml", "card", "do", "go", "anchor", "postfield"
  };
  public static final List<String> htmlTagNamesList =
    Arrays.asList(htmlTagNames);
}
