/*
  Copyright (C) 2000-2003 SKYRIX Software AG

  This file is part of OGo

  OGo 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.

  OGo 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 OGo; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/
// $Id$

#include "NGJavaScriptObjectMappingContext.h"
#include "NGJavaScriptContext.h"
#include "NGJavaScriptObjectHandler.h"
#include "NGJavaScriptObject.h"
#include "NGJavaScriptShadow.h"
#include "NGJavaScriptFunction.h"
#include "NSString+JS.h"
#include "common.h"
#include "globals.h"

#if GNUSTEP_BASE_LIBRARY
#  include <GNUstepBase/behavior.h>
#endif

@interface NSObject(CombinedObjects)
- (void)jsObjectFinalized:(void *)_handle;
- (BOOL)_jsGetValue:(void *)_value inJSContext:(NGJavaScriptContext *)_ctx;
- (id)_js_parentObject;
@end

@interface NGJavaScriptObjectMappingContext(Privates)
- (void)_jsFinalizeCombinedObject:(id)_object;
@end

typedef struct {
  JSObject                         *jso;
  NGJavaScriptObjectMappingContext *ctx;
  NGJavaScriptObjectHandler        *handler;
  BOOL                             rootRef;
  unsigned short                   rc;
} JSCombinedObjInfo;

extern JSClass ObjCShadow_JSClass;

@implementation NGJavaScriptObjectMappingContext

static BOOL       logHandleForObject = NO;
static BOOL       logValueConversion = NO;
static NSMapTable *combinedToInfo = NULL; // combined objects

+ (void)initialize {
  static BOOL didInit = NO;
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  if (didInit) return;
  
  NGJavaScriptBridge_LOG_PROP_DEFINITION
    = [[ud objectForKey:@"jsLogPropDef"] boolValue];
  NGJavaScriptBridge_LOG_FUNC_DEFINITION
    = [[ud objectForKey:@"jsLogFuncDef"] boolValue];

  logHandleForObject = [ud boolForKey:@"JSLogHandleForObject"];
  logValueConversion = [ud boolForKey:@"JSLogValueConversion"];
  
  didInit = YES;
}

- (id)initWithJSContext:(NGJavaScriptContext *)_ctx {
  if ((self = [super init])) {
    self->jsContext = [_ctx retain];
    
    /* 'combined' ObjC-JS objects */
    if (combinedToInfo == NULL) {
      combinedToInfo =
        NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
                         NSOwnedPointerMapValueCallBacks,
                         200);
    }
    
    /* 'pure' ObjC objects */
    self->objcToJS =
      NSCreateMapTable(NSObjectMapKeyCallBacks,
                       NSNonOwnedPointerMapValueCallBacks,
                       200);
    
    /* 'pure' JS objects */
    self->jsToObjC =
      NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
                       NSNonRetainedObjectMapValueCallBacks,
                       200);
    
    /* make default global */
    
    [self pushContext];
    {
      NGJavaScriptObject *glob;

      glob = [[NGJavaScriptObject alloc] init];
      [glob applyStandardClasses];
      [self setGlobalObject:glob];
      [glob release];
    }
    [self popContext];
  }
  return self;
}
- (id)init {
  NGJavaScriptContext *ctx;
  
  ctx = [[NGJavaScriptContext alloc] init];
  self = [self initWithJSContext:ctx];
  [ctx release]; ctx = nil;
  return self;
}

- (void)dealloc {
  if (self->jsToObjC) NSFreeMapTable(self->jsToObjC);
  if (self->objcToJS) NSFreeMapTable(self->objcToJS);
  [self->jsContext release];
  [super dealloc];
}

- (NGJavaScriptContext *)jsContext {
  return self->jsContext;
}

/* hierachy */

- (void)setGlobalObject:(id)_object {
  JSObject *glob;
  
  glob = [self handleForObject:_object];
  JS_SetGlobalObject([self->jsContext handle], glob);
}
- (id)globalObject {
  JSObject *glob;
  
  if ((glob = JS_GetGlobalObject([self->jsContext handle])) == NULL)
    return nil;
  
  return [self objectForHandle:glob];
}

/* proxy factory */

