package org.opengroupware.jope.ext.dtree;

import java.util.ArrayList;
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.WOContext;
import org.opengroupware.jope.appserver.core.WODynamicElement;
import org.opengroupware.jope.appserver.core.WOElement;
import org.opengroupware.jope.appserver.core.WOResponse;
import org.opengroupware.jope.appserver.elements.WOFormatter;
import org.opengroupware.jope.appserver.elements.WOJavaScriptWriter;
import org.opengroupware.jope.appserver.elements.WOListWalker;
import org.opengroupware.jope.appserver.elements.links.WOLinkGenerator;

/**
 * WEDTree
 * <p>
 * This element generates the HTML required to produce a tree using the dTree
 * script by Geir Landrö:<pre>
 *   http://www.destroydrop.com/javascript/tree/
 *   
 *   dTree itself is: Copyright (c) 2002-2003 Geir Landrö</pre>
 * <p>
 * Limitations:
 *   The whole tree is generated in one run, we can't load on-demand.
 * <p>
 * Bindings:<pre>
 *   id                   [in]  - int
 *   render               [in]  - boolean
 *   
 *   root                 [in]  - Object
 *   list                 [in]  - java.util.List | Collection | ...
 *   item                 [out] - Object
 *   path                 [out] - java.util.List
 *   index                [out] - int</pre>
 * <p>
 * Configuration Bindings:<pre>
 *   defaultTarget        [in]  - String (Target for all the nodes)
 *   folderLinks          [in]  - bool   (Should folders be links)  [true]
 *   useSelection         [in]  - bool   (Nodes can be selected)    [true]
 *   useCookies           [in]  - bool   (The tree uses cookies to rember it's 
 *                                        state) [true]
 *   useLines             [in]  - bool   (Tree is drawn with lines) [true]
 *   useIcons             [in]  - bool   (Tree is drawn with icons) [true]
 *   useStatusText        [in]  - bool   (Displays node names in the statusbar
 *                                        instead of the url)       [false]
 *   closeSameLevel       [in]  - bool   (Only one node within a parent can be 
 *                                        expanded at the same time. openAll()
 *                                        and closeAll() functions do not work
 *                                        when this is enabled)     [false]</pre>
 * <p>
 * Item Bindings:<pre>
 *   icon                 [in]  - String
 *   iconOpen             [in]  - String
 *   nodeID               [in]  - int
 *   label                [in]  - String
 *   tooltip              [in]  - String
 *   target               [in]  - String
 *   open                 [in]  - boolean</pre>
 *   
 *   TODO
 * <p>
 * Bindings (WOLinkGenerator):<pre>
 *   href                 [in]  - string
 *   action               [in]  - action
 *   pageName             [in]  - string
 *   directActionName     [in]  - string
 *   actionClass          [in]  - string
 *   fragmentIdentifier   [in]  - string
 *   queryDictionary      [in]  - Map<String,String>
 *   - all bindings starting with a ? are stored as query parameters.</pre>
 */
public class WEDTree extends WODynamicElement {
  
  protected WOAssociation id;
  protected WOAssociation render;
  
  protected WOAssociation root;
  protected WOAssociation list;
  protected WOAssociation item;
  protected WOAssociation path;
  protected WOAssociation index;
  
  /* item settings */
  protected WOLinkGenerator url;
  protected WOAssociation icon;
  protected WOAssociation iconOpen;
  
  protected WOAssociation nodeID;
  protected WOAssociation label;
  protected WOAssociation tooltip;
  protected WOAssociation target;
  protected WOAssociation open;
  protected int addArgc;
  
  protected WOFormatter formatter;
  
  /* tree configuration */
  protected WOAssociation defaultTarget;
  protected Map<String, WOAssociation> nameToBoolConfig;
  
  protected static final String[] boolConfigKeys = {
    "folderLinks", "useSelection", "useCookies", "useLines", "useIcons",
    "useStatusText", "closeSameLevel"
  };

