"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RemoteHost = void 0;
const tslib_1 = require("tslib");
const events_1 = require("events");
const stream = require("stream");
const t = require("io-ts");
const jsonrpc_ts_1 = require("@fitbit/jsonrpc-ts");
const fdb_protocol_1 = require("@fitbit/fdb-protocol");
const lodash = require("lodash");
const JSZip = require("jszip");
const package_json_1 = require("../package.json");
const BulkDataReceiver_1 = require("./BulkDataReceiver");
const ConfigurableEncode_1 = require("./ConfigurableEncode");
const componentBundle_1 = require("./componentBundle");
const FBOS3_EVAL_QUIRK = /^[a-zA-Z]+ \d+\.33\.1\.[1-3]?\d$/;
class RemoteHost extends events_1.EventEmitter {
    constructor(timeout) {
        super();
        this.dispatcher = new jsonrpc_ts_1.TypesafeRequestDispatcher();
        this.rpc = new jsonrpc_ts_1.Peer(this.dispatcher);
        this.bulkDataStreams = new fdb_protocol_1.BulkData();
        this.screenshotReceiver = new BulkDataReceiver_1.default(this.bulkDataStreams, 'screenshot');
        this.appContentsListReceiver = new BulkDataReceiver_1.default(this.bulkDataStreams, 'app component contents list');
        this.heapSnapshotReceiver = new BulkDataReceiver_1.default(this.bulkDataStreams, 'heap snapshot');
        this.serializerTransform = new ConfigurableEncode_1.default();
        this.handleMessage = (params) => {
            if (params.timestamp) {
                this.emit('consoleMessage', Object.assign(Object.assign({}, params), { timestamp: this.convertTimestamp(params.timestamp) }));
            }
            else {
                this.emit('consoleMessage', params);
            }
        };
        this.handleTrace = (params) => {
            if (params.timestamp) {
                this.emit('consoleTrace', Object.assign(Object.assign({}, params), { timestamp: this.convertTimestamp(params.timestamp) }));
            }
            else {
                this.emit('consoleTrace', params);
            }
        };
        this.handleAppRunning = (params) => {
            this.emit('appRunning', params);
        };
        this.handleAppClosed = (params) => {
            this.emit('appClosed', params);
        };
        this.initialize = this.bindMethod('initialize', fdb_protocol_1.FDBTypes.InitializeParams, fdb_protocol_1.FDBTypes.InitializeResult);
        this.ping = () => this.rpc.callMethod('ping', undefined, { timeout: this.timeout });
        this.ioWrite = this.bindMethod('io.write', fdb_protocol_1.FDBTypes.IOWriteParams, t.any, { timeoutEnabled: false });
        this.beginStreamingInstall = this.bindMethod('app.install.stream.begin', fdb_protocol_1.FDBTypes.AppInstallStreamBeginParams, fdb_protocol_1.FDBTypes.StreamOpenResponse);
        this.finalizeStreamingInstall = this.bindMethod('app.install.stream.finalize', fdb_protocol_1.FDBTypes.StreamCloseParams, fdb_protocol_1.FDBTypes.AppInstallResult, { minTimeout: 300000 });
        this.abortStreamingInstall = this.bindMethod('app.install.stream.abort', fdb_protocol_1.FDBTypes.StreamCloseParams, t.any);
        this.launchAppComponent = this.bindMethod('app.launchComponent', fdb_protocol_1.FDBTypes.LaunchComponentParams, fdb_protocol_1.FDBTypes.AppComponent);
        this.changeSerialization = (serialization) => {
            this.rpc.sendNotification('protocol.serialization.change', { serialization });
            this.serializerTransform.setEncoder(serialization);
        };
        this.beginStreamingScreenshotCapture = this.bindMethod('app.screenshot.stream.capture', fdb_protocol_1.FDBTypes.AppScreenshotStreamCaptureParams, fdb_protocol_1.FDBTypes.AppScreenshotStreamCaptureResult);
        this.sendEvalCmd = this.bindMethod('app.debug.evalToString', fdb_protocol_1.FDBTypes.AppDebugEvalParams, fdb_protocol_1.FDBTypes.AppDebugEvalResult);
        this.beginStreamingAppComponentContents = this.bindMethod('app.contents.stream.list', fdb_protocol_1.FDBTypes.AppComponentContentsRequest, t.any);
        this.beginHeapSnapshotCapture = this.bindMethod('app.debug.heapSnapshot.capture', fdb_protocol_1.FDBTypes.AppHeapSnapshotRequest, t.any);
        this.timeout = timeout;
        this.dispatcher
            .method('ping', t.undefined, () => { })
            .notification('console.message', fdb_protocol_1.FDBTypes.ConsoleMessage, this.handleMessage)
            .notification('console.traceMessage', fdb_protocol_1.FDBTypes.TraceMessage, this.handleTrace)
            .notification('experimental.lifecycle.appRunning', fdb_protocol_1.FDBTypes.App, this.handleAppRunning)
            .notification('experimental.lifecycle.appClosed', fdb_protocol_1.FDBTypes.App, this.handleAppClosed);
        this.bulkDataStreams.register(this.dispatcher);
        this.screenshotReceiver.registerCloserMethods(this.dispatcher, 'app.screenshot.stream');
        this.appContentsListReceiver.registerCloserMethods(this.dispatcher, 'app.contents.stream');
        this.heapSnapshotReceiver.registerCloserMethods(this.dispatcher, 'app.debug.heapSnapshot');
    }
    static connect(hostStream, { userAgentSuffix = '', timeout = 10000, postDeserializeTransform = new stream.PassThrough({ objectMode: true }), preSerializeTransform = new stream.PassThrough({ objectMode: true }), } = {}) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let userAgent = this.USER_AGENT;
            if (userAgentSuffix) {
                userAgent = `${userAgent} ${userAgentSuffix}`;
            }
            const host = new this(timeout);
            hostStream
                .pipe(new jsonrpc_ts_1.ParseJSON)
                .pipe(postDeserializeTransform)
                .pipe(host.rpc)
                .pipe(preSerializeTransform)
                .pipe(host.serializerTransform)
                .pipe(hostStream);
            const reqTime = Date.now();
            host.info = yield host.initialize({
                userAgent,
                capabilities: this.CAPABILITIES,
            });
            if (host.hasCapability('protocol.additionalSerializations') &&
                host.info.capabilities.protocol.additionalSerializations.includes('cbor-definite')) {
                host.changeSerialization('cbor-definite');
            }
            host.epoch = new Date((reqTime + Date.now()) / 2);
            return host;
        });
    }
    convertTimestamp(relativeTS) {
        return new Date(this.epoch.getTime() + relativeTS * 1000);
    }
    hasCapability(path) {
        return lodash.get(this.info.capabilities, path) !== undefined;
    }
    get maxMessageSize() {
        const protocolDefaultSize = 8192;
        const capabilitySize = lodash.get(this.info.capabilities, 'protocol.maxMessageSize', 0);
        return Math.max(protocolDefaultSize, capabilitySize);
    }
    bindMethod(method, paramsType, resultType, { timeoutEnabled = true, minTimeout = 0, } = {}) {
        return (params) => this.rpc.callMethod(method, params, {
            timeout: timeoutEnabled ? Math.max(this.timeout, minTimeout) : undefined,
        }).then(jsonrpc_ts_1.decode(resultType));
    }
    writeToStream(stream, data, { onProgress = (() => { }), } = {}) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const overheadChars = 256;
            const maxDataBytes = fdb_protocol_1.maxBase64DecodedSize(this.maxMessageSize - overheadChars);
            if (maxDataBytes < 1) {
                throw new Error('Cannot fit any data into an io.write message');
            }
            let expireWriteTimeout;
            const writeTimedOut = new Promise((_, reject) => {
                expireWriteTimeout = () => {
                    reject(new Error('io.write timed out'));
                };
            });
            let timeoutTimer = setTimeout(expireWriteTimeout, this.timeout);
            const resetWriteTimeout = () => {
                clearTimeout(timeoutTimer);
                timeoutTimer = setTimeout(expireWriteTimeout, this.timeout);
            };
            const writes = [];
            for (let cursor = 0; cursor < data.length; cursor += maxDataBytes) {
                const chunk = this.serializerTransform.canAcceptRawBuffers() ? {
                    stream,
                    data: data.slice(cursor, cursor + maxDataBytes),
                    encoding: 'none',
                } : {
                    stream,
                    data: data.toString('base64', cursor, cursor + maxDataBytes),
                };
                writes.push(this.ioWrite(chunk).then(() => {
                    resetWriteTimeout();
                    onProgress(Math.min(cursor + maxDataBytes, data.length), data.length);
                }));
            }
            yield Promise.race([Promise.all(writes), writeTimedOut]);
            clearTimeout(timeoutTimer);
        });
    }
    installApp(componentBundle, data, { onProgress = (() => { }), } = {}) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let bundleData = data;
            if (this.supportsPartialAppInstall()) {
                try {
                    const bundleZip = yield JSZip.loadAsync(data);
                    const uuid = yield componentBundle_1.getAppUUID(bundleZip);
                    const existingContents = yield this.getInstalledAppContents(uuid, componentBundle);
                    const partialBundle = yield componentBundle_1.makePartialBundle(bundleZip, existingContents);
                    if (partialBundle == null) {
                        return null;
                    }
                    bundleData = partialBundle;
                }
                catch (_a) {
                }
            }
            const { stream } = yield this.beginStreamingInstall({ componentBundle });
            try {
                yield this.writeToStream(stream, bundleData, { onProgress });
            }
            catch (e) {
                this.abortStreamingInstall({ stream });
                throw e;
            }
            return this.finalizeStreamingInstall({ stream })
                .then(result => (Object.assign({ installType: 'full' }, result)));
        });
    }
    canTakeScreenshot() {
        return (this.hasCapability('appHost.screenshot') &&
            !!this.info.capabilities.appHost.screenshot.stream);
    }
    screenshotFormats() {
        if (!this.canTakeScreenshot())
            return [];
        return this.info.capabilities.appHost.screenshot.imageFormats;
    }
    takeScreenshot(format, onWrite) {
        return this.screenshotReceiver.receiveFromStream(stream => this.beginStreamingScreenshotCapture({ stream: stream.token, imageFormat: format })
            .then(({ length }) => {
            if (onWrite) {
                stream.onWrite = (_, received) => onWrite(received, length);
            }
        }));
    }
    hasEvalSupport() {
        return this.hasCapability('appHost.debug.app.evalToString.supported') &&
            this.info.capabilities.appHost.debug.app.evalToString.supported &&
            !FBOS3_EVAL_QUIRK.test(this.info.device);
    }
    eval(cmd) {
        return this.sendEvalCmd({ cmd });
    }
    supportsPartialAppInstall() {
        return this.hasCapability('appHost.install.partialBundle') &&
            this.info.capabilities.appHost.install.partialBundle;
    }
    getInstalledAppContents(uuid, componentBundle) {
        return this.appContentsListReceiver.receiveFromStream(stream => this.beginStreamingAppComponentContents({
            componentBundle,
            uuid,
            stream: stream.token,
        }))
            .then(buffer => JSON.parse(buffer.toString()))
            .then(jsonrpc_ts_1.decode(fdb_protocol_1.FDBTypes.AppComponentContentsList));
    }
    getHeapSnapshotSupport() {
        return Object.assign({ supported: false, requiresInstrumentedLaunch: false, formats: [] }, (this.hasCapability('appHost.debug.app.heapSnapshot') &&
            this.info.capabilities.appHost.debug.app.heapSnapshot));
    }
    captureHeapSnapshot(format) {
        return this.heapSnapshotReceiver.receiveFromStream(stream => this.beginHeapSnapshotCapture({ format, stream: stream.token }));
    }
}
exports.RemoteHost = RemoteHost;
RemoteHost.CAPABILITIES = {
    protocol: { maxMessageSize: 1024 * 1024 },
    console: { appLogging: true },
    io: { write: true },
};
RemoteHost.USER_AGENT = `fdb-debugger/${package_json_1.version}`;
//# sourceMappingURL=index.js.map