- (void *)proxyForObject:(id)_object {
  /* this is called by handleForObject: */
  NGJavaScriptObjectHandler *jsHandler;
  void *jso;
  
  jsHandler =
    [[NGJavaScriptObjectHandler alloc] initWithObject:_object
                                       inMappingContext:self];
  
  jso = [jsHandler handle];
  
  [jsHandler autorelease];
  
  return jso;
}
- (id)proxyForPureJSObject:(void *)_handle {
  Class proxyClass;
  id    proxy;
  JSContext *cx;
  void      *jsClazz;
  
  if (_handle == NULL)
    return nil;
  
  /* create a proxy for a 'pure' JavaScript object */
  
  cx = [self->jsContext handle];
  jsClazz = OBJ_GET_CLASS(cx, (JSObject *)_handle);
  
  /* use a configurable mapping of JS-class to ObjC class here ? */
  if (jsClazz == &js_ArrayClass)
    proxyClass = [NGJavaScriptArray class];
  else if (jsClazz == &js_FunctionClass)
    proxyClass = [NGJavaScriptFunction class];
  else
    proxyClass = [NGJavaScriptObject class];
  
  proxy = [[proxyClass alloc] initWithHandle:_handle inMappingContext:self];
  return AUTORELEASE(proxy);
}

/* mappings */

- (void *)handleForObject:(id)_object {
  /*
    What is the _object in this context ?
    - I guess it's a NGJavaScriptObject
      - often, but not always, see testjs: called with Blah and MyNum
      - only "custom" objects, not NGJavaScriptObject are registered !
    
    - it checks whether the object itself can return handle 
      (_jsHandleInMapContext:)
      - this seems always true for NGJavaScriptObject's !
    - checks, whether a proxy is already registered
      - I guess a proxy is a JS-object with an attached JSObjectHandler ?
    - otherwise create a new one
    - register new one
    - check parent object of new one
  */
  JSCombinedObjInfo *combinedObjInfo;
  void *jso;
  id   parent;
  
  if (logHandleForObject)
    NSLog(@"-proxyForObject:0x%08X %@", _object, _object);
  
  if (_object == nil) {
    jso = JSVAL_TO_OBJECT(JSVAL_NULL);
    if (logHandleForObject) NSLog(@"  => is nil: 0x%08X", jso);
    return jso;
  }
  
  if ([_object respondsToSelector:@selector(_jsHandleInMapContext:)]) {
    jso = [_object _jsHandleInMapContext:self];
    if (logHandleForObject) {
      NSLog(@"  obj (class %@) handles itself: 0x%08X", 
	    NSStringFromClass([_object class]), jso);
    }
    return jso;
  }
  
  if ((jso = NSMapGet(self->objcToJS, _object))) {
    /* a proxy is already registered */
    if (logHandleForObject) NSLog(@"  proxy already registered: 0x%08X", jso);
    return jso;
  }
  
  if ((combinedObjInfo = NSMapGet(combinedToInfo, _object))) {
    /* check for correct context */
    if (combinedObjInfo->ctx != self) {
      NSLog(@"%s: tried to access combined object 0x%08X<%@> in "
            @"different mapping context (ctx=0x%08X, required=0x%08X)  !",
            __PRETTY_FUNCTION__, _object, NSStringFromClass([_object class]),
            self, combinedObjInfo->ctx);
      return nil;
    }
    if (logHandleForObject) NSLog(@"  proxy is combined object: 0x%08X", jso);
    return combinedObjInfo->jso;
  }
  
  /* create a new proxy */
  
  if (logHandleForObject) NSLog(@"  creating proxy ...");
  if ((jso = [self proxyForObject:_object])) {
    /* register proxy */
#if DEBUG
    NSAssert1(NSMapGet(self->objcToJS, _object) == NULL,
              @"already registered a proxy for object o0x%08X", _object);
#endif
    if (logHandleForObject) NSLog(@"  register handle 0x%08X ...", jso);
    NSMapInsertKnownAbsent(self->objcToJS, _object, jso);
  }
  else {
    NSLog(@"ERROR(%s): proxy creation failed: %@", 
	  __PRETTY_FUNCTION__, _object);
    return NULL;
  }
  
  /* look for parent of new proxy */
  
  if ((parent = [_object _js_parentObject])) {
    void *pjso;
    JSBool res;
    
    if (logHandleForObject) 
      NSLog(@"register parent 0x%08X for object .."), parent;
    
    pjso = [self handleForObject:parent];
    
    res = JS_SetParent([self->jsContext handle], jso, pjso);
    
    if (res == JS_FALSE) {
      NSLog(@"WARNING: ctx %@ couldn't register JS parent %@ on object %@",
            self, parent, _object);
    }
  }
  
  return jso;
}

