/*
 * Runtime.js
 * Objective-J
 *
 * Created by Francisco Tolmasky.
 * Copyright 2008-2010, 280 North, Inc.
 *
 * This library 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.1 of the License, or (at your option) any later version.
 *
 * This library 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 this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

var CLS_CLASS           = 0x1,
    CLS_META            = 0x2,
    CLS_INITIALIZED     = 0x4,
    CLS_INITIALIZING    = 0x8;

#define CHANGEINFO(aClass, aSetInfoMask, aClearInfoMask) aClass.info = (aClass.info | (aSetInfoMask)) & ~(aClearInfoMask)
#define GETINFO(aClass, anInfoMask) (aClass.info & (anInfoMask))
#define SETINFO(aClass, anInfoMask) CHANGEINFO(aClass, anInfoMask, 0)
#define CLEARINFO(aClass, anInfoMask) CHANGEINFO(aClass, 0, anInfoMask)
#define ISMETA(aClass) (GETINFO(aClass, CLS_META))
#define GETMETA(aClass) (ISMETA(aClass) ? aClass : aClass.isa)
#define ISINITIALIZED(aClass) GETINFO(GETMETA(aClass), CLS_INITIALIZED)


// MAXIMUM_RECURSION_CHECKS
// If defined, objj_msgSend will check for recursion deeper than MAXIMUM_RECURSION_DEPTH and
// throw an error if found. While crude, this can be helpful when your JavaScript debugger
// crashes on recursion errors (e.g. Safari) or ignores them (e.g. Chrome).

// #define MAXIMUM_RECURSION_CHECKS
#define MAXIMUM_RECURSION_DEPTH 80

GLOBAL(objj_ivar) = function(/*String*/ aName, /*String*/ aType)
{
    this.name = aName;
    this.type = aType;
}

DISPLAY_NAME(objj_ivar);

GLOBAL(objj_method) = function(/*String*/ aName, /*IMP*/ anImplementation, /*String*/ types)
{
    this.name = aName;
    this.method_imp = anImplementation;
    this.types = types;
}

DISPLAY_NAME(objj_method);

GLOBAL(objj_class) = function(displayName)
{
    this.isa            = NULL;

    this.version        = 0;

    this.super_class    = NULL;
    this.sub_classes    = [];

    this.name           = NULL;
    this.info           = 0;

    this.ivar_list      = [];
    this.ivar_store     = function() { };
    this.ivar_dtable    = this.ivar_store.prototype;

    this.method_list    = [];
    this.method_store   = function() { };
    this.method_dtable  = this.method_store.prototype;

#if DEBUG
    // Naming the allocator allows the WebKit heap snapshot tool to display object class names correctly
    // HACK: displayName property is not respected so we must eval a function to name it
    eval("this.allocator = function " + (displayName || "OBJJ_OBJECT").replace(/\W/g, "_") + "() { }");
#else
    this.allocator      = function() { };
#endif

    this._UID           = -1;
}

DISPLAY_NAME(objj_class);

GLOBAL(objj_object) = function()
{
    this.isa    = NULL;
    this._UID   = -1;
}

DISPLAY_NAME(objj_object);

// Working with Classes

GLOBAL(class_getName) = function(/*Class*/ aClass)
{
    if (aClass == Nil)
        return "";

    return aClass.name;
}

DISPLAY_NAME(class_getName);

GLOBAL(class_isMetaClass) = function(/*Class*/ aClass)
{
    if (!aClass)
        return NO;

    return ISMETA(aClass);
}

DISPLAY_NAME(class_isMetaClass);

GLOBAL(class_getSuperclass) = function(/*Class*/ aClass)
{
    if (aClass == Nil)
        return Nil;

    return aClass.super_class;
}

DISPLAY_NAME(class_getSuperclass)

GLOBAL(class_setSuperclass) = function(/*Class*/ aClass, /*Class*/ aSuperClass)
{
    // Set up the actual class hierarchy.
    aClass.super_class = aSuperClass;
    aClass.isa.super_class = aSuperClass.isa;
}

DISPLAY_NAME(class_setSuperclass);

GLOBAL(class_addIvar) = function(/*Class*/ aClass, /*String*/ aName, /*String*/ aType)
{
    var thePrototype = aClass.allocator.prototype;

    // FIXME: Use getInstanceVariable
    if (typeof thePrototype[aName] != "undefined")
        return NO;

    var ivar = new objj_ivar(aName, aType);

    aClass.ivar_list.push(ivar);
    aClass.ivar_dtable[aName] = ivar;

    thePrototype[aName] = NULL;

    return YES;
}

