<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use dom::bindings::codegen::PrototypeList;
use dom::bindings::codegen::PrototypeList::MAX_PROTO_CHAIN_LENGTH;
use dom::bindings::node;
use dom::node::{AbstractNode, ScriptView};
use script_task::page_from_context;

use std::cast;
use std::hashmap::HashMap;
use std::libc;
use std::ptr;
use std::ptr::{null, to_unsafe_ptr};
use std::str;
use std::unstable::intrinsics;
use js::glue::*;
use js::glue::{DefineFunctionWithReserved, GetObjectJSClass, RUST_OBJECT_TO_JSVAL};
use js::glue::{js_IsObjectProxyClass, js_IsFunctionProxyClass, IsProxyHandlerFamily};
use js::glue::{PROPERTY_STUB, STRICT_PROPERTY_STUB, ENUMERATE_STUB, CONVERT_STUB, RESOLVE_STUB};
use js::jsapi::{JS_AlreadyHasOwnProperty, JS_NewObject, JS_NewFunction, JS_GetGlobalObject};
use js::jsapi::{JS_DefineProperties, JS_WrapValue, JS_ForwardGetPropertyTo};
use js::jsapi::{JS_EncodeString, JS_free, JS_GetStringCharsAndLength};
use js::jsapi::{JS_GetClass, JS_LinkConstructorAndPrototype};
use js::jsapi::{JS_GetFunctionPrototype, JS_InternString, JS_GetFunctionObject};
use js::jsapi::{JS_HasPropertyById, JS_GetPrototype, JS_GetGlobalForObject};
use js::jsapi::{JS_NewStringCopyN, JS_DefineFunctions, JS_DefineProperty};
use js::jsapi::{JS_ValueToString, JS_GetReservedSlot, JS_SetReservedSlot};
use js::jsapi::{JSContext, JSObject, JSBool, jsid, JSClass, JSNative};
use js::jsapi::{JSFunctionSpec, JSPropertySpec, JSVal, JSPropertyDescriptor};
use js::jsfriendapi::bindgen::JS_NewObjectWithUniqueType;
use js::rust::Compartment;
use js::{JSCLASS_HAS_RESERVED_SLOTS, JSPROP_ENUMERATE, JSVAL_NULL};
use js::{JSPROP_PERMANENT, JSID_VOID, JSPROP_NATIVE_ACCESSORS, JSPROP_GETTER};
use js::{JSPROP_SETTER, JSVAL_VOID, JSVAL_TRUE, JSVAL_FALSE};
use js::{JS_THIS_OBJECT, JSFUN_CONSTRUCTOR, JS_CALLEE, JSPROP_READONLY};
use js;

static TOSTRING_CLASS_RESERVED_SLOT: libc::size_t = 0;
static TOSTRING_NAME_RESERVED_SLOT: libc::size_t = 1;

struct GlobalStaticData {
    proxy_handlers: HashMap<uint, *libc::c_void>,
    attribute_ids: HashMap<uint, ~[jsid]>,
    method_ids: HashMap<uint, ~[jsid]>,
    constant_ids: HashMap<uint, ~[jsid]>
}

pub fn GlobalStaticData() -> GlobalStaticData {
    GlobalStaticData {
        proxy_handlers: HashMap::new(),
        attribute_ids: HashMap::new(),
        method_ids: HashMap::new(),
        constant_ids: HashMap::new()
    }
}

extern fn InterfaceObjectToString(cx: *JSContext, _argc: uint, vp: *mut JSVal) -> JSBool {
  unsafe {
    let callee = RUST_JSVAL_TO_OBJECT(*JS_CALLEE(cx, cast::transmute(&vp)));
    let obj = JS_THIS_OBJECT(cx, cast::transmute(&vp));
    if obj.is_null() {
        //XXXjdm figure out JSMSG madness
        /*JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_CANT_CONVERT_TO,
                             "null", "object");*/
        return 0;
    }

    let v = GetFunctionNativeReserved(callee, TOSTRING_CLASS_RESERVED_SLOT);
    let clasp: *JSClass = cast::transmute(RUST_JSVAL_TO_PRIVATE(*v));

    let v = GetFunctionNativeReserved(callee, TOSTRING_NAME_RESERVED_SLOT);

    if GetObjectJSClass(obj) != clasp {
      /*let jsname: *JSString = RUST_JSVAL_TO_STRING(*v);
      let length = 0;
      let name = JS_GetInternedStringCharsAndLength(jsname, &length);*/
        //XXXjdm figure out JSMSG madness
        /*JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_INCOMPATIBLE_PROTO,
                             NS_ConvertUTF16toUTF8(name).get(), "toString",
                             "object");*/
        return 0;
    }

    let name = jsval_to_str(cx, *v).unwrap();
    let retval = str(~"function " + name + "() {\n    [native code]\n}");
    *vp = domstring_to_jsval(cx, &retval);
    return 1;
  }
}

#[deriving(Clone)]
pub enum DOMString {
    str(~str),
    null_string
}

impl DOMString {
    pub fn to_str(&self) -> ~str {
        match *self {
          str(ref s) => s.clone(),
          null_string => ~""
        }
    }

    pub fn get_ref<'a>(&'a self) -> &'a str {
        match *self {
            str(ref s) => s.as_slice(),
            null_string => &'a "",
        }
    }
}

pub struct rust_box<T> {
    rc: uint,
    td: *intrinsics::TyDesc,
    next: *(),
    prev: *(),
    payload: T
}

fn is_dom_class(clasp: *JSClass) -> bool {
    unsafe {
        ((*clasp).flags & js::JSCLASS_IS_DOMJSCLASS) != 0
    }
}

pub fn is_dom_proxy(obj: *JSObject) -> bool {
    unsafe {
        (js_IsObjectProxyClass(obj) || js_IsFunctionProxyClass(obj)) &&
            IsProxyHandlerFamily(obj)
    }
}

