// Copyright 2012-2022 David Robillard <d@drobilla.net>
// Copyright 2021-2025 Filipe Coelho <falktx@falktx.com>
// SPDX-License-Identifier: ISC

#include "wasm.h"

#include "../pugl-upstream/src/internal.h"
#include "../pugl-upstream/src/platform.h"

#include <stdio.h>

#include <emscripten/html5.h>

#ifdef __cplusplus
#  define PUGL_INIT_STRUCT \
    {}
#else
#  define PUGL_INIT_STRUCT \
    {                      \
      0                    \
    }
#endif

#ifdef __MOD_DEVICES__
#  define MOD_SCALE_FACTOR_MULT 1
#endif

// #define PUGL_WASM_AUTO_POINTER_LOCK
// #define PUGL_WASM_NO_KEYBOARD_INPUT
// #define PUGL_WASM_NO_MOUSEWHEEL_INPUT

PuglWorldInternals*
puglInitWorldInternals(const PuglWorldType type, const PuglWorldFlags flags)
{
  PuglWorldInternals* impl =
    (PuglWorldInternals*)calloc(1, sizeof(PuglWorldInternals));

  impl->scaleFactor = emscripten_get_device_pixel_ratio();
#ifdef __MOD_DEVICES__
  impl->scaleFactor *= MOD_SCALE_FACTOR_MULT;
#endif

  printf("DONE: %s %d | -> %f\n", __func__, __LINE__, impl->scaleFactor);

  return impl;
}

void*
puglGetNativeWorld(PuglWorld*)
{
  printf("DONE: %s %d\n", __func__, __LINE__);
  return NULL;
}

PuglInternals*
puglInitViewInternals(PuglWorld* const world)
{
  printf("DONE: %s %d\n", __func__, __LINE__);
  PuglInternals* impl = (PuglInternals*)calloc(1, sizeof(PuglInternals));

  impl->buttonPressTimeout = -1;
  impl->supportsTouch = PUGL_DONT_CARE; // not yet known

#ifdef PUGL_WASM_ASYNC_CLIPBOARD
  impl->supportsClipboardRead = (PuglViewHintValue)EM_ASM_INT({
    if (typeof(navigator.clipboard) !== 'undefined' && typeof(navigator.clipboard.readText) === 'function' && window.isSecureContext) {
      return 1; // PUGL_TRUE
    }
    return 0; // PUGL_FALSE
  });

  impl->supportsClipboardWrite = (PuglViewHintValue)EM_ASM_INT({
    if (typeof(navigator.clipboard) !== 'undefined' && typeof(navigator.clipboard.writeText) === 'function' && window.isSecureContext) {
      return 1; // PUGL_TRUE
    }
    if (typeof(document.queryCommandSupported) !== 'undefined' && document.queryCommandSupported("copy")) {
      return 1; // PUGL_TRUE
    }
    return 0; // PUGL_FALSE
  });
#endif

  return impl;
}

PuglStatus
puglApplySizeHint(PuglView* const view, const PuglSizeHint PUGL_UNUSED(hint))
{
  // No fine-grained updates, hints are always recalculated together
  return puglUpdateSizeHints(view);
}

PuglStatus
puglUpdateSizeHints(PuglView* const view)
{
  const char* const className = view->world->strings[PUGL_CLASS_NAME];

  if (!view->hints[PUGL_RESIZABLE]) {
    PuglArea size = puglGetSizeHint(view, PUGL_CURRENT_SIZE);
    if (!puglIsValidSize(size.width, size.height)) {
      size = puglGetSizeHint(view, PUGL_DEFAULT_SIZE);
    }
    EM_ASM({
      var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
      var width = parseInt($1 / window.devicePixelRatio);
      var height = parseInt($2 / window.devicePixelRatio);
      canvasWrapper.style.setProperty("min-width", width + 'px');
      canvasWrapper.style.setProperty("max-width", width + 'px');
      canvasWrapper.style.setProperty("min-height", height + 'px');
      canvasWrapper.style.setProperty("max-height", height + 'px');
    }, className, size.width, size.height);
  } else {
    const PuglArea minSize = view->sizeHints[PUGL_MIN_SIZE];
    if (puglIsValidArea(minSize)) {
      EM_ASM({
        var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
        canvasWrapper.style.setProperty("min-width", parseInt($1 / window.devicePixelRatio) + 'px');
        canvasWrapper.style.setProperty("min-height", parseInt($2 / window.devicePixelRatio) + 'px');
      }, className, minSize.width, minSize.height);
    } else {
      EM_ASM({
        var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
        canvasWrapper.style.removeProperty("min-width");
        canvasWrapper.style.removeProperty("min-height");
      }, className);
    }

    const PuglArea maxSize = view->sizeHints[PUGL_MAX_SIZE];
    if (puglIsValidArea(maxSize)) {
      EM_ASM({
        var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
        canvasWrapper.style.setProperty("max-width", parseInt($1 / window.devicePixelRatio) + 'px');
        canvasWrapper.style.setProperty("max-height", parseInt($2 / window.devicePixelRatio) + 'px');
      }, className, maxSize.width, maxSize.height);
    } else {
      EM_ASM({
        var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
        canvasWrapper.style.removeProperty("max-width");
        canvasWrapper.style.removeProperty("max-height");
      }, className);
    }

    /* TODO
     const PuglArea minAspect = view->sizeHints[PUGL_MIN_ASPECT];
     const PuglArea maxAspect = view->sizeHints[PUGL_MAX_ASPECT];
     const PuglArea fixedAspect = view->sizeHints[PUGL_FIXED_ASPECT];
     */
  }

  return PUGL_SUCCESS;
}

