package org.opengroupware.jope.appserver;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.opengroupware.jope.foundation.NSKeyValueCoding;
import org.opengroupware.jope.foundation.NSObject;
import org.opengroupware.jope.foundation.UString;

/*
 * WOSession
 * 
 * TODO: document me
 * 
 * THREAD: this object should only be accessed from one thread at a time.
 *         Locking should be ensured by the WOSessionStore checkout/in
 *         mechanism.
 */
// TODO: properly generate sessionID
public class WOSession extends NSObject {
  
  protected String       sessionID          = null;
  protected boolean      storesIDsInURLs    = true;
  protected boolean      storesIDsInCookies = false;
  protected double       timeOut            = 3600; /* 1 hour */
  protected boolean      isTerminating      = false;
  protected List<String> languages          = null;

  private static AtomicInteger snIdCounter = new AtomicInteger(0); 
  
  public WOSession() {
    this.sessionID = this.createSessionID();
  }
  
  /* session-id generator */
  
  protected String createSessionID() {
    // TODO: better place in app object to allow for 'weird' IDs ;-), like
    //       using a session per basic-auth user
    long   now        = new Date().getTime();
    String baseString = "\txyyyzSID\n" + 
      now + "\t" + snIdCounter.incrementAndGet() + "\t" + Math.random() * 10;
    
    return UString.md5HashForString(baseString);
  }
  
  /* accessors */
  
  public String sessionID() {
    return this.sessionID;
  }
  
  public void setStoresIDsInURLs(boolean _flag) {
    this.storesIDsInURLs = _flag;
  }
  public boolean storesIDsInURLs() {
    return this.storesIDsInURLs;
  }
  
  public void setStoresIDsInCookies(boolean _flag) {
    this.storesIDsInCookies = _flag;
  }
  public boolean storesIDsInCookies() {
    return this.storesIDsInCookies;
  }
  
  public void setTimeOut(double _value) {
    this.timeOut = _value;
  }
  public double timeOut() {
    return this.timeOut;
  }
  public long timeOutMillis() {
    return (long)(this.timeOut * 1000);
  }
  
  public void setLanguages(List<String> _languages) {
    this.languages = _languages;
  }
  public List<String> languages() {
    return this.languages;
  }
  
  /* notifications */
  
  protected boolean isAwake = false;
  
  public void awake() {
  }
  public void sleep() {
  }
  
  public void _awakeWithContext(WOContext _ctx) {
    // in SOPE we also setup context/application
    if (!this.isAwake) {
      this.awake();
      this.isAwake = true;
    }
  }
  
  public void _sleepWithContext(WOContext _ctx) {
    if (this.isAwake) {
      this.sleep();
      this.isAwake = false;
    }
    // in SOPE we also tear down context/application
  }
  
  /* termination */
  
  public void terminate() {
    this.isTerminating = true;
  }
  public boolean isTerminating() {
    return this.isTerminating;
  }
  
  /* responder */
  
  public void takeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    String senderID = _ctx.senderID();
    
    if (senderID == null || senderID.length() == 0) {
      /* no element URL is available */
      WOComponent page = _ctx.page();
      
      if (page != null) {
        /* But we do have a page set in the context. This usually means that the
         * -takeValues got triggered by the WODirectActionRequestHandler in
         * combination with a WOComponent being the DirectAction object.
         */
        _ctx.enterComponent(page, null /* component-content */);
        page.takeValuesFromRequest(_rq, _ctx);
        _ctx.leaveComponent(page);
      }
      
      return;
    }
    
    /* regular component action */
    
    if ("GET".equals(_rq.method())) {
      if (_rq.uri.indexOf('?') == -1) {
        /* no form content to apply */
        // TODO: we should run the takeValues nevertheless to clear values?
        return;
      }
    }
    
    // TODO: SOPE: if reqCtxId = ctx.currentElementID() == null
    String reqCtxId = "0";
    
    _ctx.appendElementIDComponent(reqCtxId);
    {
      WOComponent page = _ctx.page();
      
      if (page != null) {
        _ctx.enterComponent(page, null /* component-content */);
        page.takeValuesFromRequest(_rq, _ctx);
        _ctx.leaveComponent(page);
      }
    }
    _ctx.deleteLastElementIDComponent();
  }
  
  public WOActionResults invokeAction(WORequest _rq, WOContext _ctx) {
    WOActionResults result = null;
    
    // TODO: SOPE: if reqCtxId = ctx.currentElementID() == null return null;
    String reqCtxId = null; // TODO
    if (reqCtxId == null)
      /* no sender element ID */
      return null;
    
    _ctx.appendElementIDComponent(reqCtxId);
    {
      WOComponent page = _ctx.page();
      
      if (page != null) {
        // TODO: SOPE: if (_ctx.consumeElementID())
        
        _ctx.enterComponent(page, null /* component-content */);
        result = page.invokeAction(_rq, _ctx);
        _ctx.leaveComponent(page);
      }
    }
    _ctx.deleteLastElementIDComponent();
    
    return result != null ? result : _ctx.page();
  }
  
  public void appendToResponse(WOResponse _r, WOContext _ctx) {
    /* HTTP/1.1 caching directive, prevents browser from caching dyn pages */
    if (_ctx.application().isPageRefreshOnBacktrackEnabled()) {
      String ctype = _r.headerForKey("content-type");
      if (ctype != null) {
        if (ctype.indexOf("html") != -1)
          _r.disableClientCaching();
      }
    }
    
    /* append page */
    
    _ctx.deleteAllElementIDComponents();
    _ctx.appendElementIDComponent(_ctx.contextID());
    {
      WOComponent page = _ctx.page();
      
      if (page != null) {
        _ctx.enterComponent(page, null /* component-content */);
        page.appendToResponse(_r, _ctx);
        _ctx.leaveComponent(page);
      }
    }
    _ctx.deleteLastElementIDComponent();
    
    /* record statistics */
    
    WOStatisticsStore stats = _ctx.application().statisticsStore();
    if (stats != null)
      stats.recordStatisticsForResponse(_r, _ctx);
  }
  
  /* 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.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.sessionID != null)
      _d.append(" id=" + this.sessionID);
    
    if (this.isTerminating)
      _d.append(" terminating");
    else
      _d.append(" timeout=" + this.timeOut);
  }
}
