/*global Module*/
/*global _malloc, _free, _memcpy*/
/*global FUNCTION_TABLE, HEAP32*/
/*global Pointer_stringify, writeStringToMemory*/
/*global __emval_register, _emval_handle_array, __emval_decref*/

function createNamedFunction(name, body) {
    /*jshint evil:true*/
    return new Function(
        "body",
        "return function " + name + "() {\n" +
        "    return body.apply(this, arguments);\n" +
        "};\n"
    )(body);
}

function _embind_repr(v) {
    var t = typeof v;
    if (t === 'object' || t === 'array' || t === 'function') {
        return v.toString();
    } else {
        return '' + v;
    }
}

var typeRegistry = {};

function validateType(type, name) {
    if (!type) {
        throw new BindingError('type "' + name + '" must have a positive integer typeid pointer');
    }
    if (undefined !== typeRegistry[type]) {
        throw new BindingError('cannot register type "' + name + '" twice');
    }
}

function __embind_register_void(voidType, name) {
    name = Pointer_stringify(name);
    validateType(voidType, name);
    typeRegistry[voidType] = {
        name: name,
        fromWireType: function() {
            return undefined;
        }
    };
}

function __embind_register_bool(boolType, name, trueValue, falseValue) {
    name = Pointer_stringify(name);
    validateType(boolType, name);
    typeRegistry[boolType] = {
        name: name,
        toWireType: function(destructors, o) {
            return o ? trueValue : falseValue;
        },
        fromWireType: function(wt) {
            return wt === trueValue;
        },
    };
}

function __embind_register_integer(primitiveType, name) {
    name = Pointer_stringify(name);
    validateType(primitiveType, name);
    typeRegistry[primitiveType] = {
        name: name,
        toWireType: function(destructors, value) {
            if (typeof value !== "number") {
                throw new TypeError('Cannot convert "' + _embind_repr(value) + '" to ' + name);
            }
            return value | 0;
        },
        fromWireType: function(value) {
            return value;
        }
    };
}

function __embind_register_float(primitiveType, name) {
    name = Pointer_stringify(name);
    validateType(primitiveType, name);
    typeRegistry[primitiveType] = {
        name: name,
        toWireType: function(destructors, value) {
            if (typeof value !== "number") {
                throw new TypeError('Cannot convert "' + _embind_repr(value) + '" to ' + name);
            }
            return value;
        },
        fromWireType: function(value) {
            return value;
        }
    };
}

function __embind_register_cstring(stringType, name) {
    name = Pointer_stringify(name);
    validateType(stringType, name);
    typeRegistry[stringType] = {
        name: name,
        toWireType: function(destructors, value) {
            var ptr = _malloc(value.length + 1);
            writeStringToMemory(value, ptr);
            destructors.push(_free);
            destructors.push(ptr);
            return ptr;
        },
        fromWireType: function(value) {
            var rv = Pointer_stringify(value);
            _free(value);
            return rv;
        }
    };
}

function __embind_register_emval(emvalType, name) {
    name = Pointer_stringify(name);
    validateType(emvalType, name);
    typeRegistry[emvalType] = {
        name: name,
        toWireType: function(destructors, value) {
            return __emval_register(value);
        },
        fromWireType: function(handle) {
            var rv = _emval_handle_array[handle].value;
            __emval_decref(handle);
            return rv;
        }
    };
}

var BindingError = Error;
/** @expose */
Module.BindingError = BindingError;

function typeName(typeID) {
    // could use our carnal knowledge of RTTI but for now just return the pointer...
    return typeID;
}

function requireRegisteredType(type, humanName) {
    var impl = typeRegistry[type];
    if (undefined === impl) {
        throw new BindingError(humanName + " has unknown type: " + typeName(type));
    }
    return impl;
}

function requireArgumentTypes(argCount, argTypes, name) {
    var argTypeImpls = new Array(argCount);
    for (var i = 0; i < argCount; ++i) {
        var argType = HEAP32[(argTypes >> 2) + i];
        argTypeImpls[i] = requireRegisteredType(argType, name + " parameter " + i);
    }
    return argTypeImpls;
}

function runDestructors(destructors) {
    while (destructors.length) {
        var ptr = destructors.pop();
        var del = destructors.pop();
        del(ptr);
    }
}

