/*
 * Copyright (C) 2004 Apple Computer, Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include "kjs/wk_config.h"
#include "objc_class.h"

#include "objc_instance.h"
#include "WebScriptObject.h"

namespace KJS {
namespace Bindings {
    
static void deleteMethod(CFAllocatorRef, const void* value)
{
    delete static_cast<const Method*>(value);
}
    
static void deleteField(CFAllocatorRef, const void* value)
{
    delete static_cast<const Field*>(value);
}

const CFDictionaryValueCallBacks MethodDictionaryValueCallBacks = { 0, 0, &deleteMethod, 0 , 0 };
const CFDictionaryValueCallBacks FieldDictionaryValueCallBacks = { 0, 0, &deleteField, 0 , 0 };    
    
ObjcClass::ObjcClass(ClassStructPtr aClass)
    : _isa(aClass)
    , _methods(AdoptCF, CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &MethodDictionaryValueCallBacks))
    , _fields(AdoptCF, CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &FieldDictionaryValueCallBacks))
{
}

static CFMutableDictionaryRef classesByIsA = 0;

static void _createClassesByIsAIfNecessary()
{
    if (!classesByIsA)
        classesByIsA = CFDictionaryCreateMutable(NULL, 0, NULL, NULL);
}

ObjcClass* ObjcClass::classForIsA(ClassStructPtr isa)
{
    _createClassesByIsAIfNecessary();

    ObjcClass* aClass = (ObjcClass*)CFDictionaryGetValue(classesByIsA, isa);
    if (!aClass) {
        aClass = new ObjcClass(isa);
        CFDictionaryAddValue(classesByIsA, isa, aClass);
    }

    return aClass;
}

const char* ObjcClass::name() const
{
    return object_getClassName(_isa);
}

MethodList ObjcClass::methodsNamed(const Identifier& identifier, Instance*) const
{
    MethodList methodList;
    char fixedSizeBuffer[1024];
    char* buffer = fixedSizeBuffer;
    const char* JSName = identifier.ascii();
    if (!convertJSMethodNameToObjc(JSName, buffer, sizeof(fixedSizeBuffer))) {
        int length = strlen(JSName) + 1;
        buffer = new char[length];
        if (!buffer || !convertJSMethodNameToObjc(JSName, buffer, length))
            return methodList;
    }

    
    RetainPtr<CFStringRef> methodName(AdoptCF, CFStringCreateWithCString(NULL, buffer, kCFStringEncodingASCII));
    Method* method = (Method*)CFDictionaryGetValue(_methods.get(), methodName.get());
    if (method) {
        methodList.append(method);
        return methodList;
    }

    ClassStructPtr thisClass = _isa;
    while (thisClass && methodList.isEmpty()) {
#if defined(OBJC_API_VERSION) && OBJC_API_VERSION >= 2
        unsigned numMethodsInClass = 0;
        MethodStructPtr* objcMethodList = class_copyMethodList(thisClass, &numMethodsInClass);
#else
        void* iterator = 0;
        struct objc_method_list* objcMethodList;
        while ((objcMethodList = class_nextMethodList(thisClass, &iterator))) {
            unsigned numMethodsInClass = objcMethodList->method_count;
#endif
            for (unsigned i = 0; i < numMethodsInClass; i++) {
#if defined(OBJC_API_VERSION) && OBJC_API_VERSION >= 2
                MethodStructPtr objcMethod = objcMethodList[i];
                SEL objcMethodSelector = method_getName(objcMethod);
#else
                struct objc_method* objcMethod = &objcMethodList->method_list[i];
                SEL objcMethodSelector = objcMethod->method_name;
#endif
                const char* objcMethodSelectorName = sel_getName(objcMethodSelector);
                NSString* mappedName = nil;

                // See if the class wants to exclude the selector from visibility in JavaScript.
                if ([thisClass respondsToSelector:@selector(isSelectorExcludedFromWebScript:)])
                    if ([thisClass isSelectorExcludedFromWebScript:objcMethodSelector])
                        continue;

                // See if the class want to provide a different name for the selector in JavaScript.
                // Note that we do not do any checks to guarantee uniqueness. That's the responsiblity
                // of the class.
                if ([thisClass respondsToSelector:@selector(webScriptNameForSelector:)])
                    mappedName = [thisClass webScriptNameForSelector:objcMethodSelector];

                if ((mappedName && [mappedName isEqual:(NSString*)methodName.get()]) || strcmp(objcMethodSelectorName, buffer) == 0) {
                    Method* aMethod = new ObjcMethod(thisClass, objcMethodSelectorName); // deleted when the dictionary is destroyed
                    CFDictionaryAddValue(_methods.get(), methodName.get(), aMethod);
                    methodList.append(aMethod);
                    break;
                }
            }
#if defined(OBJC_API_VERSION) && OBJC_API_VERSION >= 2
            thisClass = class_getSuperclass(thisClass);
            free(objcMethodList);
#else
        }
        thisClass = thisClass->super_class;
#endif
    }

    if (buffer != fixedSizeBuffer)
        delete [] buffer;

    return methodList;
}

Field* ObjcClass::fieldNamed(const Identifier& identifier, Instance* instance) const
{
    ClassStructPtr thisClass = _isa;

    const char* name = identifier.ascii();
    RetainPtr<CFStringRef> fieldName(AdoptCF, CFStringCreateWithCString(NULL, name, kCFStringEncodingASCII));
    Field* aField = (Field*)CFDictionaryGetValue(_fields.get(), fieldName.get());
    if (aField)
        return aField;

    id targetObject = (static_cast<ObjcInstance*>(instance))->getObject();
    id attributes = [targetObject attributeKeys];
    if (attributes) {
        // Class overrides attributeKeys, use that array of key names.
        unsigned count = [attributes count];
        for (unsigned i = 0; i < count; i++) {
            NSString* keyName = [attributes objectAtIndex:i];
            const char* UTF8KeyName = [keyName UTF8String]; // ObjC actually only supports ASCII names.

            // See if the class wants to exclude the selector from visibility in JavaScript.
            if ([thisClass respondsToSelector:@selector(isKeyExcludedFromWebScript:)])
                if ([thisClass isKeyExcludedFromWebScript:UTF8KeyName])
                    continue;

            // See if the class want to provide a different name for the selector in JavaScript.
            // Note that we do not do any checks to guarantee uniqueness. That's the responsiblity
            // of the class.
            NSString* mappedName = nil;
            if ([thisClass respondsToSelector:@selector(webScriptNameForKey:)])
                mappedName = [thisClass webScriptNameForKey:UTF8KeyName];

            if ((mappedName && [mappedName isEqual:(NSString*)fieldName.get()]) || [keyName isEqual:(NSString*)fieldName.get()]) {
                aField = new ObjcField((CFStringRef)keyName); // deleted when the dictionary is destroyed
                CFDictionaryAddValue(_fields.get(), fieldName.get(), aField);
                break;
            }
        }
    } else {
        // Class doesn't override attributeKeys, so fall back on class runtime
        // introspection.

        while (thisClass) {
#if defined(OBJC_API_VERSION) && OBJC_API_VERSION >= 2
            unsigned numFieldsInClass = 0;
            IvarStructPtr* ivarsInClass = class_copyIvarList(thisClass, &numFieldsInClass);
#else
            struct objc_ivar_list* fieldsInClass = thisClass->ivars;
            if (fieldsInClass) {
                unsigned numFieldsInClass = fieldsInClass->ivar_count;
#endif
                for (unsigned i = 0; i < numFieldsInClass; i++) {
#if defined(OBJC_API_VERSION) && OBJC_API_VERSION >= 2
                    IvarStructPtr objcIVar = ivarsInClass[i];
                    const char* objcIvarName = ivar_getName(objcIVar);
#else
                    IvarStructPtr objcIVar = &fieldsInClass->ivar_list[i];
                    const char* objcIvarName = objcIVar->ivar_name;
#endif
                    NSString* mappedName = 0;

                    // See if the class wants to exclude the selector from visibility in JavaScript.
                    if ([thisClass respondsToSelector:@selector(isKeyExcludedFromWebScript:)])
                        if ([thisClass isKeyExcludedFromWebScript:objcIvarName])
                            continue;

                    // See if the class want to provide a different name for the selector in JavaScript.
                    // Note that we do not do any checks to guarantee uniqueness. That's the responsiblity
                    // of the class.
                    if ([thisClass respondsToSelector:@selector(webScriptNameForKey:)])
                        mappedName = [thisClass webScriptNameForKey:objcIvarName];

                    if ((mappedName && [mappedName isEqual:(NSString*)fieldName.get()]) || strcmp(objcIvarName, name) == 0) {
                        aField = new ObjcField(objcIVar); // deleted when the dictionary is destroyed
                        CFDictionaryAddValue(_fields.get(), fieldName.get(), aField);
                        break;
                    }
                }
#if defined(OBJC_API_VERSION) && OBJC_API_VERSION >= 2
            thisClass = class_getSuperclass(thisClass);
            free(ivarsInClass);
#else
            }
            thisClass = thisClass->super_class;
#endif
        }
    }

    return aField;
}

JSValue* ObjcClass::fallbackObject(ExecState*, Instance* instance, const Identifier &propertyName)
{
    ObjcInstance* objcInstance = static_cast<ObjcInstance*>(instance);
    id targetObject = objcInstance->getObject();
    
    if (![targetObject respondsToSelector:@selector(invokeUndefinedMethodFromWebScript:withArguments:)])
        return jsUndefined();
    return new ObjcFallbackObjectImp(objcInstance, propertyName);
}

}
}
