/*
 * 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.Wrapper;
import org.opengroupware.jope.appserver.core.WOComponent;
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.publisher.IJoContext;
import org.opengroupware.jope.foundation.NSException;
import org.opengroupware.jope.foundation.UObject;
import org.opengroupware.jope.foundation.UString;

/**
 * JSComponent
 * <p>
 * The JSComponent class manages a component written in JavaScript. Its needs to
 * coordinate between JS, Java, KVC and JOPE.
 */
public class JSComponent extends WOComponent {
  protected static final Log jslog = LogFactory.getLog("JSBridge");
  
  public static Scriptable jsScopeForComponent(WOComponent _comp) {
    if (_comp == null)
      return null;
    
    WOContext  ctx       = _comp.context();
    Context    jscx      = ((JSContext)ctx).jsContext();
    Scriptable rootScope = ((JSApplication)ctx.application()).jsRootScope();
    if (rootScope == null)
      rootScope = jscx.initStandardObjects();
    
    Scriptable componentScope = (Scriptable)jscx.getWrapFactory().wrap(
        jscx,
        rootScope /* parent scope */,
        _comp     /* java object */,
        null      /* static type */);
    return componentScope;
  }
  
  /* accessors */

  /**
   * Wrap the component in the JSComponentAdapter.
   */
  public Scriptable jsScope() {
    return jsScopeForComponent(this);
  }
  
  /**
   * Retrieves the JavaScript context from the WOContext.
   * 
   * @return the current Rhino Context object
   */
  public Context jsContext() {
    return ((JSContext)this.context()).jsContext();
  }
  
  
  /* direct action invocation */

  @Override
  public Object performActionNamed(String _name) {
    // TBD: we need to override this because the WOComponent variant uses Java
    //      Reflection to find the class. In JS we need to check for a slot
    //      containing the function
    Object jv = this.objectForKey(_name + "Action");
    if (jv != Scriptable.NOT_FOUND && jv != null)
      return this.performScriptActionNamed(jv, _name);
    
    return super.performActionNamed(_name);
  }
  
  public Object performScriptActionNamed(Object jv, String _name) {
    if (jv == null)
      return null;
    
    if (jslog != null && jslog.isDebugEnabled())
      jslog.debug("perform nat-JS action: " + _name);
    
    Context cx = ((JSContext)this.context()).jsContext();
      
    Scriptable locScope = this.jsScope();
    
    /* all JS functions are varargs, hence we can pass a lot of information */
    // TBD: should we pass method parameters instead?
    Object args[] = new Object[] {
        Context.javaToJS(this.context().request(), locScope), /* request*/
        Context.javaToJS(_name, locScope), /* name */
        Context.javaToJS(this,  locScope), /* component */
        Context.javaToJS(this.context(), locScope), /* context */
    };

    Object jr;
    try {
      /*
       * Hm, we can set 'this' to something else but the scope! Interesting.
       * But rather obvious that this would work ;-)
       * 
       * If we set 'this' to the Java object itself, this gives us access to
       * the JS methods.
       * 
       * Note: the scope in javaToJS() is the scope parameter being passed to
       *       the NativeJavaObject, which is then stored as the 'parent'
       */
      jr = ((Function)jv).call(cx,
          locScope /* scope */,
          locScope /* this  */,
          args);
    }
    catch (Exception e) {
      // TBD: better error handling. Eg this returns a
      // org.mozilla.javascript.EcmaError: ReferenceError: "pageWithName" ...
      // (would be just a better renderer or do we need to wrap the
      //  exception?)
      return e;
    }
    
    /* fixup result */

    if (jr instanceof Wrapper)
      jr = ((Wrapper)jr).unwrap();
    else if (jr == Scriptable.NOT_FOUND)
      jr = null;

    if (jr == null || jr instanceof Undefined) {
      /* Rhino apparently returns 'Undefined' if the function had no explicit
       * return. I thought JS would return the last expression?
       */
      jr = this.context().page();
    }

    if (jslog != null && jslog.isDebugEnabled())
      jslog.debug("action result: " + jr);
    return jr;
  }

  
  /* override KVC */
  
  @Override
  public void takeValueForKey(Object _value, String _key) {
    // in theory we could move this to a KVC hander, no? One which is triggered
    // based on the class (JSExtraVarClass => JSExtraVarKVCHandler) or something
    // like that
    
    // check whether extra vars contain the key and whether its a JS callable
    if (this.extraAttributes != null) {
      Object v;


      /* first check for a setter */
      
      v = this.extraAttributes.get("set" + UString.capitalizedString(_key));
      if (v instanceof Callable) {
        Scriptable scope = this.jsScope();
        
        Object args[] = new Object[] { Context.javaToJS(_value, scope) };
        ((Callable)v).call(this.jsContext(),
            scope /* scope */,
            scope /* this */,
            args);
        return;
      }
      
      /* If there is no setter, check for a variable. But ensure that we do not
       * override the getter!
       */
      
      v = this.extraAttributes.get(_key);
      if (v != null) {
        if (jslog != null && jslog.isDebugEnabled()) {
          jslog.debug("JSComponent.setValForKey " + _key + " to " + _value+
            " (" + (_value != null ? _value.getClass() : "[null]") + ")");
        }
        
        if (v instanceof Callable) {
          /* its a getter function, do not overwrite the slot */
          // TBD: should we just return and do nothing? Might be better in
          //      bindings
          throw new NSException("attempt to write readonly slot via KVC");
        }
        
        this.extraAttributes.put(_key, _value);
        return;
      }
    }
    
    super.takeValueForKey(_value, _key);
  }