static PuglStatus
puglDispatchEventWithContext(PuglView* const view, const PuglEvent* event)
{
  PuglStatus st0 = PUGL_SUCCESS;
  PuglStatus st1 = PUGL_SUCCESS;

  if (!(st0 = view->backend->enter(view, NULL))) {
    st0 = view->eventFunc(view, event);
    st1 = view->backend->leave(view, NULL);
  }

  return st0 ? st0 : st1;
}

static PuglMods
translateModifiers(const EM_BOOL ctrlKey,
                   const EM_BOOL shiftKey,
                   const EM_BOOL altKey,
                   const EM_BOOL metaKey)
{
  return (ctrlKey  ? PUGL_MOD_CTRL  : 0u) |
         (shiftKey ? PUGL_MOD_SHIFT : 0u) |
         (altKey   ? PUGL_MOD_ALT   : 0u) |
         (metaKey  ? PUGL_MOD_SUPER : 0u);
}

#ifndef PUGL_WASM_NO_KEYBOARD_INPUT
static PuglKey
keyCodeToSpecial(const unsigned long code, const unsigned long location)
{
  switch (code) {
  case 0x08: return PUGL_KEY_BACKSPACE;
  case 0x1B: return PUGL_KEY_ESCAPE;
  case 0x2E: return PUGL_KEY_DELETE;
  case 0x70: return PUGL_KEY_F1;
  case 0x71: return PUGL_KEY_F2;
  case 0x72: return PUGL_KEY_F3;
  case 0x73: return PUGL_KEY_F4;
  case 0x74: return PUGL_KEY_F5;
  case 0x75: return PUGL_KEY_F6;
  case 0x76: return PUGL_KEY_F7;
  case 0x77: return PUGL_KEY_F8;
  case 0x78: return PUGL_KEY_F9;
  case 0x79: return PUGL_KEY_F10;
  case 0x7A: return PUGL_KEY_F11;
  case 0x7B: return PUGL_KEY_F12;
  case 0x25: return PUGL_KEY_LEFT;
  case 0x26: return PUGL_KEY_UP;
  case 0x27: return PUGL_KEY_RIGHT;
  case 0x28: return PUGL_KEY_DOWN;
  case 0x21: return PUGL_KEY_PAGE_UP;
  case 0x22: return PUGL_KEY_PAGE_DOWN;
  case 0x24: return PUGL_KEY_HOME;
  case 0x23: return PUGL_KEY_END;
  case 0x2D: return PUGL_KEY_INSERT;
  case 0x10: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_SHIFT_R : PUGL_KEY_SHIFT_L;
  case 0x11: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_CTRL_R : PUGL_KEY_CTRL_L;
  case 0x12: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_ALT_R : PUGL_KEY_ALT_L;
  case 0xE0: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_SUPER_R : PUGL_KEY_SUPER_L;
  case 0x5D: return PUGL_KEY_MENU;
  case 0x14: return PUGL_KEY_CAPS_LOCK;
  case 0x91: return PUGL_KEY_SCROLL_LOCK;
  case 0x90: return PUGL_KEY_NUM_LOCK;
  case 0x2C: return PUGL_KEY_PRINT_SCREEN;
  case 0x13: return PUGL_KEY_PAUSE;
  case '\r': return (PuglKey)'\r';
  default: break;
  }

  return (PuglKey)0;
}

static bool
decodeCharacterString(const unsigned long keyCode,
                      const EM_UTF8 key[EM_HTML5_SHORT_STRING_LEN_BYTES],
                      char str[8])
{
  if (key[1] == 0)
  {
    str[0] = key[0];
    return true;
  }

  return false;
}

static EM_BOOL
puglKeyCallback(const int eventType, const EmscriptenKeyboardEvent* const keyEvent, void* const userData)
{
  PuglView* const view = (PuglView*)userData;

  if (!view->impl->visible) {
    return EM_FALSE;
  }

  if (keyEvent->repeat && view->hints[PUGL_IGNORE_KEY_REPEAT])
    return EM_TRUE;

  PuglStatus st0 = PUGL_SUCCESS;
  PuglStatus st1 = PUGL_SUCCESS;

  const uint state = translateModifiers(keyEvent->ctrlKey,
                                        keyEvent->shiftKey,
                                        keyEvent->altKey,
                                        keyEvent->metaKey);

  const PuglKey special = keyCodeToSpecial(keyEvent->keyCode, keyEvent->location);

  uint key = keyEvent->key[0] >= ' ' && keyEvent->key[0] <= '~' && keyEvent->key[1] == '\0'
           ? keyEvent->key[0]
           : keyEvent->keyCode;

  if (key >= 'A' && key <= 'Z' && !keyEvent->shiftKey)
      key += 'a' - 'A';

  PuglEvent event = {{PUGL_NOTHING, 0}};
  event.key.type = eventType == EMSCRIPTEN_EVENT_KEYDOWN ? PUGL_KEY_PRESS : PUGL_KEY_RELEASE;
  event.key.time = keyEvent->timestamp / 1e3;
  // event.key.x     = xevent.xkey.x;
  // event.key.y     = xevent.xkey.y;
  // event.key.xRoot = xevent.xkey.x_root;
  // event.key.yRoot = xevent.xkey.y_root;
  event.key.key     = special ? special : key;
  event.key.keycode = keyEvent->keyCode;
  event.key.state   = state;
  st0 = puglDispatchEventWithContext(view, &event);

  d_debug("key event \n"
           "\tdown:     %d\n"
           "\trepeat:   %d\n"
           "\tlocation: %d\n"
           "\tstate:    0x%x\n"
           "\tkey[]:    '%s'\n"
           "\tcode[]:   '%s'\n"
           "\tlocale[]: '%s'\n"
           "\tkeyCode:  0x%lx:'%c' [deprecated, use key]\n"
           "\twhich:    0x%lx:'%c' [deprecated, use key, same as keycode?]\n"
           "\tspecial:  0x%x",
           eventType == EMSCRIPTEN_EVENT_KEYDOWN,
           keyEvent->repeat,
           keyEvent->location,
           state,
           keyEvent->key,
           keyEvent->code,
           keyEvent->locale,
           keyEvent->keyCode, keyEvent->keyCode >= ' ' && keyEvent->keyCode <= '~' ? keyEvent->keyCode : 0,
           keyEvent->which, keyEvent->which >= ' ' && keyEvent->which <= '~' ? keyEvent->which : 0,
           special);

  if (event.type == PUGL_KEY_PRESS && !special && !(keyEvent->ctrlKey|keyEvent->altKey|keyEvent->metaKey)) {
    char str[8] = PUGL_INIT_STRUCT;

    if (decodeCharacterString(keyEvent->keyCode, keyEvent->key, str)) {
      d_debug("resulting string is '%s'", str);

      event.text.type      = PUGL_TEXT;
      event.text.character = event.key.key;
      memcpy(event.text.string, str, sizeof(event.text.string));
      st1 = puglDispatchEventWithContext(view, &event);
    }
  }

  return (st0 ? st0 : st1) == PUGL_SUCCESS ? EM_TRUE : EM_FALSE;
}
#endif