function __embind_register_function(name, returnType, argCount, argTypes, invoker, fn) {
    name = Pointer_stringify(name);
    returnType = requireRegisteredType(returnType, "Function " + name + " return value");
    invoker = FUNCTION_TABLE[invoker];
    argTypes = requireArgumentTypes(argCount, argTypes, name);

    Module[name] = function() {
        if (arguments.length !== argCount) {
            throw new BindingError('emscripten binding function ' + name + ' called with ' + arguments.length + ' arguments, expected ' + argCount);
        }
        var destructors = [];
        var args = new Array(argCount + 1);
        args[0] = fn;
        for (var i = 0; i < argCount; ++i) {
            args[i + 1] = argTypes[i].toWireType(destructors, arguments[i]);
        }
        var rv = returnType.fromWireType(invoker.apply(null, args));
        runDestructors(destructors);
        return rv;
    };
}

function __embind_register_tuple(tupleType, name, constructor, destructor) {
    name = Pointer_stringify(name);
    constructor = FUNCTION_TABLE[constructor];
    destructor = FUNCTION_TABLE[destructor];

    var elements = [];

    typeRegistry[tupleType] = {
        name: name,
        elements: elements,
        fromWireType: function(ptr) {
            var len = elements.length;
            var rv = new Array(len);
            for (var i = 0; i < len; ++i) {
                rv[i] = elements[i].read(ptr);
            }
            destructor(ptr);
            return rv;
        },
        toWireType: function(destructors, o) {
            var len = elements.length;
            if (len !== o.length) {
                throw new TypeError("Incorrect number of tuple elements");
            }
            var ptr = constructor();
            for (var i = 0; i < len; ++i) {
                elements[i].write(ptr, o[i]);
            }
            destructors.push(destructor);
            destructors.push(ptr);
            return ptr;
        }
    };
}

function copyMemberPointer(memberPointer, memberPointerSize) {
    var copy = _malloc(memberPointerSize);
    if (!copy) {
        throw new Error('Failed to allocate member pointer copy');
    }
    _memcpy(copy, memberPointer, memberPointerSize);
    return copy;
}

function __embind_register_tuple_element(
    tupleType,
    elementType,
    getter,
    setter,
    memberPointerSize,
    memberPointer
) {
    tupleType = requireRegisteredType(tupleType, 'tuple');
    elementType = requireRegisteredType(elementType, "element " + tupleType.name + "[" + tupleType.elements.length + "]");
    getter = FUNCTION_TABLE[getter];
    setter = FUNCTION_TABLE[setter];
    memberPointer = copyMemberPointer(memberPointer, memberPointerSize);

    tupleType.elements.push({
        read: function(ptr) {
            return elementType.fromWireType(getter(ptr, memberPointer));
        },
        write: function(ptr, o) {
            var destructors = [];
            setter(ptr, memberPointer, elementType.toWireType(destructors, o));
            runDestructors(destructors);
        }
    });
}

function __embind_register_tuple_element_accessor(
    tupleType,
    elementType,
    staticGetter,
    getterSize,
    getter,
    staticSetter,
    setterSize,
    setter
) {
    tupleType = requireRegisteredType(tupleType, 'tuple');
    elementType = requireRegisteredType(elementType, "element " + tupleType.name + "[" + tupleType.elements.length + "]");
    staticGetter = FUNCTION_TABLE[staticGetter];
    getter = copyMemberPointer(getter, getterSize);
    staticSetter = FUNCTION_TABLE[staticSetter];
    setter = copyMemberPointer(setter, setterSize);

    tupleType.elements.push({
        read: function(ptr) {
            return elementType.fromWireType(staticGetter(ptr, HEAP32[getter >> 2]));
        },
        write: function(ptr, o) {
            var destructors = [];
            staticSetter(
                ptr,
                HEAP32[setter >> 2],
                elementType.toWireType(destructors, o));
            runDestructors(destructors);
        }
    });
}

function __embind_register_struct(
    structType,
    name,
    constructor,
    destructor
) {
    name = Pointer_stringify(name);
    constructor = FUNCTION_TABLE[constructor];
    destructor = FUNCTION_TABLE[destructor];

    typeRegistry[structType] = {
        fields: {},
        fromWireType: function(ptr) {
            var fields = this.fields;
            var rv = {};
            for (var i in fields) {
                rv[i] = fields[i].read(ptr);
            }
            destructor(ptr);
            return rv;
        },
        toWireType: function(destructors, o) {
            var fields = this.fields;
            for (var fieldName in fields) {
                if (!(fieldName in o)) {
                    throw new TypeError('Missing field');
                }
            }
            var ptr = constructor();
            for (var fieldName in fields) {
                fields[fieldName].write(ptr, o[fieldName]);
            }
            destructors.push(destructor);
            destructors.push(ptr);
            return ptr;
        }
    };
}

