package org.opengroupware.jope.appserver;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

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

/*
 * WOApplication
 * 
 * TODO: document me
 * TODO: document how it works in a servlet environment
 * TODO: document how properties are located and loaded
 */
public class WOApplication extends NSObject {
  private static AtomicInteger requestCounter = new AtomicInteger(0); 

  protected static final Log log     = LogFactory.getLog("WOApplication");
  protected static final Log pageLog = LogFactory.getLog("WOPages");
  protected static final Log profile = LogFactory.getLog("WOProfiling");
  
  protected WORequestHandler  defaultRequestHandler;
  protected Map<String,WORequestHandler> requestHandlerRegistry;
  
  protected Properties        properties;
  protected WOSessionStore    sessionStore;
  protected WOStatisticsStore statisticsStore;
  protected Class             contextClass;
  protected Class             sessionClass;
  
  public WOApplication() {
    this.init();
  }
  
  public void init() {
    /* at the very beginning, load configuration */
    this.loadProperties();
    
    this.linkFrameworksAndSetupResourceManager();
    this.registerInitialRequestHandlers();
    this.setupDefaultClasses();
    
    this.sessionStore    = WOSessionStore.serverSessionStore();
    this.statisticsStore = new WOStatisticsStore();
  }
  
  protected void linkFrameworksAndSetupResourceManager() {
    WOPackageLinker linker = new WOPackageLinker(this.isCachingEnabled());
    
    linker.linkClass(this.getClass());
    linker.linkWithSpecification(this.getClass().getResource("jopelink.txt"));
    
    /* link system frameworks */
    String sysbase = WOApplication.class.getPackage().getName();
    linker.linkFramework(sysbase + ".elements");
    linker.linkFramework(sysbase);
    
    /* retrieve the resulting resource manager */
    this.resourceManager = linker.resourceManager();
  }
  
  protected void registerInitialRequestHandlers() {
    WORequestHandler rh;
    
    this.requestHandlerRegistry = 
      new ConcurrentHashMap<String, WORequestHandler>(4);
    
    rh = new WODirectActionRequestHandler(this);
    this.registerRequestHandler(rh, this.directActionRequestHandlerKey());
    this.registerRequestHandler(rh, "x");
    this.setDefaultRequestHandler(rh);
    
    rh = new WOResourceRequestHandler(this);
    this.registerRequestHandler(rh, this.resourceRequestHandlerKey());
    this.registerRequestHandler(rh, "WebServerResources");
    this.registerRequestHandler(rh, "Resources");
    
    rh = new WOComponentRequestHandler(this);
    this.registerRequestHandler(rh, this.componentRequestHandlerKey());
  }
  
  protected void setupDefaultClasses() {
    /* try to find a Context/Session in the application package */
    String pkgname = this.getClass().getName();
    int idx = pkgname.lastIndexOf('.');
    pkgname = (idx == -1) ? "" : pkgname.substring(0, idx + 1);
    
    this.contextClass = NSJavaRuntime.NSClassFromString(pkgname + "Context");
    this.sessionClass = NSJavaRuntime.NSClassFromString(pkgname + "Session");
  }
  
  /* notifications */
  
  public void awake() {
  }
  
  public void sleep() {
  }
  
  /* request handling */
  
  public WOResponse dispatchRequest(WORequest _rq) {
    WOResponse r = null;
    int rqId = requestCounter.incrementAndGet();
    
    if (profile.isInfoEnabled()) {
      profile.info("WOApp: [" + rqId + "] start dispatch: " + 
                   (_rq != null ? _rq.method() + " " + _rq.uri() : null));
    }
    
    WORequestHandler rh = this.requestHandlerForRequest(_rq);
    if (rh == null) {
      log.error("got no request handler for request: " + _rq);
      r = null;
    }
    else {
      try {
        r = rh.handleRequest(_rq);
      }
      catch (Exception e) {
        log.error("WOApplication catched exception", e);
        r = null;
      }
    }

    if (profile.isInfoEnabled()) {
      profile.info("WOApp: [" + rqId + "] finished dispatch: " + 
                   (r != null ? r.status() : "-"));
    }
    return r;
  }
  
  /* request handler */
  
  public WORequestHandler requestHandlerForRequest(WORequest _rq) {
    WORequestHandler rh;
    String k;
    
    if ("/favicon.ico".equals(_rq.uri())) {
      log.debug("detected favicon.ico request, use resource handler.");
      rh = this.requestHandlerRegistry.get(this.resourceRequestHandlerKey());
      if (rh != null) return rh;
    }
    
    if ((k = _rq.requestHandlerKey()) == null) {
      log.debug("no request handler key in request, using default:" +
                     _rq.uri());
      return this.defaultRequestHandler();
    }
    
    if ((rh = this.requestHandlerRegistry.get(k)) != null)
      return rh;
    
    log.debug("did not find request handler key, using default: " + k +
                   " / " + _rq.uri());
    return this.defaultRequestHandler();
  }
  public void registerRequestHandler(WORequestHandler _rh, String _key) {
    this.requestHandlerRegistry.put(_key, _rh);
  }
  
