"use strict";
/*
 * Tencent is pleased to support the open source community by making
 * Hippy available.
 *
 * Copyright (C) 2017-2019 THL A29 Limited, a Tencent company.
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateIWDPAppClient = exports.subscribeByIWDP = exports.cleanAllDebugTargets = exports.cleanDebugTarget = exports.subscribeCommand = void 0;
const lodash_1 = require("lodash");
const enum_1 = require("@debug-server-next/@types/enum");
const db_1 = require("@debug-server-next/db");
const client_1 = require("@debug-server-next/client");
const middlewares_1 = require("@debug-server-next/middlewares");
const pub_sub_channel_1 = require("@debug-server-next/utils/pub-sub-channel");
const log_1 = require("@debug-server-next/utils/log");
const debug_target_1 = require("@debug-server-next/utils/debug-target");
const history_event_protocol_1 = require("@debug-server-next/utils/history-event-protocol");
const log = new log_1.Logger('pub-sub-manager', enum_1.WinstonColor.BrightGreen);
// store the data of a DebugTarget, key: clientId
const channelMap = new Map();
/**
 * subscribe to the upward command, trigger occasion:
 *  1. tunnel appConnect event
 *  2. app ws connection: maybe repeat subscribe because the iOS close event is later connect event
 *  3. get IWDP pages: should filter this situation, because frontend will request every 2s
 */
const subscribeCommand = async (debugTarget, ws) => {
    const { clientId, title, platform } = debugTarget;
    if (!channelMap.has(clientId))
        addChannelItem(debugTarget);
    else if (isIWDPPage(clientId))
        return;
    const { appClientList, upwardSubHandlerMap, upwardSubscriber, internalSubscriber } = channelMap.get(clientId);
    createAppClientList(debugTarget, ws);
    /**
     * subscribe upward message.
     * because there maybe multiple devtools client, such as multiple chrome extensions,
     * we need use batch subscribe (pSubscribe)
     */
    if (!upwardSubHandlerMap.has(clientId)) {
        const upwardSubHandler = createUpwardSubHandler(clientId);
        const internalHandler = createInternalHandler(clientId);
        upwardSubHandlerMap.set(clientId, { upwardSubHandler, internalHandler });
    }
    const { upwardSubHandler, internalHandler } = upwardSubHandlerMap.get(clientId);
    // must unsubscribe first to avoid subscribe multiple times
    upwardSubscriber.pUnsubscribe(upwardSubHandler);
    upwardSubscriber.pSubscribe(upwardSubHandler);
    // publish downward message to devtools frontend
    appClientList.forEach((appClient) => {
        appClient.removeAllListeners("message" /* AppClientEvent.Message */);
        appClient.on("message" /* AppClientEvent.Message */, (msg) => {
            const handler = getAppClientMessageHandler(debugTarget);
            if (handler)
                handler(msg);
        });
    });
    // publish history logs
    await internalSubscriber.unsubscribe(internalHandler);
    await internalSubscriber.subscribe(internalHandler);
};
exports.subscribeCommand = subscribeCommand;
/**
 * clean cache of one DebugTarget
 * should invoke when tunnel appDisconnect event, or WSAppClient ws close event.
 */
const cleanDebugTarget = async (clientId, closeDevtools, cleanCache = false) => {
    if (cleanCache) {
        await (0, debug_target_1.removeDebugTarget)(clientId);
        log.verbose('removeDebugTarget %s', clientId);
        return;
    }
    const debugTarget = await (0, debug_target_1.decreaseRefAndSave)(clientId);
    if (debugTarget)
        return;
    const channelInfo = channelMap.get(clientId);
    if (!channelInfo)
        return;
    const { publisherMap, upwardSubscriber, internalPublisher, internalSubscriber } = channelInfo;
    if (closeDevtools) {
        await internalPublisher.publish(enum_1.InternalChannelEvent.AppWSClose);
    }
    process.nextTick(() => {
        Array.from(publisherMap.values()).forEach((publisher) => publisher.disconnect());
        upwardSubscriber.disconnect();
        internalPublisher.disconnect();
        internalSubscriber.disconnect();
        channelMap.delete(clientId);
    });
};
exports.cleanDebugTarget = cleanDebugTarget;
/**
 * clean all cache of DebugTarget
 */