function __embind_register_struct_field(
    structType,
    fieldName,
    fieldType,
    getter,
    setter,
    memberPointerSize,
    memberPointer
) {
    structType = requireRegisteredType(structType, 'struct');
    fieldName = Pointer_stringify(fieldName);
    fieldType = requireRegisteredType(fieldType, 'field "' + structType.name + '.' + fieldName + '"');
    getter = FUNCTION_TABLE[getter];
    setter = FUNCTION_TABLE[setter];
    memberPointer = copyMemberPointer(memberPointer, memberPointerSize);

    structType.fields[fieldName] = {
        read: function(ptr) {
            return fieldType.fromWireType(getter(ptr, memberPointer));
        },
        write: function(ptr, o) {
            var destructors = [];
            setter(ptr, memberPointer, fieldType.toWireType(destructors, o));
            runDestructors(destructors);
        }
    };
}

function __embind_register_class(
    classType,
    name,
    destructor
) {
    name = Pointer_stringify(name);
    destructor = FUNCTION_TABLE[destructor];

    var Handle = createNamedFunction(name, function(ptr) {
        this.count = {value: 1};
        this.ptr = ptr;
    });

    Handle.prototype.clone = function() {
        if (!this.ptr) {
            throw new BindingError(classType.name + ' instance already deleted');
        }

        var clone = Object.create(Handle.prototype);
        clone.count = this.count;
        clone.ptr = this.ptr;
        
        clone.count.value += 1;
        return clone;
    };

    Handle.prototype.move = function() {
        var rv = this.clone();
        this.delete();
        return rv;
    };

    Handle.prototype['delete'] = function() {
        if (!this.ptr) {
            throw new BindingError(classType.name + ' instance already deleted');
        }

        this.count.value -= 1;
        if (0 === this.count.value) {
            destructor(this.ptr);
        }
        this.ptr = undefined;
    };

    var constructor = createNamedFunction(name, function() {
        var body = constructor.body;
        body.apply(this, arguments);
    });
    constructor.prototype = Object.create(Handle.prototype);
    
    typeRegistry[classType] = {
        name: name,
        constructor: constructor,
        Handle: Handle,
        fromWireType: function(ptr) {
            return new Handle(ptr);
        },
        toWireType: function(destructors, o) {
            return o.ptr;
        }
    };

    Module[name] = constructor;
}

function __embind_register_class_constructor(
    classType,
    argCount,
    argTypes,
    constructor
) {
    classType = requireRegisteredType(classType, 'class');
    var humanName = 'constructor ' + classType.name;
    argTypes = requireArgumentTypes(argCount, argTypes, humanName);
    constructor = FUNCTION_TABLE[constructor];

    classType.constructor.body = function() {
        if (arguments.length !== argCount) {
            throw new BindingError('emscripten binding ' + humanName + ' called with ' + arguments.length + ' arguments, expected ' + argCount);
        }
        var destructors = [];
        var args = new Array(argCount);
        for (var i = 0; i < argCount; ++i) {
            args[i] = argTypes[i].toWireType(destructors, arguments[i]);
        }

        var ptr = constructor.apply(null, args);
        runDestructors(destructors);
        classType.Handle.call(this, ptr);
    };
}

