/*
 * Copyright (C) 2007 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.jsapp;

import java.io.File;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.WrapFactory;
import org.opengroupware.jope.appserver.core.WOActionResults;
import org.opengroupware.jope.appserver.core.WOAssociation;
import org.opengroupware.jope.appserver.core.WOContext;
import org.opengroupware.jope.appserver.core.WORequest;
import org.opengroupware.jope.appserver.core.WOResponse;
import org.opengroupware.jope.appserver.core.WOSession;
import org.opengroupware.jope.appserver.publisher.IJoContext;
import org.opengroupware.jope.ofs.OFSApplication;

/**
 * JSApplication
 * <p>
 * Subclass of WOApplication which manages a JavaScript based JOPE application.
 * It registers a new JS specific request handler, resource manager, manages
 * JS specific core classes like JSSession/JSContext, etc etc
 * <p>
 * From a Rhino perspective the application contains a sealed 'root scope' and
 * manages the wrap factory.
 */
public class JSApplication extends OFSApplication {

  public static File appRoot; // static for apprunner, need to fix this
  
  public Scriptable  jsRootScope;
  public Scriptable  jsComponentScope;
  public WrapFactory jsWrapFactory;
  
  protected JSCachedScriptFile applicationScript;
  protected JSCachedScriptFile sessionScript;
  protected JSCachedScriptFile contextScript;
  protected JSCachedScriptFile componentScript;

  /**
   * This method gets called when the application is setup in the servlet
   * context. Do not forget to call super, otherwise a whole lot will not
   * be setup properly!
   */
  @Override
  public void init() {
    super.init();

    this.defaultRestorationFactory = new JSRestorationFactory();
    
    if (log.isInfoEnabled()) log.info("JSApplication: " + appRoot);
    this.applicationScript = new JSCachedScriptFile(appRoot, "Application.js");
    this.sessionScript     = new JSCachedScriptFile(appRoot, "Session.js");
    this.contextScript     = new JSCachedScriptFile(appRoot, "Context.js");
    this.componentScript   = new JSCachedScriptFile(appRoot, "Component.js");
    
    this.initJavaScript();
    
    WOAssociation.registerAssociationClassForPrefix("js", JSAssociation.class);
    
    
    /* First run of Application.js (when available). We are already synchronized
     * here, but we are running outside a ctx */
    try {
      Context jscx  = Context.enter();
      jscx.setWrapFactory(this.jsWrapFactory());
      
      Scriptable scope = this.jsScope();
      
      Object script = this.applicationScript.refresh(false /* always return */);
      if (script != null)
        ((Script)script).exec(jscx, scope);

      script = this.componentScript.refresh(false /* always return */);
      if (script != null)
        ((Script)script).exec(jscx, this.jsComponentScope());
    }
    finally {
      Context.exit();
    }
  }
  
  public void initJavaScript() {
    this.jsWrapFactory = new JSWrapFactory();
    
    try {
      Context jscx = Context.enter();
      jscx.setWrapFactory(this.jsWrapFactory());
      
      if (false) {
        // this still allows java.lang.System.err.println()
        this.jsRootScope =
          jscx.initStandardObjects(null /* parent scope */, true /* sealed */);
        this.jsComponentScope = jscx.newObject(this.jsRootScope);
        this.jsComponentScope.setParentScope(null);
        this.jsComponentScope.setPrototype(this.jsRootScope);
      }
      else {
        // this does Java exposure stuff like importPackage()
        this.jsRootScope      = new ImporterTopLevel(jscx, true /* sealed */);
        this.jsComponentScope = new ImporterTopLevel(jscx, false);
        this.jsComponentScope.setParentScope(null);
        this.jsComponentScope.setPrototype(this.jsRootScope);
      }
      

      // TBD: do we need any classes? (ScriptableObject.defineClass())
    }
    finally {
      Context.exit();
    }
  }
  
  
  /* accessors */
  
  public Scriptable jsRootScope() {
    return this.jsRootScope;
  }
  
