package org.opengroupware.jope.appserver.elements;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.appserver.WOActionResults;
import org.opengroupware.jope.appserver.WOAssociation;
import org.opengroupware.jope.appserver.WOContext;
import org.opengroupware.jope.appserver.WODynamicElement;
import org.opengroupware.jope.appserver.WOElement;
import org.opengroupware.jope.appserver.WOMessage;
import org.opengroupware.jope.appserver.WORequest;
import org.opengroupware.jope.appserver.WOResourceManager;
import org.opengroupware.jope.appserver.WOResponse;
import org.opengroupware.jope.appserver.associations.WOKeyPathAssociation;

/*
 * WOLinkGenerator
 * 
 * This is a helper class to generate URLs for hyperlinks, forms and such. It
 * encapsulates the various options available.
 */
public abstract class WOLinkGenerator extends WOElement {
  protected static Log log = LogFactory.getLog("WOLinks");
  
  // TODO: important: do not initialize ivars here, it overrides the ctor! 
  
  public    WOAssociation fragmentIdentifier;
  protected WOAssociation queryDictionary;
  protected boolean       sidInUrl;

  /* associations beginning with a ? */
  // TODO: better use arrays for speed
  protected Map<String,WOAssociation> queryParameters;
  
  /* primary entry point, use this to create link generators */

  public static WOLinkGenerator linkGeneratorForAssociations
    (Map<String, WOAssociation> _assocs)
  {
    if (_assocs == null)
      return null;
    
    // TODO: improve checks for incorrect combinations?
    
    if (_assocs.containsKey("href"))
      return new WOHrefLinkGenerator("href", _assocs);
    if (_assocs.containsKey("directActionName"))
      return new WODirectActionLinkGenerator(_assocs);
    if (_assocs.containsKey("pageName"))
      return new WOPageNameLinkGenerator(_assocs);
    
    if (_assocs.containsKey("action")) {
      /* use WODirectAction for constant action strings! */
      WOAssociation a = _assocs.get("action");
      if (a.isValueConstant() && (a.valueInComponent(null) instanceof String))
        return new WODirectActionLinkGenerator(_assocs);

      return new WOActionLinkGenerator(_assocs);
    }
    
    log.debug("did not generate a link for the given associations");
    return null;
  }
  
  public static WOLinkGenerator rsrcLinkGeneratorForAssociations
    (String _staticKey, Map<String, WOAssociation> _assocs)
  {
    if (_assocs == null)
      return null;
    
    // TODO: improve checks for incorrect combinations?
    
    if (_assocs.containsKey(_staticKey))
      return new WOHrefLinkGenerator(_staticKey, _assocs);
    if (_assocs.containsKey("directActionName"))
      return new WODirectActionLinkGenerator(_assocs);
    if (_assocs.containsKey("filename"))
      return new WOFileLinkGenerator(_assocs);

    if (_assocs.containsKey("action")) {
      /* use WODirectAction for constant action strings! */
      WOAssociation a = _assocs.get("action");
      if (a.isValueConstant() && (a.valueInComponent(null) instanceof String))
        return new WODirectActionLinkGenerator(_assocs);
    }
    
    log.debug("did not generate a link for the given associations");
    return null;
  }
  
  /* common constructor */
  
  public WOLinkGenerator(Map<String, WOAssociation> _assocs) {
    this.fragmentIdentifier =
      WODynamicElement.grabAssociation(_assocs, "fragmentIdentifier");
    this.queryDictionary    =
      WODynamicElement.grabAssociation(_assocs, "queryDictionary");
    
    WOAssociation a = WODynamicElement.grabAssociation(_assocs, "?wosid");
    if (a == null)
      this.sidInUrl = true;
    else
      this.sidInUrl = a.booleanValueInComponent(null);
    
    this.queryParameters = extractQueryParameters(_assocs);
  }
  
  /* methods */
  
  public abstract String hrefInContext(WOContext _ctx);
  
