"use strict";
/**
 * Copyright 2023 Google LLC.
 * Copyright (c) Microsoft Corporation.
 *
 * 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.ActionDispatcher = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const assert_js_1 = require("../../../utils/assert.js");
const USKeyboardLayout_js_1 = require("./USKeyboardLayout.js");
const keyUtils_js_1 = require("./keyUtils.js");
/** https://w3c.github.io/webdriver/#dfn-center-point */
const CALCULATE_IN_VIEW_CENTER_PT_DECL = ((i) => {
    const t = i.getClientRects()[0], e = Math.max(0, Math.min(t.x, t.x + t.width)), n = Math.min(window.innerWidth, Math.max(t.x, t.x + t.width)), h = Math.max(0, Math.min(t.y, t.y + t.height)), m = Math.min(window.innerHeight, Math.max(t.y, t.y + t.height));
    return [e + ((n - e) >> 1), h + ((m - h) >> 1)];
}).toString();
const IS_MAC_DECL = (() => {
    return navigator.platform.toLowerCase().includes('mac');
}).toString();
async function getElementCenter(context, element) {
    const { result } = await (await context.getOrCreateSandbox(undefined)).callFunction(CALCULATE_IN_VIEW_CENTER_PT_DECL, { type: 'undefined' }, [element], false, 'none', {});
    if (result.type === 'exception') {
        throw new protocol_js_1.Message.NoSuchElementException(`Origin element ${element.sharedId} was not found`);
    }
    (0, assert_js_1.assert)(result.result.type === 'array');
    (0, assert_js_1.assert)(result.result.value?.[0]?.type === 'number');
    (0, assert_js_1.assert)(result.result.value?.[1]?.type === 'number');
    const { result: { value: [{ value: x }, { value: y }], }, } = result;
    return { x: x, y: y };
}
class ActionDispatcher {
    static isMacOS = async (context) => {
        const { result } = await (await context.getOrCreateSandbox(undefined)).callFunction(IS_MAC_DECL, { type: 'undefined' }, [], false, 'none', {});
        (0, assert_js_1.assert)(result.type !== 'exception');
        (0, assert_js_1.assert)(result.result.type === 'boolean');
        return result.result.value;
    };
    #tickStart = 0;
    #tickDuration = 0;
    #inputState;
    #context;
    #isMacOS;
    constructor(inputState, context, isMacOS) {
        this.#inputState = inputState;
        this.#context = context;
        this.#isMacOS = isMacOS;
    }
    async dispatchActions(optionsByTick) {
        await this.#inputState.queue.run(async () => {
            for (const options of optionsByTick) {
                await this.dispatchTickActions(options);
            }
        });
    }
    async dispatchTickActions(options) {
        this.#tickStart = performance.now();
        this.#tickDuration = 0;
        for (const { action } of options) {
            if ('duration' in action && action.duration !== undefined) {
                this.#tickDuration = Math.max(this.#tickDuration, action.duration);
            }
        }
        const promises = [
            new Promise((resolve) => setTimeout(resolve, this.#tickDuration)),
        ];
        for (const option of options) {
            promises.push(this.#dispatchAction(option));
        }
        await Promise.all(promises);
    }
    async #dispatchAction({ id, action }) {
        const source = this.#inputState.get(id);
        const keyState = this.#inputState.getGlobalKeyState();
        switch (action.type) {
            case protocol_js_1.Input.ActionType.KeyDown: {
                // SAFETY: The source is validated before.
                await this.#dispatchKeyDownAction(source, action);
                this.#inputState.cancelList.push({
                    id,
                    action: {
                        ...action,
                        type: protocol_js_1.Input.ActionType.KeyUp,
                    },
                });
                break;
            }
            case protocol_js_1.Input.ActionType.KeyUp: {
                // SAFETY: The source is validated before.
                await this.#dispatchKeyUpAction(source, action);
                break;
            }
            case protocol_js_1.Input.ActionType.Pause: {
                // TODO: Implement waiting on the input source.
                break;
            }
            case protocol_js_1.Input.ActionType.PointerDown: {
                // SAFETY: The source is validated before.
                await this.#dispatchPointerDownAction(source, keyState, action);
                this.#inputState.cancelList.push({
                    id,
                    action: {
                        ...action,
                        type: protocol_js_1.Input.ActionType.PointerUp,
                    },
                });
                break;
            }
            case protocol_js_1.Input.ActionType.PointerMove: {
                // SAFETY: The source is validated before.
                await this.#dispatchPointerMoveAction(source, keyState, action);
                break;
            }
            case protocol_js_1.Input.ActionType.PointerUp: {
                // SAFETY: The source is validated before.
                await this.#dispatchPointerUpAction(source, keyState, action);
                break;
            }
            case protocol_js_1.Input.ActionType.Scroll: {
                // SAFETY: The source is validated before.
                await this.#dispatchScrollAction(source, keyState, action);
                break;
            }
        }
    }
    #dispatchPointerDownAction(source, keyState, action) {
        const { button } = action;
        if (source.pressed.has(button)) {
            return;
        }
        source.pressed.add(button);
        const { x, y, subtype: pointerType } = source;
        const { width, height, pressure, twist, tangentialPressure } = action;
        const { tiltX, tiltY } = 'tiltX' in action ? action : {};
        // TODO: Implement azimuth/altitude angle.
        // --- Platform-specific code begins here ---
        const { modifiers } = keyState;
        switch (pointerType) {
            case protocol_js_1.Input.PointerType.Mouse:
            case protocol_js_1.Input.PointerType.Pen:
                source.setClickCount({ x, y, timeStamp: performance.now() });
                // TODO: Implement width and height when available.
                return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
                    type: 'mousePressed',
                    x,
                    y,
                    modifiers,
                    button: (() => {
                        switch (button) {
                            case 0:
                                return 'left';
                            case 1:
                                return 'middle';
                            case 2:
                                return 'right';
                            case 3:
                                return 'back';
                            case 4:
                                return 'forward';
                            default:
                                return 'none';
                        }
                    })(),
                    buttons: source.buttons,
                    clickCount: source.clickCount,
                    pointerType,
                    tangentialPressure,
                    tiltX,
                    tiltY,
                    twist,
                    force: pressure,
                });
            case protocol_js_1.Input.PointerType.Touch:
                return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
                    type: 'touchStart',
                    touchPoints: [
                        {
                            x,
                            y,
                            radiusX: width,
                            radiusY: height,
                            tangentialPressure,
                            tiltX,
                            tiltY,
                            twist,
                            force: pressure,
                            id: source.pointerId,
                        },
                    ],
                    modifiers,
                });
        }
        // --- Platform-specific code ends here ---
    }
    #dispatchPointerUpAction(source, keyState, action) {
        const { button } = action;
        if (!source.pressed.has(button)) {
            return;
        }
        source.pressed.delete(button);
        const { x, y, subtype: pointerType } = source;
        // --- Platform-specific code begins here ---
        const { modifiers } = keyState;
        switch (pointerType) {
            case protocol_js_1.Input.PointerType.Mouse:
            case protocol_js_1.Input.PointerType.Pen:
                // TODO: Implement width and height when available.
                return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
                    type: 'mouseReleased',
                    x,
                    y,
                    modifiers,
                    button: (() => {
                        switch (button) {
                            case 0:
                                return 'left';
                            case 1:
                                return 'middle';
                            case 2:
                                return 'right';
                            case 3:
                                return 'back';
                            case 4:
                                return 'forward';
                            default:
                                return 'none';
                        }
                    })(),
                    buttons: source.buttons,
                    clickCount: source.clickCount,
                    pointerType,
                });
            case protocol_js_1.Input.PointerType.Touch:
                return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
                    type: 'touchEnd',
                    touchPoints: [
                        {
                            x,
                            y,
                            id: source.pointerId,
                        },
                    ],
                    modifiers,
                });
        }
        // --- Platform-specific code ends here ---
    }
    async #dispatchPointerMoveAction(source, keyState, action) {
        const { x: startX, y: startY, subtype: pointerType } = source;
        const { width, height, pressure, twist, tangentialPressure, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
        const { tiltX, tiltY } = 'tiltX' in action ? action : {};
        // TODO: Implement azimuth/altitude angle.
        const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY);
        if (targetX < 0 || targetY < 0) {
            throw new protocol_js_1.Message.MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
        }
        let last;
        do {
            const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
            last = ratio >= 1;
            let x;
            let y;
            if (last) {
                x = targetX;
                y = targetY;
            }
            else {
                x = Math.round(ratio * (targetX - startX) + startX);
                y = Math.round(ratio * (targetY - startY) + startY);
            }
            if (source.x !== x || source.y !== y) {
                // --- Platform-specific code begins here ---
                const { modifiers } = keyState;
                switch (pointerType) {
                    case protocol_js_1.Input.PointerType.Mouse:
                    case protocol_js_1.Input.PointerType.Pen:
                        // TODO: Implement width and height when available.
                        await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
                            type: 'mouseMoved',
                            x,
                            y,
                            modifiers,
                            clickCount: 0,
                            buttons: source.buttons,
                            pointerType,
                            tangentialPressure,
                            tiltX,
                            tiltY,
                            twist,
                            force: pressure,
                        });
                        break;
                    case protocol_js_1.Input.PointerType.Touch:
                        await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
                            type: 'touchMove',
                            touchPoints: [
                                {
                                    x,
                                    y,
                                    radiusX: width,
                                    radiusY: height,
                                    tangentialPressure,
                                    tiltX,
                                    tiltY,
                                    twist,
                                    force: pressure,
                                    id: source.pointerId,
                                },
                            ],
                            modifiers,
                        });
                        break;
                }
                // --- Platform-specific code ends here ---
                source.x = x;
                source.y = y;
            }
        } while (!last);
    }
    async #getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY) {
        let targetX;
        let targetY;
        switch (origin) {
            case 'viewport':
                targetX = offsetX;
                targetY = offsetY;
                break;
            case 'pointer':
                targetX = startX + offsetX;
                targetY = startY + offsetY;
                break;
            default: {
                const { x: posX, y: posY } = await getElementCenter(this.#context, origin.element);
                // SAFETY: These can never be special numbers.
                targetX = posX + offsetX;
                targetY = posY + offsetY;
                break;
            }
        }
        return { targetX, targetY };
    }
    async #dispatchScrollAction(_source, keyState, action) {
        const { deltaX: targetDeltaX, deltaY: targetDeltaY, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
        if (origin === 'pointer') {
            throw new protocol_js_1.Message.InvalidArgumentException('"pointer" origin is invalid for scrolling.');
        }
        const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, 0, 0);
        if (targetX < 0 || targetY < 0) {
            throw new protocol_js_1.Message.MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
        }
        let currentDeltaX = 0;
        let currentDeltaY = 0;
        let last;
        do {
            const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
            last = ratio >= 1;
            let deltaX;
            let deltaY;
            if (last) {
                deltaX = targetDeltaX - currentDeltaX;
                deltaY = targetDeltaY - currentDeltaY;
            }
            else {
                deltaX = Math.round(ratio * targetDeltaX - currentDeltaX);
                deltaY = Math.round(ratio * targetDeltaY - currentDeltaY);
            }
            if (deltaX !== 0 || deltaY !== 0) {
                // --- Platform-specific code begins here ---
                const { modifiers } = keyState;
                await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
                    type: 'mouseWheel',
                    deltaX,
                    deltaY,
                    x: targetX,
                    y: targetY,
                    modifiers,
                });
                // --- Platform-specific code ends here ---
                currentDeltaX += deltaX;
                currentDeltaY += deltaY;
            }
        } while (!last);
    }
    #dispatchKeyDownAction(source, action) {
        const rawKey = action.value;
        const key = (0, keyUtils_js_1.getNormalizedKey)(rawKey);
        const repeat = source.pressed.has(key);
        const code = (0, keyUtils_js_1.getKeyCode)(rawKey);
        const location = (0, keyUtils_js_1.getKeyLocation)(rawKey);
        switch (key) {
            case 'Alt':
                source.alt = true;
                break;
            case 'Shift':
                source.shift = true;
                break;
            case 'Control':
                source.ctrl = true;
                break;
            case 'Meta':
                source.meta = true;
                break;
        }
        source.pressed.add(key);
        const { modifiers } = source;
        // --- Platform-specific code begins here ---
        // The spread is a little hack so JS gives us an array of unicode characters
        // to measure.
        const unmodifiedText = getKeyEventUnmodifiedText(key, source);
        const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
        let command;
        // The following commands need to be declared because Chromium doesn't
        // handle them. See
        // https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/blink/renderer/core/editing/editing_behavior.cc;l=169;drc=b8143cf1dfd24842890fcd831c4f5d909bef4fc4;bpv=0;bpt=1.
        if (this.#isMacOS && source.meta) {
            switch (code) {
                case 'KeyA':
                    command = 'SelectAll';
                    break;
                case 'KeyC':
                    command = 'Copy';
                    break;
                case 'KeyV':
                    command = source.shift ? 'PasteAndMatchStyle' : 'Paste';
                    break;
                case 'KeyX':
                    command = 'Cut';
                    break;
                case 'KeyZ':
                    command = source.shift ? 'Redo' : 'Undo';
                    break;
                default:
            }
        }
        return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
            type: text ? 'keyDown' : 'rawKeyDown',
            windowsVirtualKeyCode: USKeyboardLayout_js_1.KeyToKeyCode[key],
            key,
            code,
            text,
            unmodifiedText,
            autoRepeat: repeat,
            isSystemKey: source.alt || undefined,
            location: location < 3 ? location : undefined,
            isKeypad: location === 3,
            modifiers,
            commands: command ? [command] : undefined,
        });
        // --- Platform-specific code ends here ---
    }
    #dispatchKeyUpAction(source, action) {
        const rawKey = action.value;
        const key = (0, keyUtils_js_1.getNormalizedKey)(rawKey);
        if (!source.pressed.has(key)) {
            return;
        }
        const code = (0, keyUtils_js_1.getKeyCode)(rawKey);
        const location = (0, keyUtils_js_1.getKeyLocation)(rawKey);
        switch (key) {
            case 'Alt':
                source.alt = false;
                break;
            case 'Shift':
                source.shift = false;
                break;
            case 'Control':
                source.ctrl = false;
                break;
            case 'Meta':
                source.meta = false;
                break;
        }
        source.pressed.delete(key);
        const { modifiers } = source;
        // --- Platform-specific code begins here ---
        // The spread is a little hack so JS gives us an array of unicode characters
        // to measure.
        const unmodifiedText = getKeyEventUnmodifiedText(key, source);
        const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
        return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
            type: 'keyUp',
            windowsVirtualKeyCode: USKeyboardLayout_js_1.KeyToKeyCode[key],
            key,
            code,
            text,
            unmodifiedText,
            location: location < 3 ? location : undefined,
            isSystemKey: source.alt || undefined,
            isKeypad: location === 3,
            modifiers,
        });
        // --- Platform-specific code ends here ---
    }
}
exports.ActionDispatcher = ActionDispatcher;
const getKeyEventUnmodifiedText = (key, source) => {
    if (key === 'Enter') {
        return '\r';
    }
    return [...key].length === 1
        ? source.shift
            ? key.toLocaleUpperCase('en-US')
            : key
        : undefined;
};
const getKeyEventText = (code, source) => {
    if (source.ctrl) {
        switch (code) {
            case 'Digit2':
                if (source.shift) {
                    return '\x00';
                }
                break;
            case 'KeyA':
                return '\x01';
            case 'KeyB':
                return '\x02';
            case 'KeyC':
                return '\x03';
            case 'KeyD':
                return '\x04';
            case 'KeyE':
                return '\x05';
            case 'KeyF':
                return '\x06';
            case 'KeyG':
                return '\x07';
            case 'KeyH':
                return '\x08';
            case 'KeyI':
                return '\x09';
            case 'KeyJ':
                return '\x0A';
            case 'KeyK':
                return '\x0B';
            case 'KeyL':
                return '\x0C';
            case 'KeyM':
                return '\x0D';
            case 'KeyN':
                return '\x0E';
            case 'KeyO':
                return '\x0F';
            case 'KeyP':
                return '\x10';
            case 'KeyQ':
                return '\x11';
            case 'KeyR':
                return '\x12';
            case 'KeyS':
                return '\x13';
            case 'KeyT':
                return '\x14';
            case 'KeyU':
                return '\x15';
            case 'KeyV':
                return '\x16';
            case 'KeyW':
                return '\x17';
            case 'KeyX':
                return '\x18';
            case 'KeyY':
                return '\x19';
            case 'KeyZ':
                return '\x1A';
            case 'BracketLeft':
                return '\x1B';
            case 'Backslash':
                return '\x1C';
            case 'BracketRight':
                return '\x1D';
            case 'Digit6':
                if (source.shift) {
                    return '\x1E';
                }
                break;
            case 'Minus':
                return '\x1F';
        }
        return '';
    }
    if (source.alt) {
        return '';
    }
    return;
};
//# sourceMappingURL=ActionDispatcher.js.map