DISPLAY_NAME(class_addIvar);

GLOBAL(class_addIvars) = function(/*Class*/ aClass, /*Array*/ivars)
{
    var index = 0,
        count = ivars.length,
        thePrototype = aClass.allocator.prototype;

    for (; index < count; ++index)
    {
        var ivar = ivars[index],
            name = ivar.name;

        // FIXME: Use getInstanceVariable
        if (typeof thePrototype[name] === "undefined")
        {
            aClass.ivar_list.push(ivar);
            aClass.ivar_dtable[name] = ivar;

            thePrototype[name] = NULL;
        }
    }
}

DISPLAY_NAME(class_addIvars);

GLOBAL(class_copyIvarList) = function(/*Class*/ aClass)
{
    return aClass.ivar_list.slice(0);
}

DISPLAY_NAME(class_copyIvarList);

//#define class_copyIvarList(aClass) (aClass.ivar_list.slice(0))

#define METHOD_DISPLAY_NAME(aClass, aMethod) (ISMETA(aClass) ? '+' : '-') + " [" + class_getName(aClass) + ' ' + method_getName(aMethod) + ']'

GLOBAL(class_addMethod) = function(/*Class*/ aClass, /*SEL*/ aName, /*IMP*/ anImplementation, /*Array<String>*/ types)
{
    // FIXME: return NO if it exists?
    var method = new objj_method(aName, anImplementation, types);

    aClass.method_list.push(method);
    aClass.method_dtable[aName] = method;

#if DEBUG
    // Give this function a "pretty" name for the console.
    method.method_imp.displayName = METHOD_DISPLAY_NAME(aClass, method);
#endif

    // FIXME: Should this be done here?
    // If this is a root class...
    if (!ISMETA(aClass) && GETMETA(aClass).isa === GETMETA(aClass))
        class_addMethod(GETMETA(aClass), aName, anImplementation, types);

    return YES;
}

DISPLAY_NAME(class_addMethod);

GLOBAL(class_addMethods) = function(/*Class*/ aClass, /*Array*/ methods)
{
    var index = 0,
        count = methods.length,

        method_list = aClass.method_list,
        method_dtable = aClass.method_dtable;

    for (; index < count; ++index)
    {
        var method = methods[index];

        // FIXME: Don't do it if it exists?
        method_list.push(method);
        method_dtable[method.name] = method;

#if DEBUG
        // Give this function a "pretty" name for the console.
        method.method_imp.displayName = METHOD_DISPLAY_NAME(aClass, method);
#endif
    }

    // If this is a root class...
    if (!ISMETA(aClass) && GETMETA(aClass).isa === GETMETA(aClass))
        class_addMethods(GETMETA(aClass), methods);
}

DISPLAY_NAME(class_addMethods);

GLOBAL(class_getInstanceMethod) = function(/*Class*/ aClass, /*SEL*/ aSelector)
{
    if (!aClass || !aSelector)
        return NULL;

    var method = aClass.method_dtable[aSelector];

    return method ? method : NULL;
}

DISPLAY_NAME(class_getInstanceMethod);

GLOBAL(class_getInstanceVariable) = function(/*Class*/ aClass, /*String*/ aName)
{
    if (!aClass || !aName)
        return NULL;

    // FIXME: this doesn't appropriately deal with Object's properties.
    var variable = aClass.ivar_dtable[aName];

    return variable;
}

DISPLAY_NAME(class_getInstanceVariable);

GLOBAL(class_getClassMethod) = function(/*Class*/ aClass, /*SEL*/ aSelector)
{
    if (!aClass || !aSelector)
        return NULL;

    var method = GETMETA(aClass).method_dtable[aSelector];

    return method ? method : NULL;
}

DISPLAY_NAME(class_getClassMethod);

GLOBAL(class_respondsToSelector) = function(/*Class*/ aClass, /*SEL*/ aSelector)
{
    return class_getClassMethod(aClass, aSelector) != NULL;
}

DISPLAY_NAME(class_respondsToSelector);

GLOBAL(class_copyMethodList) = function(/*Class*/ aClass)
{
    return aClass.method_list.slice(0);
}

