Control your mouse cursor with your phone (windows only)
document.addEventListener("DOMContentLoaded", main);

function getState() {
  const _mouseAcceleration = buildValue({
    defaultValue: 10,
    localStorageKey: "_mouseAcceleration",
  });

  const _scrollSpeed = buildValue({
    defaultValue: 5,
    localStorageKey: "_scrollSpeed",
  });

  const _gyromouseSwitch = buildValue({
    defaultValue: false,
    localStorageKey: "_gyromouseSwitch",
  });

  const _gyromouseSpeed = buildValue({
    defaultValue: 10,
    localStorageKey: "_gyromouseSpeed",
  });

  let _WSConnected = false;

  function buildValue(param) {
    function nullIfThrows(callback) {
      try {
        return callback();
      } catch {
        return null;
      }
    }

    function getJsonStoreFromLocalStorage(param) {
      const data = localStorage.getItem(param.localStorageKey);
      if (data == null) {
        return param.defaultValue;
      }
      const parsed = nullIfThrows(() => JSON.parse(data));
      if (parsed == null) {
        return param.defaultValue;
      }
      const value = parsed.store;
      if (value == null) {
        return param.defaultValue;
      }
      return value;
    }

    const value = getJsonStoreFromLocalStorage(param);

    return {
      ...param,
      value,
    };
  }

  function setValue(param) {
    // without this setItem would be blocking
    setTimeout(() => {
      localStorage.setItem(
        param.localStorageKey,
        JSON.stringify({ store: param.value })
      );
    });
  }

  return {
    get WSConnected() {
      return _WSConnected;
    },
    set WSConnected(value) {
      _WSConnected = value;
      connected.style.display = value ? "" : "none";
      connect.style.display = value ? "none" : "";
      light.classList.toggle("on", value);
    },
    get mouseAcceleration() {
      return _mouseAcceleration.value;
    },
    set mouseAcceleration(value) {
      _mouseAcceleration.value = value;
      setValue(_mouseAcceleration);
    },
    get scrollSpeed() {
      return _scrollSpeed.value;
    },
    set scrollSpeed(value) {
      _scrollSpeed.value = value;
      setValue(_scrollSpeed);
    },
    get gyromouseSwitch() {
      return _gyromouseSwitch.value;
    },
    set gyromouseSwitch(value) {
      touchpad.style.display = value ? "none" : "";
      gryropad.style.display = value ? "" : "none";
      _gyromouseSwitch.value = value;
      setValue(_gyromouseSwitch);
    },
    get gyromouseSpeed() {
      return _gyromouseSpeed.value;
    },
    set gyromouseSpeed(value) {
      _gyromouseSpeed.value = value;
      setValue(_gyromouseSpeed);
    },
    ws: null,
  };
}

const state = getState();

function main() {
  touchpadBinding(touchpad);
  gryropadBinding(gryropad);

  leftbtn.addEventListener("click", () => {
    lefthold.checked = false;
    lefthold.parentElement.open = false;
    click("left");
  });
  leftholdBinding(lefthold);
  middlebtnBinding(middlebtn);
  rightbtn.addEventListener("click", () => click("right"));

  kbrdcut.addEventListener("click", () => {
    keyboardAction({ action: "cut" });
  });
  kbrdcopy.addEventListener("click", () => {
    keyboardAction({ action: "copy" });
  });
  kbrdpaste.addEventListener("click", () => {
    keyboardAction({ action: "paste" });
  });
  kbrdenter.addEventListener("click", () => {
    keyboardAction({ action: "enter" });
  });

  form.addEventListener("submit", onsubmit);

  fullscreen_button.addEventListener("click", (e) => {
    const element = document.body;
    if (document.fullscreenElement == null) {
      element.requestFullscreen();
    } else {
      document.exitFullscreen();
    }
  });

  connect.addEventListener("click", WSConnect);

  maccelBinding(maccel);

  scrollSpeedBinding(scroll_speed);

  gyromouseSwitchBinding(gyromouse_switch);

  gyromouseSpeedBinding(gyromouse_speed);

  WSConnect();

  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "visible") {
      WSConnect();
    }
  });
}

function WSConnect() {
  if (state.WSConnected) return;

  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";

  state.ws = new WebSocket(`${protocol}//${window.location.host}/realtime`);

  state.ws.addEventListener("open", (event) => {
    state.WSConnected = true;
    console.log("[websocket] connection open", event);
  });

  state.ws.addEventListener("close", (event) => {
    state.WSConnected = false;
    console.log("[websocket] connection closed", event);
  });

  state.ws.addEventListener("message", (event) => {
    console.log("[websocket] message received", event);
  });
}

function remap(value, low_from, high_from, low_to, high_to) {
  return (
    low_to + ((high_to - low_to) * (value - low_from)) / (high_from - low_from)
  );
}

function touchpadBinding(node) {
  let ongoingtouch = null;
  let starttouch = null;

  const ontouchstart = (e) => {
    if (ongoingtouch != null) {
      return;
    }

    ongoingtouch = {
      x: e.touches[0].clientX,
      y: e.touches[0].clientY,
    };

    starttouch = ongoingtouch;
  };

  const ontouchmove = (e) => {
    if (ongoingtouch == null) {
      return;
    }

    const position = {
      x: e.touches[0].clientX,
      y: e.touches[0].clientY,
    };

    const windowWidth = window.innerWidth;

    const factor = {
      x:
        1 +
        (state.mouseAcceleration - 1) *
          (Math.abs(starttouch.x - position.x) / windowWidth),
      y:
        1 +
        (state.mouseAcceleration - 1) *
          (Math.abs(starttouch.y - position.y) / windowWidth),
    };

    const difference = {
      x: (-ongoingtouch.x + position.x) * factor.x,
      y: (-ongoingtouch.y + position.y) * factor.y,
    };

    state.ws?.send(JSON.stringify({ difference }));

    ongoingtouch = position;
  };

  const ontouchend = () => {
    if (starttouch != null && starttouch == ongoingtouch) {
      state.ws?.send(JSON.stringify({ click: "left" }));
    }
    ongoingtouch = null;
    starttouch = null;
  };

  node.addEventListener("touchstart", ontouchstart);
  node.addEventListener("touchmove", ontouchmove);
  node.addEventListener("touchend", ontouchend);
}

