#!/usr/bin/env python3
#
# DOOM control application
#
# Copyright (C) 2021 Sylvain Munaut
# SPDX-License-Identifier: MIT
#

import base64
import pygame
import serial
import struct
import sys

from io import BytesIO

# Add local WASD binding ...
if True:
    # Tweaked ...
    KEYBIND = {
        pygame.K_w: pygame.K_UP,        # Forward
        pygame.K_a: pygame.K_COMMA,     # Strafe left
        pygame.K_s: pygame.K_DOWN,      # Back
        pygame.K_d: pygame.K_PERIOD,    # Strafe right
        pygame.K_e: pygame.K_SPACE,     # Use
    }
    DISABLE_MOUSE_Y = True
else:
    # Original controls
    KEYBIND = {}
    DISABLE_MOUSE_Y = False


def map_key(k):

    KEYVAL = {
        'KEY_LEFTARROW':     0,
        'KEY_RIGHTARROW':    1,
        'KEY_DOWNARROW':     2,
        'KEY_UPARROW':       3,
        'KEY_RSHIFT':        4,
        'KEY_RCTRL':         5,
        'KEY_RALT':          6,
        'KEY_ESCAPE':        7,
        'KEY_ENTER':         8,
        'KEY_TAB':           9,
        'KEY_BACKSPACE':    10,
        'KEY_PAUSE':        11,
        'KEY_EQUALS':       12,
        'KEY_MINUS':        13,
        'KEY_F1':           14,
        'KEY_F2':           15,
        'KEY_F3':           16,
        'KEY_F4':           17,
        'KEY_F5':           18,
        'KEY_F6':           19,
        'KEY_F7':           20,
        'KEY_F8':           21,
        'KEY_F9':           22,
        'KEY_F10':          23,
        'KEY_F11':          24,
        'KEY_F12':          25,
    }

    KEYMAP = {
        pygame.K_LEFT:      'KEY_LEFTARROW',
        pygame.K_RIGHT:     'KEY_RIGHTARROW',
        pygame.K_DOWN:      'KEY_DOWNARROW',
        pygame.K_UP:        'KEY_UPARROW',
        pygame.K_RSHIFT:    'KEY_RSHIFT',
        pygame.K_LSHIFT:    'KEY_RSHIFT',
        pygame.K_RCTRL:     'KEY_RCTRL',
        pygame.K_LCTRL:     'KEY_RCTRL',
        pygame.K_RALT:      'KEY_RALT',
        pygame.K_LALT:      'KEY_RALT',
        pygame.K_ESCAPE:    'KEY_ESCAPE',
        pygame.K_RETURN:    'KEY_ENTER',
        pygame.K_TAB:       'KEY_TAB',
        pygame.K_BACKSPACE: 'KEY_BACKSPACE',
        pygame.K_PAUSE:     'KEY_PAUSE',
        pygame.K_EQUALS:    'KEY_EQUALS',
        pygame.K_MINUS:     'KEY_MINUS',
        pygame.K_F1:        'KEY_F1',
        pygame.K_F2:        'KEY_F2',
        pygame.K_F3:        'KEY_F3',
        pygame.K_F4:        'KEY_F4',
        pygame.K_F5:        'KEY_F5',
        pygame.K_F6:        'KEY_F6',
        pygame.K_F7:        'KEY_F7',
        pygame.K_F8:        'KEY_F8',
        pygame.K_F9:        'KEY_F9',
        pygame.K_F10:       'KEY_F10',
        pygame.K_F11:       'KEY_F11',
        pygame.K_F12:       'KEY_F12',
    }

    # Default is no mapping
    rc = None

    # Check for local keybindings
    if k in KEYBIND:
        k = KEYBIND[k]

    # Check for spcial mapping
    if k in KEYMAP:
        rc = KEYVAL[KEYMAP[k]]

    # Map upper case to lowercase
    elif ord('A') <= k <= ord('Z'):
        rc = k - ord('A') + ord('a')

    # Default is to map 1:1 for anything in printable ascii range
    elif 32 <= k <= 127:
        rc = k

    return rc