pub unsafe fn unwrap<T>(obj: *JSObject) -> T {
    let clasp = JS_GetClass(obj);
    let slot = if is_dom_class(clasp) {
        DOM_OBJECT_SLOT
    } else {
        assert!(is_dom_proxy(obj));
        DOM_PROXY_OBJECT_SLOT
    } as u32;
    let val = JS_GetReservedSlot(obj, slot);
    cast::transmute(RUST_JSVAL_TO_PRIVATE(val))
}

pub unsafe fn get_dom_class(obj: *JSObject) -> Result<DOMClass, ()> {
    let clasp = JS_GetClass(obj);
    if is_dom_class(clasp) {
        debug!("plain old dom object");
        let domjsclass: *DOMJSClass = cast::transmute(clasp);
        return Ok((*domjsclass).dom_class);
    }
    if is_dom_proxy(obj) {
        debug!("proxy dom object");
        let dom_class: *DOMClass = cast::transmute(GetProxyHandlerExtra(obj));
        return Ok(*dom_class);
    }
    debug!("not a dom object");
    return Err(());
}

pub fn unwrap_object<T>(obj: *JSObject, proto_id: PrototypeList::id::ID, proto_depth: uint) -> Result<T, ()> {
    unsafe {
        do get_dom_class(obj).chain |dom_class| {
            if dom_class.interface_chain[proto_depth] == proto_id {
                debug!("good prototype");
                Ok(unwrap(obj))
            } else {
                debug!("bad prototype");
                Err(())
            }
        }
    }
}

pub fn unwrap_value<T>(val: *JSVal, proto_id: PrototypeList::id::ID, proto_depth: uint) -> Result<T, ()> {
    unsafe {
        let obj = RUST_JSVAL_TO_OBJECT(*val);
        unwrap_object(obj, proto_id, proto_depth)
    }
}

pub unsafe fn squirrel_away<T>(x: @mut T) -> *rust_box<T> {
    let y: *rust_box<T> = cast::transmute(x);
    cast::forget(x);
    y
}

//XXX very incomplete
pub fn jsval_to_str(cx: *JSContext, v: JSVal) -> Result<~str, ()> {
    unsafe {
        let jsstr;
        if RUST_JSVAL_IS_STRING(v) == 1 {
            jsstr = RUST_JSVAL_TO_STRING(v)
        } else {
            jsstr = JS_ValueToString(cx, v);
            if jsstr.is_null() {
                return Err(());
            }
        }

        let strbuf = JS_EncodeString(cx, jsstr);
        let buf = str::raw::from_c_str(strbuf);
        JS_free(cx, strbuf as *libc::c_void);
        Ok(buf)
    }
}

pub unsafe fn domstring_to_jsval(cx: *JSContext, string: &DOMString) -> JSVal {
    match string {
      &null_string => {
        JSVAL_NULL
      }
      &str(ref s) => {
        do s.as_imm_buf |buf, len| {
            let cbuf = cast::transmute(buf);
            RUST_STRING_TO_JSVAL(JS_NewStringCopyN(cx, cbuf, len as libc::size_t))
        }
      }
    }
}

pub fn get_compartment(cx: *JSContext) -> @mut Compartment {
    unsafe {
        let page = page_from_context(cx);
        let compartment = (*page).js_info.get_ref().js_compartment;
        assert!(cx == compartment.cx.ptr);
        compartment
    }
}

extern fn has_instance(_cx: *JSContext, obj: **JSObject, v: *JSVal, bp: *mut JSBool) -> JSBool {
    //XXXjdm this is totally broken for non-object values
    unsafe {
        let mut o = RUST_JSVAL_TO_OBJECT(*v);
        let obj = *obj;
        *bp = 0;
        while o.is_not_null() {
            if o == obj {
                *bp = 1;
                break;
            }
            o = JS_GetPrototype(o);
        }
        return 1;
    }
}

pub fn prototype_jsclass(name: ~str) -> @fn(compartment: @mut Compartment) -> JSClass {
    let f: @fn(@mut Compartment) -> JSClass = |compartment: @mut Compartment| {
        unsafe {
            JSClass {
                name: compartment.add_name(name.to_owned()),
                flags: 0,
                addProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                delProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                getProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                setProperty: GetJSClassHookStubPointer(STRICT_PROPERTY_STUB) as *u8,
                enumerate: GetJSClassHookStubPointer(ENUMERATE_STUB) as *u8,
                resolve: GetJSClassHookStubPointer(RESOLVE_STUB) as *u8,
                convert: GetJSClassHookStubPointer(CONVERT_STUB) as *u8,
                finalize: null(),
                checkAccess: null(),
                call: null(),
                hasInstance: has_instance,
                construct: null(),
                trace: null(),
                reserved: (null(), null(), null(), null(), null(),  // 05
                           null(), null(), null(), null(), null(),  // 10
                           null(), null(), null(), null(), null(),  // 15
                           null(), null(), null(), null(), null(),  // 20
                           null(), null(), null(), null(), null(),  // 25
                           null(), null(), null(), null(), null(),  // 30
                           null(), null(), null(), null(), null(),  // 35
                           null(), null(), null(), null(), null())  // 40
            }
        }
    };
    return f;
}

pub fn instance_jsclass(name: ~str, finalize: *u8, trace: *u8)
                     -> @fn(compartment: @mut Compartment) -> JSClass {
    let f: @fn(@mut Compartment) -> JSClass = |compartment: @mut Compartment| {
        unsafe {
            JSClass {
                name: compartment.add_name(name.to_owned()),
                flags: JSCLASS_HAS_RESERVED_SLOTS(1) | js::JSCLASS_IS_DOMJSCLASS,
                addProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                delProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                getProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                setProperty: GetJSClassHookStubPointer(STRICT_PROPERTY_STUB) as *u8,
                enumerate: GetJSClassHookStubPointer(ENUMERATE_STUB) as *u8,
                resolve: GetJSClassHookStubPointer(RESOLVE_STUB) as *u8,
                convert: GetJSClassHookStubPointer(CONVERT_STUB) as *u8,
                finalize: finalize,
                checkAccess: null(),
                call: null(),
                hasInstance: has_instance,
                construct: null(),
                trace: trace,
                reserved: (null(), null(), null(), null(), null(),  // 05
                           null(), null(), null(), null(), null(),  // 10
                           null(), null(), null(), null(), null(),  // 15
                           null(), null(), null(), null(), null(),  // 20
                           null(), null(), null(), null(), null(),  // 25
                           null(), null(), null(), null(), null(),  // 30
                           null(), null(), null(), null(), null(),  // 35
                           null(), null(), null(), null(), null())  // 40
            }
        }
    };
    return f;
}