static EM_BOOL
puglMouseCallback(const int eventType, const EmscriptenMouseEvent* const mouseEvent, void* const userData)
{
  PuglView* const view = (PuglView*)userData;

  if (!view->impl->visible) {
    return EM_FALSE;
  }

  PuglEvent event = {{PUGL_NOTHING, 0}};

  const double   time  = mouseEvent->timestamp / 1e3;
  const PuglMods state = translateModifiers(mouseEvent->ctrlKey,
                                            mouseEvent->shiftKey,
                                            mouseEvent->altKey,
                                            mouseEvent->metaKey);

  double scaleFactor = view->world->impl->scaleFactor;
#ifdef __MOD_DEVICES__
  if (!view->impl->isFullscreen) {
    scaleFactor /= EM_ASM_DOUBLE({
      return parseFloat(
        RegExp('^scale\\\((.*)\\\)$')
        .exec(document.getElementById("pedalboard-dashboard").style.transform)[1]
      );
    }) * MOD_SCALE_FACTOR_MULT;
  }
#endif

  // workaround missing pointer lock callback, see https://github.com/emscripten-core/emscripten/issues/9681
  EmscriptenPointerlockChangeEvent e;
  if (emscripten_get_pointerlock_status(&e) == EMSCRIPTEN_RESULT_SUCCESS)
      view->impl->pointerLocked = e.isActive;

#ifdef __MOD_DEVICES__
  const long canvasX = mouseEvent->canvasX;
  const long canvasY = mouseEvent->canvasY;
#else
  const char* const className = view->world->strings[PUGL_CLASS_NAME];
  const double canvasX = mouseEvent->clientX - EM_ASM_DOUBLE({
    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
    return canvasWrapper.getBoundingClientRect().x;
  }, className);
  const double canvasY = mouseEvent->clientY - EM_ASM_DOUBLE({
    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
    return canvasWrapper.getBoundingClientRect().y;
  }, className);
#endif

  switch (eventType) {
  case EMSCRIPTEN_EVENT_MOUSEDOWN:
  case EMSCRIPTEN_EVENT_MOUSEUP:
    event.button.type  = eventType == EMSCRIPTEN_EVENT_MOUSEDOWN ? PUGL_BUTTON_PRESS : PUGL_BUTTON_RELEASE;
    event.button.time  = time;
    event.button.x     = canvasX * scaleFactor;
    event.button.y     = canvasY * scaleFactor;
    event.button.xRoot = mouseEvent->screenX * scaleFactor;
    event.button.yRoot = mouseEvent->screenY * scaleFactor;
    event.button.state = state;
    switch (mouseEvent->button) {
    case 1:
      event.button.button = 2;
      break;
    case 2:
      event.button.button = 1;
      break;
    default:
      event.button.button = mouseEvent->button;
      break;
    }
    break;
  case EMSCRIPTEN_EVENT_MOUSEMOVE:
    event.motion.type = PUGL_MOTION;
    event.motion.time = time;
    if (view->impl->pointerLocked) {
      // adjust local values for delta
      const double movementX = mouseEvent->movementX * scaleFactor;
      const double movementY = mouseEvent->movementY * scaleFactor;
      view->impl->lastMotion.x += movementX;
      view->impl->lastMotion.y += movementY;
      view->impl->lastMotion.xRoot += movementX;
      view->impl->lastMotion.yRoot += movementY;
      // now set x, y, xRoot and yRoot
      event.motion.x = view->impl->lastMotion.x;
      event.motion.y = view->impl->lastMotion.y;
      event.motion.xRoot = view->impl->lastMotion.xRoot;
      event.motion.yRoot = view->impl->lastMotion.yRoot;
    } else {
      // cache values for possible pointer lock movement later
      view->impl->lastMotion.x = event.motion.x = canvasX * scaleFactor;
      view->impl->lastMotion.y = event.motion.y = canvasY * scaleFactor;
      view->impl->lastMotion.xRoot = event.motion.xRoot = mouseEvent->screenX * scaleFactor;
      view->impl->lastMotion.yRoot = event.motion.yRoot = mouseEvent->screenY * scaleFactor;
    }
    event.motion.state = state;
    break;
  case EMSCRIPTEN_EVENT_MOUSEENTER:
  case EMSCRIPTEN_EVENT_MOUSELEAVE:
    event.crossing.type  = eventType == EMSCRIPTEN_EVENT_MOUSEENTER ? PUGL_POINTER_IN : PUGL_POINTER_OUT;
    event.crossing.time  = time;
    event.crossing.x     = canvasX * scaleFactor;
    event.crossing.y     = canvasY * scaleFactor;
    event.crossing.xRoot = mouseEvent->screenX * scaleFactor;
    event.crossing.yRoot = mouseEvent->screenY * scaleFactor;
    event.crossing.state = state;
    event.crossing.mode  = PUGL_CROSSING_NORMAL;
    break;
  }

  if (event.type == PUGL_NOTHING)
    return EM_FALSE;

  puglDispatchEventWithContext(view, &event);

#ifdef PUGL_WASM_AUTO_POINTER_LOCK
  switch (eventType) {
  case EMSCRIPTEN_EVENT_MOUSEDOWN:
    emscripten_request_pointerlock(view->world->strings[PUGL_CLASS_NAME], false);
    break;
  case EMSCRIPTEN_EVENT_MOUSEUP:
    emscripten_exit_pointerlock();
    break;
  }
#endif

  // note: we must always return false, otherwise canvas never gets keyboard input
  return EM_FALSE;
}