- (void)registerObject:(id)_object forImportedHandle:(void *)_handle {
#if DEBUG
  NSAssert(_object, @"missing object");
  NSAssert(_handle, @"missing handle");
#endif
  NSMapInsertKnownAbsent(self->jsToObjC, _handle, _object);
}

- (id)objectForHandle:(void *)_handle {
  /*
    What does it do ? What is the return value ? TODO: Document !
  */
  extern JSClass NGJavaScriptObjectHandler_JSClass;
  JSClass *handleClass;
  id obj;
  
  if (_handle == NULL)
    return nil;
  
  if ((handleClass = JS_GetClass(_handle)) == NULL) {
    NSLog(@"couldn't get class of handle 0x%08X", (unsigned)_handle);
    return nil;
  }
  
  /* check for 'reflected' JavaScript objects (combined or ObjC exported) */
  
  if (handleClass == &NGJavaScriptObjectHandler_JSClass) {
    NGJavaScriptObjectHandler *h;
    
    if ((h = JS_GetPrivate([self->jsContext handle], _handle)) == nil) {
      NSLog(@"couldn't get private of JS object 0x%08X "
            @"(NGJavaScriptObjectHandler)", _handle);
      return nil;
    }
    
    return AUTORELEASE(RETAIN([h managedObject]));
  }
  
  if (handleClass == &ObjCShadow_JSClass) {
    NGJavaScriptShadow *h;
    
    if ((h = JS_GetPrivate([self->jsContext handle], _handle)) == nil) {
      NSLog(@"couldn't get private of JS shadow object 0x%08X "
            @"(NGJavaScriptShadow)", _handle);
      return nil;
    }
    
    return AUTORELEASE(RETAIN([h masterObject]));
  }
  
  /* check for 'pure' JavaScript objects */
  
  if ((obj = NSMapGet(self->jsToObjC, _handle)))
    /* found */
    return AUTORELEASE(RETAIN(obj));
  
  if ((obj = [self proxyForPureJSObject:_handle])) {
    /* register proxy */
    [self registerObject:obj forImportedHandle:_handle];
    return obj;
  }
  
  /* couldn't build a proxy */
  return nil;
}

- (void)forgetObject:(id)_object {
  JSCombinedObjInfo *combinedObjInfo;
  
  NSAssert(_object, @"missing object ..");
  
  if ((combinedObjInfo = NSMapGet(combinedToInfo, _object))) {
    if (combinedObjInfo->ctx != self) {
      NSLog(@"forget combined object 0x%08X in wrong context !", _object);
    }
    else
      [self _jsFinalizeCombinedObject:_object];
  }
  else {
    if (NGJavaScriptBridge_TRACK_FORGET) {
      JSObject *jso;
      
      jso = NSMapGet(self->objcToJS, _object);
      NSLog(@"forgetting non-combined object o0x%08X<%@> j0x%08X rc %d",
            _object, NSStringFromClass([_object class]),
            jso,
            [_object retainCount]);
    }
    
    NSMapRemove(self->objcToJS, _object);

#if 0
    [self->jsContext performSelector:@selector(collectGarbage)
                     withObject:nil
                     afterDelay:0.0];
#endif
  }
}
- (void)forgetImportedHandle:(void *)_handle {
  NSMapRemove(self->jsToObjC, _handle);
}

/* handler */

- (NGJavaScriptObjectHandler *)handlerForObject:(id)_object {
  JSObject *jso;
  
  if ((jso = [self handleForObject:_object]) == NULL) {
    NSLog(@"did not find handle for object 0x%08X", _object);
    return nil;
  }
  
  return JS_GetPrivate([self->jsContext handle], jso);
}

/* garbage collection */

- (id)popContext {
  [self->jsContext collectGarbage];
  return [super popContext];
}

- (void)collectGarbage {
  [self pushContext];
  [self->jsContext collectGarbage];
  [self popContext];
}

/* logging */