  public Scriptable jsScope() {
    Context jscx = this.jsContext();
    
    Scriptable scope = (Scriptable)jscx.getWrapFactory().wrap(
        jscx,
        this.jsRootScope /* parent scope */,
        this             /* java object */,
        null             /* static type */);
    scope.setParentScope(null); /* we take the globals of Application.js */
    scope.setPrototype(this.jsRootScope);
    return scope;
  }
  
  public Scriptable jsComponentScope() {
    return this.jsComponentScope;
  }
  
  public Context jsContext() {
    return Context.getCurrentContext();
  }
  
  @Override
  public String contextClassName() {
    /* replace WOContext with our JSContext. This is el importante. */
    return JSContext.class.getName();
  }
  
  /**
   * Returns the wrap factory associated with this JS application.
   * 
   * @return
   */
  public WrapFactory jsWrapFactory() {
    return this.jsWrapFactory;
  }
  
  public File jsAppDirectory() {
    return appRoot;
  }
  
  
  /* context maintenance */
  
  @Override
  public WOResponse dispatchRequest(WORequest _rq) {
    /* we override this to ensure that a JS context is active */
    WOResponse res  = null;
    Context    jscx = null;
    try {
      jscx = Context.enter();
      jscx.setWrapFactory(this.jsWrapFactory());
      
      res = super.dispatchRequest(_rq);
    }
    finally {
      if (jscx != null) {
        Context.exit();
        jscx = null;
      }
    }
    return res;
  }
  
  
  /* notifications */
  
  @Override
  public void awake() {
    super.awake();
    
    Scriptable scope = this.jsScope();
    
    /* refresh from Application.js */
    
    Object script = this.applicationScript.refresh(true /* only on change */);
    if (script != null) {
      synchronized(this) {
        /* we run this synchronized to avoid concurrent updates (even
         * though the slots itself are already protected)
         */
        ((Script)script).exec(this.jsContext(), scope);
      }
    }
    
    /* refresh Component.js */
    
    script = this.componentScript.refresh(true /* only on change */);
    if (script != null) {
      synchronized(this) {
        /* we run this synchronized to avoid concurrent updates (even
         * though the slots itself are already protected)
         */
        ((Script)script).exec(this.jsContext(), this.jsComponentScope());
      }
    }
    
    /* call awake */
    JSUtil.callJSFuncWhenAvailable
      (scope, this.extraAttributes, this.jsContext(),
       "awake", JSUtil.emptyArgs);
  }
  
  @Override
  public void sleep() {
    JSUtil.callJSFuncWhenAvailable
      (this.jsScope(), this.extraAttributes, this.jsContext(),
       "sleep", JSUtil.emptyArgs);
    
    super.sleep();
  }
  
  
  /* replace JOPE lookup */

  @Override
  public Object lookupName(String _name, IJoContext _ctx, boolean _acquire) {
    Object v = JSUtil.callJSFuncWhenAvailable
      (this.jsScope(), this.extraAttributes, this.jsContext(),
       "lookupName", new Object[] { _name, _ctx, _acquire });

    return (v != Scriptable.NOT_FOUND)
      ? v
      : super_lookupName(_name, _ctx, _acquire);
  }
  public Object super_lookupName
    (String _name, IJoContext _ctx, boolean _acquire)
  {
    return super.lookupName(_name, _ctx, _acquire);
  }
  
  @Override
  public String ofsDatabasePathInContext(WOContext _ctx, String[] _path) {
    return this.jsAppDirectory().getPath();
  }
  
  
  /* defaults */

  @Override
  protected File userDomainPropertiesFile() {
    return new File(appRoot, "Defaults.properties");
  }
  
  
  /* Subclassing API */

