"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FrameContextTracker = void 0;
exports.getPageFrameSelectorInfo = getPageFrameSelectorInfo;
const asyncUtil_1 = require("../../util/asyncUtil");
const frame_utils_1 = require("./frame-utils");
const stringUtils_1 = require("../sharedUtils/stringUtils");
const HEARTBEAT_INTERVAL = 1_000;
class PageIdMapping {
    constructor() {
        this.default2override = {};
        this.override2default = {};
    }
    set(defaultId, overrideId) {
        this.default2override[defaultId] = overrideId;
        this.override2default[overrideId] = defaultId;
    }
    getFrameIdFromPageId(pageId) {
        return this.override2default[pageId] ?? pageId;
    }
    getPageIdFromFrameId(frameId) {
        return this.default2override[frameId] ?? frameId;
    }
}
const MABL_FRAME_INFO = `mablFrameInfo`;
const MABL_IS_WALKTHROUGH = `__mablIsWalkthrough`;
class FrameContextTracker {
    constructor(options) {
        this.options = options;
        this.trackedFrames = {};
        this.trackedPages = {};
        this.currentFrameId = 0;
        this.currentPageId = '';
        this.pageIdMapping = new PageIdMapping();
    }
    get runInMainWorld() {
        return this.options.runInMainWorld;
    }
    performInitialFrameTracking(frameId, frame, selectorInfo) {
        if ((0, frame_utils_1.isRootFrameId)(frameId)) {
            void this.updatePageFrameSelectorInfo(frame);
            return;
        }
        if (this.trackedFrames[frameId]) {
            this.trackedFrames[frameId].frame = frame;
        }
        else {
            this.trackedFrames[frameId] = { frame };
        }
        if (selectorInfo) {
            this.trackedFrames[frameId].selectorInfo = selectorInfo;
        }
    }
    trackFrameSelectorInfo(frameId, selectorInfo) {
        if (this.trackedFrames[frameId]) {
            this.trackedFrames[frameId].selectorInfo = selectorInfo;
        }
        else {
            this.trackedFrames[frameId] = { selectorInfo };
        }
    }
    getFrameSelectorInfo(frameId) {
        const frameInfo = this.trackedFrames[frameId];
        return frameInfo?.selectorInfo ?? {};
    }
    getCurrentFrameId() {
        return this.currentFrameId;
    }
    getFrame(frameId) {
        return this.trackedFrames[frameId]?.frame;
    }
    getFrameIdBySelector(selector) {
        if (!selector) {
            return;
        }
        const frameIds = Object.keys(this.trackedFrames);
        let matchedFrameIds = frameIds.filter((frameId) => {
            const frame = this.trackedFrames[frameId];
            const selectorId = frame.selectorInfo?.id ??
                frame.selectorInfo?.elementId;
            return (((selector.id && selectorId === selector.id) ||
                (selector.name && frame?.selectorInfo?.name === selector.name)) &&
                frame.frame &&
                !frame.frame.isDetached());
        });
        if (!matchedFrameIds.length) {
            matchedFrameIds = frameIds.filter((frameId) => {
                const frame = this.trackedFrames[frameId];
                return (selector.src &&
                    frame?.selectorInfo?.src === selector.src &&
                    frame.frame &&
                    !frame.frame.isDetached());
            });
        }
        if (!matchedFrameIds.length) {
            matchedFrameIds = frameIds.filter((frameId) => {
                const frame = this.trackedFrames[frameId];
                const frameClassName = frame.selectorInfo?.class_name ??
                    frame.selectorInfo?.className;
                if (frameClassName && selector.class_name) {
                    return (selector.class_name &&
                        (0, stringUtils_1.compareStringsAsWordSets)(frameClassName, selector.class_name) &&
                        frame.frame &&
                        !frame.frame.isDetached());
                }
                return;
            });
        }
        if (matchedFrameIds.length) {
            const currentPage = this.trackedPages[this.currentPageId];
            let matchedFrame = matchedFrameIds[0];
            if (currentPage) {
                matchedFrameIds = matchedFrameIds.filter((frameId) => this.trackedFrames[frameId].frame?.page() ===
                    currentPage.frame.page());
                if (matchedFrameIds.length === 1) {
                    matchedFrame = matchedFrameIds[0];
                }
                else if (matchedFrameIds.length > 1) {
                    const currentFrame = this.trackedFrames[this.currentFrameId];
                    if (currentFrame) {
                        matchedFrameIds = matchedFrameIds.filter((frameId) => this.trackedFrames[frameId].frame?.parentFrame() ===
                            currentFrame.frame);
                    }
                    if (matchedFrameIds.length) {
                        matchedFrame = matchedFrameIds[0];
                    }
                }
                return matchedFrame;
            }
        }
        return;
    }
    getPage(pageId) {
        const frame = this.trackedPages[pageId]?.frame;
        return frame?.page();
    }
    setCurrentFrameId(frameId) {
        this.currentFrameId = frameId;
    }
    setCurrentFrame(frame) {
        const frameId = (0, frame_utils_1.getFrameId)(frame);
        this.setCurrentFrameId(frameId);
    }
    getCurrentPageId() {
        return this.currentPageId;
    }
    getCurrentPageFrame() {
        return this.trackedPages[this.currentPageId].frame;
    }
    setCurrentPageId(pageId) {
        this.currentPageId = pageId;
    }
    async getAllActivePageIds() {
        const pageStates = await Promise.all(Object.keys(this.trackedPages).map(async (id) => {
            try {
                const page = this.getPage(id);
                const active = !!page &&
                    !page.isClosed() &&
                    (await page.evaluate(() => !document.hidden));
                return { id, active };
            }
            catch (error) {
                console.error(`[Frame tracking] ERROR unable to determine if page '${id}' is active: ${error}`);
            }
            return { id, active: false };
        }));
        const activePageStates = pageStates.filter((pageState) => pageState.active);
        if (activePageStates.length) {
            return activePageStates.map((pageState) => pageState.id);
        }
        console.log(`[Frame tracking] no active page found; default to current page id '${this.currentPageId}'`);
        return [this.currentPageId];
    }
    async getActivePageId() {
        const activePageIds = await this.getAllActivePageIds();
        if (activePageIds.length > 1 &&
            activePageIds.includes(this.getCurrentPageId())) {
            return this.getCurrentPageId();
        }
        return activePageIds[activePageIds.length - 1];
    }
    getRootFrameId(frame) {
        const pathToRoot = this.findPathToRoot(frame);
        return pathToRoot[pathToRoot.length - 1];
    }
    async trackAndInjectContentJs(frame, contentScript, forceInjection = false, isInWalkthrough = false) {
        const frameId = !!frame.parentFrame() ? (0, frame_utils_1.getFrameId)(frame) : 0;
        const pageId = this.pageIdMapping.getPageIdFromFrameId((0, frame_utils_1.isRootFrameId)(frameId) ? (0, frame_utils_1.getFrameId)(frame) : this.getRootFrameId(frame));
        const frameInfo = {
            frameId,
            tabId: pageId,
        };
        const isInjected = await this.isMablContentInjected(frame);
        this.performInitialFrameTracking(frameInfo.frameId, frame);
        if (!isInjected || forceInjection) {
            try {
                console.log(`[Frame tracking] Injecting content js: page[${pageId}] frame[${frameId}] forceInjection[${forceInjection}]`);
                const parentFrame = frame.parentFrame();
                if (parentFrame) {
                    await this.waitUntilInitialized(parentFrame);
                }
                if (!this.options.runInMainWorld) {
                    await frame.initializeExposedFunctionInSecondaryWorld(this.options.exposedMessagingFunction);
                }
                const injectionPayload = `
          window.${MABL_FRAME_INFO} = ${JSON.stringify(frameInfo)};
          window.${MABL_IS_WALKTHROUGH} = ${!!isInWalkthrough};
          ${contentScript}
        `;
                const playwrightInjectedScript = this.options.runInMainWorld
                    ? await frame.getInjectedScriptInMainWorld()
                    : await frame.getInjectedScriptInSecondaryWorld();
                await playwrightInjectedScript.evaluate((injected) => {
                    window.mablShadowDOMQuerySelectorAll = (selector) => {
                        const parsed = injected.parseSelector(selector);
                        return injected.querySelectorAll(parsed, document);
                    };
                });
                await this.executeCodeInFrame(frame, injectionPayload);
            }
            catch (error) {
                if (!error.message?.includes('Script already loaded')) {
                    throw error;
                }
                console.debug('[Frame tracking] ignoring "Script already loaded" error while loading content.js');
            }
            finally {
                if (frame.parentFrame()) {
                    void this.waitUntilInitialized(frame).then(() => {
                        global.setTimeout(() => void this.runHeartbeat(frame), HEARTBEAT_INTERVAL);
                    });
                }
                else {
                    global.setTimeout(() => void this.runHeartbeat(frame), HEARTBEAT_INTERVAL);
                }
            }
        }
    }
    async overridePageInfo(page, pageIdOverride) {
        const frame = page.mainFrame();
        const pageId = (0, frame_utils_1.getFrameId)(frame);
        this.pageIdMapping.set(pageId, pageIdOverride);
        console.log(`[Frame tracking] page '${pageId}' will be known as '${pageIdOverride}'`);
        try {
            const frameInfo = await this.getFrameInfo(frame);
            frameInfo.tabId = pageIdOverride;
            await this.updateFrameInfo(frame, frameInfo);
            await this.updatePageFrameSelectorInfo(frame);
            console.log('[Frame tracking] frameInfo in page content has been updated');
        }
        catch (error) {
            console.log('[Frame tracking] frameInfo in page content cannot be updated at this time');
        }
    }
    getFrameInfo(frame) {
        return (0, asyncUtil_1.awaitResult)(async () => {
            const frameInfo = await this.executeCodeInFrame(frame, `window.${MABL_FRAME_INFO}`);
            if (frameInfo) {
                return frameInfo;
            }
            throw new Error(`Waiting for ${MABL_FRAME_INFO} to show up in Frame: ${(0, frame_utils_1.getFrameId)(frame)}`);
        }, 30_000);
    }
    updateFrameInfo(frame, frameInfo) {
        const frameInfoPayload = `window.${MABL_FRAME_INFO} = ${JSON.stringify(frameInfo)};`;
        return this.executeCodeInFrame(frame, frameInfoPayload);
    }
    findPathToRoot(startingFrame) {
        const frameIdsPath = [];
        let rootFrameId;
        let crawlDepth = 0;
        if (startingFrame) {
            let currentFrame = startingFrame;
            while (!rootFrameId && crawlDepth <= 1000) {
                frameIdsPath.push((0, frame_utils_1.getFrameId)(currentFrame));
                const parentFrame = currentFrame.parentFrame();
                if (!parentFrame) {
                    rootFrameId = (0, frame_utils_1.getFrameId)(currentFrame);
                }
                else {
                    currentFrame = parentFrame;
                }
                crawlDepth += 1;
            }
        }
        return frameIdsPath;
    }
    findPathToRootFromFrameId(frameId) {
        const frame = this.trackedFrames[frameId];
        return this.findPathToRoot(frame.frame);
    }
    trackFramePage(pageFrameId) {
        const pageFrame = this.trackedPages[pageFrameId];
        if (pageFrame) {
            void this.updatePageFrameSelectorInfo(pageFrame.frame);
        }
    }
    getPageSelectorInfo(pageFrameId) {
        if (this.trackedPages[pageFrameId]) {
            return this.trackedPages[pageFrameId].selectorInfo;
        }
        return {};
    }
    updatePageFrameSelectorInfo(frame) {
        const frameId = (0, frame_utils_1.getFrameId)(frame);
        const pageId = this.pageIdMapping.getPageIdFromFrameId(frameId);
        return getPageFrameSelectorInfo(frame, pageId)
            .then((selectorInfo) => {
            const pageFrameSelector = {
                selectorInfo,
                frame,
            };
            this.trackedPages[frameId] = pageFrameSelector;
            this.trackedPages[pageId] = pageFrameSelector;
        })
            .catch((error) => console.error(`Issue tracking page frame: ${error}`));
    }
    isMablContentInjected(frame) {
        return this.executeCodeInFrame(frame, () => !!window.__mablInit);
    }
    isExposedFunctionDeclared(frame) {
        return this.executeCodeInFrame(frame, ({ functionName }) => !!window[functionName], {
            functionName: this.options.exposedMessagingFunction,
        });
    }
    executeCodeInFrame(frame, code, arg) {
        return this.options.runInMainWorld
            ? frame.evaluate(code, arg)
            : frame.evaluateCodeInSecondaryWorld(code, arg);
    }
    evaluateHandleInFrame(frame, code, arg) {
        return this.options.runInMainWorld
            ? frame.evaluateHandle(code, arg)
            : frame.evaluateHandleInSecondaryWorld(code, arg);
    }
    waitUntilReady(frame, timeoutMs = 5000) {
        return (0, asyncUtil_1.awaitResult)(async () => {
            const injected = await this.isMablContentInjected(frame);
            if (!injected) {
                throw new Error(`Waiting for frame '${(0, frame_utils_1.getFrameId)(frame)}' to be ready`);
            }
            return true;
        }, timeoutMs);
    }
    waitUntilInitialized(frame, timeoutMs = 60_000) {
        return (0, asyncUtil_1.awaitResult)(async () => {
            const frameId = (0, frame_utils_1.getFrameId)(frame);
            if (frame.isDetached()) {
                return true;
            }
            if (this.trackedFrames[frameId]?.selectorInfo) {
                return true;
            }
            try {
                if (this.options.onInitializeFrame) {
                    await this.options.onInitializeFrame(frame, false);
                }
                else {
                    console.warn('[Frame Tracking] No onInitializeFrame callback provided, cannot re-initialize frame');
                }
            }
            catch (e) {
                console.log(`[Frame Tracking] Error while waiting for frame to be initialized: ${e}`);
            }
            return false;
        }, timeoutMs);
    }
    async sendMessageToAllFramesInPages(message, pages) {
        const allFrames = pages.flatMap((p) => p.frames());
        const messagePromises = allFrames.map((frame) => this.sendMessageToFrame(frame, message));
        await Promise.all(messagePromises);
        return;
    }
    async sendMessageToAllFramesInPage(message, page) {
        const allFrames = page.frames();
        const messagePromises = allFrames.map((frame) => this.sendMessageToFrame(frame, message));
        await Promise.all(messagePromises);
        return;
    }
    async sendMessageToRootFrameInCurrentPage(message, returnHandle = false) {
        const activePageId = await this.getActivePageId();
        const currentPage = this.trackedPages[activePageId];
        if (currentPage) {
            return this.sendMessageToFrame(currentPage.frame, message, returnHandle);
        }
        return undefined;
    }
    async sendMessageToCurrentFrame(message, returnHandle = false) {
        const currentFrame = this.getFrame(this.getCurrentFrameId());
        if (currentFrame) {
            return this.sendMessageToFrame(currentFrame, message, returnHandle);
        }
        return this.sendMessageToRootFrameInCurrentPage(message, returnHandle);
    }
    sendMessageToRootFrameOfAllPages(message) {
        const allPages = Object.keys(this.trackedPages);
        allPages.forEach((pageId) => this.sendMessageToPageRoot(pageId, message));
    }
    async sendMessageToPageRoot(pageId, message, returnHandle = false) {
        const page = this.trackedPages[pageId];
        if (!page) {
            console.debug(`Page id not found to send message to: ${pageId}`);
            return;
        }
        return this.sendMessageToFrame(page.frame, message, returnHandle);
    }
    async sendMessageToFrameById(frameId, message, returnHandle = false) {
        const frame = this.trackedFrames[frameId];
        if (!frame?.frame) {
            throw new Error(`Frame with id not found to send message to: ${frameId}`);
        }
        return this.sendMessageToFrame(frame.frame, message, returnHandle);
    }
    async sendMessageToFrame(frame, message, returnHandle = false, retry = true) {
        try {
            if (returnHandle) {
                return (await this.evaluateHandleInFrame(frame, (m) => window.__mabl.messageListener(m), message));
            }
            return (await this.executeCodeInFrame(frame, (m) => window.__mabl.messageListener(m), message));
        }
        catch (error) {
            if (retry && error.message.includes(`reading 'messageListener'`)) {
                if (this.options.onInitializeFrame) {
                    await this.options.onInitializeFrame(frame, true);
                    await this.sendMessageToFrame(frame, message, returnHandle, false);
                }
                else {
                    console.warn('[Frame Tracking] No onInitializeFrame callback provided, cannot retry message send');
                }
                return;
            }
            console.error(`Error sending message to frame: ${error}`);
        }
        return;
    }
    async sendMessageToAllFramesOfCurrentPage(message) {
        const currentPageFrame = this.getCurrentPageFrame();
        const currentPage = currentPageFrame.page();
        if (currentPage) {
            await this.sendMessageToAllFramesInPages(message, [currentPage]);
        }
    }
    async runHeartbeat(frame) {
        try {
            if (frame.isDetached()) {
                return;
            }
            if (!this.options.runInMainWorld) {
                const exposedFunctionDeclared = await this.isExposedFunctionDeclared(frame);
                if (!exposedFunctionDeclared) {
                    console.debug(`[Frame tracking] Heartbeat detected frame ${frame.id()} has no exposed function declared.`);
                    await frame.initializeExposedFunctionInSecondaryWorld(this.options.exposedMessagingFunction);
                }
            }
            const injected = await this.isMablContentInjected(frame);
            if (!injected) {
                console.debug(`[Frame tracking] Heartbeat detected frame ${frame.id()} has no mabl content inject.`);
                if (this.options.onInitializeFrame) {
                    await this.options.onInitializeFrame(frame, false);
                }
                else {
                    console.warn('[Frame Tracking] No onInitializeFrame callback provided, cannot re-initialize frame in heartbeat');
                }
                return;
            }
            global.setTimeout(() => this.runHeartbeat(frame), HEARTBEAT_INTERVAL);
        }
        catch (error) {
            console.debug(`[Frame tracking] Heartbeat for frame ${frame.id()} stopped. Reason ${error}`);
            return;
        }
    }
}
exports.FrameContextTracker = FrameContextTracker;
async function getPageFrameSelectorInfo(frame, uuid) {
    let title = '';
    let url = '';
    try {
        title = await frame.title();
        url = await frame.url();
    }
    catch (error) {
        console.log(`[Frame Tracking] Error while getting frame title or URL: ${error}`);
    }
    return {
        uuid: uuid.toString(),
        selector_type: 'tab',
        title,
        url,
    };
}