  public String fullHrefInContext(WOContext _ctx) {
    String url;
    
    if ((url = this.hrefInContext(_ctx)) == null)
      return null;
    
    // TODO: I think this fails with direct actions?
    if (this.fragmentIdentifier != null) {
      String s = this.fragmentIdentifier.stringValueInComponent(_ctx.cursor());
      if (s != null && s.length() > 0)
        url += "#" + s;
    }
    
    if (url.indexOf('?') == -1) { /* if we have one, its a direct action */
      String s = this.queryStringInContext(_ctx);
      if (s != null && s.length() > 0)
        url += "?" + s;
    }
    // TODO: for href links with query parameters we might want to add our
    //       own? (so ? scan will succeed, but we would still add)
    
    return url;
  }
  
  public boolean shouldFormTakeValues(WORequest _rq, WOContext _ctx) {
    return true;
  }
  
  /* common features */
  
  public static Map<String,WOAssociation> extractQueryParameters
    (Map<String, WOAssociation> _assocs)
  {
    if (_assocs == null)
      return null;
    
    Map<String,WOAssociation> qp = null;
    List<String> toBeRemoved = null; /* not necessary for Collections? */
    
    for (String k: _assocs.keySet()) {
      if (!k.startsWith("?"))
        continue;
      
      if (qp == null) {
        qp = new HashMap<String, WOAssociation>(8);
        toBeRemoved = new ArrayList<String>(16);
      }
      qp.put(k.substring(1), _assocs.get(k));
      toBeRemoved.add(k);
    }
    
    /* removed grabbed associations */
    if (toBeRemoved != null) {
      for (String k: toBeRemoved)
        _assocs.remove(k);
    }
    return qp;
  }
  
  protected Map<String, String> queryDictionaryInContext(WOContext _ctx) {
    if (this.queryDictionary == null && this.queryParameters == null)
      return null;
    
    Map<String, String> qd = new HashMap<String, String>(16);
    Object cursor = _ctx.cursor();
    
    if (this.queryDictionary != null) {
      Map dqd = (Map)this.queryDictionary.valueInComponent(cursor);
      if (dqd != null) {
        for (Object k: dqd.keySet())
          qd.put(k.toString(), dqd.get(k).toString());
      }
    }
    
    if (this.queryParameters != null) {
      for (String k: this.queryParameters.keySet()) {
        String v;
        
        v = this.queryParameters.get(k).stringValueInComponent(cursor);
        if (v == null) /* do not write query parameters w/o values */
          continue;
        
        qd.put(k, v);
      }
    }
    
    return qd;
  }
  
  protected String queryStringInContext(WOContext _ctx) {
    if (this.queryDictionary == null && this.queryParameters == null)
      return null;
    
    StringBuffer sb = new StringBuffer(512);
    Object cursor  = _ctx.cursor();
    String charset = WOMessage.defaultURLEncoding();
    String s;
    
    try {
      if (this.queryDictionary != null) {
        Map dqd = (Map)this.queryDictionary.valueInComponent(cursor);
        if (dqd != null) {
          for (Object k: dqd.keySet()) {
            if (sb.length() > 0) sb.append("&");
            s = URLEncoder.encode(k.toString(), charset);
            sb.append(s);
            sb.append("=");
            s = URLEncoder.encode(dqd.get(k).toString(), charset);
            sb.append(s);
          }
        }
      }
      
      if (this.queryParameters != null) {
        for (String k: this.queryParameters.keySet()) {
          String v;
          
          v = this.queryParameters.get(k).stringValueInComponent(cursor);
          if (v == null) /* do not write query parameters w/o values */
            continue;
          
          if (sb.length() > 0) sb.append("&");
          sb.append(URLEncoder.encode(k, charset));
          sb.append("=");
          sb.append(URLEncoder.encode(v, charset));
        }
      }
    }
    catch (UnsupportedEncodingException e) {
      log.error("could not encode form parameters due to charset", e);
    }
    
    return sb.toString();
  }
  