DISPLAY_NAME(class_copyMethodList);

GLOBAL(class_getVersion) = function(/*Class*/ aClass)
{
    return aClass.version;
}

DISPLAY_NAME(class_getVersion);

GLOBAL(class_setVersion) = function(/*Class*/ aClass, /*Integer*/ aVersion)
{
    aClass.version = parseInt(aVersion, 10);
}

DISPLAY_NAME(class_setVersion);

GLOBAL(class_replaceMethod) = function(/*Class*/ aClass, /*SEL*/ aSelector, /*IMP*/ aMethodImplementation)
{
    if (!aClass || !aSelector)
        return NULL;

    var method = aClass.method_dtable[aSelector],
        method_imp = NULL;

    if (method)
        method_imp = method.method_imp;

    method.method_imp = aMethodImplementation;

    return method_imp;
}

DISPLAY_NAME(class_replaceMethod);

var _class_initialize = function(/*Class*/ aClass)
{
    var meta = GETMETA(aClass);

    if (GETINFO(aClass, CLS_META))
        aClass = objj_getClass(aClass.name);

    if (aClass.super_class && !ISINITIALIZED(aClass.super_class))
        _class_initialize(aClass.super_class);

    if (!GETINFO(meta, CLS_INITIALIZED) && !GETINFO(meta, CLS_INITIALIZING))
    {
        SETINFO(meta, CLS_INITIALIZING);

        objj_msgSend(aClass, "initialize");

        CHANGEINFO(meta, CLS_INITIALIZED, CLS_INITIALIZING);
    }
}

var _objj_forward = function(self, _cmd)
{
    var isa = self.isa,
        implementation = isa.method_dtable[SEL_forwardingTargetForSelector_];

    if (implementation)
    {
        var target = implementation.method_imp.call(this, self, SEL_forwardingTargetForSelector_, _cmd);

        if (target && target !== self)
        {
            arguments[0] = target;

            return objj_msgSend.apply(this, arguments);
        }
    }

    implementation = isa.method_dtable[SEL_methodSignatureForSelector_];

    if (implementation)
    {
        var forwardInvocationImplementation = isa.method_dtable[SEL_forwardInvocation_];

        if (forwardInvocationImplementation)
        {
            var signature = implementation.method_imp.call(this, self, SEL_methodSignatureForSelector_, _cmd);

            if (signature)
            {
                var invocationClass = objj_lookUpClass("CPInvocation");

                if (invocationClass)
                {
                    var invocation = objj_msgSend(invocationClass, SEL_invocationWithMethodSignature_, signature),
                        index = 0,
                        count = arguments.length;

                    for (; index < count; ++index)
                        objj_msgSend(invocation, SEL_setArgument_atIndex_, arguments[index], index);

                    forwardInvocationImplementation.method_imp.call(this, self, SEL_forwardInvocation_, invocation);

                    return objj_msgSend(invocation, SEL_returnValue);
                }
            }
        }
    }

    implementation = isa.method_dtable[SEL_doesNotRecognizeSelector_];

    if (implementation)
        return implementation.method_imp.call(this, self, SEL_doesNotRecognizeSelector_, _cmd);

    throw class_getName(isa) + " does not implement doesNotRecognizeSelector:. Did you forget a superclass for " + class_getName(isa) + "?";
};

// I think this forward:: may need to be a common method, instead of defined in CPObject.
#define CLASS_GET_METHOD_IMPLEMENTATION(aMethodImplementation, aClass, aSelector)\
    if (!ISINITIALIZED(aClass))\
        _class_initialize(aClass);\
    \
    var method = aClass.method_dtable[aSelector];\
    \
    aMethodImplementation = method ? method.method_imp : _objj_forward;

GLOBAL(class_getMethodImplementation) = function(/*Class*/ aClass, /*SEL*/ aSelector)
{
    CLASS_GET_METHOD_IMPLEMENTATION(var implementation, aClass, aSelector);

    return implementation;
}

DISPLAY_NAME(class_getMethodImplementation);

// Adding Classes
var REGISTERED_CLASSES  = { };