pub fn define_empty_prototype(name: ~str, proto: Option<~str>, compartment: @mut Compartment)
    -> js::rust::jsobj {
    compartment.register_class(prototype_jsclass(name.to_owned()));

    //TODO error checking
    let obj = (
        match proto {
            Some(s) => compartment.new_object_with_proto(name.to_owned(),
                                                         s, 
                                                         compartment.global_obj.ptr),
            None => compartment.new_object(name.to_owned(), null(), compartment.global_obj.ptr)
        }).unwrap();

    unsafe {
        compartment.define_property(name.to_owned(), RUST_OBJECT_TO_JSVAL(obj.ptr),
                                    GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                                    GetJSClassHookStubPointer(STRICT_PROPERTY_STUB) as *u8,
                                    JSPROP_ENUMERATE);
        compartment.stash_global_proto(name, obj);
        return obj;
    }
}

// We use slot 0 for holding the raw object.  This is safe for both
// globals and non-globals.
pub static DOM_OBJECT_SLOT: uint = 0;
static DOM_PROXY_OBJECT_SLOT: uint = js::JSSLOT_PROXY_PRIVATE as uint;

// NOTE: This is baked into the Ion JIT as 0 in codegen for LGetDOMProperty and
// LSetDOMProperty. Those constants need to be changed accordingly if this value
// changes.
static DOM_PROTO_INSTANCE_CLASS_SLOT: u32 = 0;

// All DOM globals must have a slot at DOM_PROTOTYPE_SLOT.
pub static DOM_PROTOTYPE_SLOT: u32 = js::JSCLASS_GLOBAL_SLOT_COUNT;

// NOTE: This is baked into the Ion JIT as 0 in codegen for LGetDOMProperty and
// LSetDOMProperty. Those constants need to be changed accordingly if this value
// changes.
static JSCLASS_DOM_GLOBAL: u32 = js::JSCLASS_USERBIT1;

pub struct NativeProperties {
    staticMethods: *JSFunctionSpec,
    staticMethodIds: *jsid,
    staticMethodsSpecs: *JSFunctionSpec,
    staticAttributes: *JSPropertySpec,
    staticAttributeIds: *jsid,
    staticAttributeSpecs: *JSPropertySpec,
    methods: *JSFunctionSpec,
    methodIds: *jsid,
    methodsSpecs: *JSFunctionSpec,
    attributes: *JSPropertySpec,
    attributeIds: *jsid,
    attributeSpecs: *JSPropertySpec,
    unforgeableAttributes: *JSPropertySpec,
    unforgeableAttributeIds: *jsid,
    unforgeableAttributeSpecs: *JSPropertySpec,
    constants: *ConstantSpec,
    constantIds: *jsid,
    constantSpecs: *ConstantSpec
}

pub struct NativePropertyHooks {
    resolve_own_property: *u8,
    resolve_property: *u8,
    enumerate_own_properties: *u8,
    enumerate_properties: *u8,
    proto_hooks: *NativePropertyHooks
}

pub struct JSNativeHolder {
    native: js::jsapi::JSNative,
    propertyHooks: *NativePropertyHooks
}

#[deriving(Clone)]
pub enum ConstantVal {
    IntVal(i32),
    UintVal(u32),
    DoubleVal(f64),
    BoolVal(bool),
    NullVal,
    VoidVal
}

#[deriving(Clone)]
pub struct ConstantSpec {
    name: *libc::c_char,
    value: ConstantVal
}

pub struct DOMClass {
    // A list of interfaces that this object implements, in order of decreasing
    // derivedness.
    interface_chain: [PrototypeList::id::ID, ..MAX_PROTO_CHAIN_LENGTH],

    unused: bool, // DOMObjectIsISupports (always false)
    native_hooks: *NativePropertyHooks
}

pub struct DOMJSClass {
    base: JSClass,
    dom_class: DOMClass
}

pub fn GetProtoOrIfaceArray(global: *JSObject) -> **JSObject {
    unsafe {
        /*assert ((*JS_GetClass(global)).flags & JSCLASS_DOM_GLOBAL) != 0;*/
        cast::transmute(RUST_JSVAL_TO_PRIVATE(JS_GetReservedSlot(global, DOM_PROTOTYPE_SLOT)))
    }
}

pub fn CreateInterfaceObjects2(cx: *JSContext, global: *JSObject, receiver: *JSObject,
                               protoProto: *JSObject, protoClass: *JSClass,
                               constructorClass: *JSClass, constructor: JSNative,
                               ctorNargs: u32,
                               domClass: *DOMClass,
                               methods: *JSFunctionSpec,
                               properties: *JSPropertySpec,
                               constants: *ConstantSpec,
                               staticMethods: *JSFunctionSpec,
                               name: &str) -> *JSObject {
    let mut proto = ptr::null();
    if protoClass.is_not_null() {
        proto = CreateInterfacePrototypeObject(cx, global, protoProto,
                                               protoClass, methods,
                                               properties, constants);
        if proto.is_null() {
            return ptr::null();
        }

        unsafe {
            JS_SetReservedSlot(proto, DOM_PROTO_INSTANCE_CLASS_SLOT,
                               RUST_PRIVATE_TO_JSVAL(domClass as *libc::c_void));
        }
    }

    let mut interface = ptr::null();
    if constructorClass.is_not_null() || constructor.is_not_null() {
        interface = do name.to_c_str().with_ref |s| {
            CreateInterfaceObject(cx, global, receiver, constructorClass,
                                  constructor, ctorNargs, proto,
                                  staticMethods, constants, s)
        };
        if interface.is_null() {
            return ptr::null();
        }
    }

    if protoClass.is_not_null() {
        proto
    } else {
        interface
    }
}

