"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogLevel = exports.MessageType = exports.ScriptRuntime = exports.Script = void 0;
const cancellable_1 = require("./cancellable");
const signals_1 = require("./signals");
const util_1 = require("util");
class Script {
    constructor(impl) {
        this.impl = impl;
        this.logHandlerImpl = log;
        const services = new ScriptServices(this, impl.signals);
        const rpcController = services;
        this.exportsProxy = new ScriptExportsProxy(rpcController);
        const signals = services;
        this.destroyed = new signals_1.Signal(signals, "destroyed");
        this.message = new signals_1.Signal(signals, "message");
    }
    get isDestroyed() {
        return this.impl.isDestroyed;
    }
    get exports() {
        return this.exportsProxy;
    }
    get logHandler() {
        return this.logHandlerImpl;
    }
    set logHandler(handler) {
        this.logHandlerImpl = handler;
    }
    get defaultLogHandler() {
        return log;
    }
    load(cancellable) {
        return this.impl.load(cancellable);
    }
    unload(cancellable) {
        return this.impl.unload(cancellable);
    }
    eternalize(cancellable) {
        return this.impl.eternalize(cancellable);
    }
    post(message, data = null) {
        this.impl.post(message, data);
    }
    enableDebugger(options = {}, cancellable) {
        const { port = 0 } = options;
        return this.impl.enableDebugger(port, cancellable);
    }
    disableDebugger(cancellable) {
        return this.impl.disableDebugger(cancellable);
    }
    [util_1.inspect.custom](depth, options) {
        return "Script {}";
    }
}
exports.Script = Script;
var ScriptRuntime;
(function (ScriptRuntime) {
    ScriptRuntime["Default"] = "default";
    ScriptRuntime["QJS"] = "qjs";
    ScriptRuntime["V8"] = "v8";
})(ScriptRuntime = exports.ScriptRuntime || (exports.ScriptRuntime = {}));
var MessageType;
(function (MessageType) {
    MessageType["Send"] = "send";
    MessageType["Error"] = "error";
})(MessageType = exports.MessageType || (exports.MessageType = {}));
var LogLevel;
(function (LogLevel) {
    LogLevel["Info"] = "info";
    LogLevel["Warning"] = "warning";
    LogLevel["Error"] = "error";
})(LogLevel = exports.LogLevel || (exports.LogLevel = {}));
class ScriptServices extends signals_1.SignalAdapter {
    constructor(script, signals) {
        super(signals);
        this.script = script;
        this.pendingRequests = {};
        this.nextRequestId = 1;
        this.onDestroyed = () => {
            this.signals.disconnect("destroyed", this.onDestroyed);
            this.signals.disconnect("message", this.onMessage);
        };
        this.onMessage = (message, data) => {
            if (message.type === MessageType.Send && isRpcSendMessage(message)) {
                const [, id, operation, ...params] = message.payload;
                this.onRpcMessage(id, operation, params, data);
            }
            else if (isLogMessage(message)) {
                const opaqueMessage = message;
                const logMessage = opaqueMessage;
                this.script.logHandler(logMessage.level, logMessage.payload);
            }
        };
        this.signals.connect("destroyed", this.onDestroyed);
        this.signals.connect("message", this.onMessage);
    }
    getProxy(name, userHandler) {
        if (name === "message") {
            return (message, data) => {
                if (!isInternalMessage(message)) {
                    userHandler(message, data);
                }
            };
        }
        return null;
    }
    request(operation, params, cancellable) {
        return new Promise((resolve, reject) => {
            const id = this.nextRequestId++;
            const complete = (error, result) => {
                if (cancellable !== undefined) {
                    cancellable.cancelled.disconnect(onOperationCancelled);
                }
                this.signals.disconnect("destroyed", onScriptDestroyed);
                delete this.pendingRequests[id];
                if (error === null) {
                    resolve(result);
                }
                else {
                    reject(error);
                }
            };
            function onScriptDestroyed() {
                complete(new Error("Script is destroyed"));
            }
            function onOperationCancelled() {
                complete(new Error("Operation was cancelled"));
            }
            this.pendingRequests[id] = complete;
            this.script.post(["frida:rpc", id, operation].concat(params));
            this.signals.connect("destroyed", onScriptDestroyed);
            if (cancellable !== undefined) {
                cancellable.cancelled.connect(onOperationCancelled);
                if (cancellable.isCancelled) {
                    onOperationCancelled();
                    return;
                }
            }
            if (this.script.isDestroyed) {
                onScriptDestroyed();
            }
        });
    }
    onRpcMessage(id, operation, params, data) {
        if (operation === RpcOperation.Ok || operation === RpcOperation.Error) {
            const callback = this.pendingRequests[id];
            if (callback === undefined) {
                return;
            }
            let value = null;
            let error = null;
            if (operation === RpcOperation.Ok) {
                value = (data !== null) ? data : params[0];
            }
            else {
                const [message, name, stack, rawErr] = params;
                error = new Error(message);
                error.name = name;
                error.stack = stack;
                Object.assign(error, rawErr);
            }
            callback(error, value);
        }
    }
}
function ScriptExportsProxy(rpcController) {
    return new Proxy(this, {
        has(target, property) {
            return !isReservedMethodName(property);
            ;
        },
        get(target, property, receiver) {
            if (property in target) {
                return target[property];
            }
            if (property === util_1.inspect.custom) {
                return inspectProxy;
            }
            if (isReservedMethodName(property)) {
                return undefined;
            }
            return (...args) => {
                let cancellable;
                if (args[args.length - 1] instanceof cancellable_1.Cancellable) {
                    cancellable = args.pop();
                }
                return rpcController.request("call", [property, args], cancellable);
            };
        },
        set(target, property, value, receiver) {
            target[property] = value;
            return true;
        },
        ownKeys(target) {
            return Object.getOwnPropertyNames(target);
        },
        getOwnPropertyDescriptor(target, property) {
            if (property in target) {
                return Object.getOwnPropertyDescriptor(target, property);
            }
            if (isReservedMethodName(property)) {
                return undefined;
            }
            return {
                writable: true,
                configurable: true,
                enumerable: true
            };
        },
    });
}
function inspectProxy() {
    return "ScriptExportsProxy {}";
}
var RpcOperation;
(function (RpcOperation) {
    RpcOperation["Ok"] = "ok";
    RpcOperation["Error"] = "error";
})(RpcOperation || (RpcOperation = {}));
function isInternalMessage(message) {
    return isRpcMessage(message) || isLogMessage(message);
}
function isRpcMessage(message) {
    return message.type === MessageType.Send && isRpcSendMessage(message);
}
function isRpcSendMessage(message) {
    const payload = message.payload;
    if (!Array.isArray(payload)) {
        return false;
    }
    return payload[0] === "frida:rpc";
}
function isLogMessage(message) {
    return message.type === "log";
}
function log(level, text) {
    switch (level) {
        case LogLevel.Info:
            console.log(text);
            break;
        case LogLevel.Warning:
            console.warn(text);
            break;
        case LogLevel.Error:
            console.error(text);
            break;
    }
}
const reservedMethodNames = new Set([
    "then",
    "catch",
    "finally",
]);
function isReservedMethodName(name) {
    return reservedMethodNames.has(name.toString());
}