  @Override
  public WOSession restoreSessionWithID(String _sid, WOContext _ctx) {
    Object v = JSUtil.callJSFuncWhenAvailable
      (this.jsScope(), this.extraAttributes, this.jsContext(),
       "restoreSessionWithID",
       new Object[] { _sid, _ctx });

    return (v != Scriptable.NOT_FOUND)
      ? (WOSession)v
      : super_restoreSessionWithID(_sid, _ctx);
  }
  public WOSession super_restoreSessionWithID(String _sid, WOContext _ctx) {
    WOSession sn = super.restoreSessionWithID(_sid, _ctx);
    if (sn != null) {
      Object script = this.sessionScript.refresh(true /* only on change */);
      if (script != null) {
        Scriptable scope = (Scriptable)Context.javaToJS(sn, this.jsScope());
        ((Script)script).exec(this.jsContext(), scope);
      }
    }
    return sn;
  }
  
  @Override
  public WOSession createSessionForRequest(WORequest _rq) {
    Object v = JSUtil.callJSFuncWhenAvailable
      (this.jsScope(), this.extraAttributes, this.jsContext(),
       "createSessionForRequest", new Object[] { _rq });

    return (v != Scriptable.NOT_FOUND)
      ? (WOSession)v
      : this.super_createSessionForRequest(_rq);
  }
  public WOSession super_createSessionForRequest(WORequest _rq) {
    // TBD: read Session.js (and reread on changes?)
    WOSession sn = new JSSession(); // hm, we actually implement that :-)
    if (sn != null) {
      Object script = this.sessionScript.refresh(false /* always return */);
      if (script != null) {
        Scriptable scope = (Scriptable)Context.javaToJS(sn, this.jsScope());
        ((Script)script).exec(this.jsContext(), scope);
      }
    }
    return sn;
  }
  
  @Override
  public WOContext createContextForRequest(WORequest _rq) {
    // TBD: to call a JS backend, we would need to push the Rhino Context. This
    //      is usually done by the JSContext!
    return super_createContextForRequest(_rq);
  }
  public WOContext super_createContextForRequest(WORequest _rq) {
    WOContext ctx = super.createContextForRequest(_rq);

    if (ctx != null) {
      /* the Context is pushed when the script gets -awake, hence its not
       * ready here.
       */
      try {
        Context jscx  = Context.enter();
        jscx.setWrapFactory(this.jsWrapFactory());

        Object script = this.contextScript.refresh(false /* always return */);
        if (script != null) {
          Scriptable lRoot = this.jsRootScope();
          Scriptable scope = (Scriptable)Context.javaToJS(ctx, lRoot);
          scope.setPrototype(lRoot);
          scope.setParentScope(null); /* we are a global variable root */
          ((Script)script).exec(jscx, scope);
        }
      }
      finally {
        // TBD: can't we just do the push in here? And release it by overriding
        //      dispatchRequest or handleRequest? Would remove the dependency
        //      from JSContext (would work with any Context)
        Context.exit();
      }
    }
    return ctx;
  }

  @Override
  public WOResponse handleSessionRestorationError(WOContext _ctx) {
    Object v = JSUtil.callJSFuncWhenAvailable
      (this.jsScope(), this.extraAttributes, this.jsContext(),
       "handleSessionRestorationError", new Object[] { _ctx });

    if (v == Scriptable.NOT_FOUND)
      return super_handleSessionRestorationError(_ctx);
    
    if (v instanceof WOResponse)
      return ((WOResponse)v);
    
    if (v instanceof WOActionResults)
      return ((WOActionResults)v).generateResponse();
    
    log.error("cannot use JS result for handleSessionRestorationError: " + v);
    return null;
  }
  public WOResponse super_handleSessionRestorationError(WOContext _ctx) {
    return super.handleSessionRestorationError(_ctx);
  }

  @Override
  public WOResponse handleMissingAction(String _action, WOContext _ctx) {
    Object v = JSUtil.callJSFuncWhenAvailable
      (this.jsScope(), this.extraAttributes, this.jsContext(),
       "handleMissingAction", new Object[] { _action, _ctx });
    
    return (v != Scriptable.NOT_FOUND)
      ? (WOResponse)v
      : super_handleMissingAction(_action, _ctx);
  }
  public WOResponse super_handleMissingAction(String _action, WOContext _ctx) {
    return super.handleMissingAction(_action, _ctx);
  }
}