fn CreateInterfaceObject(cx: *JSContext, global: *JSObject, receiver: *JSObject,
                         constructorClass: *JSClass, constructorNative: JSNative,
                         ctorNargs: u32, proto: *JSObject,
                         staticMethods: *JSFunctionSpec,
                         constants: *ConstantSpec,
                         name: *libc::c_char) -> *JSObject {
    unsafe {
        let constructor = if constructorClass.is_not_null() {
            let functionProto = JS_GetFunctionPrototype(cx, global);
            if functionProto.is_null() {
                ptr::null()
            } else {
                JS_NewObject(cx, constructorClass, functionProto, global)
            }
        } else {
            assert!(constructorNative.is_not_null());
            let fun = JS_NewFunction(cx, constructorNative, ctorNargs,
                                     JSFUN_CONSTRUCTOR, global, name);
            if fun.is_null() {
                ptr::null()
            } else {
                JS_GetFunctionObject(fun)
            }
        };

        if constructor.is_null() {
            return ptr::null();
        }

        if staticMethods.is_not_null() &&
            !DefineMethods(cx, constructor, staticMethods) {
            return ptr::null();
        }

        if constructorClass.is_not_null() {
            let toString = do "toString".to_c_str().with_ref |s| {
                DefineFunctionWithReserved(cx, constructor, s,
                                           InterfaceObjectToString,
                                           0, 0)
            };
            if toString.is_null() {
                return ptr::null();
            }

            let toStringObj = JS_GetFunctionObject(toString);
            SetFunctionNativeReserved(toStringObj, TOSTRING_CLASS_RESERVED_SLOT,
                                      &RUST_PRIVATE_TO_JSVAL(constructorClass as *libc::c_void));
            let s = JS_InternString(cx, name);
            if s.is_null() {
                return ptr::null();
            }
            SetFunctionNativeReserved(toStringObj, TOSTRING_NAME_RESERVED_SLOT,
                                      &RUST_STRING_TO_JSVAL(s));
        }

        if constants.is_not_null() &&
            !DefineConstants(cx, constructor, constants) {
            return ptr::null();
        }

        if proto.is_not_null() && JS_LinkConstructorAndPrototype(cx, constructor, proto) == 0 {
            return ptr::null();
        }

        let alreadyDefined = 0;
        if JS_AlreadyHasOwnProperty(cx, receiver, name, &alreadyDefined) == 0 {
            return ptr::null();
        }

        if alreadyDefined == 0 &&
            JS_DefineProperty(cx, receiver, name, RUST_OBJECT_TO_JSVAL(constructor),
                              ptr::null(), ptr::null(), 0) == 0 {
            return ptr::null();
        }

        return constructor;
    }
}

fn DefineConstants(cx: *JSContext, obj: *JSObject, constants: *ConstantSpec) -> bool {
    let mut i = 0;
    loop {
        unsafe {
            let spec = *constants.offset(i);
            if spec.name.is_null() {
                return true;
            }
            let jsval = match spec.value {
                NullVal => JSVAL_NULL,
                IntVal(i) => RUST_INT_TO_JSVAL(i),
                UintVal(u) => RUST_UINT_TO_JSVAL(u),
                DoubleVal(d) => RUST_DOUBLE_TO_JSVAL(d),
                BoolVal(b) if b => JSVAL_TRUE,
                BoolVal(_) => JSVAL_FALSE,
                VoidVal => JSVAL_VOID
            };
            if JS_DefineProperty(cx, obj, spec.name,
                                 jsval, ptr::null(),
                                 ptr::null(),
                                 JSPROP_ENUMERATE | JSPROP_READONLY |
                                 JSPROP_PERMANENT) == 0 {
                return false;
            }
        }
        i += 1;
    }
}

fn DefineMethods(cx: *JSContext, obj: *JSObject, methods: *JSFunctionSpec) -> bool {
    unsafe {
        JS_DefineFunctions(cx, obj, methods) != 0
    }
}

fn DefineProperties(cx: *JSContext, obj: *JSObject, properties: *JSPropertySpec) -> bool {
    unsafe {
        JS_DefineProperties(cx, obj, properties) != 0
    }
}

fn CreateInterfacePrototypeObject(cx: *JSContext, global: *JSObject,
                                  parentProto: *JSObject, protoClass: *JSClass,
                                  methods: *JSFunctionSpec,
                                  properties: *JSPropertySpec,
                                  constants: *ConstantSpec) -> *JSObject {
    unsafe {
        let ourProto = JS_NewObjectWithUniqueType(cx, protoClass, parentProto, global);
        if ourProto.is_null() {
            return ptr::null();
        }

        if methods.is_not_null() && !DefineMethods(cx, ourProto, methods) {
            return ptr::null();
        }

        if properties.is_not_null() && !DefineProperties(cx, ourProto, properties) {
            return ptr::null();
        }

        if constants.is_not_null() && !DefineConstants(cx, ourProto, constants) {
            return ptr::null();
        }

        return ourProto;
    }
}

pub extern fn ThrowingConstructor(_cx: *JSContext, _argc: uint, _vp: *JSVal) -> JSBool {
    //XXX should trigger exception here
    return 0;
}