static void
puglTouchStartDelay(void* const userData)
{
  PuglView*      const view = (PuglView*)userData;
  PuglInternals* const impl  = view->impl;

  impl->buttonPressTimeout = -1;
  impl->nextButtonEvent.button.time += 2000;
  puglDispatchEventWithContext(view, &impl->nextButtonEvent);
}

static EM_BOOL
puglTouchCallback(const int eventType, const EmscriptenTouchEvent* const touchEvent, void* const userData)
{
  if (touchEvent->numTouches <= 0) {
    return EM_FALSE;
  }

  PuglView*      const view   = (PuglView*)userData;
  PuglInternals* const impl   = view->impl;
  const char* const className = view->world->strings[PUGL_CLASS_NAME];

  if (impl->supportsTouch == PUGL_DONT_CARE) {
    impl->supportsTouch = PUGL_TRUE;

    // stop using mouse press events which conflict with touch
    emscripten_set_mousedown_callback(className, view, false, NULL);
    emscripten_set_mouseup_callback(className, view, false, NULL);
  }

  if (!view->impl->visible) {
    return EM_FALSE;
  }

  PuglEvent event = {{PUGL_NOTHING, 0}};

  const double   time  = touchEvent->timestamp / 1e3;
  const PuglMods state = translateModifiers(touchEvent->ctrlKey,
                                            touchEvent->shiftKey,
                                            touchEvent->altKey,
                                            touchEvent->metaKey);

  double scaleFactor = view->world->impl->scaleFactor;
#ifdef __MOD_DEVICES__
  if (!view->impl->isFullscreen) {
    scaleFactor /= EM_ASM_DOUBLE({
      return parseFloat(
        RegExp('^scale\\\((.*)\\\)$')
        .exec(document.getElementById("pedalboard-dashboard").style.transform)[1]
      );
    }) * MOD_SCALE_FACTOR_MULT;
  }
#endif

  d_debug("touch %d|%s %d || %ld",
          eventType,
          eventType == EMSCRIPTEN_EVENT_TOUCHSTART ? "start" :
          eventType == EMSCRIPTEN_EVENT_TOUCHEND ? "end" : "cancel",
          touchEvent->numTouches,
          impl->buttonPressTimeout);

  const EmscriptenTouchPoint* point = &touchEvent->touches[0];

  if (impl->buttonPressTimeout != -1 || eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL) {
    // if we received an event while touch is active, trigger initial click now
    if (impl->buttonPressTimeout != -1) {
      emscripten_clear_timeout(impl->buttonPressTimeout);
      impl->buttonPressTimeout = -1;
      if (eventType != EMSCRIPTEN_EVENT_TOUCHCANCEL) {
        impl->nextButtonEvent.button.button = 0;
      }
    }
    impl->nextButtonEvent.button.time = time;
    puglDispatchEventWithContext(view, &impl->nextButtonEvent);
  }

#ifdef __MOD_DEVICES__
  const long canvasX = point->canvasX;
  const long canvasY = point->canvasY;
#else
  const double canvasX = point->clientX - EM_ASM_DOUBLE({
    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
    return canvasWrapper.getBoundingClientRect().x;
  }, className);
  const double canvasY = point->clientY - EM_ASM_DOUBLE({
    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
    return canvasWrapper.getBoundingClientRect().y;
  }, className);
#endif

  switch (eventType) {
  case EMSCRIPTEN_EVENT_TOUCHEND:
  case EMSCRIPTEN_EVENT_TOUCHCANCEL:
    event.button.type   = PUGL_BUTTON_RELEASE;
    event.button.time   = time;
    event.button.button = eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL ? 1 : 0;
    event.button.x      = canvasX * scaleFactor;
    event.button.y      = canvasY * scaleFactor;
    event.button.xRoot  = point->screenX * scaleFactor;
    event.button.yRoot  = point->screenY * scaleFactor;
    event.button.state  = state;
    break;

  case EMSCRIPTEN_EVENT_TOUCHSTART:
    // this event can be used for a couple of things, store it until we know more
    event.button.type   = PUGL_BUTTON_PRESS;
    event.button.time   = time;
    event.button.button = 1; // if no other event occurs soon, treat it as right-click
    event.button.x      = canvasX * scaleFactor;
    event.button.y      = canvasY * scaleFactor;
    event.button.xRoot  = point->screenX * scaleFactor;
    event.button.yRoot  = point->screenY * scaleFactor;
    event.button.state  = state;
    memcpy(&impl->nextButtonEvent, &event, sizeof(PuglEvent));
    impl->buttonPressTimeout = emscripten_set_timeout(puglTouchStartDelay, 2000, view);
    // fall through, moving "mouse" to touch position

  case EMSCRIPTEN_EVENT_TOUCHMOVE:
    event.motion.type  = PUGL_MOTION;
    event.motion.time  = time;
    event.motion.x     = canvasX * scaleFactor;
    event.motion.y     = canvasY * scaleFactor;
    event.motion.xRoot = point->screenX * scaleFactor;
    event.motion.yRoot = point->screenY * scaleFactor;
    event.motion.state = state;
    break;
  }

  if (event.type == PUGL_NOTHING)
    return EM_FALSE;

  puglDispatchEventWithContext(view, &event);

  // FIXME we must always return false??
  return EM_FALSE;
}