- (void)_logExportedJavaScriptObjects {
  NSMapEnumerator e;
  JSObject *jso;
  JSClass  *jsClass;
  id       proxy;

  if (NSCountMapTable(jsToObjC) < 1) {
    printf("no imported JavaScript objects.\n");
    return;
  }
  
  e = NSEnumerateMapTable(jsToObjC);
  printf("imported JavaScript objects:\n");
  printf("  %-10s %-20s %-10s %-26s %-2s\n",
	 "JS",
         "JS-Class",
	 "ObjC",
	 "ObjC-Class",
	 "rc");
  while (NSNextMapEnumeratorPair(&e, (void*)&jso, (void*)&proxy)) {
    jsClass = jso ? JS_GetClass(jso) : NULL;
    
    printf("  0x%08X %-20s 0x%08X %-26s %2d\n",
	   (unsigned)jso,
           jsClass ? jsClass->name : "<null>",
	   (unsigned)proxy,
	   [NSStringFromClass([proxy class]) cString],
	   [proxy retainCount]);
  }
}

- (void)_logExportedObjCObjects {
  NSMapEnumerator e;
  id       object;
  JSObject *jsProxy;

  if (NSCountMapTable(objcToJS) < 1) {
    printf("no exported Objective-C objects.\n");
    return;
  }
  
  printf("exported Objective-C objects:\n");
  printf("  %-10s %-20s %-10s %-26s %-2s\n",
	 "ObjC",
	 "ObjC-Class",
	 "JS",
	 "JS-Class",
	 "rc");
  e = NSEnumerateMapTable(objcToJS);
  while (NSNextMapEnumeratorPair(&e, (void*)&object, (void*)&jsProxy)) {
    JSClass *jsClass;
    
    jsClass = jsProxy ? JS_GetClass(jsProxy) : NULL;
    
    printf("  0x%08X %-20s 0x%08X %-26s %2d\n",
	   (unsigned)object,
	   [NSStringFromClass([object class]) cString],
	   (unsigned)jsProxy,
           jsClass ? jsClass->name : "<null>",
	   [object retainCount]);
  }  
}

/* values */

- (id)objectForJSValue:(void *)_value {
  JSType    jsType;
  JSBool    couldConvert;
  JSContext *cx;
  
  couldConvert = JS_FALSE;
  
  if (JSVAL_IS_NULL(*(jsval *)_value))
    return nil;
  
  cx = [self->jsContext handle];
  jsType = JS_TypeOfValue(cx, *(jsval *)_value);
  switch (jsType) {
    case JSTYPE_VOID:
      return nil;
      
    case JSTYPE_FUNCTION:
    case JSTYPE_OBJECT: {
      JSObject *obj;
      
      if (!(couldConvert = JS_ValueToObject(cx, *(jsval *)_value, &obj)))
        break;
      
      return [self objectForHandle:obj];
    }
    
#if 0
    case JSTYPE_FUNCTION: {
      JSFunction *func;
      
      if ((func = JS_ValueToFunction(cx, *(jsval *)_value))) {
        static Class FuncClass = Nil;
        
        if (FuncClass == Nil)
          FuncClass = NSClassFromString(@"NGJavaScriptFunction");

        NSAssert(FuncClass, @"missing JS function class ..");
        
        return AUTORELEASE([[FuncClass alloc] initWithHandle:func
                                              mappingContext:self]);
      }
      else {
        NSLog(@"%s: couldn't get JS function ..", __PRETTY_FUNCTION__);
        couldConvert = NO;
      }
      break;
    }
#endif
      
    case JSTYPE_STRING: {
      JSString *s;

      if ((s = JS_ValueToString(cx, *(jsval *)_value))) {
        return [NSString stringWithJavaScriptString:s];
      }
      else
        couldConvert = NO;
      
      break;
    }
    
    case JSTYPE_NUMBER:
      if (JSVAL_IS_INT(*(jsval *)_value)) {
        int32 i;
        
        if ((couldConvert = JS_ValueToInt32(cx, *(jsval *)_value, &i)))
          return [NSNumber numberWithInt:i];
      }
      else {
        jsdouble d;

        if ((couldConvert = JS_ValueToNumber(cx, *(jsval *)_value, &d)))
          return [NSNumber numberWithDouble:d];
      }
      break;
      
    case JSTYPE_BOOLEAN: {
      JSBool b;
      
      couldConvert = JS_ValueToBoolean(cx, *(jsval *)_value, &b);
      if (couldConvert)
        return [NSNumber numberWithBool:b ? YES : NO];
      break;
    }

    default:
      [NSException raise:@"InvalidJavaScriptTypeException"
                   format:@"JavaScript value has unknown type %i !", jsType];
  }
  
  if (!couldConvert) {
      [NSException raise:@"JavaScriptTypeConvertException"
                   format:@"Could not convert JavaScript value of type %i !",
                     jsType];
  }
  
  return nil;
}