  @Override
  public Object valueForKey(String _key) {
    // check whether extra vars contain the key and whether its a JS callable
    if (this.extraAttributes != null) {
      Object v = this.extraAttributes.get(_key);
      if (v != null) {
        if (jslog != null && jslog.isDebugEnabled()) {
          jslog.debug("JSComponent.valForKey('" + _key + "') => " + 
            v + " (" + (v!= null?v.getClass():"[null]") + ")");
        }
        
        /* check whether the value is a getter */

        if (v instanceof Callable) {
          Scriptable scope = this.jsScope();
          // TBD: we could pass in various args
          v = ((Callable)v).call(this.jsContext(),
              scope /* scope */,
              scope /* this  */,
              JSUtil.emptyArgs);
        }
        
        if (v instanceof Wrapper)
          v = ((Wrapper)v).unwrap();

        return v;
      }
    }
    
    return super.valueForKey(_key);
  }
  
  
  
  /* override relevant methods (subclass API towards JavaScript) */
  
  protected Object callJSFuncWhenAvailable(String _name, Object[] _args) {
    return JSUtil.callJSFuncWhenAvailable
      (this.jsScope(), this.extraAttributes, this.jsContext(), _name, _args);
  }

  @Override
  public void awake() {
    super.awake();
    this.callJSFuncWhenAvailable("awake", JSUtil.emptyArgs);
  }
  @Override
  public void sleep() {
    this.callJSFuncWhenAvailable("sleep", JSUtil.emptyArgs);
    super.sleep();
  }

  @Override
  public boolean synchronizesVariablesWithBindings() {
    Object v = this.callJSFuncWhenAvailable
      ("synchronizesVariablesWithBindings", JSUtil.emptyArgs);
    
    return (v == Scriptable.NOT_FOUND)
      ? super.synchronizesVariablesWithBindings()
      : UObject.boolValue(v);
  }
  
  @Override
  public boolean shouldTakeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    Object v = this.callJSFuncWhenAvailable
      ("shouldTakeValuesFromRequest", new Object[] { _rq, _ctx } );
    
    return (v == Scriptable.NOT_FOUND)
      ? super.shouldTakeValuesFromRequest(_rq, _ctx)
      : UObject.boolValue(v);
  }
  public boolean super_shouldTakeValuesFromRequest(WORequest _r, WOContext _c) {
    return super.shouldTakeValuesFromRequest(_r, _c);
  }
  
  @Override
  public void takeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    Object v = this.callJSFuncWhenAvailable
      ("takeValuesFromRequest", new Object[] { _rq, _ctx } );
    
    if (v == Scriptable.NOT_FOUND)
      super.takeValuesFromRequest(_rq, _ctx);
  }
  public void super_takeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    super.takeValuesFromRequest(_rq, _ctx);
  }
  
  @Override
  public Object invokeAction(WORequest _rq, WOContext _ctx) {
    Object v = this.callJSFuncWhenAvailable
      ("invokeAction", new Object[] { _rq, _ctx } );
  
    return (v == Scriptable.NOT_FOUND)
      ? super.invokeAction(_rq, _ctx)
      : v;
  }
  public Object super_invokeAction(WORequest _rq, WOContext _ctx) {
    return super.invokeAction(_rq, _ctx);
  }

  @Override
  public void appendToResponse(WOResponse _r, WOContext _ctx) {
    Object v = this.callJSFuncWhenAvailable
      ("appendToResponse", new Object[] { _r, _ctx } );
  
  if (v == Scriptable.NOT_FOUND)
    super.appendToResponse(_r, _ctx);
  }
  public void super_appendToResponse(WOResponse _r, WOContext _ctx) {
    super.appendToResponse(_r, _ctx);
  }
  
  @Override
  public WOResponse generateResponse() {
    Object v =
      this.callJSFuncWhenAvailable("generateResponse", JSUtil.emptyArgs);
    
    return (v == Scriptable.NOT_FOUND)
      ? super.generateResponse() : (WOResponse)v;
  }
  public WOResponse super_generateResponse() {
    return super.generateResponse();
  }

  @Override
  public Object lookupName(String _name, IJoContext _ctx, boolean _acquire) {
    Object v = this.callJSFuncWhenAvailable
      ("lookupName", new Object[] { _name, _ctx, _acquire } );
    
    return (v == Scriptable.NOT_FOUND)
      ? super.lookupName(_name, _ctx, _acquire)
      : v;
  }
  public Object super_lookupName(String _name, IJoContext _ctx, boolean _acq) {
    return super.lookupName(_name, _ctx, _acq);
  }

}