static EM_BOOL
puglFocusCallback(const int eventType, const EmscriptenFocusEvent* /*const focusEvent*/, void* const userData)
{
  PuglView* const view = (PuglView*)userData;

  if (!view->impl->visible) {
    return EM_FALSE;
  }

  d_debug("focus %d|%s", eventType, eventType == EMSCRIPTEN_EVENT_FOCUSIN ? "focus-in" : "focus-out");

  PuglEvent event = {{eventType == EMSCRIPTEN_EVENT_FOCUSIN ? PUGL_FOCUS_IN : PUGL_FOCUS_OUT, 0}};
  event.focus.mode = PUGL_CROSSING_NORMAL;

  puglDispatchEventWithContext(view, &event);

  // note: we must always return false, otherwise canvas never gets proper focus
  return EM_FALSE;
}

static EM_BOOL
puglPointerLockChangeCallback(const int eventType, const EmscriptenPointerlockChangeEvent* event, void* const userData)
{
  PuglView* const view = (PuglView*)userData;

  view->impl->pointerLocked = event->isActive;
  return EM_TRUE;
}

#ifndef PUGL_WASM_NO_MOUSEWHEEL_INPUT
static EM_BOOL
puglWheelCallback(const int eventType, const EmscriptenWheelEvent* const wheelEvent, void* const userData)
{
  PuglView* const view = (PuglView*)userData;

  if (!view->impl->visible) {
    return EM_FALSE;
  }

  double scaleFactor = view->world->impl->scaleFactor;
#ifdef __MOD_DEVICES__
  if (!view->impl->isFullscreen) {
    scaleFactor /= EM_ASM_DOUBLE({
      return parseFloat(
        RegExp('^scale\\\((.*)\\\)$')
        .exec(document.getElementById("pedalboard-dashboard").style.transform)[1]
      );
    }) * MOD_SCALE_FACTOR_MULT;
  }
#endif

#ifdef __MOD_DEVICES__
  const long canvasX = wheelEvent->mouse.canvasX;
  const long canvasY = wheelEvent->mouse.canvasY;
#else
  const char* const className = view->world->strings[PUGL_CLASS_NAME];
  const double canvasX = wheelEvent->mouse.canvasX - EM_ASM_INT({
    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
    return canvasWrapper.getBoundingClientRect().x;
  }, className);
  const double canvasY = wheelEvent->mouse.canvasY - EM_ASM_INT({
    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
    return canvasWrapper.getBoundingClientRect().y;
  }, className);
#endif

  PuglEvent event = {{PUGL_SCROLL, 0}};
  event.scroll.time  = wheelEvent->mouse.timestamp / 1e3;
  event.scroll.x     = canvasX;
  event.scroll.y     = canvasY;
  event.scroll.xRoot = wheelEvent->mouse.screenX;
  event.scroll.yRoot = wheelEvent->mouse.screenY;
  event.scroll.state = translateModifiers(wheelEvent->mouse.ctrlKey,
                                          wheelEvent->mouse.shiftKey,
                                          wheelEvent->mouse.altKey,
                                          wheelEvent->mouse.metaKey);
  event.scroll.direction = PUGL_SCROLL_SMOOTH;
  // FIXME handle wheelEvent->deltaMode
  event.scroll.dx = wheelEvent->deltaX * 0.01 * scaleFactor;
  event.scroll.dy = -wheelEvent->deltaY * 0.01 * scaleFactor;

  return puglDispatchEventWithContext(view, &event) == PUGL_SUCCESS ? EM_TRUE : EM_FALSE;
}
#endif