const cleanAllDebugTargets = async () => Promise.all(Array.from(channelMap.values()).map(({ debugTarget }) => (0, exports.cleanDebugTarget)(debugTarget.clientId, true, true)));
exports.cleanAllDebugTargets = cleanAllDebugTargets;
let oldIWDPDebugTargets = [];
/**
 * subscribe upward message from IWDP, and clean the outdated IWDP page
 */
const subscribeByIWDP = (debugTargets) => {
    const outdatedDebugTargets = (0, lodash_1.differenceBy)(oldIWDPDebugTargets, debugTargets, 'clientId');
    if (outdatedDebugTargets.length)
        log.verbose('outdatedDebugTargets %j', outdatedDebugTargets);
    outdatedDebugTargets.forEach(({ clientId }) => {
        (0, exports.cleanDebugTarget)(clientId, true);
    });
    debugTargets.forEach((debugTarget) => {
        const oldDebugTarget = oldIWDPDebugTargets.find((item) => item.clientId === debugTarget.clientId);
        if (oldDebugTarget)
            debugTarget.ts = oldDebugTarget.ts;
        (0, exports.subscribeCommand)(debugTarget);
    });
    oldIWDPDebugTargets = debugTargets;
};
exports.subscribeByIWDP = subscribeByIWDP;
const addChannelItem = (debugTarget) => {
    const { clientId } = debugTarget;
    const { Publisher, Subscriber } = (0, db_1.getDBOperator)();
    const internalChannelId = (0, pub_sub_channel_1.createInternalChannel)(clientId, '');
    const upwardSubscriber = createUpwardSubscriber(clientId);
    const internalPublisher = new Publisher(internalChannelId);
    const internalSubscriber = new Subscriber(internalChannelId);
    channelMap.set(clientId, {
        downwardChannelSet: new Set(),
        cmdIdChannelIdMap: new Map(),
        publisherMap: new Map(),
        upwardSubscriber,
        internalPublisher,
        internalSubscriber,
        debugTarget,
        appClientList: [],
        upwardSubHandlerMap: new Map(),
    });
};
const createUpwardSubscriber = (clientId) => {
    const { Subscriber } = (0, db_1.getDBOperator)();
    const upwardChannelId = (0, pub_sub_channel_1.createUpwardChannel)(clientId, '*');
    log.verbose('subscribe to redis channel %s', upwardChannelId);
    return new Subscriber(upwardChannelId);
};
/**
 * create matched debug tunnels by DebugTarget
 */
const createAppClientList = (debugTarget, ws) => {
    const { clientId } = debugTarget;
    const { appClientList } = channelMap.get(clientId);
    const options = client_1.appClientManager.getAppClientOptions(debugTarget.platform);
    return options
        .map(({ Ctor: AppClientCtor, ...option }) => {
        try {
            const outdatedAppClientIndex = appClientList.findIndex((appClient) => appClient.constructor.name === AppClientCtor.name);
            if (outdatedAppClientIndex !== -1) {
                const outdatedAppClient = appClientList.splice(outdatedAppClientIndex, 1)[0];
                outdatedAppClient.destroy();
                log.verbose('%s is outdated, re-constructor now', outdatedAppClient.constructor.name);
            }
            const urlParsedContext = (0, middlewares_1.debugTargetToUrlParsedContext)(debugTarget);
            const newOption = {
                ...option,
                urlParsedContext,
                iWDPWsUrl: debugTarget.iWDPWsUrl,
                ws,
            };
            if (AppClientCtor.name === "WSAppClient" /* AppClientType.WS */ && !ws) {
                log.verbose('WSAppClient constructor option need ws');
                return;
            }
            if (AppClientCtor.name === "IWDPAppClient" /* AppClientType.IWDP */ && !debugTarget.iWDPWsUrl) {
                log.verbose('IWDPAppClient constructor option need iWDPWsUrl, if you are debug iOS without USB, please ignore this warning.');
                return;
            }
            log.verbose(`create app client ${AppClientCtor.name}`);
            const appClient = new AppClientCtor(clientId, newOption);
            appClientList.push(appClient);
            return appClient;
        }
        catch (e) {
            log.error('create app client error: %s', e === null || e === void 0 ? void 0 : e.stack);
            return null;
        }
    })
        .filter((v) => v);
};
/**
 * when re-plug iOS device, IWDPAppClient ws url will change, so need update new IWDPAppClient
 */