  public WEDTree
    (String _name, Map<String, WOAssociation> _assocs, WOElement _template)
  {
    super(_name, _assocs, _template);
    
    this.id       = grabAssociation(_assocs, "id");
    this.render   = grabAssociation(_assocs, "render");
    
    this.root     = grabAssociation(_assocs, "root");
    this.list     = grabAssociation(_assocs, "list");
    this.item     = grabAssociation(_assocs, "item");
    this.path     = grabAssociation(_assocs, "path");
    
    this.url      = WOLinkGenerator.linkGeneratorForAssociations(_assocs);
    this.icon     = grabAssociation(_assocs, "icon");
    this.iconOpen = grabAssociation(_assocs, "iconOpen");
    this.nodeID   = grabAssociation(_assocs, "nodeID");
    this.label    = grabAssociation(_assocs, "label");
    this.tooltip  = grabAssociation(_assocs, "tooltip");
    this.target   = grabAssociation(_assocs, "target");
    this.open     = grabAssociation(_assocs, "open");

    /* this is for indexed parameter generation */
    this.addArgc = 3;
    if (this.url      != null) this.addArgc = 4;
    if (this.tooltip  != null) this.addArgc = 5;
    if (this.target   != null) this.addArgc = 6;
    if (this.icon     != null) this.addArgc = 7;
    if (this.iconOpen != null) this.addArgc = 8;
    if (this.open     != null) this.addArgc = 9;
    
    this.formatter = WOFormatter.formatterForAssociations(_assocs);
    
    /* configuration bindings */
    
    this.defaultTarget = grabAssociation(_assocs, "defaultTarget");
    this.nameToBoolConfig = new HashMap<String, WOAssociation>(8);
    
    for (String key: boolConfigKeys) {
      WOAssociation a = grabAssociation(_assocs, key);
      if (a != null) this.nameToBoolConfig.put(key, a);
    }
    
    if (this.nameToBoolConfig.size() == 0)
      this.nameToBoolConfig = null;
  }
  
  /* generate response */
  
  @SuppressWarnings("unchecked")
  protected int appendTreeItemToResponse
    (String _treeID, Object _item, int _parentID, int _counter,
     List<Object> _pathStack, WOJavaScriptWriter _js, WOContext _ctx)
  {
    if (_item == null)
      return _counter;
    
    Object cursor = _ctx.cursor();
    
    /* maintain stack */
    
    if (_pathStack != null) {
      _pathStack.add(_item);
      
      /* Note: not strictly necessary but safer with sideeffects */
      this.path.setValue(_pathStack, cursor);
    }
    
    /* generate my ID */
    
    int myID = 0;
    if (this.nodeID != null)
      myID = this.nodeID.intValueInComponent(cursor);
    else {
      myID = _counter;
      _counter++; /* consume one ID */
    }
    
    /* generate JavaScript */
    
    _js.append(_treeID + ".add(");
    _js.appendConstant(myID);
    _js.append(",");
    _js.appendConstant(_parentID);
    _js.append(",");
    
    String s = null;
    if (this.label != null) {
      Object o = this.label.valueInComponent(cursor);
      if (this.formatter != null)
        s = this.formatter.stringForObjectValue(o, _ctx);
      else if (o != null)
        s = o.toString();
      else
        s = "[" + myID + "]";
    }
    else
      s = "[" + myID + "]";
    _js.appendConstant(s);
    
    /* optional parameters */
    if (this.addArgc > 3) {
      _js.append(",");
      if (this.url != null)
        _js.appendConstant(this.url.fullHrefInContext(_ctx));
      else
        _js.append("undefined");
    }
    if (this.addArgc > 4) {
      _js.append(",");
      if (this.tooltip != null)
        _js.appendConstant(this.tooltip.stringValueInComponent(cursor));
      else
        _js.append("undefined");
    }
    if (this.addArgc > 5) {
      _js.append(",");
      if (this.target != null)
      _js.appendConstant(this.target.stringValueInComponent(cursor));
      else
        _js.append("undefined");
    }
    if (this.addArgc > 6) {
      _js.append(",");
      if (this.icon != null)
      _js.appendConstant(this.icon.stringValueInComponent(cursor));
      else
        _js.append("undefined");
    }
    if (this.addArgc > 7) {
      _js.append(",");
      if (this.iconOpen != null)
      _js.appendConstant(this.iconOpen.stringValueInComponent(cursor));
      else
        _js.append("undefined");
    }
    if (this.addArgc > 8) {
      _js.append(",");
      if (this.open != null)
        _js.appendConstant(this.open.stringValueInComponent(cursor));
      else
        _js.append("undefined");
    }
    
    _js.append(");\n");
    
    /* generate children */
    
    if (this.list != null) {
      // TODO: should we use WOListWalker
      List lList =
        WOListWalker.listForValue(this.list.valueInComponent(cursor));
      
      if (lList != null) {
        int len = lList.size();
        for (int i = 0; i < len; i++) {
          Object child = lList.get(i);
          
          /* push environment */
          
          if (this.index != null) this.index.setIntValue(i, cursor);
          if (this.item  != null) this.item.setValue(child, cursor);
          
          /* call next step */
          
          _counter = this.appendTreeItemToResponse
            (_treeID, child, myID, _counter /* id-state */, _pathStack,
             _js, _ctx);
        }
      }
    }
    
    /* fixup stack */
    
    if (_pathStack != null) {
      _pathStack.remove(_pathStack.size() - 1);
      
      /* Note: not strictly necessary but safer with sideeffects */
      this.path.setValue(_pathStack, cursor);
    }
    
    return _counter;
  }
  