static EM_BOOL
puglUiCallback(const int eventType, const EmscriptenUiEvent* const uiEvent, void* const userData)
{
  PuglView* const view = (PuglView*)userData;
  const char* const className = view->world->strings[PUGL_CLASS_NAME];
  const bool isFullscreen = view->impl->isFullscreen;

  // FIXME
  const int width = EM_ASM_INT({
    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
    canvasWrapper.style.setProperty("--device-pixel-ratio", window.devicePixelRatio);
    if ($1) {
      return window.innerWidth;
    } else {
      return canvasWrapper.clientWidth;
    }
  }, className, isFullscreen);

  const int height = EM_ASM_INT({
    if ($1) {
      return window.innerHeight;
    } else {
      var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
      return canvasWrapper.clientHeight;
    }
  }, className, isFullscreen);

  if (!width || !height)
    return EM_FALSE;

  double scaleFactor = emscripten_get_device_pixel_ratio();
#ifdef __MOD_DEVICES__
  scaleFactor *= MOD_SCALE_FACTOR_MULT;
#endif
  view->world->impl->scaleFactor = scaleFactor;

  PuglEvent event        = {{PUGL_CONFIGURE, 0}};
  event.configure.x      = view->lastConfigure.x;
  event.configure.y      = view->lastConfigure.y;
  event.configure.width  = width * scaleFactor;
  event.configure.height = height * scaleFactor;
  puglDispatchEvent(view, &event);

  emscripten_set_canvas_element_size(view->world->strings[PUGL_CLASS_NAME],
                                     width * scaleFactor,
                                     height * scaleFactor);

#ifdef __MOD_DEVICES__
  if (isFullscreen) {
    EM_ASM({
      document.getElementById("pedalboard-dashboard").style.transform = "scale(1.0)";
    });
  }
#endif

  return EM_TRUE;
}

static EM_BOOL
puglFullscreenChangeCallback(const int eventType, const EmscriptenFullscreenChangeEvent* const fscEvent, void* const userData)
{
  PuglView* const view = (PuglView*)userData;

  view->impl->isFullscreen = fscEvent->isFullscreen;

  double scaleFactor = emscripten_get_device_pixel_ratio();
#ifdef __MOD_DEVICES__
  scaleFactor *= MOD_SCALE_FACTOR_MULT;
#endif

  view->world->impl->scaleFactor = scaleFactor;

  if (!fscEvent->isFullscreen)
    return puglUiCallback(0, NULL, userData);

  const int width = EM_ASM_INT({
    return window.innerWidth;
  });

  const int height = EM_ASM_INT({
    return window.innerHeight;
  });

  PuglEvent event        = {{PUGL_CONFIGURE, 0}};
  event.configure.x      = 0;
  event.configure.y      = 0;
  event.configure.width  = width;
  event.configure.height = height;
  puglDispatchEvent(view, &event);

  emscripten_set_canvas_element_size(view->world->strings[PUGL_CLASS_NAME], width, height);

#ifdef __MOD_DEVICES__
  EM_ASM({
    document.getElementById("pedalboard-dashboard").style.transform = "scale(1.0)";
  });
#endif

  return EM_TRUE;
}

static EM_BOOL
puglVisibilityChangeCallback(const int eventType, const EmscriptenVisibilityChangeEvent* const visibilityChangeEvent, void* const userData)
{
  PuglView* const view = (PuglView*)userData;

  view->impl->visible = visibilityChangeEvent->hidden == EM_FALSE;
  return EM_FALSE;
}

PuglStatus
puglRealize(PuglView* const view)
{
  printf("TODO: %s %d\n", __func__, __LINE__);
  PuglStatus st = PUGL_SUCCESS;

  // Ensure that we do not have a parent
  if (view->parent) {
  printf("TODO: %s %d\n", __func__, __LINE__);
    return PUGL_FAILURE;
  }

  if (!view->backend || !view->backend->configure) {
  printf("TODO: %s %d\n", __func__, __LINE__);
    return PUGL_BAD_BACKEND;
  }

  const char* const className = view->world->strings[PUGL_CLASS_NAME];
  d_stdout("className is %s", className);

  const PuglPoint defaultPos = view->positionHints[PUGL_DEFAULT_POSITION];
  const PuglArea defaultSize = view->sizeHints[PUGL_DEFAULT_SIZE];
  if (!defaultSize.width || !defaultSize.height) {
    return PUGL_BAD_CONFIGURATION;
  }

  // Configure and create the backend
  if ((st = view->backend->configure(view)) || (st = view->backend->create(view))) {
    view->backend->destroy(view);
    return st;
  }

  if (view->strings[PUGL_WINDOW_TITLE]) {
    emscripten_set_window_title(view->strings[PUGL_WINDOW_TITLE]);
  }

  puglDispatchSimpleEvent(view, PUGL_REALIZE);

  PuglEvent event        = {{PUGL_CONFIGURE, 0}};
  event.configure.x      = defaultPos.x;
  event.configure.y      = defaultPos.y;
  event.configure.width  = defaultSize.width;
  event.configure.height = defaultSize.height;
  puglDispatchEvent(view, &event);

  view->impl->created = true;

  EM_ASM({
   var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
   canvasWrapper.style.setProperty("--device-pixel-ratio", window.devicePixelRatio);
  }, className);

  puglUpdateSizeHints(view);

  emscripten_set_canvas_element_size(className, defaultSize.width, defaultSize.height);
#ifndef PUGL_WASM_NO_KEYBOARD_INPUT
//   emscripten_set_keypress_callback(className, view, false, puglKeyCallback);
  emscripten_set_keydown_callback(className, view, false, puglKeyCallback);
  emscripten_set_keyup_callback(className, view, false, puglKeyCallback);
#endif
  emscripten_set_touchstart_callback(className, view, false, puglTouchCallback);
  emscripten_set_touchend_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglTouchCallback);
  emscripten_set_touchmove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglTouchCallback);
  emscripten_set_touchcancel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglTouchCallback);
  emscripten_set_mousedown_callback(className, view, false, puglMouseCallback);
  emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglMouseCallback);
  emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglMouseCallback);
  emscripten_set_mouseenter_callback(className, view, false, puglMouseCallback);
  emscripten_set_mouseleave_callback(className, view, false, puglMouseCallback);
  emscripten_set_focusin_callback(className, view, false, puglFocusCallback);
  emscripten_set_focusout_callback(className, view, false, puglFocusCallback);