  public String[] registeredRequestHandlerKeys() {
    return (String[])(this.requestHandlerRegistry.keySet().toArray());
  }
  
  public void setDefaultRequestHandler(WORequestHandler _rh) {
    // THREAD: may not be called at runtime
    this.defaultRequestHandler = _rh;
  }
  public WORequestHandler defaultRequestHandler() {
    return this.defaultRequestHandler;
  }
  
  public String directActionRequestHandlerKey() {
    return this.properties.getProperty("WODirectActionRequestHandlerKey", "wa");
  }
  public String componentRequestHandlerKey() {
    return this.properties.getProperty("WOComponentRequestHandlerKey", "wo");
  }
  public String resourceRequestHandlerKey() {
    return this.properties.getProperty("WOResourceRequestHandlerKey", "wr");
  }
  
  public WOContext createContextForRequest(WORequest _rq) {
    if (this.contextClass == null)
      return new WOContext(this, _rq);
    
    return (WOContext)NSJavaRuntime.NSAllocateObject
      (this.contextClass,
       new Class[]  { WOApplication.class, WORequest.class },
       new Object[] { this, _rq });
  }
  
  /* page handling */
  
  public WOComponent pageWithName(String _pageName, WOContext _ctx) {
    // TODO: implement
    pageLog.debug("pageWithName:" + _pageName);
    
    WOResourceManager rm = null;
    WOComponent cursor = _ctx.component();
    if (cursor != null)
      rm = cursor.resourceManager();
    if (rm == null)
      rm = this.resourceManager();
    
    if (rm == null) {
      pageLog.error("did not find a resource manager!");
      return null;
    }
    
    WOComponent page = rm.pageWithName(_pageName, _ctx);
    if (page == null) {
      pageLog.error("could not create page: " + _pageName);
      return null;      
    }
    page.ensureAwakeInContext(_ctx);
    return page;
  }
  
  /* sessions */
  
  public void setSessionStore(WOSessionStore _wss) {
    // NOTE: only call in threadsafe sections!
    this.sessionStore = _wss;
  }
  public WOSessionStore sessionStore() {
    return this.sessionStore;
  }
  
  public WOSession restoreSessionWithID(String _sid, WOContext _ctx) {
    if (_sid == null) {
      log.info("attempt to restore session w/o session-id");
      return null;
    }
    
    WOSessionStore st = this.sessionStore();
    if (st == null) {
      log.info("can't restore session, no store is available: " + _sid);
      return null;
    }
    
    if (log.isDebugEnabled())
      log.debug("checkout session: " + _sid);
    
    WOSession sn = st.checkOutSessionForID(_sid, _ctx.request());
    if (sn != null) {
      _ctx.setSession(sn);
      sn._awakeWithContext(_ctx);
    }
    return sn;
  }
  
  public boolean saveSessionForContext(WOContext _ctx) {
    if (_ctx == null)
      return false;
    
    if (!_ctx.hasSession())
      return false;
    
    WOSessionStore st = this.sessionStore();
    if (st == null) {
      log.error("cannot save session, missing a session store!");
      return false;
    }
    
    log.info("checking in session ...");
    st.checkInSessionForContext(_ctx);
    return true;
  }
  
  public boolean refusesNewSessions() {
    return false;
  }
  
  public WOSession initializeSession(WOContext _ctx) {
    if (_ctx == null) {
      log.info("got no context in initializeSession!");
      return null;
    }
    
    WOSession sn = this.createSessionForRequest(_ctx.request());
    if (sn == null) {
      log.debug("createSessionForRequest returned null ...");
      return null;
    }
    
    _ctx.setNewSession(sn);
    sn._awakeWithContext(_ctx);
    // TODO: post WOSessionDidCreateNotification
    return sn;
  }
  
  public WOSession createSessionForRequest(WORequest _rq) {
    if (this.sessionClass == null)
      return new WOSession();
    
    return (WOSession)NSJavaRuntime.NSAllocateObject(this.sessionClass);
  }
  
  public boolean isPageRefreshOnBacktrackEnabled() {
    return true;
  }
  
  /* resource manager */
  
  // TODO: consider threading issues
  protected WOResourceManager resourceManager; /* Note: set by linker */
  
  public void setResourceManager(WOResourceManager _rm) {
    this.resourceManager = _rm;
  }
  public WOResourceManager resourceManager() {
    return this.resourceManager;
  }
  
  /* error handling */
  
  public WOResponse handleException(Throwable _e, WOContext _ctx) {
    // TODO: improve exception page, eg include stacktrace
    _ctx.response().appendContentHTMLString("fail: " + _e.toString());
    _e.printStackTrace();
    return _ctx.response();
  }
  
  public WOResponse handleSessionRestorationError(WOContext _ctx) {
    // TODO: improve exception page
    _ctx.response().appendContentHTMLString("sn fail: " + _ctx.toString());
    return _ctx.response();
  }
  