function __embind_register_class_method(
    classType,
    methodName,
    returnType,
    argCount,
    argTypes,
    invoker,
    memberFunctionSize,
    memberFunction
) {
    classType = requireRegisteredType(classType, 'class');
    methodName = Pointer_stringify(methodName);
    var humanName = classType.name + '.' + methodName;
    returnType = requireRegisteredType(returnType, 'method ' + humanName + ' return value');
    argTypes = requireArgumentTypes(argCount, argTypes, 'method ' + humanName);
    invoker = FUNCTION_TABLE[invoker];
    memberFunction = copyMemberPointer(memberFunction, memberFunctionSize);

    classType.Handle.prototype[methodName] = function() {
        if (!this.ptr) {
            throw new BindingError('cannot call emscripten binding method ' + humanName + ' on deleted object');
        }
        if (arguments.length !== argCount) {
            throw new BindingError('emscripten binding method ' + humanName + ' called with ' + arguments.length + ' arguments, expected ' + argCount);
        }
        
        var destructors = [];
        var args = new Array(argCount + 2);
        args[0] = this.ptr;
        args[1] = memberFunction;
        for (var i = 0; i < argCount; ++i) {
            args[i + 2] = argTypes[i].toWireType(destructors, arguments[i]);
        }

        var rv = returnType.fromWireType(invoker.apply(null, args));
        runDestructors(destructors);
        return rv;
    };
}

function __embind_register_class_classmethod(
    classType,
    methodName,
    returnType,
    argCount,
    argTypes,
    method
) {
    classType = requireRegisteredType(classType, 'class');
    methodName = Pointer_stringify(methodName);
    var humanName = classType.name + '.' + methodName;
    returnType = requireRegisteredType(returnType, 'classmethod ' + humanName + ' return value');
    argTypes = requireArgumentTypes(argCount, argTypes, 'classmethod ' + humanName);
    method = FUNCTION_TABLE[method];

    classType.constructor[methodName] = function() {
        if (arguments.length !== argCount) {
            throw new BindingError('emscripten binding method ' + humanName + ' called with ' + arguments.length + ' arguments, expected ' + argCount);
        }

        var destructors = [];
        var args = new Array(argCount);
        for (var i = 0; i < argCount; ++i) {
            args[i] = argTypes[i].toWireType(destructors, arguments[i]);
        }

        var rv = returnType.fromWireType(method.apply(null, args));
        runDestructors(destructors);
        return rv;
    };
}

function __embind_register_class_field(
    classType,
    fieldName,
    fieldType,
    getter,
    setter,
    memberPointerSize,
    memberPointer
) {
    classType = requireRegisteredType(classType, 'class');
    fieldName = Pointer_stringify(fieldName);
    var humanName = classType.name + '.' + fieldName;
    fieldType = requireRegisteredType(fieldType, 'field ' + humanName);
    getter = FUNCTION_TABLE[getter];
    setter = FUNCTION_TABLE[setter];
    memberPointer = copyMemberPointer(memberPointer, memberPointerSize);

    Object.defineProperty(classType.Handle.prototype, fieldName, {
        get: function() {
            if (!this.ptr) {
                throw new BindingError('cannot access emscripten binding field ' + humanName + ' on deleted object');
            }
            return fieldType.fromWireType(getter(this.ptr, memberPointer));
        },
        set: function(v) {
            if (!this.ptr) {
                throw new BindingError('cannot modify emscripten binding field ' + humanName + ' on deleted object');
            }
            var destructors = [];
            setter(this.ptr, memberPointer, fieldType.toWireType(destructors, v));
            runDestructors(destructors);
        },
        enumerable: true
    });
}

function __embind_register_enum(
    enumType,
    name
) {
    name = Pointer_stringify(name);

    function Enum() {
    }
    Enum.values = {};

    typeRegistry[enumType] = {
        name: name,
        constructor: Enum,
        toWireType: function(destructors, c) {
            return c.value;
        },
        fromWireType: function(c) {
            return Enum.values[c];
        },
    };
    
    Module[name] = Enum;
}

function __embind_register_enum_value(
    enumType,
    name,
    enumValue
) {
    enumType = requireRegisteredType(enumType, 'enum');
    name = Pointer_stringify(name);

    var Enum = enumType.constructor;

    var Value = Object.create(enumType.constructor.prototype, {
        value: {value: enumValue},
        constructor: {value: createNamedFunction(enumType.name + '_' + name, function() {})},
    });
    Enum.values[enumValue] = Value;
    Enum[name] = Value;
}

function __embind_register_interface(
    interfaceType,
    name,
    constructor,
    destructor
) {
    name = Pointer_stringify(name);
    constructor = FUNCTION_TABLE[constructor];
    destructor = FUNCTION_TABLE[destructor];

    typeRegistry[interfaceType] = {
        name: name,
        toWireType: function(destructors, o) {
            var handle = __emval_register(o);
            var ptr = constructor(handle);
            destructors.push(destructor);
            destructors.push(ptr);
            return ptr;
        },
    };
}

