"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createFunctionWithReturnValue = exports.getGlobal = exports.getCurrentWebContents = exports.getCurrentWindow = exports.getBuiltin = void 0;
const callbacks_registry_1 = require("./callbacks-registry");
const type_utils_1 = require("../common/type-utils");
const electron_1 = require("electron");
const module_names_1 = require("../common/module-names");
const get_electron_binding_1 = require("../common/get-electron-binding");
const v8Util = get_electron_binding_1.getElectronBinding('v8_util');
const { hasSwitch } = get_electron_binding_1.getElectronBinding('command_line');
const callbacksRegistry = new callbacks_registry_1.CallbacksRegistry();
const remoteObjectCache = new Map();
const finalizationRegistry = new FinalizationRegistry((id) => {
    const ref = remoteObjectCache.get(id);
    if (ref !== undefined && ref.deref() === undefined) {
        remoteObjectCache.delete(id);
        electron_1.ipcRenderer.send("REMOTE_BROWSER_DEREFERENCE" /* BROWSER_DEREFERENCE */, contextId, id, 0);
    }
});
const electronIds = new WeakMap();
const isReturnValue = new WeakSet();
function getCachedRemoteObject(id) {
    const ref = remoteObjectCache.get(id);
    if (ref !== undefined) {
        const deref = ref.deref();
        if (deref !== undefined)
            return deref;
    }
}
function setCachedRemoteObject(id, value) {
    const wr = new WeakRef(value);
    remoteObjectCache.set(id, wr);
    finalizationRegistry.register(value, id);
    return value;
}
// An unique ID that can represent current context.
const contextId = v8Util.getHiddenValue(global, 'contextId');
// Notify the main process when current context is going to be released.
// Note that when the renderer process is destroyed, the message may not be
// sent, we also listen to the "render-view-deleted" event in the main process
// to guard that situation.
process.on('exit', () => {
    const command = "REMOTE_BROWSER_CONTEXT_RELEASE" /* BROWSER_CONTEXT_RELEASE */;
    electron_1.ipcRenderer.send(command, contextId);
});
const IS_REMOTE_PROXY = Symbol('is-remote-proxy');
// Convert the arguments object into an array of meta data.
function wrapArgs(args, visited = new Set()) {
    const valueToMeta = (value) => {
        // Check for circular reference.
        if (visited.has(value)) {
            return {
                type: 'value',
                value: null
            };
        }
        if (value && value.constructor && value.constructor.name === 'NativeImage') {
            return { type: 'nativeimage', value: type_utils_1.serialize(value) };
        }
        else if (Array.isArray(value)) {
            visited.add(value);
            const meta = {
                type: 'array',
                value: wrapArgs(value, visited)
            };
            visited.delete(value);
            return meta;
        }
        else if (value instanceof Buffer) {
            return {
                type: 'buffer',
                value
            };
        }
        else if (type_utils_1.isSerializableObject(value)) {
            return {
                type: 'value',
                value
            };
        }
        else if (typeof value === 'object') {
            if (type_utils_1.isPromise(value)) {
                return {
                    type: 'promise',
                    then: valueToMeta(function (onFulfilled, onRejected) {
                        value.then(onFulfilled, onRejected);
                    })
                };
            }
            else if (electronIds.has(value)) {
                return {
                    type: 'remote-object',
                    id: electronIds.get(value)
                };
            }
            const meta = {
                type: 'object',
                name: value.constructor ? value.constructor.name : '',
                members: []
            };
            visited.add(value);
            for (const prop in value) { // eslint-disable-line guard-for-in
                meta.members.push({
                    name: prop,
                    value: valueToMeta(value[prop])
                });
            }
            visited.delete(value);
            return meta;
        }
        else if (typeof value === 'function' && isReturnValue.has(value)) {
            return {
                type: 'function-with-return-value',
                value: valueToMeta(value())
            };
        }
        else if (typeof value === 'function') {
            return {
                type: 'function',
                id: callbacksRegistry.add(value),
                location: callbacksRegistry.getLocation(value),
                length: value.length
            };
        }
        else {
            return {
                type: 'value',
                value
            };
        }
    };
    return args.map(valueToMeta);
}
// Populate object's members from descriptors.
// The |ref| will be kept referenced by |members|.
// This matches |getObjectMemebers| in rpc-server.
function setObjectMembers(ref, object, metaId, members) {
    if (!Array.isArray(members))
        return;
    for (const member of members) {
        if (Object.prototype.hasOwnProperty.call(object, member.name))
            continue;
        const descriptor = { enumerable: member.enumerable };
        if (member.type === 'method') {
            const remoteMemberFunction = function (...args) {
                let command;
                if (this && this.constructor === remoteMemberFunction) {
                    command = "REMOTE_BROWSER_MEMBER_CONSTRUCTOR" /* BROWSER_MEMBER_CONSTRUCTOR */;
                }
                else {
                    command = "REMOTE_BROWSER_MEMBER_CALL" /* BROWSER_MEMBER_CALL */;
                }
                const ret = electron_1.ipcRenderer.sendSync(command, contextId, metaId, member.name, wrapArgs(args));
                return metaToValue(ret);
            };
            let descriptorFunction = proxyFunctionProperties(remoteMemberFunction, metaId, member.name);
            descriptor.get = () => {
                descriptorFunction.ref = ref; // The member should reference its object.
                return descriptorFunction;
            };
            // Enable monkey-patch the method
            descriptor.set = (value) => {
                descriptorFunction = value;
                return value;
            };
            descriptor.configurable = true;
        }
        else if (member.type === 'get') {
            descriptor.get = () => {
                const command = "REMOTE_BROWSER_MEMBER_GET" /* BROWSER_MEMBER_GET */;
                const meta = electron_1.ipcRenderer.sendSync(command, contextId, metaId, member.name);
                return metaToValue(meta);
            };
            if (member.writable) {
                descriptor.set = (value) => {
                    const args = wrapArgs([value]);
                    const command = "REMOTE_BROWSER_MEMBER_SET" /* BROWSER_MEMBER_SET */;
                    const meta = electron_1.ipcRenderer.sendSync(command, contextId, metaId, member.name, args);
                    if (meta != null)
                        metaToValue(meta);
                    return value;
                };
            }
        }
        Object.defineProperty(object, member.name, descriptor);
    }
}
// Populate object's prototype from descriptor.
// This matches |getObjectPrototype| in rpc-server.
function setObjectPrototype(ref, object, metaId, descriptor) {
    if (descriptor === null)
        return;
    const proto = {};
    setObjectMembers(ref, proto, metaId, descriptor.members);
    setObjectPrototype(ref, proto, metaId, descriptor.proto);
    Object.setPrototypeOf(object, proto);
}
// Wrap function in Proxy for accessing remote properties
function proxyFunctionProperties(remoteMemberFunction, metaId, name) {
    let loaded = false;
    // Lazily load function properties
    const loadRemoteProperties = () => {
        if (loaded)
            return;
        loaded = true;
        const command = "REMOTE_BROWSER_MEMBER_GET" /* BROWSER_MEMBER_GET */;
        const meta = electron_1.ipcRenderer.sendSync(command, contextId, metaId, name);
        setObjectMembers(remoteMemberFunction, remoteMemberFunction, meta.id, meta.members);
    };
    return new Proxy(remoteMemberFunction, {
        set: (target, property, value) => {
            if (property !== 'ref')
                loadRemoteProperties();
            target[property] = value;
            return true;
        },
        get: (target, property) => {
            if (property === IS_REMOTE_PROXY)
                return true;
            if (!Object.prototype.hasOwnProperty.call(target, property))
                loadRemoteProperties();
            const value = target[property];
            if (property === 'toString' && typeof value === 'function') {
                return value.bind(target);
            }
            return value;
        },
        ownKeys: (target) => {
            loadRemoteProperties();
            return Object.getOwnPropertyNames(target);
        },
        getOwnPropertyDescriptor: (target, property) => {
            const descriptor = Object.getOwnPropertyDescriptor(target, property);
            if (descriptor)
                return descriptor;
            loadRemoteProperties();
            return Object.getOwnPropertyDescriptor(target, property);
        }
    });
}
// Convert meta data from browser into real value.
function metaToValue(meta) {
    if (meta.type === 'value') {
        return meta.value;
    }
    else if (meta.type === 'array') {
        return meta.members.map((member) => metaToValue(member));
    }
    else if (meta.type === 'nativeimage') {
        return type_utils_1.deserialize(meta.value);
    }
    else if (meta.type === 'buffer') {
        return Buffer.from(meta.value.buffer, meta.value.byteOffset, meta.value.byteLength);
    }
    else if (meta.type === 'promise') {
        return Promise.resolve({ then: metaToValue(meta.then) });
    }
    else if (meta.type === 'error') {
        return metaToError(meta);
    }
    else if (meta.type === 'exception') {
        if (meta.value.type === 'error') {
            throw metaToError(meta.value);
        }
        else {
            throw new Error(`Unexpected value type in exception: ${meta.value.type}`);
        }
    }
    else {
        let ret;
        if ('id' in meta) {
            const cached = getCachedRemoteObject(meta.id);
            if (cached !== undefined) {
                return cached;
            }
        }
        // A shadow class to represent the remote function object.
        if (meta.type === 'function') {
            const remoteFunction = function (...args) {
                let command;
                if (this && this.constructor === remoteFunction) {
                    command = "REMOTE_BROWSER_CONSTRUCTOR" /* BROWSER_CONSTRUCTOR */;
                }
                else {
                    command = "REMOTE_BROWSER_FUNCTION_CALL" /* BROWSER_FUNCTION_CALL */;
                }
                const obj = electron_1.ipcRenderer.sendSync(command, contextId, meta.id, wrapArgs(args));
                return metaToValue(obj);
            };
            ret = remoteFunction;
        }
        else {
            ret = {};
        }
        setObjectMembers(ret, ret, meta.id, meta.members);
        setObjectPrototype(ret, ret, meta.id, meta.proto);
        if (ret.constructor && ret.constructor[IS_REMOTE_PROXY]) {
            Object.defineProperty(ret.constructor, 'name', { value: meta.name });
        }
        // Track delegate obj's lifetime & tell browser to clean up when object is GCed.
        electronIds.set(ret, meta.id);
        setCachedRemoteObject(meta.id, ret);
        return ret;
    }
}
function metaToError(meta) {
    const obj = meta.value;
    for (const { name, value } of meta.members) {
        obj[name] = metaToValue(value);
    }
    return obj;
}
function handleMessage(channel, handler) {
    electron_1.ipcRenderer.on(channel, (event, passedContextId, id, ...args) => {
        if (event.senderId !== 0) {
            console.error(`Message ${channel} sent by unexpected WebContents (${event.senderId})`);
            return;
        }
        if (passedContextId === contextId) {
            handler(id, ...args);
        }
        else {
            // Message sent to an un-exist context, notify the error to main process.
            electron_1.ipcRenderer.send("REMOTE_BROWSER_WRONG_CONTEXT_ERROR" /* BROWSER_WRONG_CONTEXT_ERROR */, contextId, passedContextId, id);
        }
    });
}
const enableStacks = hasSwitch('enable-api-filtering-logging');
function getCurrentStack() {
    const target = { stack: undefined };
    if (enableStacks) {
        Error.captureStackTrace(target, getCurrentStack);
    }
    return target.stack;
}
// Browser calls a callback in renderer.
handleMessage("REMOTE_RENDERER_CALLBACK" /* RENDERER_CALLBACK */, (id, args) => {
    callbacksRegistry.apply(id, metaToValue(args));
});
// A callback in browser is released.
handleMessage("REMOTE_RENDERER_RELEASE_CALLBACK" /* RENDERER_RELEASE_CALLBACK */, (id) => {
    callbacksRegistry.remove(id);
});
exports.require = (module) => {
    const command = "REMOTE_BROWSER_REQUIRE" /* BROWSER_REQUIRE */;
    const meta = electron_1.ipcRenderer.sendSync(command, contextId, module, getCurrentStack());
    return metaToValue(meta);
};
// Alias to remote.require('electron').xxx.
function getBuiltin(module) {
    const command = "REMOTE_BROWSER_GET_BUILTIN" /* BROWSER_GET_BUILTIN */;
    const meta = electron_1.ipcRenderer.sendSync(command, contextId, module, getCurrentStack());
    return metaToValue(meta);
}
exports.getBuiltin = getBuiltin;
function getCurrentWindow() {
    const command = "REMOTE_BROWSER_GET_CURRENT_WINDOW" /* BROWSER_GET_CURRENT_WINDOW */;
    const meta = electron_1.ipcRenderer.sendSync(command, contextId, getCurrentStack());
    return metaToValue(meta);
}
exports.getCurrentWindow = getCurrentWindow;
// Get current WebContents object.
function getCurrentWebContents() {
    const command = "REMOTE_BROWSER_GET_CURRENT_WEB_CONTENTS" /* BROWSER_GET_CURRENT_WEB_CONTENTS */;
    const meta = electron_1.ipcRenderer.sendSync(command, contextId, getCurrentStack());
    return metaToValue(meta);
}
exports.getCurrentWebContents = getCurrentWebContents;
// Get a global object in browser.
function getGlobal(name) {
    const command = "REMOTE_BROWSER_GET_GLOBAL" /* BROWSER_GET_GLOBAL */;
    const meta = electron_1.ipcRenderer.sendSync(command, contextId, name, getCurrentStack());
    return metaToValue(meta);
}
exports.getGlobal = getGlobal;
// Get the process object in browser.
Object.defineProperty(exports, 'process', {
    enumerable: true,
    get: () => exports.getGlobal('process')
});
// Create a function that will return the specified value when called in browser.
function createFunctionWithReturnValue(returnValue) {
    const func = () => returnValue;
    isReturnValue.add(func);
    return func;
}
exports.createFunctionWithReturnValue = createFunctionWithReturnValue;
const addBuiltinProperty = (name) => {
    Object.defineProperty(exports, name, {
        enumerable: true,
        get: () => exports.getBuiltin(name)
    });
};
module_names_1.browserModuleNames
    .forEach(addBuiltinProperty);