- (BOOL)jsValue:(void *)_value forObject:(id)_obj {
  /*
    This is used to convert ObjC object _obj to a JSVAL.
    
    Cases:
    - _obj is a proxy for a JavaScript object (eg NGJavaScriptObject),
      the proxy will return the value itself
    - _obj is a primitive Foundation object (eg NSString), the object
      will convert itself to a primitiv JavaScript type using
      _jsGetValue:inJSContext:
    - _obj is a complex object, it will be mapped to a proxy JSObject
    
    Primitive types seem to be broken in certain cases right now.
  */
  if (_obj == nil) {
    *(jsval *)_value = JSVAL_NULL;
    return YES;
  }
  
  if ([_obj respondsToSelector:@selector(_jsGetValue:inJSContext:)]) {
    if (logValueConversion) {
      NSLog(@"%s(0x%08X, 0x%08X<%@>) => own handling ..", 
	      __PRETTY_FUNCTION__,
	      _value, _obj, NSStringFromClass([_obj class]));
    }
    /* eg this is called on NSString */
    return [_obj _jsGetValue:_value inJSContext:self->jsContext];
  }
  else if (_value) {
    JSObject *jso;

    if (logValueConversion) {
      NSLog(@"%s(0x%08X, 0x%08X<%@>) => get handle ..", 
	      __PRETTY_FUNCTION__,
	      _value, _obj, NSStringFromClass([_obj class]));
    }
    
    if ((jso = [self handleForObject:_obj]) == NULL)
      return NO;
    
    *((jsval *)_value) = OBJECT_TO_JSVAL(jso);
    return YES;
  }
  else {
    if (logValueConversion) {
      NSLog(@"%s(0x%08X, 0x%08X<%@>) => missing value store ?", 
	      __PRETTY_FUNCTION__,
	      _value, _obj, NSStringFromClass([_obj class]));
    }
    return NO;
  }
}

@end /* NGJavaScriptObjectMappingContext */

@implementation NGJavaScriptObjectMappingContext(CombinedObjects)

/* combined objects */

- (void)makeObjectCombined:(id)_object {
  Class    clazz;
  unsigned oldRC;
  JSCombinedObjInfo *combinedObjInfo;
  id handler;
  
  if (NSMapGet(combinedToInfo, _object))
    /* object is already a combined one */
    return;
  
  oldRC = [_object retainCount];
  clazz = [_object class];
  
  handler = [[NGJavaScriptObjectHandler alloc]
                                        initWithObject:_object
                                        inMappingContext:self];
  
  if (![clazz isJSCombinedObjectClass]) {
    // TODO: is this correct shouldn't we add combined behaviour only
    //       *on* combined classes ??, explain !

#if NeXT_RUNTIME || APPLE_RUNTIME
    NSLog(@"ERROR(%s): combined objects not supported on this runtime!",
          __PRETTY_FUNCTION__);
    /* TODO: port to MacOSX */
#else
    static Class BehaviourClass = Nil;
    BehaviourClass = NSClassFromString(@"JSCombinedObjectBehaviour");
    NSAssert(BehaviourClass, @"did not find JSCombinedObjectBehaviour !");
#if GNUSTEP_BASE_LIBRARY
    behavior_class_add_class(clazz, BehaviourClass);
#else
    class_add_behavior(clazz, BehaviourClass);
#endif
#endif
  }
  
  combinedObjInfo = calloc(1, sizeof(JSCombinedObjInfo));
  combinedObjInfo->jso     = [handler handle];
  combinedObjInfo->handler = handler;
  combinedObjInfo->ctx     = self;
  combinedObjInfo->rc      = oldRC; // -1 ???
  
  combinedObjInfo->rootRef = YES;
  [handler jsRetain];
  AUTORELEASE(handler);
  
  NSMapInsertKnownAbsent(combinedToInfo,  _object, combinedObjInfo);
  
  if (NGJavaScriptBridge_TRACK_MEMORY) {
    NSLog(@"combine: o0x%08X<%@>->j0x%08X "
          @"(handler=0x%08X, old-rc=%d, new-rc=%d)",
          _object, NSStringFromClass([_object class]),
          combinedObjInfo->jso, combinedObjInfo->handler, oldRC, [_object retainCount]);
  }
  
#if DEBUG
  NSAssert([_object isJSCombinedObject], @"still not a combined object !");
#endif
}

