/*
  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.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.associations.WOKeyPathAssociation;
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.WOMessage;
import org.opengroupware.jope.appserver.core.WORequest;
import org.opengroupware.jope.appserver.core.WOResourceManager;
import org.opengroupware.jope.appserver.core.WOResponse;

/*
 * 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: " + _assocs);
    return null;
  }
  public static boolean containsLinkInAssociations
    (Map<String, WOAssociation> _assocs)
  {
    if (_assocs == null)
      return false;
    
    if (_assocs.containsKey("href"))             return true;
    if (_assocs.containsKey("directActionName")) return true;
    if (_assocs.containsKey("pageName"))         return true;
    if (_assocs.containsKey("action"))           return true;
    return false;
  }
  
  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, false /* no qp session */);
      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
    (String _prefix, Map<String, WOAssociation> _assocs)
  {
    /*
     * This method extract query parameter bindings from a given set of
     * associations. Those bindings start with a question mark (?) followed by
     * the name of the query parameter, eg:
     *   MyLink: WOHyperlink {
     *     directActionName = "doIt";
     *     ?id = 15;
     *   }
     */
    if (_assocs == null)
      return null;
    
    Map<String,WOAssociation> qp = null;
    List<String> toBeRemoved = null; /* not necessary for Collections? */
    int plen = _prefix.length();
    
    for (String k: _assocs.keySet()) {
      if (!k.startsWith(_prefix))
        continue;
      
      if (qp == null) {
        qp = new HashMap<String, WOAssociation>(8);
        toBeRemoved = new ArrayList<String>(16);
      }
      qp.put(k.substring(plen), _assocs.get(k));
      toBeRemoved.add(k);
    }
    
    /* removed grabbed associations */
    if (toBeRemoved != null) {
      for (String k: toBeRemoved)
        _assocs.remove(k);
    }
    return qp;
  }
  
  protected Map<String, Object> queryDictionaryInContext
    (WOContext _ctx, boolean _withQPSn)
  {
    /*
     * This method builds a map of all active query parameters in a link. This
     * includes all query session parameters of the context, a possibly bound
     * 'queryDictionary' binding plus all explicitly named "?" query parameters.
     * 
     * Values override each other with
     * - the queryDictionary values overriding query session values
     * - '?' query bindings overriding the other two 
     */
    boolean hasQPSn = _ctx != null && _ctx.hasActiveQuerySessionValues();
    if (this.queryDictionary == null && this.queryParameters == null &&
        !hasQPSn)
      return null;
    
    Map<String, Object> qd = new HashMap<String, Object>(16);
    Object cursor = _ctx != null ? _ctx.cursor() : null;
    
    if (hasQPSn && _withQPSn) {
      Map<String, Object> qs = _ctx.allQuerySessionValues();
      for (String k: _ctx.activeQuerySessionKeys())
        qd.put(k, qs.get(k));
    }
    
    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));
      }
    }
    
    if (this.queryParameters != null) {
      for (String k: this.queryParameters.keySet()) {
        Object v;
        
        v = this.queryParameters.get(k).valueInComponent(cursor);
        if (v == null) /* do not write query parameters w/o values */
          continue;
        
        qd.put(k, v);
      }
    }
    
    return qd;
  }
  
  protected String queryStringInContext(WOContext _ctx, boolean _withQPSn) {
    Map<String, Object> qp = this.queryDictionaryInContext(_ctx, _withQPSn);
    if (qp == null || qp.size() == 0)
      return null;
    
    StringBuffer sb = new StringBuffer(512);
    String charset = WOMessage.defaultURLEncoding();
    String s;
    
    try {
      for (String k: qp.keySet()) {
        Object v = qp.get(k);
        if (sb.length() > 0) sb.append("&");
        
        // TODO: we could embed type info in the query key, eg:
        //         id:int=512, id:list:int=123,456,789
        s = URLEncoder.encode(k, charset);
        sb.append(s);
        
        if (v != null) {
          sb.append("=");
          s = URLEncoder.encode(v.toString(), charset);
          sb.append(s);
        }
      }
    }
    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 Object 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, false /* no querypara session */);
      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, Object> qd = this.queryDictionaryInContext(_ctx, true);
      if (this.sidInUrl && _ctx.hasSession()) {
        /* add session ID to query parameters */
        if (qd == null)
          qd = new HashMap<String, Object>(1);
        
        qd.put(WORequest.SessionIDKey, _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 Object invokeAction(WORequest _rq, WOContext _ctx) {
      return 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 Object 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;
    }
  }
}