#ifndef PUGL_WASM_NO_MOUSEWHEEL_INPUT
  emscripten_set_wheel_callback(className, view, false, puglWheelCallback);
#endif
  emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglPointerLockChangeCallback);
  emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglUiCallback);
  emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglFullscreenChangeCallback);
  emscripten_set_visibilitychange_callback(view, false, puglVisibilityChangeCallback);

  printf("TODO: %s %d\n", __func__, __LINE__);
  return PUGL_SUCCESS;
}

PuglStatus
puglShow(PuglView* const view, PuglShowCommand)
{
  view->impl->visible = true;
  view->impl->needsRepaint = true;
  return puglObscureView(view);
}

PuglStatus
puglHide(PuglView* const view)
{
  view->impl->visible = false;
  return PUGL_FAILURE;
}

void
puglFreeViewInternals(PuglView* const view)
{
  printf("DONE: %s %d\n", __func__, __LINE__);
  if (view && view->impl) {
    if (view->backend) {
      // unregister the window events, to make sure no callbacks to old views are triggered
      emscripten_set_touchend_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
      emscripten_set_touchmove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
      emscripten_set_touchcancel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
      emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
      emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
      emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
      emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
      emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
      emscripten_set_visibilitychange_callback(NULL, false, NULL);
      view->backend->destroy(view);
    }
    free(view->impl->clipboardData);
    free(view->impl->timers);
    free(view->impl);
  }
}

void
puglFreeWorldInternals(PuglWorld* const world)
{
  printf("DONE: %s %d\n", __func__, __LINE__);
  free(world->impl);
}

PuglStatus
puglGrabFocus(PuglView*)
{
  return PUGL_FAILURE;
}

double
puglGetScaleFactor(const PuglView* const view)
{
  printf("DONE: %s %d\n", __func__, __LINE__);
  return view->world->impl->scaleFactor;
}

double
puglGetTime(const PuglWorld*)
{
  return emscripten_get_now() / 1e3;
}

PuglStatus
puglUpdate(PuglWorld* const world, const double timeout)
{
  for (size_t i = 0; i < world->numViews; ++i) {
    PuglView* const view = world->views[i];

    if (!view->impl->visible) {
      continue;
    }

    puglDispatchSimpleEvent(view, PUGL_UPDATE);

    if (!view->impl->needsRepaint) {
      continue;
    }

    view->impl->needsRepaint = false;

    PuglEvent event     = {{PUGL_EXPOSE, 0}};
    event.expose.x      = view->lastConfigure.x;
    event.expose.y      = view->lastConfigure.y;
    event.expose.width  = view->lastConfigure.width;
    event.expose.height = view->lastConfigure.height;
    puglDispatchEvent(view, &event);
  }

  return PUGL_SUCCESS;
}

PuglStatus
puglObscureView(PuglView* const view)
{
  view->impl->needsRepaint = true;
  return PUGL_SUCCESS;
}

PuglStatus
puglObscureRegion(PuglView*      view,
                  const int      x,
                  const int      y,
                  const unsigned width,
                  const unsigned height)
{
  if (!puglIsValidPosition(x, y) || !puglIsValidSize(width, height)) {
    return PUGL_BAD_PARAMETER;
  }

  view->impl->needsRepaint = true;
  return PUGL_FAILURE;
}

PuglNativeView
puglGetNativeView(const PuglView* const view)
{
  return 0;
}

PuglStatus
puglViewStringChanged(PuglView*, const PuglStringHint key, const char* const value)
{
  switch (key) {
  case PUGL_CLASS_NAME:
    break;
  case PUGL_WINDOW_TITLE:
    emscripten_set_window_title(value);
    break;
  }

  return PUGL_SUCCESS;
}

PuglStatus
puglSetWindowSize(PuglView* const view, const unsigned width, const unsigned height)
{
  if (view->impl->created) {
    const char* const className = view->world->strings[PUGL_CLASS_NAME];
    emscripten_set_canvas_element_size(className, width, height);
  }

  return PUGL_SUCCESS;
}

static EM_BOOL
puglTimerLoopCallback(double timeout, void* const arg)
{
  PuglTimer*     const timer = (PuglTimer*)arg;
  PuglInternals* const impl  = timer->view->impl;

  // only handle active timers
  for (uint32_t i=0; i<impl->numTimers; ++i)
  {
    if (impl->timers[i].id == timer->id)
    {
      PuglEvent event = {{PUGL_TIMER, 0}};
      event.timer.id  = timer->id;
      puglDispatchEventWithContext(timer->view, &event);
      return EM_TRUE;
    }
  }

  return EM_FALSE;

  // unused
  (void)timeout;
}

PuglStatus
puglStartTimer(PuglView* const view, const uintptr_t id, const double timeout)
{
  printf("DONE: %s %d\n", __func__, __LINE__);
  PuglInternals* const impl = view->impl;
  const uint32_t timerIndex = impl->numTimers++;

  if (impl->timers == NULL)
    impl->timers = (PuglTimer*)malloc(sizeof(PuglTimer));
  else
    impl->timers = (PuglTimer*)realloc(impl->timers, sizeof(PuglTimer) * timerIndex);

  PuglTimer* const timer = &impl->timers[timerIndex];
  timer->view = view;
  timer->id = id;

  emscripten_set_timeout_loop(puglTimerLoopCallback, timeout * 1e3, timer);
  return PUGL_SUCCESS;
}

