#import "EJConvert.h"

NSString *JSValueToNSString( JSContextRef ctx, JSValueRef v ) {
	JSStringRef jsString = JSValueToStringCopy( ctx, v, NULL );
	if( !jsString ) return nil;
	
	NSString *string = (NSString *)JSStringCopyCFString( kCFAllocatorDefault, jsString );
	[string autorelease];
	JSStringRelease( jsString );
	
	return string;
}

JSValueRef NSStringToJSValue( JSContextRef ctx, NSString *string ) {
	JSStringRef jstr = JSStringCreateWithCFString((CFStringRef)string);
	JSValueRef ret = JSValueMakeString(ctx, jstr);
	JSStringRelease(jstr);
	return ret;
}


// JSValueToNumberFast blindly assumes that the given JSValueRef is a
// a number. Everything else will be silently converted to 0.
// This functions comes in a 64bit and 32bit flavor, since the NaN-Boxing
// in JSC works a bit differently on each platforms. For an explanation
// of the taggging refer to JSC/runtime/JSCJSValue.h
// The 32bit version just calls the normal JSValueToNumber() function
// and is thus a lot slower.

double JSValueToNumberFast(JSContextRef ctx, JSValueRef v) {
	#if __LP64__ // arm64 version
		union {
			int64_t asInt64;
			double asDouble;
			struct { int32_t asInt; int32_t tag; } asBits;
		} taggedValue = { .asInt64 = (int64_t)v };
		
		#define DoubleEncodeOffset 0x1000000000000ll
		#define TagTypeNumber 0xffff0000
		#define ValueTrue 0x7
		
		if( (taggedValue.asBits.tag & TagTypeNumber) == TagTypeNumber ) {
			return taggedValue.asBits.asInt;
		}
		else if( taggedValue.asBits.tag & TagTypeNumber ) {
			taggedValue.asInt64 -= DoubleEncodeOffset;
			return taggedValue.asDouble;
		}
		else if( taggedValue.asBits.asInt == ValueTrue ) {
			return 1.0;
		}
		else {
			return 0; // false, undefined, null, object
		}
	#else // armv7 version
		return JSValueToNumber(ctx, v, NULL);
	#endif
}

void JSValueUnprotectSafe( JSContextRef ctx, JSValueRef v ) {
	if( ctx && v ) {
		JSValueUnprotect(ctx, v);
	}
}

JSValueRef NSObjectToJSValue( JSContextRef ctx, NSObject *obj ) {
	JSValueRef ret = NULL;
	
	// String
	if( [obj isKindOfClass:NSString.class] ) {
		ret = NSStringToJSValue(ctx, (NSString *)obj);
	}
	
	// Number or Bool
	else if( [obj isKindOfClass:NSNumber.class] ) {
		NSNumber *number = (NSNumber *)obj;
		if( strcmp(number.objCType, @encode(BOOL)) == 0 ) {
			ret = JSValueMakeBoolean(ctx, number.boolValue);
		}
		else {
			ret = JSValueMakeNumber(ctx, number.doubleValue);
		}
	}
	
	// Date
	else if( [obj isKindOfClass:NSDate.class] ) {
		NSDate *date = (NSDate *)obj;
		JSValueRef timestamp = JSValueMakeNumber(ctx, date.timeIntervalSince1970 * 1000.0);
		ret = JSObjectMakeDate(ctx, 1, &timestamp, NULL);
	}
	
	// Array
	else if( [obj isKindOfClass:NSArray.class] ) {
		NSArray *array = (NSArray *)obj;
		JSValueRef *args = malloc(array.count * sizeof(JSValueRef));
		for( int i = 0; i < array.count; i++ ) {
			args[i] = NSObjectToJSValue(ctx, array[i] );
		}
		ret = JSObjectMakeArray(ctx, array.count, args, NULL);
		free(args);
	}
	
	// Dictionary
	else if( [obj isKindOfClass:NSDictionary.class] ) {
		NSDictionary *dict = (NSDictionary *)obj;
		ret = JSObjectMake(ctx, NULL, NULL);
		for( NSString *key in dict ) {
			JSStringRef jsKey = JSStringCreateWithUTF8CString(key.UTF8String);
			JSValueRef value = NSObjectToJSValue(ctx, dict[key]);
			JSObjectSetProperty(ctx, (JSObjectRef)ret, jsKey, value, NULL, NULL);
			JSStringRelease(jsKey);
		}
	}
	
	return ret ? ret : JSValueMakeNull(ctx);
}

NSObject *JSValueToNSObject( JSContextRef ctx, JSValueRef value ) {
	JSType type = JSValueGetType(ctx, value);
	
	switch( type ) {
		case kJSTypeString: return JSValueToNSString(ctx, value);
		case kJSTypeBoolean: return [NSNumber numberWithBool:JSValueToBoolean(ctx, value)];
		case kJSTypeNumber: return [NSNumber numberWithDouble:JSValueToNumberFast(ctx, value)];
		case kJSTypeNull: return nil;
		case kJSTypeUndefined: return nil;
		case kJSTypeObject: break;
	}
	
	if( type == kJSTypeObject ) {
		JSObjectRef jsObj = (JSObjectRef)value;
		
		// Get the Array constructor to check if this Object is an Array
		JSStringRef arrayName = JSStringCreateWithUTF8CString("Array");
		JSObjectRef arrayConstructor = (JSObjectRef)JSObjectGetProperty(ctx, JSContextGetGlobalObject(ctx), arrayName, NULL);
		JSStringRelease(arrayName);
			
		if( JSValueIsInstanceOfConstructor(ctx, jsObj, arrayConstructor, NULL) ) {
			// Array
			JSStringRef lengthName = JSStringCreateWithUTF8CString("length");
			int count = JSValueToNumberFast(ctx, JSObjectGetProperty(ctx, jsObj, lengthName, NULL));
			JSStringRelease(lengthName);
			
			NSMutableArray *array = [NSMutableArray arrayWithCapacity:count];
			for( int i = 0; i < count; i++ ) {
				NSObject *obj = JSValueToNSObject(ctx, JSObjectGetPropertyAtIndex(ctx, jsObj, i, NULL));
				[array addObject:(obj ? obj : NSNull.null)];
			}
			return array;
		}
		else {
			// Plain Object
			JSPropertyNameArrayRef properties = JSObjectCopyPropertyNames(ctx, jsObj);
			size_t count = JSPropertyNameArrayGetCount(properties);
			
			NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:count];
			for( size_t i = 0; i < count; i++ ) {
				JSStringRef jsName = JSPropertyNameArrayGetNameAtIndex(properties, i);
				NSObject *obj = JSValueToNSObject(ctx, JSObjectGetProperty(ctx, jsObj, jsName, NULL));
				
				NSString *name = (NSString *)JSStringCopyCFString( kCFAllocatorDefault, jsName );
				dict[name] = obj ? obj : NSNull.null;
				[name release];
			}
			
			JSPropertyNameArrayRelease(properties);
			return dict;
		}
	}
	
	return nil;
}