function gryropadBinding(node) {
  let ongoingtouch = false;
  let orientation = null;

  const process = (orientation, previousOrientation) => {
    if (!ongoingtouch) {
      return;
    }

    if (previousOrientation == null) {
      return;
    }

    const angleDiff = (current, previous) =>
      Math.atan2(Math.sin(current - previous), Math.cos(current - previous));

    const factor = remap(state.gyromouseSpeed, 1, 20, 5, 50);

    const difference = {
      x: -angleDiff(orientation.alpha, previousOrientation.alpha) * factor,
      y: -angleDiff(orientation.beta, previousOrientation.beta) * factor,
    };
    state.ws?.send(JSON.stringify({ difference }));
  };

  const ondeviceorientation = (e) => {
    const { alpha, beta, gamma } = event;
    process({ alpha, beta, gamma }, orientation);
    orientation = { alpha, beta, gamma };
  };

  const ontouchstart = () => {
    ongoingtouch = true;
  };

  const ontouchend = () => {
    ongoingtouch = false;
  };

  node.addEventListener("touchstart", ontouchstart);
  node.addEventListener("touchend", ontouchend);
  addEventListener("deviceorientation", ondeviceorientation);
}

function click(dir) {
  state.ws?.send(JSON.stringify({ click: dir }));
}

function leftholdBinding(node) {
  node.parentElement.addEventListener("click", (e) => {
    e.stopImmediatePropagation();
  });

  node.addEventListener("input", (e) => {
    click(e.target.checked ? "left_hold" : "left_release");
  });
}

function middlebtnBinding(node) {
  let ongoingtouch = null;
  let starttouch = null;

  const ontouchstart = (e) => {
    if (ongoingtouch != null) {
      return;
    }

    ongoingtouch = {
      x: e.touches[0].clientX,
      y: e.touches[0].clientY,
    };

    starttouch = ongoingtouch;
  };

  const ontouchmove = (e) => {
    if (ongoingtouch == null) {
      return;
    }

    const position = {
      x: e.touches[0].clientX,
      y: e.touches[0].clientY,
    };

    const value = {
      x: position.x - starttouch.x,
      y: starttouch.y - position.y,
    };

    value.x = Math.abs(value.x) < Math.abs(value.y) ? 0 : value.x;
    value.y = Math.abs(value.x) >= Math.abs(value.y) ? 0 : value.y;

    const sign = {
      x: Math.sign(value.x),
      y: Math.sign(value.y),
    };

    const scrollMod = state.scrollSpeed / 20;

    state.ws?.send(
      JSON.stringify({
        scroll: {
          x: sign.x * scrollMod,
          y: sign.y * scrollMod,
        },
      })
    );

    ongoingtouch = position;
  };

  const ontouchend = () => {
    if (starttouch != null && starttouch == ongoingtouch) {
      state.ws?.send(JSON.stringify({ click: "middle" }));
    }
    ongoingtouch = null;
    starttouch = null;
  };

  node.addEventListener("touchstart", ontouchstart);
  node.addEventListener("touchmove", ontouchmove);
  node.addEventListener("touchend", ontouchend);
}

function keyboardAction(data) {
  state.ws?.send(JSON.stringify(data));
}

function onsubmit(e) {
  e.preventDefault();

  const formElement = e.target;
  const formData = new FormData(formElement);
  const text = formData.get("text");

  if (text.length === 0) {
    keyboardAction({ action: "enter" });
  } else {
    state.ws?.send(JSON.stringify({ text }));
  }
  formElement.reset();
}

function maccelBinding(node) {
  node.value = state.mouseAcceleration;
  maccel_value_display.innerText = `: ${state.mouseAcceleration}`;

  node.addEventListener("input", (e) => {
    state.mouseAcceleration = Number(e.target.value);
    maccel_value_display.innerText = `: ${state.mouseAcceleration}`;
  });
}

function scrollSpeedBinding(node) {
  node.value = state.scrollSpeed;
  scroll_speed_value_display.innerText = `: ${state.scrollSpeed}`;

  node.addEventListener("input", (e) => {
    state.scrollSpeed = Number(e.target.value);
    scroll_speed_value_display.innerText = `: ${state.scrollSpeed}`;
  });
}

function gyromouseSwitchBinding(node) {
  state.gyromouseSwitch = state.gyromouseSwitch; // triggers setter logic
  node.checked = state.gyromouseSwitch;

  node.addEventListener("input", (e) => {
    state.gyromouseSwitch = e.target.checked;
  });
}

function gyromouseSpeedBinding(node) {
  node.value = state.gyromouseSpeed;
  gyromouse_speed_value_display.innerText = `: ${state.gyromouseSpeed}`;

  node.addEventListener("input", (e) => {
    state.gyromouseSpeed = Number(e.target.value);
    gyromouse_speed_value_display.innerText = `: ${state.gyromouseSpeed}`;
  });
}