- (BOOL)isCombinedObject:(id)_object {
  return NSMapGet(combinedToInfo, _object) ? YES : NO;
}

- (void)_logCombinedObjects {
  NSMapEnumerator e;
  id                object;
  JSCombinedObjInfo *combinedObjInfo;
  
  printf("Combined objects:\n");
  printf("  %-10s %-16s %-2s %-10s %-10s %-10s %-2s %-2s\n",
	 "ObjC",
	 "Class",
	 "o#",
	 "JS",
	 "Ctx",
	 "Handler",
	 "/#",
	 "h#");
  e = NSEnumerateMapTable(combinedToInfo);
  while (NSNextMapEnumeratorPair(&e, (void*)&object, (void*)&combinedObjInfo)) {
    printf("  0x%08X %-16s %2d 0x%08X 0x%08X 0x%08X %2d %2d\n", 
	   (unsigned)object, [NSStringFromClass([object class]) cString],
	   [object retainCount],
	   (unsigned)combinedObjInfo->jso,
	   (unsigned)combinedObjInfo->ctx,
	   (unsigned)combinedObjInfo->handler,
	   [combinedObjInfo->handler jsRootRetainCount],
	   [combinedObjInfo->handler retainCount]);
  }
}

- (void)_jsFinalizeCombinedObject:(id)_object {
  /*
    This should never be called if ObjC RC > 0 !, since the ObjC object
    keeps a root-ref to the JS object !
  */
  JSCombinedObjInfo *combinedObjInfo;
  
  if (_object == nil) return;
  
  if ((combinedObjInfo = NSMapGet(combinedToInfo, _object))) {
    NSAssert(combinedObjInfo->ctx == self, @"invalid ctx for combined finalization !");
    
    if (combinedObjInfo->rc == 0) {
      if (combinedObjInfo->rootRef) {
        [combinedObjInfo->handler jsRelease];
        combinedObjInfo->rootRef = NO;
      }
      
      if (NGJavaScriptBridge_TRACK_MEMORY) {
        NSLog(@"FREEING COMBINED OBJECT o%08X<%@>-j%08X (handler 0x%08X).",
              _object, NSStringFromClass([_object class]),
              combinedObjInfo->jso, combinedObjInfo->handler);
      }
      
      NSMapRemove(combinedToInfo, _object);
      combinedObjInfo = NULL;
      
      /* deallocate Objective-C memory of object */
      [_object dealloc];
    }
    else {
      NSLog(@"WARNING: finalized JS object, but handler RC > 0 !");
    }
  }
}

@end /* NGJavaScriptObjectMappingContext */

@implementation NSObject(JSCombinedObjects)

+ (BOOL)isJSCombinedObjectClass {
  return NO;
}

- (BOOL)isJSCombinedObject {
  return NO;
}
- (NGJavaScriptObjectMappingContext *)jsObjectMappingContext {
  return nil;
}

@end /* NSObject(JSCombinedObjects) */

@implementation JSCombinedObjectBehaviour

- (NGJavaScriptObjectMappingContext *)jsObjectMappingContext {
  JSCombinedObjInfo *combinedObjInfo;
  
  if ((combinedObjInfo = NSMapGet(combinedToInfo, self)) == NULL)
    return nil;
  
  return combinedObjInfo->ctx;
}

+ (BOOL)isJSCombinedObjectClass {
  return YES;
}

- (BOOL)isJSCombinedObject {
  return NSMapGet(combinedToInfo, self) ? YES : NO;
}

/* retain-counting */