  @Override
  public void appendToResponse(WOResponse _r, WOContext _ctx) {
    Object cursor = _ctx.cursor();
    
    /* generate ID */
    
    String treeID = null;
    if (this.id != null)
      treeID = this.id.stringValueInComponent(cursor);
    if (treeID == null || treeID.length() == 0)
      treeID = "dtree_" + _ctx.elementID().replace('.', '_');
    
    /* create tree */
    
    _r.appendBeginTag("script");
    _r.appendAttribute("type", "text/javascript");
    _r.appendBeginTagEnd();
    
    WOJavaScriptWriter js = new WOJavaScriptWriter();
    
    js.append("var " + treeID + " = ");
    js.appendNewObject("dTree", treeID);
    js.append(";\n");
    
    /* configuration */
    
    js.append(treeID + ".config.inOrder = true;\n");
    
    if (this.defaultTarget != null) {
      String s = this.defaultTarget.stringValueInComponent(cursor);
      if (s != null)
        js.append(treeID + ".config.target = '" + s + "';\n");
    }
    if (this.nameToBoolConfig != null) {
      for (String name: this.nameToBoolConfig.keySet()) {
        js.append(treeID + ".config." + name + " = ");
        if (this.nameToBoolConfig.get(name).booleanValueInComponent(cursor))
          js.append("true;\n");
        else
          js.append("false;\n");
      }
    }
    
    /* append tree nodes */
    
    if (this.root != null && this.item != null) {
      /* retrieve root and push it into the item */
      Object o = this.root.valueInComponent(cursor);
      if (this.item != null) this.item.setValue(o, cursor);
      
      List<Object> pathStack = null;
      if (this.path != null && this.path.isValueSettableInComponent(cursor)) {
        pathStack = new ArrayList<Object>(8);
        this.path.setValue(pathStack, cursor);
      }
      
      this.appendTreeItemToResponse(treeID, o, -1, 0, pathStack, js, _ctx);
      
      /* cleanup */
      
      if (pathStack != null)
        this.path.setValue(null, cursor);
      if (this.item != null)
        this.item.setValue(null, cursor);
    }
    
    /* render tree */
    
    if (this.render==null || this.render.booleanValueInComponent(cursor))
      js.append("document.write(" + treeID + ");\n");
    
    /* close JavaScript */

    _r.appendContentHTMLString(js.script());
    _r.appendEndTag("script");
  }
  
  /* description */

  @Override
  public void appendAttributesToDescription(StringBuilder _d) {
    super.appendAttributesToDescription(_d);
    
    this.appendAssocToDescription(_d, "list", this.list);
    this.appendAssocToDescription(_d, "item", this.item);
  }
}