PuglStatus
puglStopTimer(PuglView* const view, const uintptr_t id)
{
  printf("DONE: %s %d\n", __func__, __LINE__);
  PuglInternals* const impl = view->impl;

  if (impl->timers == NULL || impl->numTimers == 0)
    return PUGL_FAILURE;

  for (uint32_t i=0; i<impl->numTimers; ++i)
  {
    if (impl->timers[i].id == id)
    {
      memmove(impl->timers + i, impl->timers + (i + 1), sizeof(PuglTimer) * (impl->numTimers - 1));
      --impl->numTimers;
      return PUGL_SUCCESS;
    }
  }

  return PUGL_FAILURE;
}

#ifdef PUGL_WASM_ASYNC_CLIPBOARD
EM_JS(char*, puglGetAsyncClipboardData, (), {
  var text = Asyncify.handleSleep(function(wakeUp) {
    navigator.clipboard.readText()
      .then(function(text) {
        wakeUp(text);
      })
      .catch(function() {
        wakeUp("");
      });
  });
  if (!text.length) {
    return null;
  }
  var length = lengthBytesUTF8(text) + 1;
  var str = _malloc(length);
  stringToUTF8(text, str, length);
  return str;
});
#endif

PuglStatus
puglPaste(PuglView* const view)
{
#ifdef PUGL_WASM_ASYNC_CLIPBOARD
  // abort early if we already know it is not supported
  if (view->impl->supportsClipboardRead == PUGL_FALSE) {
    return PUGL_UNSUPPORTED;
  }

  free(view->impl->clipboardData);
  view->impl->clipboardData = puglGetAsyncClipboardData();
#endif

  if (view->impl->clipboardData == NULL) {
    return PUGL_FAILURE;
  }

  const PuglDataOfferEvent offer = {
    PUGL_DATA_OFFER,
    0,
    emscripten_get_now() / 1e3,
  };

  PuglEvent offerEvent;
  offerEvent.offer = offer;
  puglDispatchEvent(view, &offerEvent);
  return PUGL_SUCCESS;
}

PuglStatus
puglAcceptOffer(PuglView* const                 view,
                const PuglDataOfferEvent* const offer,
                const uint32_t                  typeIndex)
{
  if (typeIndex != 0) {
    return PUGL_UNSUPPORTED;
  }

  const PuglDataEvent data = {
    PUGL_DATA,
    0,
    emscripten_get_now() / 1e3,
    0,
  };

  PuglEvent dataEvent;
  dataEvent.data = data;
  puglDispatchEvent(view, &dataEvent);
  return PUGL_SUCCESS;
}

uint32_t
puglGetNumClipboardTypes(const PuglView* const view)
{
  return view->impl->clipboardData != NULL ? 1u : 0u;
}

const char*
puglGetClipboardType(const PuglView* const view, const uint32_t typeIndex)
{
  return (typeIndex == 0 && view->impl->clipboardData != NULL)
           ? "text/plain"
           : NULL;
}

const void*
puglGetClipboard(PuglView* const view,
                 const uint32_t  typeIndex,
                 size_t* const   len)
{
  return view->impl->clipboardData;
}

PuglStatus
puglSetClipboard(PuglView* const   view,
                 const char* const type,
                 const void* const data,
                 const size_t      len)
{
  // only utf8 text supported for now
  if (type != NULL && strcmp(type, "text/plain") != 0) {
    return PUGL_UNSUPPORTED;
  }

  const char* const className = view->world->strings[PUGL_CLASS_NAME];
  const char* const text      = (const char*)data;

#ifdef PUGL_WASM_ASYNC_CLIPBOARD
  // abort early if we already know it is not supported
  if (view->impl->supportsClipboardWrite == PUGL_FALSE) {
    return PUGL_UNSUPPORTED;
  }
#else
  puglSetString(&view->impl->clipboardData, text);
#endif

  EM_ASM({
    if (typeof(navigator.clipboard) !== 'undefined' && typeof(navigator.clipboard.writeText) === 'function' && window.isSecureContext) {
      navigator.clipboard.writeText(UTF8ToString($1));
    } else {
      var canvasClipboardObjName = UTF8ToString($0) + "_clipboard";
      var canvasClipboardElem = document.getElementById(canvasClipboardObjName);

      if (!canvasClipboardElem) {
        canvasClipboardElem = document.createElement('textarea');
        canvasClipboardElem.id = canvasClipboardObjName;
        canvasClipboardElem.style.position = 'fixed';
        canvasClipboardElem.style.whiteSpace = 'pre';
        canvasClipboardElem.style.zIndex = '-1';
        canvasClipboardElem.setAttribute('readonly', true);
        document.body.appendChild(canvasClipboardElem);
      }

      canvasClipboardElem.textContent = UTF8ToString($1);
      canvasClipboardElem.select();
      document.execCommand("copy");
    }
  }, className, text);

  // FIXME proper return status
  return PUGL_SUCCESS;
}

PuglStatus
puglSetCursor(PuglView* const view, const PuglCursor cursor)
{
  printf("TODO: %s %d\n", __func__, __LINE__);
  return PUGL_FAILURE;
}

PuglStatus
puglSetTransientParent(PuglView* const view, const PuglNativeView parent)
{
  printf("TODO: %s %d\n", __func__, __LINE__);
  view->transientParent = parent;
  return PUGL_FAILURE;
}