GLOBAL(objj_allocateClassPair) = function(/*Class*/ superclass, /*String*/ aName)
{
    var classObject = new objj_class(aName),
        metaClassObject = new objj_class(aName),
        rootClassObject = classObject;

    // If we don't have a superclass, we are the root class.
    if (superclass)
    {
        rootClassObject = superclass;

        while (rootClassObject.superclass)
            rootClassObject = rootClassObject.superclass;

        // Give our current allocator all the instance variables of our super class' allocator.
        classObject.allocator.prototype = new superclass.allocator;

        // "Inheret" parent properties.
        classObject.ivar_dtable = classObject.ivar_store.prototype = new superclass.ivar_store;
        classObject.method_dtable = classObject.method_store.prototype = new superclass.method_store;

        metaClassObject.method_dtable = metaClassObject.method_store.prototype = new superclass.isa.method_store;

        // Set up the actual class hierarchy.
        classObject.super_class = superclass;
        metaClassObject.super_class = superclass.isa;
    }
    else
        classObject.allocator.prototype = new objj_object();

    classObject.isa = metaClassObject;
    classObject.name = aName;
    classObject.info = CLS_CLASS;
    classObject._UID = objj_generateObjectUID();

    metaClassObject.isa = rootClassObject.isa;
    metaClassObject.name = aName;
    metaClassObject.info = CLS_META;
    metaClassObject._UID = objj_generateObjectUID();

    return classObject;
}

DISPLAY_NAME(objj_allocateClassPair);

var CONTEXT_BUNDLE = nil;

GLOBAL(objj_registerClassPair) = function(/*Class*/ aClass)
{
    global[aClass.name] = aClass;
    REGISTERED_CLASSES[aClass.name] = aClass;

    addClassToBundle(aClass, CONTEXT_BUNDLE);
}

DISPLAY_NAME(objj_registerClassPair);

// Instantiating Classes

GLOBAL(class_createInstance) = function(/*Class*/ aClass)
{
    if (!aClass)
        throw new Error("*** Attempting to create object with Nil class.");

    var object = new aClass.allocator();

    object.isa = aClass;
    object._UID = objj_generateObjectUID();

    return object;
}

DISPLAY_NAME(class_createInstance);

// Opera 9.5.1 has a bug where prototypes "inheret" members from instances when "with" is used.
// Given that the Opera team is so fond of bug-testing instead of version-testing, we'll go
// ahead and do that.

var prototype_bug = function() { }

prototype_bug.prototype.member = false;

with (new prototype_bug())
    member = true;

// If the bug exists, go down the slow path.
if (new prototype_bug().member)
{

var fast_class_createInstance = class_createInstance;

class_createInstance = function(/*Class*/ aClass)
{
    var object = fast_class_createInstance(aClass);

    if (object)
    {
        var theClass = object.isa,
            actualClass = theClass;

        while (theClass)
        {
            var ivars = theClass.ivar_list,
                count = ivars.length;

            while (count--)
                object[ivars[count].name] = NULL;

            theClass = theClass.super_class;
        }

        object.isa = actualClass;
    }

    return object;
}

}

// Working with Instances

GLOBAL(object_getClassName) = function(/*id*/ anObject)
{
    if (!anObject)
        return "";

    var theClass = anObject.isa;

    return theClass ? class_getName(theClass) : "";
}

DISPLAY_NAME(object_getClassName);

//objc_getClassList
GLOBAL(objj_lookUpClass) = function(/*String*/ aName)
{
    var theClass = REGISTERED_CLASSES[aName];

    return theClass ? theClass : Nil;
}

DISPLAY_NAME(objj_lookUpClass);

GLOBAL(objj_getClass) = function(/*String*/ aName)
{
    var theClass = REGISTERED_CLASSES[aName];

    if (!theClass)
    {
        // class handler callback???
    }

    return theClass ? theClass : Nil;
}

DISPLAY_NAME(objj_getClass);

//objc_getRequiredClass
GLOBAL(objj_getMetaClass) = function(/*String*/ aName)
{
    var theClass = objj_getClass(aName);

    return GETMETA(theClass);
}

DISPLAY_NAME(objj_getMetaClass);

// Working with Instance Variables

GLOBAL(ivar_getName) = function(anIvar)
{
    return anIvar.name;
}

DISPLAY_NAME(ivar_getName);

GLOBAL(ivar_getTypeEncoding) = function(anIvar)
{
    return anIvar.type;
}

DISPLAY_NAME(ivar_getTypeEncoding);

// Sending Messages

#ifdef MAXIMUM_RECURSION_CHECKS
var __objj_msgSend__StackDepth = 0;
#endif

