package org.opengroupware.jope.appserver;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.foundation.NSKeyValueCoding;
import org.opengroupware.jope.foundation.NSObject;

public class WOContext extends NSObject
  implements JoContext
{
  protected WOApplication application;
  protected WORequest     request;
  protected WOResponse    response;
  protected WOSession     session;
  protected String        contextID;
  protected List<String>  languages;
  protected Locale        locale;
  protected boolean       hasNewSession;
  protected boolean       savePageRequired;
  protected boolean       isRenderingDisabled;
  
  private static AtomicInteger ctxIdCounter = new AtomicInteger(0); 

  protected static final Log compStackLog = 
    LogFactory.getLog("WOComponentStack");
  protected static final Log formLog      = LogFactory.getLog("WOForms");
  protected static final Log log          = LogFactory.getLog("WOContext");
  
  public WOContext(WOApplication _app, WORequest _rq) {
    this.application = _app;
    this.request     = _rq;
    this.response    = new WOResponse(_rq);
    this.contextID   = "" + ctxIdCounter.incrementAndGet();
    
    this.hasNewSession       = false;
    this.savePageRequired    = false;
    this.isRenderingDisabled = false;
  }
  
  /* accessors */
  
  public WORequest request() {
    return this.request;
  }
  public WOResponse response() {
    return this.response;
  }
  
  public WOApplication application() {
    return this.application;
  }
  
  public String contextID() {
    return this.contextID;
  }
  
  public void setLanguages(List<String> _languages) {
    this.languages = _languages;
    this.setLocale(null); /* reset locale, so you must set it afterwards! */
  }
  public List<String> languages() {
    /* language sequence: context => session => request */
    List<String> langs;
    
    if (this.languages != null)
      return this.languages;
    
    if (this.hasSession()) {
      if ((langs = this.session().languages()) != null)
        return langs;
    }
    
    return this.request().browserLanguages();
  }
  
  public void setLocale(Locale _locale) {
    this.locale = _locale;
  }
  public Locale locale() {
    if (this.locale == null)
      this.locale = WOResourceManager.localeForLanguages(languages());
    return this.locale;
  }
  
  public void enableRendering() {
    this.isRenderingDisabled = false;
  }
  public void disableRendering() {
    this.isRenderingDisabled = true;
  }
  public boolean isRenderingDisabled() {
    return this.isRenderingDisabled; 
  }
  
  /* sessions */
  
  public boolean hasSession() {
    return this.session != null ? true : false;
  }
  public boolean hasNewSession() {
    return this.hasNewSession;
  }
  
  public void setSession(WOSession _sn) {
    this.session = _sn;
  }
  public void setNewSession(WOSession _sn) {
    this.setSession(_sn);
    this.hasNewSession = true;
  }
  
  public WOSession session() {
    // TODO: create session on-demand
    if (this.session == null) {
      this.application.initializeSession(this);
      if (this.session == null)
        log.warn("missing session in context ...");
    }
    return this.session;
  }
  
  /* component tracking */
  
  protected WOComponent   page           = null;
  protected WOComponent[] componentStack = new WOComponent[20];
  protected WOElement[]   contentStack   = new WOElement[20];
  protected int stackPos = -1;
  
  public void setPage(WOComponent _page) {
    if (_page != null)
      _page.ensureAwakeInContext(this);
    
    this.page = _page;
  }
  public WOComponent page() {
    return this.page;
  }
  
  public WOComponent component() {
    return (this.stackPos == -1) ? null : this.componentStack[this.stackPos];
  }
  public WOComponent parentComponent() {
    return (this.stackPos < 1) ? null : this.componentStack[this.stackPos - 1];    
  }
  
  public WOElement componentContent() {
    return (this.stackPos == -1) ? null : this.contentStack[this.stackPos];    
  }
  
  public int componentStackCount() {
    return this.stackPos + 1;
  }
  
  public void enterComponent(WOComponent _component, WOElement _content) {
    // TODO: support increasing the array? currently we will raise an exception
    
    /* push component to stack */
    
    this.stackPos++;
    this.componentStack[this.stackPos] = _component;
    this.contentStack[this.stackPos]   = _content;
    
    /* awake component */
    
    this._awakeComponent(_component);
    
    /* sync with parent */
    
    if (this.stackPos > 0)
      _component.pullValuesFromParent();
  }
  
  public void leaveComponent(WOComponent _component) {
    if (this.stackPos < 0) {
      compStackLog.error("empty stack, tried to leave component: " + 
                         _component);
      return;
    }
    
    /* find component and compare it with the given one */
    
    WOComponent component = this.componentStack[this.stackPos];
    if (_component != null && component != _component) {
      compStackLog.error("component leave mismatch: " + 
                         component + " - " + _component);
      // TODO: scan stack for _component to see whether the _component is
      //       upcoming, do something useful
      return;
    }
    
    /* sync variables back to parent component */
    
    if (this.stackPos > 1)
      component.pushValuesToParent();
    
    /* pop component from stack */
    
    this.componentStack[this.stackPos] = null;
    this.contentStack[this.stackPos]   = null;
    this.stackPos--;
  }
  
  public Object cursor() {
    return this.component();
  }
  
  /* maintaining sleep/awake */
  
  protected Set<WOComponent> awakeComponents = new HashSet<WOComponent>(16);
  
  public void _addAwakeComponent(WOComponent _component) {
    if (_component == null)
      return;
    
    this.awakeComponents.add(_component);
  }
  
  public void _awakeComponent(WOComponent _component) {
    if (_component == null)
      return;
    if (this.awakeComponents.contains(_component)) /* already awake? */
      return;
    
    _component._awakeWithContext(this);
    this._addAwakeComponent(_component);
  }
  
  public void sleepComponents() {
    boolean sendSleepToPage = true;
    
    for (WOComponent c: this.awakeComponents) {
      c._sleepWithContext(this);
      if (c == this.page) sendSleepToPage = false;
    }
    
    if (sendSleepToPage && this.page != null)
      this.page._sleepWithContext(this);
    
    this.awakeComponents.clear();
  }
  
  /* forms */
  
  protected boolean   isInForm = false;
  protected WOElement activeFormElement = null;
  
  public void setIsInForm(boolean _flag) {
    if (this.isInForm && _flag)
      log.warn("form is already active.");
    
    this.isInForm = _flag;
  }
  public boolean isInForm() {
    return this.isInForm;
  }
  
  public void addActiveFormElement(WOElement _element) {
    if (this.activeFormElement != null) {
      formLog.error("active form element already set: " + _element);
      return;
    }
    
    this.activeFormElement = _element;
    this.setRequestSenderID(this.elementID());
  }
  public WOElement activeFormElement() {
    return this.activeFormElement;
  }
  
  /* element IDs */
  
  // TODO: improve speed
  protected String elementID    = "";
  protected String reqElementID = null;
  
  public String elementID() {
    return this.elementID;
  }
  
  public void appendElementIDComponent(String _id) {
    if (this.elementID.length() > 0)
      this.elementID += "." + _id;
    else
      this.elementID = _id;
  }
  public void appendElementIDComponent(int _id) {
    if (this.elementID.length() > 0)
      this.elementID += "." + _id;
    else
      this.elementID += _id;
  }
  public void appendZeroElementIDComponent() {
    if (this.elementID.length() > 0)
      this.elementID += ".0";
    else
      this.elementID = "0";
  }
  
  public void incrementLastElementIDComponent() {
    int v;
    int idx;
    
    // System.err.println("INCR: " + this.elementID);

    idx = this.elementID.lastIndexOf('.');
    if (idx == -1)
      v = Integer.parseInt(this.elementID);
    else {
      String s;
      
      s = this.elementID.substring(idx + 1);
      v = Integer.parseInt(s);
    }
    
    v++;
    this.deleteLastElementIDComponent();
    this.elementID += (this.elementID.length() == 0 ? "" : ".") + v;
  }
  
  public void deleteLastElementIDComponent() {
    int idx;
    
    idx = this.elementID.lastIndexOf('.');
    this.elementID = idx == -1 ? "" : this.elementID.substring(0, idx);
  }
  
  public void deleteAllElementIDComponents() {
    this.elementID = "";
  }
  
  public void setRequestSenderID(String _id) {
    this.reqElementID = _id;
  }
  public String senderID() {
    return this.reqElementID;
  }
  
  /* URL processing */
  
  public String urlWithRequestHandlerKey(String _k, String _path, String _qs) {
    // TODO: implement me
    StringBuffer sb = new StringBuffer(256);
    
    sb.append("/");
    sb.append(this.request.applicationName());
    
    sb.append("/");
    sb.append(_k);
    if (_path != null) {
      if (!_path.startsWith("/")) sb.append("/");
      sb.append(_path);
    }
    if (_qs != null && _qs.length() > 0) {
      sb.append("?");
      sb.append(_qs);
    }
    
    return sb.toString();
  }
  
  public String componentActionURL() {
    WOSession sn;
    
    if ((sn = this.session()) == null) {
      log.error("could no return action URL due to missing session");
      return null;
    }
    
    /* 
     * This makes the request handler save the page in the session at the
     * end of the request (only necessary if the page generates URLs which
     * refer the context).
     */
    this.savePageRequired = true;
    
    // TODO: generate relative link if the request URL was in the same session
    //       and used a component action path
    
    return this.urlWithRequestHandlerKey(
        this.application.componentRequestHandlerKey(), /* request handler key */
        sn.sessionID() + "/" + this.elementID(), /* path */
        null /* query string */);
  }
  
  public String directActionURLForActionNamed(String _name, Map _queryDict) {
    String qs = null;
    if (_queryDict != null) {
      try {
        StringBuffer sb = new StringBuffer(512);
        String charset = WOMessage.defaultURLEncoding();
        
        for (Object k: _queryDict.keySet()) {
          Object v = _queryDict.get(k);
          
          if (sb.length() > 0) sb.append('&');
          sb.append(URLEncoder.encode(k.toString(), charset));
          if (v != null) { // TODO: should we remove the parameter instead?
            sb.append('=');
            sb.append(URLEncoder.encode(v.toString(), charset));
          }
          else if (log.isInfoEnabled())
            log.info("got a null query parameter: " + k);
        }
        
        qs = sb.toString();
      }
      catch (UnsupportedEncodingException e) {
        log.error("could not encode form parameters due to charset", e);        
      }
    }
    
    return this.urlWithRequestHandlerKey(
        this.application.directActionRequestHandlerKey(), /* rq handler key */
        _name, /* path */
        qs     /* query string */);
  }

  /* extra attributes */
  
  // TODO: threading?
  protected Map<String,Object> extraAttributes = null;
  
  public void setObjectForKey(Object _value, String _key) {
    if (_value == null) {
      this.removeObjectForKey(_key);
      return;
    }

    if (this.extraAttributes == null)
      this.extraAttributes = new HashMap<String,Object>(16);
    
    this.extraAttributes.put(_key, _value);
  }
  
  public void removeObjectForKey(String _key) {
    if (this.extraAttributes == null)
      return;
    
    this.extraAttributes.remove(_key);
  }
  
  public Object objectForKey(String _key) {
    if (_key == null || this.extraAttributes == null)
      return null;
    
    return this.extraAttributes.get(_key);
  }
  
  /* KVC */
  
  public void takeValueForKey(Object _value, String _key) {
    if (this.extraAttributes != null) {
      if (this.extraAttributes.containsKey(_key)) {
        this.setObjectForKey(_value, _key);
        return;
      }
    }
    
    NSKeyValueCoding.DefaultImplementation.takeValueForKey(this, _value, _key);
  }
  public Object valueForKey(String _key) {
    Object v;
    
    if ((v = this.objectForKey(_key)) != null)
      return v;
    
    return NSKeyValueCoding.DefaultImplementation.valueForKey(this, _key);
  }

  public Object handleQueryWithUnboundKey(String _key) {
    return this.objectForKey(_key);
  }
  public void handleTakeValueForUnboundKey(Object _value, String _key) {
    this.setObjectForKey(_value, _key);
  }

  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.contextID != null)
      _d.append(" ctx=" + this.contextID);
    else
      _d.append(" no-id");
    
    if (this.elementID != null)
      _d.append(" eid=" + this.elementID);
  }
}