  /* responder support */

  public void takeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    /* links can take form values !!!! (for query-parameters) */
    
    if (this.queryParameters == null)
      return;
    
    /* apply values to ?style parameters */
    Object cursor = _ctx.cursor();
      
    for (String k: this.queryParameters.keySet()) {
      WOAssociation assoc = this.queryParameters.get(k);
        
      if (!assoc.isValueSettableInComponent(cursor))
        continue;
        
      assoc.setValue(_rq.formValueForKey(k), cursor);
    }
  }
  
  public WOActionResults invokeAction(WORequest _rq, WOContext _ctx) {
    /* Note: not all links will result in invokeAction ... */
    return null;
  }
  
  public void appendToResponse(WOResponse _r, WOContext _ctx) {
    String url, s;
      
    if ((url = this.hrefInContext(_ctx)) == null)
      return;
    
    if (this.fragmentIdentifier != null) {
      s = this.fragmentIdentifier.stringValueInComponent(_ctx.cursor());
      if (s != null && s.length() > 0)
        url += "#" + s;
    }
    
    if (url.indexOf('?') == -1) { /* if we have one, its a direct action */
      s = this.queryStringInContext(_ctx);
      if (s != null && s.length() > 0)
        url += "?" + s;
    }
  }  

  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.fragmentIdentifier != null)
      _d.append(" fragment=" + this.fragmentIdentifier);
    if (this.queryDictionary != null)
      _d.append(" qd=" + this.queryDictionary);
    if (this.queryParameters != null)
      _d.append(" qp=" + this.queryParameters);
    if (this.sidInUrl)
      _d.append(" ?wos=true");
  } 
  
  
  /* regular href links */
  
  static class WOHrefLinkGenerator extends WOLinkGenerator {
    WOAssociation href = null;
    
    public WOHrefLinkGenerator
      (String _staticKey, Map<String, WOAssociation> _assocs)
    {
      super(_assocs);
      this.href = WODynamicElement.grabAssociation(_assocs, _staticKey);
    }

    public String hrefInContext(WOContext _ctx) {
      return this.href.stringValueInComponent(_ctx.cursor());
    }
    
    public boolean shouldFormTakeValues(WORequest _rq, WOContext _ctx) {
      if (this.href == null)
        return false;
      
      // TODO: explain this
      String s = this.href.stringValueInComponent(_ctx);
      return s.equals(_rq.uri());
    }
  }
  
  /* regular href links */
  
  static class WOFileLinkGenerator extends WOLinkGenerator {
    WOAssociation filename  = null;
    WOAssociation framework = null;
    
    public WOFileLinkGenerator(Map<String, WOAssociation> _assocs) {
      super(_assocs);
      this.filename  = WODynamicElement.grabAssociation(_assocs, "filename");
      this.framework = WODynamicElement.grabAssociation(_assocs, "framework");
    }

    public String hrefInContext(WOContext _ctx) {
      String fn = null, fw = null;
      
      if (this.filename != null)
        fn = this.filename.stringValueInComponent(_ctx.cursor());
      if (fn == null) return null;
      
      if (this.framework != null)
        fw = this.framework.stringValueInComponent(_ctx.cursor());
      
      WOResourceManager rm = _ctx.component().resourceManager();
      if (rm == null) rm = _ctx.application().resourceManager();
      if (rm == null) return null;
      
      return rm.urlForResourceNamed(fn, fw, _ctx.languages(), _ctx);
    }    
  }
  
  /* direct action links */
  
  static class WODirectActionLinkGenerator extends WOLinkGenerator {
    // TODO: fix fragment processing? (comes after query string?)
    WOAssociation directActionName = null;
    WOAssociation actionClass      = null;
    
    public WODirectActionLinkGenerator(Map<String, WOAssociation> _assocs) {
      super(_assocs);
      
      this.directActionName =
        WODynamicElement.grabAssociation(_assocs, "directActionName");
      if (this.directActionName == null) {
        this.directActionName =
          WODynamicElement.grabAssociation(_assocs, "action");
      }
      
      this.actionClass =
        WODynamicElement.grabAssociation(_assocs, "actionClass");
      if (this.actionClass == null)
        this.actionClass = new WOKeyPathAssociation("context.page.name");
    }

    public String hrefInContext(WOContext _ctx) {
      if (this.directActionName == null)
        return null;
      
      Object cursor = _ctx.cursor();
      String lda = this.directActionName.stringValueInComponent(cursor);
      if (this.actionClass != null) {
        String lac = this.actionClass.stringValueInComponent(cursor);
        if (lac != null && lac.length() > 0)
          lda = lac + "/" + lda;
      }
      
      Map<String, String> qd = this.queryDictionaryInContext(_ctx);
      if (this.sidInUrl && _ctx.hasSession()) {
        /* add session ID to query parameters */
        if (qd == null)
          qd = new HashMap<String, String>(1);
        
        // TODO: use constant for wosid
        qd.put("wosid", _ctx.session().sessionID());
      }
      
      return _ctx.directActionURLForActionNamed(lda, qd);
    }
    
    public boolean shouldFormTakeValues(WORequest _rq, WOContext _ctx) {
      /* let page decide for direct actions ... */
      return _ctx.page().shouldTakeValuesFromRequest(_rq, _ctx);
    }

    /* description */
  
    public void appendAttributesToDescription(StringBuffer _d) {
      if (this.directActionName != null)
        _d.append(" da=" + this.directActionName);
      if (this.actionClass != null)
        _d.append(" class=" + this.actionClass);
      
      super.appendAttributesToDescription(_d);
    }
  }
  
  /* component action links */
  
  static class WOActionLinkGenerator extends WOLinkGenerator {
    WOAssociation action = null;
    
    public WOActionLinkGenerator(Map<String, WOAssociation> _assocs) {
      super(_assocs);
      this.action = WODynamicElement.grabAssociation(_assocs, "action");
    }

    public String hrefInContext(WOContext _ctx) {
      if (this.action == null)
        return null;
      
      return _ctx.componentActionURL();
    }
    
    public WOActionResults invokeAction(WORequest _rq, WOContext _ctx) {
      return (WOActionResults)this.action.valueInComponent(_ctx.cursor());
    }
    
    public boolean shouldFormTakeValues(WORequest _rq, WOContext _ctx) {
      return _ctx.elementID().equals(_ctx.senderID());
    }
  }
  
  /* page links */

  static class WOPageNameLinkGenerator extends WOLinkGenerator {
    WOAssociation pageName = null;
    
    public WOPageNameLinkGenerator(Map<String, WOAssociation> _assocs) {
      super(_assocs);
      this.pageName = WODynamicElement.grabAssociation(_assocs, "pageName");
    }

    public String hrefInContext(WOContext _ctx) {
      return _ctx.componentActionURL();
    }
    
    public WOActionResults invokeAction(WORequest _rq, WOContext _ctx) {
      if (this.pageName == null)
        return null;
      
      String pn = this.pageName.stringValueInComponent(_ctx.cursor());
      return _ctx.application().pageWithName(pn, _ctx);
    }
    
    public boolean shouldFormTakeValues(WORequest _rq, WOContext _ctx) {
      return _ctx.elementID().equals(_ctx.senderID());
    }
  }
  
  /* value links */

  static class WOValueLinkGenerator extends WOLinkGenerator {
    WOAssociation value = null;
    
    public WOValueLinkGenerator(Map<String, WOAssociation> _assocs) {
      super(_assocs);
      this.value = WODynamicElement.grabAssociation(_assocs, "value");
    }

    public String hrefInContext(WOContext _ctx) {
      // TODO: not implemented
      log.error("VALUE LINKS ARE NOT IMPLEMENTED");
      return null;
    }
  }
}