const updateIWDPAppClient = (debugTarget) => {
    const { clientId } = debugTarget;
    if (debugTarget.platform !== enum_1.DevicePlatform.IOS || !channelMap.has(clientId))
        return;
    const { appClientList } = channelMap.get(clientId);
    const options = client_1.appClientManager.getAppClientOptions(debugTarget.platform);
    const iWDPOption = options.find(({ Ctor: AppClientCtor }) => AppClientCtor.name === "IWDPAppClient" /* AppClientType.IWDP */);
    if (!iWDPOption)
        return;
    const { Ctor: AppClientCtor, ...option } = iWDPOption;
    const hasIWDPAppClient = debugTarget.appClientTypeList.indexOf("IWDPAppClient" /* AppClientType.IWDP */) !== -1;
    const outdatedIWDPAppClientIndex = appClientList.findIndex((item) => item.constructor.name === "IWDPAppClient" /* AppClientType.IWDP */);
    if (hasIWDPAppClient) {
        if (outdatedIWDPAppClientIndex === -1) {
            appendIWDPAppClient();
        }
        else {
            updateIWDPAppClient();
        }
    }
    else if (outdatedIWDPAppClientIndex !== -1) {
        removeIWDPAppClient();
    }
    else {
        appendIWDPAppClient();
    }
    function appendIWDPAppClient() {
        log.verbose('append IWDPAppClient');
        if (!debugTarget.iWDPWsUrl) {
            return log.verbose('debugTarget.iWDPWsUrl is null, could not debug jscore');
        }
        const {} = iWDPOption;
        const iWDPAppClient = new AppClientCtor(clientId, {
            ...option,
            urlParsedContext: (0, middlewares_1.debugTargetToUrlParsedContext)(debugTarget),
            iWDPWsUrl: debugTarget.iWDPWsUrl,
        });
        appClientList.push(iWDPAppClient);
        iWDPAppClient.on("message" /* AppClientEvent.Message */, (msg) => getAppClientMessageHandler(debugTarget)(msg));
    }
    function updateIWDPAppClient() {
        const iWDPAppClient = appClientList[outdatedIWDPAppClientIndex];
        if (iWDPAppClient.url === debugTarget.iWDPWsUrl)
            return;
        const outdatedAppClient = appClientList.splice(outdatedIWDPAppClientIndex, 1)[0];
        outdatedAppClient.destroy();
        if (!debugTarget.iWDPWsUrl) {
            return log.verbose('debugTarget.iWDPWsUrl is null, could not debug jscore');
        }
        const urlParsedContext = (0, middlewares_1.debugTargetToUrlParsedContext)(debugTarget);
        const newOption = {
            ...option,
            urlParsedContext,
            iWDPWsUrl: debugTarget.iWDPWsUrl,
        };
        const appClient = new AppClientCtor(clientId, newOption);
        appClientList.push(appClient);
        appClient.removeAllListeners("message" /* AppClientEvent.Message */);
        appClient.on("message" /* AppClientEvent.Message */, (msg) => getAppClientMessageHandler(debugTarget)(msg));
        log.verbose(`create app client ${AppClientCtor.name}, update iWDPWsUrl to %s`, debugTarget.iWDPWsUrl);
    }
    function removeIWDPAppClient() {
        const outdated = appClientList.splice(outdatedIWDPAppClientIndex, 1)[0];
        outdated.destroy();
        log.verbose('IWDPAppClient is outdated and destroyed!');
    }
};
exports.updateIWDPAppClient = updateIWDPAppClient;
/**
 * downward message handler
 */