def main(argv0, dev='/dev/ttyUSB6'):

    # Init PyGame
    pygame.init()

    # Load logo
    logo = pygame.image.load(
        BytesIO(base64.decodebytes(LOGO_DATA)),
        'logo.jpg'
    )

    # Init screen
    screen = pygame.display.set_mode([logo.get_width(), logo.get_height()])
    pygame.display.set_caption("Doom controller")

    pygame.time.set_timer(pygame.USEREVENT, 50)

    # Open port
    port = serial.Serial(dev, baudrate=1000000)

    # Main loop
    running = True
    held = set()

    mouse_active = False
    mdx = 0
    mdy = 0

    while running:
        # Process event
        pygame.event.pump()
        event = pygame.event.wait()

        if event.type == pygame.QUIT:
            running = False

        elif event.type == pygame.VIDEORESIZE:
            screen.blit(pygame.transform.scale(logo, event.dict['size']), (0, 0))
            pygame.display.update()

        elif event.type == pygame.VIDEOEXPOSE:
            screen.blit(pygame.transform.scale(logo, screen.get_size()), (0, 0))
            pygame.display.update()

        elif event.type == pygame.KEYDOWN:
            kv = map_key(event.key)
            if (kv is not None) and (kv not in held):
                held.add(kv)
                cmd = (kv | 0x80).to_bytes(1, 'little')
                print("Kendown")
                port.write(cmd)

        elif event.type == pygame.KEYUP:
            if mouse_active and (event.key in (pygame.K_LMETA, pygame.K_RMETA)):
                # Release mouse
                pygame.mouse.set_visible(True)
                pygame.event.set_grab(False)

                # Release all pressed buttons
                for kv in [28, 29, 30]:
                    if kv in held:
                        held.remove(kv)
                        cmd = kv.to_bytes(1, 'little')
                        port.write(cmd)

                # Not active
                mouse_active = False

            kv = map_key(event.key)
            if (kv is not None) and (kv in held):
                held.remove(kv)
                cmd = kv.to_bytes(1, 'little')
                port.write(cmd)

        elif event.type == pygame.MOUSEBUTTONDOWN:
            if mouse_active:
                # We have the mouse, send events
                kv = (28 + event.button - 1) if (1 <= event.button <= 3) else None
                if (kv is not None) and (kv not in held):
                    held.add(kv)
                    cmd = (kv | 0x80).to_bytes(1, 'little')
                    port.write(cmd)

            elif event.button == 1:
                # If button 1, grab mouse
                pygame.mouse.set_visible(False)
                pygame.event.set_grab(True)

                # Active
                mouse_active = True

        elif event.type == pygame.MOUSEBUTTONUP:
            if mouse_active:
                # We have the mouse, send events
                kv = (28 + event.button - 1) if (1 <= event.button <= 3) else None
                if (kv is not None) and (kv in held):
                    held.remove(kv)
                    cmd = kv.to_bytes(1, 'little')
                    port.write(cmd)

        elif event.type == pygame.MOUSEMOTION:
            if mouse_active:
                # We accumulate mouse events to not overwhelm
                # target ...
                mdx += event.rel[0]
                mdy += event.rel[1]

        elif event.type == pygame.USEREVENT:
            # Hack
            if DISABLE_MOUSE_Y:
                mdy = 0

            # Send pending mouse events
            if (mdx != 0) or (mdy != 0):

                # Send what we can
                x = max(min(mdx, 127), -128)
                y = max(min(mdy, 127), -128)

                mdx -= x
                mdy -= y

                # Send it
                port.write(struct.pack('bbb', 31, x, y))


LOGO_DATA = b"""
[encoded data, 24255 characters]
5/ya3sy1/o6NeEyFkLe3f7CKqXi17/yi7MfQtBiWlwS/v//Z
"""

if __name__ == '__main__':
    sys.exit(main(*sys.argv))