  public WOResponse handleMissingAction(String _action, WOContext _ctx) {
    /* this is called if a direct action could not be found */
    // TODO: improve exception page
    _ctx.response().appendContentHTMLString("missing action: " + _action);
    return _ctx.response();
  }
  
  /* licensing */
  
  public static final boolean licensingAllowsMultipleInstances() {
    return true;
  }
  public static final boolean licensingAllowsMultipleThreads() {
    return true;
  }
  public static final int licensedRequestLimit() {
    return 100000 /* number of requests (not more than that per window) */;
  }
  public static final long licensedRequestWindow() {
    return 1 /* ms */;
  }
  
  /* statistics */
  
  public WOStatisticsStore statisticsStore() {
    return this.statisticsStore;
  }

  /* responder */
  
  public void takeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    if (_ctx.hasSession())
      _ctx.session().takeValuesFromRequest(_rq, _ctx);
    else {
      WOComponent page = _ctx.page();
      
      if (page != null) {
        _ctx.enterComponent(page, null /* component content */);
        page.takeValuesFromRequest(_rq, _ctx);
        _ctx.leaveComponent(page);
      }
    }
  }

  public WOActionResults invokeAction(WORequest _rq, WOContext _ctx) {
    WOActionResults result;
    
    if (_ctx.hasSession())
      result = _ctx.session().invokeAction(_rq, _ctx);
    else {
      WOComponent page = _ctx.page();
      
      if (page != null) {
        _ctx.enterComponent(page, null /* component content */);
        result = page.invokeAction(_rq, _ctx);
        _ctx.leaveComponent(page);
      }
      else
        result = null;
    }
    return result;
  }

  public void appendToResponse(WOResponse _r, WOContext _ctx) {
    if (_ctx.hasSession())
      _ctx.session().appendToResponse(_r, _ctx);
    else {
      WOComponent page = _ctx.page();
      
      if (page != null) {
        _ctx.enterComponent(page, null /* component content */);
        page.appendToResponse(_r, _ctx);
        _ctx.leaveComponent(page);
      }
    }
  }
  
  /* properties */
  
  protected void loadProperties() {
    InputStream in;
    
    this.properties = new Properties();
    
    /* First load the internal properties of WOApplication */
    in = WOApplication.class.getResourceAsStream("Defaults.properties");
    if (!this.loadProperties(in))
      log.error("failed to load Defaults.properties of WOApplication");
    
    /* Try to load the Defaults.properties resource which is located
     * right beside the WOApplication subclass.
     */
    in = this.getClass().getResourceAsStream("Defaults.properties");
    if (!this.loadProperties(in))
      log.error("failed to load Defaults.properties of application");
    
    /* Finally load configuration from the current directory. We might want
     * to change the lookup strategy ...
     */
    File f = this.userDomainPropertiesFile();
    if (f != null && f.exists()) {
      try {
        in = new FileInputStream(f);
        if (!this.loadProperties(in))
          log.error("failed to load user domain properties: " + f);
        else
          log.info("did load user domain properties: " + f);
      }
      catch (FileNotFoundException e) {
        log.error("did not find user domains file: " + f);
      }
    }
  }
  
  protected File userDomainPropertiesFile() {
    String fn = this.getClass().getSimpleName() + ".properties";
    return new File(System.getProperty("user.dir", "."), fn); 
  }
  
  protected boolean loadProperties(InputStream _in) {
    if (_in == null)
      return true; /* yes, true, resource was not found, no load error */
    
    try {
      this.properties.load(_in);
      return true;
    }
    catch (IOException ioe) {
      return false;
    }
  }
  
  /* a trampoline to make the properties accessible via KVC */
  protected NSObject defaults = new NSObject() {
    public void takeValueForKey(Object _value, String _key) {
      // do nothing, we do not mutate properties
    }
    public Object valueForKey(String _key) {
      return WOApplication.this.properties.getProperty(_key);
    }
  };
  public NSObject defaults() {
    return this.defaults;
  }
  
  public boolean isCachingEnabled() {
    return NSJavaRuntime
      .boolValueForObject(this.properties.get("WOCachingEnabled"));
  }
  
  /* KVC */
  
  public void takeValueForKey(Object _value, String _key) {
    NSKeyValueCoding.DefaultImplementation.takeValueForKey(this, _value, _key);
  }
  public Object valueForKey(String _key) {
    return NSKeyValueCoding.DefaultImplementation.valueForKey(this, _key);
  }
  
  public void takeValueForKeyPath(Object _value, String _keyPath) {
    NSKeyValueCodingAdditions.DefaultImplementation.
      takeValueForKeyPath(this, _value, _keyPath);
  }
  public Object valueForKeyPath(String _keyPath) {
    return NSKeyValueCodingAdditions.DefaultImplementation.
             valueForKeyPath(this, _keyPath);    
  }

  /* description */
  
  public void appendAttributesToDescription(StringBuffer d) {
    // TODO: add some info
  }
}
