import * as types from '../../../generic/simulator/Types'
import {ServerEndPoint} from "../../channel/websocket/ServerEndPoint.js";
import {Envelop, Message} from "../../../generic/channel/Types.js";
import {SimulatorCommand} from "../../../generic/simulator/Commands.js";
import * as commands from "../../../generic/simulator/Commands.js";
import {filterPathAccordingToPrecision, getPathBetweenPoints, Point} from "../../../generic/util/Point.js";
import {delay} from "../../../generic/util/helper/Delay.js";


export class SimulatorServerPuppeteer extends ServerEndPoint {
    page                : any

    mouseMovePrecision  : number = 3

    // see https://github.com/octalmage/robotjs/issues/347
    afterActionDelay    : number = 100

    currentPosition     : Point = [ 0, 0 ]


    doProcessRawChannelMessage (message : Message, envelop : Envelop) {
    }


    doDispatchEnvelop (envelop : Envelop) {
        const cmd           = envelop.payload as SimulatorCommand

        let action

        switch (cmd.type) {
            case 'move_pointer'         : action = this.doMovePointer(<commands.MovePointer>cmd); break
            case 'set_pointer_state'    : action = this.doSetPointerState(<commands.SetPointerState>cmd); break
            case 'pointer_click'        : action = this.doPointerClick(<commands.PointerClick>cmd); break
            case 'pointer_double_click' : action = this.doPointerDoubleClick(<commands.PointerDoubleClick>cmd); break
            case 'mouse_wheel'          : action = this.doMouseWheel(<commands.MouseWheel>cmd); break
            case 'pointer_drag'         : action = this.doPointerDrag(<commands.PointerDrag>cmd); break
            case 'type'                 : action = this.doType(<commands.Type>cmd); break
            case 'reset'                : action = this.doSimulationReset(<commands.Reset>cmd); break
            case 'set_key_state'        : action = this.doSetKeyState(<commands.SetKeyState>cmd); break
        }

        action.then(result => this.replyWith(envelop, result), reason => {
            this.info("Simulator command failed: " + reason)

            this.replyWith(envelop, reason, true)
        })
    }


    pageMouseMove (x, y, options = undefined) : Promise<any> {
        return this.page.mouse.move(x, y, options).then(() => {
            this.currentPosition[ 0 ] = x
            this.currentPosition[ 1 ] = y
        })
    }


    siestaKeyToSimulatorKey (key : string) : string {
        if (key.length == 1) return key

        return siestaToPuppeteerKeys[ key ]
    }


    toggleModifierKeys (modifierKeys : types.ModifierKey[] = [], state : types.KeyState) : Promise<any> {
        let cont    = Promise.resolve()

        modifierKeys.forEach(modKey => {
            cont    = cont.then(() => this.page.keyboard[ state ](this.siestaKeyToSimulatorKey(modKey)))
        })

        return cont
    }


    doWithModifierKeys (action : Function, modifierKeys : types.ModifierKey[] = []) : Promise<any> {
        return this.toggleModifierKeys(modifierKeys, types.KeyState.Down).then(
            () => action()
        ).then(
            () => this.toggleModifierKeys(modifierKeys, types.KeyState.Up)
        )
    }


    doMovePointer (command : commands.MovePointer) : Promise<any> {
        return this.doWithModifierKeys(() => {
            if (command.moveKind == 'instant')
                return this.page.mouse.move(command.x, command.y)
            else if (command.moveKind == 'smooth') {
                return this.moveMouseTo(command.x, command.y, command.mouseMovePrecision)
            }
        }, command.modifierKey)
    }


    doSetPointerState (command : commands.SetPointerState) : Promise<any> {
        return this.doWithModifierKeys(
            () => this.page.mouse[ command.state ]({ button : command.modifier }),
            command.modifierKey
        ).then(
            () => delay(this.afterActionDelay)
        )
    }


    doPointerClick (command : commands.PointerClick) : Promise<any> {
        const mouse     = this.page.mouse

        return this.doWithModifierKeys(
            () => {
                return mouse.down({ button : command.modifier, clickCount : 1 }).then(
                    () => mouse.up({ button : command.modifier, clickCount : 1 })
                )
            },
            command.modifierKey
        ).then(
            () => delay(this.afterActionDelay)
        )
    }


    doPointerDoubleClick (command : commands.PointerDoubleClick) : Promise<any> {
        const mouse     = this.page.mouse

        return this.doWithModifierKeys(
            () => {
                return mouse.down({ button : command.modifier, clickCount : 1 }).then(
                    () => mouse.up({ button : command.modifier, clickCount : 1 })
                ).then(
                    () => mouse.down({ button : command.modifier, clickCount : 2 })
                ).then(
                    () => mouse.up({ button : command.modifier, clickCount : 2 })
                )
            },
            command.modifierKey
        ).then(
            () => delay(this.afterActionDelay)
        )
    }


