/*
 * 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.ScriptableObject;
import org.mozilla.javascript.WrapFactory;
import org.opengroupware.jope.appserver.core.WOAssociation;
import org.opengroupware.jope.appserver.core.WOCompoundResourceManager;
import org.opengroupware.jope.appserver.core.WOContext;
import org.opengroupware.jope.appserver.core.WOPackageLinker;
import org.opengroupware.jope.appserver.core.WORequest;
import org.opengroupware.jope.appserver.core.WORequestHandler;
import org.opengroupware.jope.appserver.core.WOResourceManager;
import org.opengroupware.jope.appserver.core.WOSession;
import org.opengroupware.jope.appserver.publisher.IJoContext;
import org.opengroupware.jope.foundation.UString;
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 ScriptableObject jsRootScope;
  public WrapFactory      jsWrapFactory;

  /**
   * 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();
    
    log.info("application root: " + appRoot);
    
    this.initJavaScript();
    
    /* hack resource manager */
    this.resourceManager = this.linkApplication();
    
    WOAssociation.registerAssociationClassForPrefix("js", JSAssociation.class);
    
    // TBD: we need to setup a JS context and read the Application.js?
    // => hm, better done in dispatch after checking for modifications to
    //    Application.js?
  }
  
  public void initJavaScript() {
    this.jsWrapFactory = new JSWrapFactory();
    
    try {
      Context jscx = Context.enter();
      
      if (false) {
        // this still allows java.lang.System.err.println()
        this.jsRootScope =
          jscx.initStandardObjects(null /* parent scope */, true /* sealed */);
      }
      else {
        // this does Java exposure stuff like importPackage()
        this.jsRootScope = new ImporterTopLevel(jscx, true /* sealed */);
      }

      /* TBD: do we need any classes?
      try {
        ScriptableObject.defineClass
          (rootScope, OGoPubDocumentJSWrapper.class);
      }
      catch (IllegalAccessException e) {
        e.printStackTrace();
      }
      catch (InstantiationException e) {
        e.printStackTrace();
      }
      catch (InvocationTargetException e) {
        e.printStackTrace();
      }
      */
    }
    finally {
      Context.exit();
    }
  }
  
  
  /* accessors */
  
  public Scriptable jsRootScope() {
    return this.jsRootScope;
  }
  
  public Scriptable jsScope() {
    Context jscx = this.jsContext();
    
    return (Scriptable)jscx.getWrapFactory().wrap(
        jscx,
        this.jsRootScope /* parent scope */,
        this             /* java object */,
        null             /* static type */);
  }
  
  public Context jsContext() {
    return Context.getCurrentContext();
  }
  
  /**
   * Returns the wrap factory associated with this JS application.
   * 
   * @return
   */
  public WrapFactory jsWrapFactory() {
    return this.jsWrapFactory;
  }
  
  public File jsAppDirectory() {
    return appRoot;
  }
  
  
  /* notifications */
  
  protected long scriptTimestamp = 0;
  protected long scriptSize      = 0;
  
  @Override
  public void awake() {
    super.awake();
    
    /* refresh from Application.js */
    
    File appScript = new File(this.jsAppDirectory(), "Application.js");
    if (appScript != null)
      this.refreshScript(appScript);
    
    /* call awake */
    JSUtil.callJSFuncWhenAvailable
      (this.jsScope(), this.extraAttributes, this.jsContext(),
       "awake", JSUtil.emptyArgs);
  }
  
  @Override
  public void sleep() {
    JSUtil.callJSFuncWhenAvailable
      (this.jsScope(), this.extraAttributes, this.jsContext(),
       "sleep", JSUtil.emptyArgs);
    
    super.sleep();
  }
  
  
  protected void refreshScript(File appScript) {
    if (appScript == null)
      return;
    
    if (appScript.exists()) {
      long newTimestamp = appScript.lastModified();
      long newSize      = appScript.length();
      
      synchronized(this) {
        if (this.scriptSize == newSize && this.scriptTimestamp == newTimestamp)
          appScript = null;
      }
      
      if (appScript != null) {
        /* so, it changed ;-), reload it */
        
        String src = UString.loadFromFile(appScript);
        String fn  = appScript.getPath();

        Context jscx = Context.getCurrentContext();
        
        Script lScript =
          jscx.compileString(src, fn, 0 /* line */, null /* security domain */);
        
        if (lScript != null) {
          synchronized(this) {
            this.scriptTimestamp = newTimestamp;
            this.scriptSize      = newSize;
          }
        }
        
        lScript.exec(jscx, this.jsScope());
      }
    }
    // Note: can we just clear all extras when the script disappears?
    //       should we store JS stuff in a separate hash?
  }
  
  
  /* request handlers */
  
  @Override
  protected void registerInitialRequestHandlers() {
    super.registerInitialRequestHandlers();
    
    /* redefine direct action handler */
    WORequestHandler rh = new JoCallDirectActionRequestHandler(this);
    this.registerRequestHandler(rh, this.directActionRequestHandlerKey());
    this.registerRequestHandler(rh, "x");
    this.setDefaultRequestHandler(rh);
  }  
  
  /* replace core objects with JS variants */

  @Override
  public WOContext createContextForRequest(WORequest _rq) {
    return new JSContext(this, _rq);
  }
  
  @Override
  public WOSession createSessionForRequest(WORequest _rq) {
    // TBD: read Session.js (and reread on changes?)
    return new JSSession();
  }
  
  
  /* replace JOPE lookup */

  @Override
  public Object lookupName(String _name, IJoContext _ctx, boolean _acquire) {
    // TBD: we could expose some things as OFS objects
    
    File dir = this.jsAppDirectory();
    if (dir != null) {
      File sdir = new File(dir, _name + ".wo");
      if (sdir != null && sdir.isDirectory())
        return new JSJoComponent(this, _name, sdir, (WOContext)_ctx);

      sdir = new File(dir, _name + ".js");
      if (sdir != null && sdir.isFile())
        return new JSJoDirectAction(this, _name, sdir, (WOContext)_ctx);
    }
    
    return super.lookupName(_name, _ctx, _acquire);
  }
  
  @Override
  public String ofsDatabasePathInContext(WOContext _ctx, String[] _path) {
    return this.jsAppDirectory().getPath();
  }
  
  @Override
  public Object rootObjectInContext(WOContext _ctx, String[] _path) {
    // for now we always return this, OFSApp actually returns a folder object
    return this;
  }
  
  /* linking */
  
  /**
   * This method hooks the JSResourceManager into the resource manager list.
   * <p>
   * @return the new resource manager for the application object
   */
  public WOResourceManager linkApplication() {
    WOResourceManager orm = this.resourceManager();
    
    // TBD: hook up own WOResourceManager
    // TBD: load jopelink.txt from below the package path
    
    WOPackageLinker linker =
      new WOPackageLinker(this.isCachingEnabled(), this.joProductManager());
    
    /* add JS manager */
    
    WOResourceManager jrm = new JSResourceManager
      (this, this.jsAppDirectory(), false /* this.isCachingEnabled() */);
    linker.addResourceManager(jrm);
    
    /* add standard managers */
    
    if (orm instanceof WOCompoundResourceManager) {
      /* flatten compound resource manager */
      WOResourceManager[] rms =
        ((WOCompoundResourceManager)orm).resourceManagers();
      if (rms != null) {
        for (WOResourceManager crm: rms)
          linker.addResourceManager(crm); // TBD: does this avoid DUPs?
      }
    }
    else if (orm != null)
      linker.addResourceManager(orm);
    
    // TBD: also load JARs in the root dir?
    
    return linker.resourceManager();
  }
  
  
  /* defaults */

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