pub fn initialize_global(global: *JSObject) {
    let protoArray = @mut ([0 as *JSObject, ..53]);
    assert!(protoArray.len() == PrototypeList::id::_ID_Count as uint);
    unsafe {
        //XXXjdm we should be storing the box pointer instead of the inner
        let box = squirrel_away(protoArray);
        let inner = ptr::to_unsafe_ptr(&(*box).payload);
        JS_SetReservedSlot(global,<commit_msg>auto merge of #760 : jdm/servo/protolist, r=kmcallister<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use dom::bindings::codegen::PrototypeList;
use dom::bindings::codegen::PrototypeList::MAX_PROTO_CHAIN_LENGTH;
use dom::bindings::node;
use dom::node::{AbstractNode, ScriptView};
use script_task::page_from_context;

use std::cast;
use std::hashmap::HashMap;
use std::libc;
use std::ptr;
use std::ptr::{null, to_unsafe_ptr};
use std::str;
use std::unstable::intrinsics;
use js::glue::*;
use js::glue::{DefineFunctionWithReserved, GetObjectJSClass, RUST_OBJECT_TO_JSVAL};
use js::glue::{js_IsObjectProxyClass, js_IsFunctionProxyClass, IsProxyHandlerFamily};
use js::glue::{PROPERTY_STUB, STRICT_PROPERTY_STUB, ENUMERATE_STUB, CONVERT_STUB, RESOLVE_STUB};
use js::jsapi::{JS_AlreadyHasOwnProperty, JS_NewObject, JS_NewFunction, JS_GetGlobalObject};
use js::jsapi::{JS_DefineProperties, JS_WrapValue, JS_ForwardGetPropertyTo};
use js::jsapi::{JS_EncodeString, JS_free, JS_GetStringCharsAndLength};
use js::jsapi::{JS_GetClass, JS_LinkConstructorAndPrototype};
use js::jsapi::{JS_GetFunctionPrototype, JS_InternString, JS_GetFunctionObject};
use js::jsapi::{JS_HasPropertyById, JS_GetPrototype, JS_GetGlobalForObject};
use js::jsapi::{JS_NewStringCopyN, JS_DefineFunctions, JS_DefineProperty};
use js::jsapi::{JS_ValueToString, JS_GetReservedSlot, JS_SetReservedSlot};
use js::jsapi::{JSContext, JSObject, JSBool, jsid, JSClass, JSNative};
use js::jsapi::{JSFunctionSpec, JSPropertySpec, JSVal, JSPropertyDescriptor};
use js::jsfriendapi::bindgen::JS_NewObjectWithUniqueType;
use js::rust::Compartment;
use js::{JSCLASS_HAS_RESERVED_SLOTS, JSPROP_ENUMERATE, JSVAL_NULL};
use js::{JSPROP_PERMANENT, JSID_VOID, JSPROP_NATIVE_ACCESSORS, JSPROP_GETTER};
use js::{JSPROP_SETTER, JSVAL_VOID, JSVAL_TRUE, JSVAL_FALSE};
use js::{JS_THIS_OBJECT, JSFUN_CONSTRUCTOR, JS_CALLEE, JSPROP_READONLY};
use js;

static TOSTRING_CLASS_RESERVED_SLOT: libc::size_t = 0;
static TOSTRING_NAME_RESERVED_SLOT: libc::size_t = 1;

struct GlobalStaticData {
    proxy_handlers: HashMap<uint, *libc::c_void>,
    attribute_ids: HashMap<uint, ~[jsid]>,
    method_ids: HashMap<uint, ~[jsid]>,
    constant_ids: HashMap<uint, ~[jsid]>
}

pub fn GlobalStaticData() -> GlobalStaticData {
    GlobalStaticData {
        proxy_handlers: HashMap::new(),
        attribute_ids: HashMap::new(),
        method_ids: HashMap::new(),
        constant_ids: HashMap::new()
    }
}

extern fn InterfaceObjectToString(cx: *JSContext, _argc: uint, vp: *mut JSVal) -> JSBool {
  unsafe {
    let callee = RUST_JSVAL_TO_OBJECT(*JS_CALLEE(cx, cast::transmute(&vp)));
    let obj = JS_THIS_OBJECT(cx, cast::transmute(&vp));
    if obj.is_null() {
        //XXXjdm figure out JSMSG madness
        /*JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_CANT_CONVERT_TO,
                             "null", "object");*/
        return 0;
    }

    let v = GetFunctionNativeReserved(callee, TOSTRING_CLASS_RESERVED_SLOT);
    let clasp: *JSClass = cast::transmute(RUST_JSVAL_TO_PRIVATE(*v));

    let v = GetFunctionNativeReserved(callee, TOSTRING_NAME_RESERVED_SLOT);

    if GetObjectJSClass(obj) != clasp {
      /*let jsname: *JSString = RUST_JSVAL_TO_STRING(*v);
      let length = 0;
      let name = JS_GetInternedStringCharsAndLength(jsname, &length);*/
        //XXXjdm figure out JSMSG madness
        /*JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_INCOMPATIBLE_PROTO,
                             NS_ConvertUTF16toUTF8(name).get(), "toString",
                             "object");*/
        return 0;
    }

    let name = jsval_to_str(cx, *v).unwrap();
    let retval = str(~"function " + name + "() {\n    [native code]\n}");
    *vp = domstring_to_jsval(cx, &retval);
    return 1;
  }
}

#[deriving(Clone)]
pub enum DOMString {
    str(~str),
    null_string
}

impl DOMString {
    pub fn to_str(&self) -> ~str {
        match *self {
          str(ref s) => s.clone(),
          null_string => ~""
        }
    }

    pub fn get_ref<'a>(&'a self) -> &'a str {
        match *self {
            str(ref s) => s.as_slice(),
            null_string => &'a "",
        }
    }
}

pub struct rust_box<T> {
    rc: uint,
    td: *intrinsics::TyDesc,
    next: *(),
    prev: *(),
    payload: T
}

fn is_dom_class(clasp: *JSClass) -> bool {
    unsafe {
        ((*clasp).flags & js::JSCLASS_IS_DOMJSCLASS) != 0
    }
}

pub fn is_dom_proxy(obj: *JSObject) -> bool {
    unsafe {
        (js_IsObjectProxyClass(obj) || js_IsFunctionProxyClass(obj)) &&
            IsProxyHandlerFamily(obj)
    }
}

pub unsafe fn unwrap<T>(obj: *JSObject) -> T {
    let clasp = JS_GetClass(obj);
    let slot = if is_dom_class(clasp) {
        DOM_OBJECT_SLOT
    } else {
        assert!(is_dom_proxy(obj));
        DOM_PROXY_OBJECT_SLOT
    } as u32;
    let val = JS_GetReservedSlot(obj, slot);
    cast::transmute(RUST_JSVAL_TO_PRIVATE(val))
}

pub unsafe fn get_dom_class(obj: *JSObject) -> Result<DOMClass, ()> {
    let clasp = JS_GetClass(obj);
    if is_dom_class(clasp) {
        debug!("plain old dom object");
        let domjsclass: *DOMJSClass = cast::transmute(clasp);
        return Ok((*domjsclass).dom_class);
    }
    if is_dom_proxy(obj) {
        debug!("proxy dom object");
        let dom_class: *DOMClass = cast::transmute(GetProxyHandlerExtra(obj));
        return Ok(*dom_class);
    }
    debug!("not a dom object");
    return Err(());
}

pub fn unwrap_object<T>(obj: *JSObject, proto_id: PrototypeList::id::ID, proto_depth: uint) -> Result<T, ()> {
    unsafe {
        do get_dom_class(obj).chain |dom_class| {
            if dom_class.interface_chain[proto_depth] == proto_id {
                debug!("good prototype");
                Ok(unwrap(obj))
            } else {
                debug!("bad prototype");
                Err(())
            }
        }
    }
}

pub fn unwrap_value<T>(val: *JSVal, proto_id: PrototypeList::id::ID, proto_depth: uint) -> Result<T, ()> {
    unsafe {
        let obj = RUST_JSVAL_TO_OBJECT(*val);
        unwrap_object(obj, proto_id, proto_depth)
    }
}

pub unsafe fn squirrel_away<T>(x: @mut T) -> *rust_box<T> {
    let y: *rust_box<T> = cast::transmute(x);
    cast::forget(x);
    y
}

//XXX very incomplete
pub fn jsval_to_str(cx: *JSContext, v: JSVal) -> Result<~str, ()> {
    unsafe {
        let jsstr;
        if RUST_JSVAL_IS_STRING(v) == 1 {
            jsstr = RUST_JSVAL_TO_STRING(v)
        } else {
            jsstr = JS_ValueToString(cx, v);
            if jsstr.is_null() {
                return Err(());
            }
        }

        let strbuf = JS_EncodeString(cx, jsstr);
        let buf = str::raw::from_c_str(strbuf);
        JS_free(cx, strbuf as *libc::c_void);
        Ok(buf)
    }
}

pub unsafe fn domstring_to_jsval(cx: *JSContext, string: &DOMString) -> JSVal {
    match string {
      &null_string => {
        JSVAL_NULL
      }
      &str(ref s) => {
        do s.as_imm_buf |buf, len| {
            let cbuf = cast::transmute(buf);
            RUST_STRING_TO_JSVAL(JS_NewStringCopyN(cx, cbuf, len as libc::size_t))
        }
      }
    }
}

pub fn get_compartment(cx: *JSContext) -> @mut Compartment {
    unsafe {
        let page = page_from_context(cx);
        let compartment = (*page).js_info.get_ref().js_compartment;
        assert!(cx == compartment.cx.ptr);
        compartment
    }
}

extern fn has_instance(_cx: *JSContext, obj: **JSObject, v: *JSVal, bp: *mut JSBool) -> JSBool {
    //XXXjdm this is totally broken for non-object values
    unsafe {
        let mut o = RUST_JSVAL_TO_OBJECT(*v);
        let obj = *obj;
        *bp = 0;
        while o.is_not_null() {
            if o == obj {
                *bp = 1;
                break;
            }
            o = JS_GetPrototype(o);
        }
        return 1;
    }
}

pub fn prototype_jsclass(name: ~str) -> @fn(compartment: @mut Compartment) -> JSClass {
    let f: @fn(@mut Compartment) -> JSClass = |compartment: @mut Compartment| {
        unsafe {
            JSClass {
                name: compartment.add_name(name.to_owned()),
                flags: 0,
                addProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                delProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                getProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                setProperty: GetJSClassHookStubPointer(STRICT_PROPERTY_STUB) as *u8,
                enumerate: GetJSClassHookStubPointer(ENUMERATE_STUB) as *u8,
                resolve: GetJSClassHookStubPointer(RESOLVE_STUB) as *u8,
                convert: GetJSClassHookStubPointer(CONVERT_STUB) as *u8,
                finalize: null(),
                checkAccess: null(),
                call: null(),
                hasInstance: has_instance,
                construct: null(),
                trace: null(),
                reserved: (null(), null(), null(), null(), null(),  // 05
                           null(), null(), null(), null(), null(),  // 10
                           null(), null(), null(), null(), null(),  // 15
                           null(), null(), null(), null(), null(),  // 20
                           null(), null(), null(), null(), null(),  // 25
                           null(), null(), null(), null(), null(),  // 30
                           null(), null(), null(), null(), null(),  // 35
                           null(), null(), null(), null(), null())  // 40
            }
        }
    };
    return f;
}

pub fn instance_jsclass(name: ~str, finalize: *u8, trace: *u8)
                     -> @fn(compartment: @mut Compartment) -> JSClass {
    let f: @fn(@mut Compartment) -> JSClass = |compartment: @mut Compartment| {
        unsafe {
            JSClass {
                name: compartment.add_name(name.to_owned()),
                flags: JSCLASS_HAS_RESERVED_SLOTS(1) | js::JSCLASS_IS_DOMJSCLASS,
                addProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                delProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                getProperty: GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                setProperty: GetJSClassHookStubPointer(STRICT_PROPERTY_STUB) as *u8,
                enumerate: GetJSClassHookStubPointer(ENUMERATE_STUB) as *u8,
                resolve: GetJSClassHookStubPointer(RESOLVE_STUB) as *u8,
                convert: GetJSClassHookStubPointer(CONVERT_STUB) as *u8,
                finalize: finalize,
                checkAccess: null(),
                call: null(),
                hasInstance: has_instance,
                construct: null(),
                trace: trace,
                reserved: (null(), null(), null(), null(), null(),  // 05
                           null(), null(), null(), null(), null(),  // 10
                           null(), null(), null(), null(), null(),  // 15
                           null(), null(), null(), null(), null(),  // 20
                           null(), null(), null(), null(), null(),  // 25
                           null(), null(), null(), null(), null(),  // 30
                           null(), null(), null(), null(), null(),  // 35
                           null(), null(), null(), null(), null())  // 40
            }
        }
    };
    return f;
}

pub fn define_empty_prototype(name: ~str, proto: Option<~str>, compartment: @mut Compartment)
    -> js::rust::jsobj {
    compartment.register_class(prototype_jsclass(name.to_owned()));

    //TODO error checking
    let obj = (
        match proto {
            Some(s) => compartment.new_object_with_proto(name.to_owned(),
                                                         s, 
                                                         compartment.global_obj.ptr),
            None => compartment.new_object(name.to_owned(), null(), compartment.global_obj.ptr)
        }).unwrap();

    unsafe {
        compartment.define_property(name.to_owned(), RUST_OBJECT_TO_JSVAL(obj.ptr),
                                    GetJSClassHookStubPointer(PROPERTY_STUB) as *u8,
                                    GetJSClassHookStubPointer(STRICT_PROPERTY_STUB) as *u8,
                                    JSPROP_ENUMERATE);
        compartment.stash_global_proto(name, obj);
        return obj;
    }
}

// We use slot 0 for holding the raw object.  This is safe for both
// globals and non-globals.
pub static DOM_OBJECT_SLOT: uint = 0;
static DOM_PROXY_OBJECT_SLOT: uint = js::JSSLOT_PROXY_PRIVATE as uint;

// NOTE: This is baked into the Ion JIT as 0 in codegen for LGetDOMProperty and
// LSetDOMProperty. Those constants need to be changed accordingly if this value
// changes.
static DOM_PROTO_INSTANCE_CLASS_SLOT: u32 = 0;

// All DOM globals must have a slot at DOM_PROTOTYPE_SLOT.
pub static DOM_PROTOTYPE_SLOT: u32 = js::JSCLASS_GLOBAL_SLOT_COUNT;

// NOTE: This is baked into the Ion JIT as 0 in codegen for LGetDOMProperty and
// LSetDOMProperty. Those constants need to be changed accordingly if this value
// changes.
static JSCLASS_DOM_GLOBAL: u32 = js::JSCLASS_USERBIT1;

pub struct NativeProperties {
    staticMethods: *JSFunctionSpec,
    staticMethodIds: *jsid,
    staticMethodsSpecs: *JSFunctionSpec,
    staticAttributes: *JSPropertySpec,
    staticAttributeIds: *jsid,
    staticAttributeSpecs: *JSPropertySpec,
    methods: *JSFunctionSpec,
    methodIds: *jsid,
    methodsSpecs: *JSFunctionSpec,
    attributes: *JSPropertySpec,
    attributeIds: *jsid,
    attributeSpecs: *JSPropertySpec,
    unforgeableAttributes: *JSPropertySpec,
    unforgeableAttributeIds: *jsid,
    unforgeableAttributeSpecs: *JSPropertySpec,
    constants: *ConstantSpec,
    constantIds: *jsid,
    constantSpecs: *ConstantSpec
}

pub struct NativePropertyHooks {
    resolve_own_property: *u8,
    resolve_property: *u8,
    enumerate_own_properties: *u8,
    enumerate_properties: *u8,
    proto_hooks: *NativePropertyHooks
}

pub struct JSNativeHolder {
    native: js::jsapi::JSNative,
    propertyHooks: *NativePropertyHooks
}

#[deriving(Clone)]
pub enum ConstantVal {
    IntVal(i32),
    UintVal(u32),
    DoubleVal(f64),
    BoolVal(bool),
    NullVal,
    VoidVal
}

#[deriving(Clone)]
pub struct ConstantSpec {
    name: *libc::c_char,
    value: ConstantVal
}

pub struct DOMClass {
    // A list of interfaces that this object implements, in order of decreasing
    // derivedness.
    interface_chain: [PrototypeList::id::ID, ..MAX_PROTO_CHAIN_LENGTH],

    unused: bool, // DOMObjectIsISupports (always false)
    native_hooks: *NativePropertyHooks
}

pub struct DOMJSClass {
    base: JSClass,
    dom_class: DOMClass
}

pub fn GetProtoOrIfaceArray(global: *JSObject) -> **JSObject {
    unsafe {
        /*assert ((*JS_GetClass(global)).flags & JSCLASS_DOM_GLOBAL) != 0;*/
        cast::transmute(RUST_JSVAL_TO_PRIVATE(JS_GetReservedSlot(global, DOM_PROTOTYPE_SLOT)))
    }
}

pub fn CreateInterfaceObjects2(cx: *JSContext, global: *JSObject, receiver: *JSObject,
                               protoProto: *JSObject, protoClass: *JSClass,
                               constructorClass: *JSClass, constructor: JSNative,
                               ctorNargs: u32,
                               domClass: *DOMClass,
                               methods: *JSFunctionSpec,
                               properties: *JSPropertySpec,
                               constants: *ConstantSpec,
                               staticMethods: *JSFunctionSpec,
                               name: &str) -> *JSObject {
    let mut proto = ptr::null();
    if protoClass.is_not_null() {
        proto = CreateInterfacePrototypeObject(cx, global, protoProto,
                                               protoClass, methods,
                                               properties, constants);
        if proto.is_null() {
            return ptr::null();
        }

        unsafe {
            JS_SetReservedSlot(proto, DOM_PROTO_INSTANCE_CLASS_SLOT,
                               RUST_PRIVATE_TO_JSVAL(domClass as *libc::c_void));
        }
    }

    let mut interface = ptr::null();
    if constructorClass.is_not_null() || constructor.is_not_null() {
        interface = do name.to_c_str().with_ref |s| {
            CreateInterfaceObject(cx, global, receiver, constructorClass,
                                  constructor, ctorNargs, proto,
                                  staticMethods, constants, s)
        };
        if interface.is_null() {
            return ptr::null();
        }
    }

    if protoClass.is_not_null() {
        proto
    } else {
        interface
    }
}

fn CreateInterfaceObject(cx: *JSContext, global: *JSObject, receiver: *JSObject,
                         constructorClass: *JSClass, constructorNative: JSNative,
                         ctorNargs: u32, proto: *JSObject,
                         staticMethods: *JSFunctionSpec,
                         constants: *ConstantSpec,
                         name: *libc::c_char) -> *JSObject {
    unsafe {
        let constructor = if constructorClass.is_not_null() {
            let functionProto = JS_GetFunctionPrototype(cx, global);
            if functionProto.is_null() {
                ptr::null()
            } else {
                JS_NewObject(cx, constructorClass, functionProto, global)
            }
        } else {
            assert!(constructorNative.is_not_null());
            let fun = JS_NewFunction(cx, constructorNative, ctorNargs,
                                     JSFUN_CONSTRUCTOR, global, name);
            if fun.is_null() {
                ptr::null()
            } else {
                JS_GetFunctionObject(fun)
            }
        };

        if constructor.is_null() {
            return ptr::null();
        }

        if staticMethods.is_not_null() &&
            !DefineMethods(cx, constructor, staticMethods) {
            return ptr::null();
        }

        if constructorClass.is_not_null() {
            let toString = do "toString".to_c_str().with_ref |s| {
                DefineFunctionWithReserved(cx, constructor, s,
                                           InterfaceObjectToString,
                                           0, 0)
            };
            if toString.is_null() {
                return ptr::null();
            }

            let toStringObj = JS_GetFunctionObject(toString);
            SetFunctionNativeReserved(toStringObj, TOSTRING_CLASS_RESERVED_SLOT,
                                      &RUST_PRIVATE_TO_JSVAL(constructorClass as *libc::c_void));
            let s = JS_InternString(cx, name);
            if s.is_null() {
                return ptr::null();
            }
            SetFunctionNativeReserved(toStringObj, TOSTRING_NAME_RESERVED_SLOT,
                                      &RUST_STRING_TO_JSVAL(s));
        }

        if constants.is_not_null() &&
            !DefineConstants(cx, constructor, constants) {
            return ptr::null();
        }

        if proto.is_not_null() && JS_LinkConstructorAndPrototype(cx, constructor, proto) == 0 {
            return ptr::null();
        }

        let alreadyDefined = 0;
        if JS_AlreadyHasOwnProperty(cx, receiver, name, &alreadyDefined) == 0 {
            return ptr::null();
        }

        if alreadyDefined == 0 &&
            JS_DefineProperty(cx, receiver, name, RUST_OBJECT_TO_JSVAL(constructor),
                              ptr::null(), ptr::null(), 0) == 0 {
            return ptr::null();
        }

        return constructor;
    }
}

fn DefineConstants(cx: *JSContext, obj: *JSObject, constants: *ConstantSpec) -> bool {
    let mut i = 0;
    loop {
        unsafe {
            let spec = *constants.offset(i);
            if spec.name.is_null() {
                return true;
            }
            let jsval = match spec.value {
                NullVal => JSVAL_NULL,
                IntVal(i) => RUST_INT_TO_JSVAL(i),
                UintVal(u) => RUST_UINT_TO_JSVAL(u),
                DoubleVal(d) => RUST_DOUBLE_TO_JSVAL(d),
                BoolVal(b) if b => JSVAL_TRUE,
                BoolVal(_) => JSVAL_FALSE,
                VoidVal => JSVAL_VOID
            };
            if JS_DefineProperty(cx, obj, spec.name,
                                 jsval, ptr::null(),
                                 ptr::null(),
                                 JSPROP_ENUMERATE | JSPROP_READONLY |
                                 JSPROP_PERMANENT) == 0 {
                return false;
            }
        }
        i += 1;
    }
}

fn DefineMethods(cx: *JSContext, obj: *JSObject, methods: *JSFunctionSpec) -> bool {
    unsafe {
        JS_DefineFunctions(cx, obj, methods) != 0
    }
}

fn DefineProperties(cx: *JSContext, obj: *JSObject, properties: *JSPropertySpec) -> bool {
    unsafe {
        JS_DefineProperties(cx, obj, properties) != 0
    }
}

fn CreateInterfacePrototypeObject(cx: *JSContext, global: *JSObject,
                                  parentProto: *JSObject, protoClass: *JSClass,
                                  methods: *JSFunctionSpec,
                                  properties: *JSPropertySpec,
                                  constants: *ConstantSpec) -> *JSObject {
    unsafe {
        let ourProto = JS_NewObjectWithUniqueType(cx, protoClass, parentProto, global);
        if ourProto.is_null() {
            return ptr::null();
        }

        if methods.is_not_null() && !DefineMethods(cx, ourProto, methods) {
            return ptr::null();
        }

        if properties.is_not_null() && !DefineProperties(cx, ourProto, properties) {
            return ptr::null();
        }

        if constants.is_not_null() && !DefineConstants(cx, ourProto, constants) {
            return ptr::null();
        }

        return ourProto;
    }
}

pub extern fn ThrowingConstructor(_cx: *JSContext, _argc: uint, _vp: *JSVal) -> JSBool {
    //XXX should trigger exception here
    return 0;
}

pub fn initialize_global(global: *JSObject) {
    let protoArray = @mut ([0 as *JSObject, ..PrototypeList::id::_ID_Count as uint]);
    unsafe {
        //XXXjdm we should be storing the box pointer instead of the inner
        let box = squirrel_away(protoArray);
        let inner = ptr::to_unsafe_ptr(&(*box).payload);
        JS_SetReservedSlot(global,
{'fraction_non_alphanumeric': 0.11658983096144852, 'fraction_numerical': 0.005233736442395492, 'mean_word_length': 2.1446179704461095, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 127, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 56, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5649330', 'n_tokens_mistral': 13861, 'n_tokens_neox': 13558, 'n_words': 2952}