GLOBAL(objj_msgSend) = function(/*id*/ aReceiver, /*SEL*/ aSelector)
{
    if (aReceiver == nil)
        return nil;

    var isa = aReceiver.isa;

    CLASS_GET_METHOD_IMPLEMENTATION(var implementation, isa, aSelector);

#ifdef MAXIMUM_RECURSION_CHECKS
    if (__objj_msgSend__StackDepth++ > MAXIMUM_RECURSION_DEPTH)
        throw new Error("Maximum call stack depth exceeded.");

    try {
#endif

    switch(arguments.length)
    {
        case 2: return implementation(aReceiver, aSelector);
        case 3: return implementation(aReceiver, aSelector, arguments[2]);
        case 4: return implementation(aReceiver, aSelector, arguments[2], arguments[3]);
    }

    return implementation.apply(aReceiver, arguments);

#ifdef MAXIMUM_RECURSION_CHECKS
    } finally {
        __objj_msgSend__StackDepth--;
    }
#endif
}

DISPLAY_NAME(objj_msgSend);

GLOBAL(objj_msgSendSuper) = function(/*id*/ aSuper, /*SEL*/ aSelector)
{
    var super_class = aSuper.super_class;

    arguments[0] = aSuper.receiver;

    CLASS_GET_METHOD_IMPLEMENTATION(var implementation, super_class, aSelector);

    return implementation.apply(aSuper.receiver, arguments);
}

DISPLAY_NAME(objj_msgSendSuper);

// Working with Methods

GLOBAL(method_getName) = function(/*Method*/ aMethod)
{
    return aMethod.name;
}

DISPLAY_NAME(method_getName);

GLOBAL(method_getImplementation) = function(/*Method*/ aMethod)
{
    return aMethod.method_imp;
}

DISPLAY_NAME(method_getImplementation);

GLOBAL(method_setImplementation) = function(/*Method*/ aMethod, /*IMP*/ anImplementation)
{
    var oldImplementation = aMethod.method_imp;

    aMethod.method_imp = anImplementation;

    return oldImplementation;
}

DISPLAY_NAME(method_setImplementation);

GLOBAL(method_exchangeImplementations) = function(/*Method*/ lhs, /*Method*/ rhs)
{
    var lhs_imp = method_getImplementation(lhs),
        rhs_imp = method_getImplementation(rhs);

    method_setImplementation(lhs, rhs_imp);
    method_setImplementation(rhs, lhs_imp);
}

DISPLAY_NAME(method_exchangeImplementations);

// Working with Selectors

GLOBAL(sel_getName) = function(aSelector)
{
    return aSelector ? aSelector : "<null selector>";
}

DISPLAY_NAME(sel_getName);

GLOBAL(sel_getUid) = function(/*String*/ aName)
{
    return aName;
}

DISPLAY_NAME(sel_getUid);

GLOBAL(sel_isEqual) = function(/*SEL*/ lhs, /*SEL*/ rhs)
{
    return lhs === rhs;
}

DISPLAY_NAME(sel_isEqual);

GLOBAL(sel_registerName) = function(/*String*/ aName)
{
    return aName;
}

DISPLAY_NAME(sel_registerName);

objj_class.prototype.toString = objj_object.prototype.toString = function()
{
    var isa = this.isa;

    if (class_getInstanceMethod(isa, SEL_description))
        return objj_msgSend(this, SEL_description);

    if (class_isMetaClass(isa))
        return this.name;

    return "[" + isa.name + " Object](-description not implemented)";
}

var SEL_description                     = sel_getUid("description"),
    SEL_forwardingTargetForSelector_    = sel_getUid("forwardingTargetForSelector:"),
    SEL_methodSignatureForSelector_     = sel_getUid("methodSignatureForSelector:"),
    SEL_forwardInvocation_              = sel_getUid("forwardInvocation:"),
    SEL_doesNotRecognizeSelector_       = sel_getUid("doesNotRecognizeSelector:"),
    SEL_invocationWithMethodSignature_  = sel_getUid("invocationWithMethodSignature:"),
    SEL_setTarget_                      = sel_getUid("setTarget:"),
    SEL_setSelector_                    = sel_getUid("setSelector:"),
    SEL_setArgument_atIndex_            = sel_getUid("setArgument:atIndex:"),
    SEL_returnValue                     = sel_getUid("returnValue");