    doSimulationReset (command : commands.Reset) : Promise<any> {
        let cont    = Promise.resolve()

        Object.keys(siestaToPuppeteerKeys).forEach(siestaKey => {
            cont    = cont.then(
                () => this.page.keyboard.up(this.siestaKeyToSimulatorKey(siestaKey))
            )
        })

        Object.keys(types.PointerModifier).forEach(button => {
            if (isNaN(Number(button))) {
                cont    = cont.then(
                    () => this.page.mouse.up({ button : types.PointerModifier[ button ] })
                )
            }
        })

        cont        = cont.then(
            () => this.pageMouseMove(0, 0)
        ).then(
            () => delay(this.afterActionDelay)
        )

        return cont
    }


    doMouseWheel (command : commands.MouseWheel) : Promise<any> {
        return this.doWithModifierKeys(
            () => {
            },
            command.modifierKey
        ).then(
            () => delay(this.afterActionDelay)
        )
    }


    doPointerDrag (command : commands.PointerDrag) : Promise<any> {
        const mouse     = this.page.mouse

        return this.doWithModifierKeys(
            () => {
                return this.moveMouseTo(command.fromX, command.fromY).then(
                    () => mouse.down({ button : command.modifier })
                ).then(
                    () => this.moveMouseTo(command.toX, command.toY, 1)
                ).then(
                    () => command.dragOnly ? Promise.resolve() : mouse.up({ button : command.modifier })
                )
            },
            command.modifierKey
        ).then(
            () => delay(this.afterActionDelay)
        )
    }


    doSetKeyState (command : commands.SetKeyState) : Promise<any> {
        const keyboard  = this.page.keyboard

        return keyboard[ command.state ](this.siestaKeyToSimulatorKey(command.key))
    }


    doType (command : commands.Type) : Promise<any> {
        const text      = command.text

        let cont        = Promise.resolve()

        for (let i = 0; i < text.length; i++) {
            cont        = cont.then(() => this.typeSingleChar(text[ i ], command.modifierKey))
        }

        return cont.then(() => delay(this.afterActionDelay))
    }


    typeSingleChar (key : string, modifierKey : types.ModifierKey[] = []) : Promise<any> {
        const keyboard  = this.page.keyboard

        if (key.length > 1)
            return this.doWithModifierKeys(
                () => keyboard.press(this.siestaKeyToSimulatorKey(key)),
                modifierKey
            )
        else {
            // seems no special processing is needed for Puppeteer, see analogous RobotJS method however
            return this.doWithModifierKeys(
                () => keyboard.press(this.siestaKeyToSimulatorKey(key)),
                modifierKey
            )
        }
    }


    moveMouseTo (x : number, y : number, mouseMovePrecision : number = null) : Promise<any> {
        var path        = getPathBetweenPoints(this.currentPosition, [ x, y ])

        path            = filterPathAccordingToPrecision(path, mouseMovePrecision || this.mouseMovePrecision)

        let cont        = Promise.resolve()

        path.forEach(step => {
            cont        = cont.then(() => this.pageMouseMove(step[ 0 ], step[ 1 ]))
        })

        return cont
    }
}

const siestaToPuppeteerKeys = {
    'BACKSPACE'     : 'Backspace',

    'TAB'           : 'Tab',

    'RETURN'        : 'Enter',
    'ENTER'         : 'Enter',

    //special
    'SHIFT'         : 'Shift',
    'CTRL'          : 'Control',
    'ALT'           : 'Alt',
    'CMD'           : 'Meta', // Mac

    // //weird
    'PAUSE-BREAK'   : 'Pause',
    'CAPS'          : 'CapsLock',

    'ESCAPE'        : 'Escape',
    'ESC'           : 'Escape',
    'NUM-LOCK'      : 'NumLock',
    'SCROLL-LOCK'   : 'ScrollLock',
    'PRINT'         : 'Print',

    //navigation
    'PAGE-UP'       : 'PageUp',
    'PAGE-DOWN'     : 'PageDown',
    'END'           : 'End',
    'HOME'          : 'Home',
    'LEFT'          : 'ArrowLeft',
    'ARROWLEFT'     : 'ArrowLeft',
    'UP'            : 'ArrowUp',
    'ARROWUP'       : 'ArrowUp',
    'RIGHT'         : 'ArrowRight',
    'ARROWRIGHT'    : 'ArrowRight',
    'DOWN'          : 'ArrowDown',
    'ARROWDOWN'     : 'ArrowDown',
    'INSERT'        : 'Insert',
    'DELETE'        : 'Delete',

    //NORMAL-CHARACTERS, NUMPAD
    'NUM0'          : 'Numpad0',
    'NUM1'          : 'Numpad1',
    'NUM2'          : 'Numpad2',
    'NUM3'          : 'Numpad3',
    'NUM4'          : 'Numpad4',
    'NUM5'          : 'Numpad5',
    'NUM6'          : 'Numpad6',
    'NUM7'          : 'Numpad7',
    'NUM8'          : 'Numpad8',
    'NUM9'          : 'Numpad9',

    'F1'            : 'F1',
    'F2'            : 'F2',
    'F3'            : 'F3',
    'F4'            : 'F4',
    'F5'            : 'F5',
    'F6'            : 'F6',
    'F7'            : 'F7',
    'F8'            : 'F8',
    'F9'            : 'F9',
    'F10'           : 'F10',
    'F11'           : 'F11',
    'F12'           : 'F12'
}

export const SimulatorServer = SimulatorServerPuppeteer
