/* eslint-env mozilla/chrome-worker */

const EVENT_OBJECT_SHOW = 0x8002;
const EVENT_OBJECT_HIDE = 0x8003;
const WINEVENT_OUTOFCONTEXT = 0;
const WINEVENT_SKIPOWNPROCESS = 2;
const QS_ALLINPUT = 0x04ff;
const INFINITE = 0xffffffff;
const WAIT_OBJECT_0 = 0;
const WAIT_TIMEOUT = 258;
const PM_NOREMOVE = 0;
var user32;

function DialogWatcher(titleText, onDialogStart, onDialogEnd) {
  this.titleText = titleText;
  this.onDialogStart = onDialogStart;
  this.onDialogEnd = onDialogEnd;
}

DialogWatcher.prototype.init = function() {
  this.hwnd = undefined;
  if (!this.user32) {
    this.user32 = ctypes.open("user32.dll");
  }
  if (!this.findWindow) {
    this.findWindow = user32.declare(
      "FindWindowW",
      ctypes.winapi_abi,
      ctypes.uintptr_t,
      ctypes.char16_t.ptr,
      ctypes.char16_t.ptr
    );
  }
  if (!this.winEventProcType) {
    this.winEventProcType = ctypes.FunctionType(
      ctypes.stdcall_abi,
      ctypes.void_t,
      [
        ctypes.uintptr_t,
        ctypes.uint32_t,
        ctypes.uintptr_t,
        ctypes.long,
        ctypes.long,
        ctypes.uint32_t,
        ctypes.uint32_t,
      ]
    ).ptr;
  }
  if (!this.setWinEventHook) {
    this.setWinEventHook = user32.declare(
      "SetWinEventHook",
      ctypes.winapi_abi,
      ctypes.uintptr_t,
      ctypes.uint32_t,
      ctypes.uint32_t,
      ctypes.uintptr_t,
      this.winEventProcType,
      ctypes.uint32_t,
      ctypes.uint32_t,
      ctypes.uint32_t
    );
  }
  if (!this.unhookWinEvent) {
    this.unhookWinEvent = user32.declare(
      "UnhookWinEvent",
      ctypes.winapi_abi,
      ctypes.int,
      ctypes.uintptr_t
    );
  }
  if (!this.pointType) {
    this.pointType = ctypes.StructType("tagPOINT", [
      { x: ctypes.long },
      { y: ctypes.long },
    ]);
  }
  if (!this.msgType) {
    this.msgType = ctypes.StructType("tagMSG", [
      { hwnd: ctypes.uintptr_t },
      { message: ctypes.uint32_t },
      { wParam: ctypes.uintptr_t },
      { lParam: ctypes.intptr_t },
      { time: ctypes.uint32_t },
      { pt: this.pointType },
    ]);
  }
  if (!this.peekMessage) {
    this.peekMessage = user32.declare(
      "PeekMessageW",
      ctypes.winapi_abi,
      ctypes.int,
      this.msgType.ptr,
      ctypes.uintptr_t,
      ctypes.uint32_t,
      ctypes.uint32_t,
      ctypes.uint32_t
    );
  }
  if (!this.msgWaitForMultipleObjects) {
    this.msgWaitForMultipleObjects = user32.declare(
      "MsgWaitForMultipleObjects",
      ctypes.winapi_abi,
      ctypes.uint32_t,
      ctypes.uint32_t,
      ctypes.uintptr_t.ptr,
      ctypes.int,
      ctypes.uint32_t,
      ctypes.uint32_t
    );
  }
  if (!this.getWindowTextW) {
    this.getWindowTextW = user32.declare(
      "GetWindowTextW",
      ctypes.winapi_abi,
      ctypes.int,
      ctypes.uintptr_t,
      ctypes.char16_t.ptr,
      ctypes.int
    );
  }
  if (!this.messageBox) {
    // Handy for debugging this code
    this.messageBox = user32.declare(
      "MessageBoxW",
      ctypes.winapi_abi,
      ctypes.int,
      ctypes.uintptr_t,
      ctypes.char16_t.ptr,
      ctypes.char16_t.ptr,
      ctypes.uint32_t
    );
  }
};

DialogWatcher.prototype.getWindowText = function(hwnd) {
  var bufType = ctypes.ArrayType(ctypes.char16_t);
  var buffer = new bufType(256);

  if (this.getWindowTextW(hwnd, buffer, buffer.length)) {
    return buffer.readString();
  }
  return undefined;
};

DialogWatcher.prototype.processWindowEvents = function(timeout) {
  var onWinEvent = function(
    self,
    hook,
    event,
    hwnd,
    idObject,
    idChild,
    dwEventThread,
    dwmsEventTime
  ) {
    var nhwnd = Number(hwnd);
    if (event == EVENT_OBJECT_SHOW) {
      if (nhwnd == self.hwnd) {
        // We've already picked up this event via FindWindow
        return;
      }
      var windowText = self.getWindowText(hwnd);
      if (windowText == self.titleText && self.onDialogStart) {
        self.hwnd = nhwnd;
        self.onDialogStart(nhwnd);
      }
    } else if (
      event == EVENT_OBJECT_HIDE &&
      nhwnd == self.hwnd &&
      self.onDialogEnd
    ) {
      self.onDialogEnd();
      self.hwnd = null;
    }
  };
  var self = this;
  var callback = this.winEventProcType(function(
    hook,
    event,
    hwnd,
    idObject,
    idChild,
    dwEventThread,
    dwmsEventTime
  ) {
    onWinEvent(
      self,
      hook,
      event,
      hwnd,
      idObject,
      idChild,
      dwEventThread,
      dwmsEventTime
    );
  });
  var hook = this.setWinEventHook(
    EVENT_OBJECT_SHOW,
    EVENT_OBJECT_HIDE,
    0,
    callback,
    0,
    0,
    WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS
  );
  if (!hook) {
    return null;
  }
  // Check if the window is already showing
  var hwnd = this.findWindow(null, this.titleText);
  if (hwnd && hwnd > 0) {
    this.hwnd = Number(hwnd);
    if (this.onDialogStart) {
      this.onDialogStart(this.hwnd);
    }
  }

  if (!timeout) {
    timeout = INFINITE;
  }

  var waitStatus = WAIT_OBJECT_0;
  var expectingStart = this.onDialogStart && this.hwnd === undefined;
  var startWaitTime = Date.now();
  while (this.hwnd === undefined || (this.onDialogEnd && this.hwnd)) {
    waitStatus = this.msgWaitForMultipleObjects(
      0,
      null,
      0,
      expectingStart ? INFINITE : timeout,
      0
    );
    if (waitStatus == WAIT_OBJECT_0) {
      var msg = new this.msgType();
      this.peekMessage(msg.address(), 0, 0, 0, PM_NOREMOVE);
    }
    if (waitStatus == WAIT_TIMEOUT || Date.now() - startWaitTime >= timeout) {
      break;
    }
  }

  this.unhookWinEvent(hook);
  // Returns true if the hook was successful, something was found, and we never timed out
  return this.hwnd !== undefined && waitStatus == WAIT_OBJECT_0;
};