const getAppClientMessageHandler = (debugTarget) => async (msg) => {
    const { clientId, platform } = debugTarget;
    const channelInfo = channelMap.get(clientId);
    if (!channelInfo) {
        return log.verbose('channelInfo does not exist, maybe app page is closed!');
    }
    const { downwardChannelSet, cmdIdChannelIdMap, publisherMap } = channelInfo;
    msg.ts = Date.now();
    const msgStr = JSON.stringify(msg);
    const { Publisher } = (0, db_1.getDBOperator)();
    if ('id' in msg) {
        // publish CommandRes to `downwardChannelId`
        const commandRes = msg;
        const downwardChannelId = cmdIdChannelIdMap.get(commandRes.id);
        if (!downwardChannelId)
            return;
        if (!publisherMap.has(downwardChannelId)) {
            const publisher = new Publisher(downwardChannelId);
            publisherMap.set(downwardChannelId, publisher);
        }
        const publisher = publisherMap.get(downwardChannelId);
        publisher.publish(msgStr);
    }
    else {
        if (downwardChannelSet.size === 0) {
            downwardChannelSet.add((0, pub_sub_channel_1.createDownwardChannel)(clientId));
        }
        if (platform === enum_1.DevicePlatform.IOS && (0, history_event_protocol_1.isHistoryProtocol)(msg.method, platform)) {
            (0, history_event_protocol_1.saveHistoryProtocol)(clientId, msgStr);
        }
        // broadcast to all channel, because could'n determine the receiver of EventRes
        downwardChannelSet.forEach((channelId) => {
            if (!publisherMap.has(channelId)) {
                const publisher = new Publisher(channelId);
                publisherMap.set(channelId, publisher);
            }
            const publisher = publisherMap.get(channelId);
            publisher.publish(msgStr);
        });
    }
};
const isIWDPPage = (clientId) => {
    const { appClientList } = channelMap.get(clientId);
    return (appClientList === null || appClientList === void 0 ? void 0 : appClientList.length) === 1 && appClientList[0].constructor.name === "IWDPAppClient" /* AppClientType.IWDP */;
};
const createUpwardSubHandler = (clientId) => (message, upwardChannelId) => {
    const { downwardChannelSet, cmdIdChannelIdMap } = channelMap.get(clientId);
    if (!upwardChannelId)
        return log.verbose('pSubscribe without channelId');
    if (upwardChannelId.includes(pub_sub_channel_1.vueDevtoolsExtensionName) || upwardChannelId.includes(pub_sub_channel_1.reactDevtoolsExtensionName))
        return log.verbose('ignore vue/react channel');
    let msgObj;
    try {
        msgObj = JSON.parse(message);
    }
    catch (e) {
        log.error('%s channel message are invalid JSON, %s', upwardChannelId, message);
    }
    const downwardChannelId = (0, pub_sub_channel_1.upwardChannelToDownwardChannel)(upwardChannelId);
    cmdIdChannelIdMap.set(msgObj.id, downwardChannelId);
    downwardChannelSet.add(downwardChannelId);
    /**
     * when devtools publish resumeCommands, maybe app had closed
     */
    const channelInfo = channelMap.get(clientId);
    if (!channelInfo)
        return;
    const { appClientList: latestAppClientList } = channelInfo;
    latestAppClientList.forEach((appClient) => {
        appClient.sendToApp(msgObj).catch((e) => {
            if (e !== 1 /* ErrorCode.DomainFiltered */) {
                return log.error('%s app client send error: %j', appClient.constructor.name, e);
            }
        });
    });
};
const createInternalHandler = (clientId) => async (msg) => {
    const { downwardChannelSet, publisherMap, debugTarget } = channelMap.get(clientId);
    const { platform } = debugTarget;
    if (msg !== enum_1.InternalChannelEvent.DevtoolsConnected)
        return;
    const list = await (0, history_event_protocol_1.getHistoryProtocol)(clientId);
    if (!list.length)
        return;
    /**
     * delay to send history log after ConsoleModel of devtools is ready
     */
    setTimeout(() => {
        downwardChannelSet.forEach(async (channelId) => {
            if (!publisherMap.has(channelId)) {
                const { Publisher } = (0, db_1.getDBOperator)();
                const publisher = new Publisher(channelId);
                publisherMap.set(channelId, publisher);
            }
            const publisher = publisherMap.get(channelId);
            /**
             * mock a clear protocol to clear existed history logs in console panel
             */
            if (platform !== enum_1.DevicePlatform.Android) {
                await publisher.publish({
                    method: 'Log.cleared',
                    params: {},
                });
            }
            list.forEach((item) => {
                var _a;
                try {
                    const cmd = JSON.parse(item);
                    if (!((_a = cmd.method) === null || _a === void 0 ? void 0 : _a.startsWith('Network.'))) {
                        publisher.publish(item);
                    }
                }
                catch (e) { }
            });
        });
    }, 1500);
};