- (id)retain {
  JSCombinedObjInfo *combinedObjInfo;
  
  if ((combinedObjInfo = NSMapGet(combinedToInfo, self)) == NULL) {
    if (NGJavaScriptBridge_TRACK_NOINFO_MEMORY) {
      NSLog(@"CO: NO INFO retain: o%08X<%@>, rc=%d",
            self, NSStringFromClass([self class]), [self retainCount]);
    }
    return [super retain];
  }
  
  if (combinedObjInfo->handler == nil) {
    if (NGJavaScriptBridge_TRACK_MEMORY) {
      NSLog(@"CO: NO HANDLER retain: o%08X<%@>-j0x%08X, rc=%d",
            self, NSStringFromClass([self class]),
            combinedObjInfo->jso, [self retainCount]);
    }
    return [super retain];
  }
  
  if (combinedObjInfo->rc == 0) {
    /* life, but not specially retained (RC=1) */
    
    if (!combinedObjInfo->rootRef) {
      /* ensure that the JS object is life */
      [combinedObjInfo->handler jsRetain];
      combinedObjInfo->rootRef = YES;
    }
  }
  combinedObjInfo->rc++;
  
  if (NGJavaScriptBridge_TRACK_MEMORY_RC) {
    NSLog(@"CO: retain: o%08X<%@>-j%08X (handler=0x%08X), rc=%d, root-rc=%d",
          self, NSStringFromClass([self class]), combinedObjInfo->jso, combinedObjInfo->handler,
          combinedObjInfo->rc, [combinedObjInfo->handler jsRootRetainCount]);
  }
  
  return self;
}

- (oneway void)release {
  JSCombinedObjInfo *combinedObjInfo;

  if ((combinedObjInfo = NSMapGet(combinedToInfo, self)) == NULL) {
    if (NGJavaScriptBridge_TRACK_NOINFO_MEMORY)
      NSLog(@"CO: NO INFO release: o%08X, rc=%d", self, [self retainCount]);
    
    [super release];
    return;
  }
  
  if (combinedObjInfo->handler == nil) {
    if (NGJavaScriptBridge_TRACK_MEMORY) {
      NSLog(@"CO: NO HANDLER release: o%08X<%@>-j0x%08X, rc=%d",
            self, NSStringFromClass([self class]),
            combinedObjInfo->jso, [self retainCount]);
    }
    
    [super release];
    return;
  }
  
  if (NGJavaScriptBridge_TRACK_MEMORY_RC) {
    NSLog(@"CO: release: o%08X<%@>-j%08X (handler=0x%08X), rc=%d, root-rc=%d",
          self, NSStringFromClass([self class]), combinedObjInfo->jso, combinedObjInfo->handler,
          [self retainCount], [combinedObjInfo->handler jsRootRetainCount]);
  }
  NSAssert1(combinedObjInfo->handler,
            @"missing handler for combined object 0x%08X ..", self);

  
  /*
    this does never dealloc the ObjC object - the ObjC object is deallocated
    in the JS destructor !
  */

  combinedObjInfo->rc--;
  
  if (combinedObjInfo->rc == 0) {
    /* not specially retained in the ObjC side anymore */
    
    /* JS object is still live, release our root-ref .. */
    NSAssert(combinedObjInfo->rootRef, @"missing JS root-reference");
    [combinedObjInfo->handler jsRelease];
    combinedObjInfo->rootRef = NO;
    
    if (NGJavaScriptBridge_TRACK_MEMORY) {
      NSLog(@"%s: released last ObjC reference of o%08X-j%08X, %d root-refs ..",
            __PRETTY_FUNCTION__,
            self, combinedObjInfo->jso, [combinedObjInfo->handler jsRootRetainCount]);
    }
    
    [combinedObjInfo->ctx performSelector:@selector(collectGarbage)
                withObject:nil
                afterDelay:0.0];
  }
}

- (unsigned)retainCount {
  JSCombinedObjInfo *combinedObjInfo;
  
  if ((combinedObjInfo = NSMapGet(combinedToInfo, self)) == NULL)
    return [super retainCount];
  
  if (combinedObjInfo->handler == nil)
    return [super retainCount];

  return combinedObjInfo->rc;
}

/* evaluation */

- (id)evaluateScript:(NSString *)_js language:(NSString *)_language {
  JSCombinedObjInfo *combinedObjInfo;
  
  if ((combinedObjInfo = NSMapGet(combinedToInfo, self)) == NULL) {
    /* what to do ? */
    return [[[NGJavaScriptObjectMappingContext activeObjectMappingContext]
                                               handlerForObject:self]
                                               evaluateScript:_js];
  }
  
  return [combinedObjInfo->handler evaluateScript:_js];
}
- (id)evaluateJavaScript:(NSString *)_js {
  /* deprecated */
  return [self evaluateScript:_js language:@"javascript"];
}

@end /* JSCombinedObjectBehaviour */
