/*
 * Copyright © 2026 Em Zakharov
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

document.addEventListener("DOMContentLoaded", (_) => {
  const microphoneSticker   = document.getElementById("microphone-sticker");
  const inputCable          = document.querySelector(".input.cable");
  const inputCableImage     = document.querySelector(".input.cable > img");
  const inputSelect         = document.getElementById("input-select");
  const screenBacklight     = document.getElementById("screen-backlight");
  const screenBacklightGlow = document.getElementById("screen-backlight-glow");
  const screenTopReel       = document.getElementById("screen-top-reel");
  const screenBottomReel    = document.getElementById("screen-bottom-reel");
  const screenNeedle        = document.getElementById("screen-needle");
  const screenTimerSec      = document.getElementById("screen-timer-sec");
  const screenTimerDecisec  = document.getElementById("screen-timer-deci");
  const playbackButton      = document.getElementsByName("playback")[0];
  const playbackToggle      = document.getElementsByName("playback-toggle-switch")[0];
  const playbackTogglePress = document.getElementsByName("playback-toggle-press")[0];
  const playbackToggleHold  = document.getElementsByName("playback-toggle-hold")[0];
  const recordButton        = document.getElementsByName("record")[0];
  const recordToggle        = document.getElementsByName("record-toggle-switch")[0];
  const recordTogglePress   = document.getElementsByName("record-toggle-press")[0];
  const recordToggleHold    = document.getElementsByName("record-toggle-hold")[0];
  const dummyElement        = document.getElementById("device-frame");
  const audio               = document.getElementById("recording");
  const audioCableIn        = document.getElementById("cable-in");
  const audioCableOut       = document.getElementById("cable-out");
  const mainDiv             = document.querySelector("main > div");


  // Audio recording ===
  // ===================
  let chunks = [];
  let mediaRecorder = null;
  const audioContext = new AudioContext({ sampleRate: 48000 });
  function setMediaRecorder(aMediaRecorder) {
    mediaRecorder = aMediaRecorder;

    // Creating an AudioContext (see above) and adding the mediaRecorder stream to it
    // solves multiple problems:
    //   - The second and subsequent playbacks of a recording are no longer muffled on iOS Safari.
    //   - Prevents the "A MediaStreamTrack ended due to a capture failure" error on iOS Safari,
    //     which makes subsequent recordings impossible.
    // However, on Mac, if one browser (or Safari) has taken hold of CoreAudio, playback in
    // other browsers will sound muffled until it is released. Can't fix that.
    const audioSourceNode = audioContext.createMediaStreamSource(mediaRecorder.stream);

    mediaRecorder.ondataavailable = (e) => { chunks.push(e.data) };
    mediaRecorder.onstop = (e) => {
      const mimeType = MediaRecorder.isTypeSupported("audio/mp4; codecs=mp4a") ?
                       "audio/mp4; codecs=mp4a" :
                       "audio/webm; codecs=opus";
      const blob = new Blob(chunks, { type: mimeType });
      chunks = [];
      const audioURL = window.URL.createObjectURL(blob);
      audio.src = audioURL;
    };
  }
  // ===================


  // Toggle switches ===
  // ===================
  let debouncing = false;
  function setDebounceTimeout() { setTimeout(() => { debouncing = false }, 200) }
      // 200 ms, because slow enough to absorb event delays (problems noticed at 100 ms),
      // but fast enough to be unnoticeable (~ human reaction time).
  function toggleSwitch(e) {
    if (debouncing) { return }

    operateToggleSwitch(e, "flip", false);
  }
  playbackToggle.onclick = toggleSwitch;
  recordToggle.onclick   = toggleSwitch;

  // To allow swiping the toggle switches up/down on touch devices
  let toggleTouchStartY = 0;
  let toggleTouchEndY   = 0;
  function handleTouchStart(e) { toggleTouchStartY = e.changedTouches[0].screenY; }
  function handleTouchEnd(e) {
    toggleTouchEndY = e.changedTouches[0].screenY;

    const direction = checkDirection(toggleTouchStartY, toggleTouchEndY);
    // If the user's intention is to swipe the switch (`direction` is not "none"), it can happen
    // that the switch is also considered `click`ed. This causes a double flip of the switch: one
    // from the user's intetion, then reversal by the `click` event's "flip". To prevent that
    // reversing "flip", when a valid swipe is registered, we enter a `debouncing` state. During
    // this state, any `click` events on the switch are ignored. Hence, double flips are avoided.
    // The `debouncing` state is left after a timeout to allow intentional `click` events to be
    // correctly handled again.
    if (direction === "up" || direction === "down") {
      debouncing = true;
      setDebounceTimeout()
    }
    operateToggleSwitch(e, direction, false);
  }
  playbackToggle.ontouchstart = handleTouchStart;
  playbackToggle.ontouchend   = handleTouchEnd;
  recordToggle.ontouchstart   = handleTouchStart;
  recordToggle.ontouchend     = handleTouchEnd;

  loadToggleStates([playbackToggle, recordToggle]);
  // ===================


  // Screen animation functions ===
  // ==============================
  let reelSpinStartTime;
  let recordingTime = 0; // deciseconds
  let playbackTime = 0; // deciseconds
  let deciCounter = 0;
  let screenTimeTimer;

  // There are cases (such as quickly spamming r-f a few times) where a rogue interval
  // that does not get cleared persists and causes problems. This makes sure no rogue
  // interval keeps running in the background:
  function resetIntervals() {
    // Upper bound the rogue interval ID
    const highestIntervalId = setTimeout(0);
    // Create a window that should capture the rogue interval ID
    const intervalIdWindow = highestIntervalId - 20;
    for (var i = highestIntervalId; i >= intervalIdWindow; i--) {
      // Clearing the delayKeyPointerUpTimer causes incorrect behavior
      if (i === delayKeyPointerUpTimer) { continue }

      clearInterval(i);
    }
  }

  function animateScreenStartRecording() {
    // Reset inline rewind styles
    screenTopReel.style = "";
    screenBottomReel.style = "";

    // New recording overwrites previous one: reset timers
    recordingTime = 0;
    deciCounter = 0;
    screenTimerSec.textContent = `0:00`;
    screenTimerDecisec.textContent = `0`;

    screenNeedle.classList.add("recording");
    reelSpinStartTime = Date.now();
    screenTopReel.classList.add("spinning");
    screenBottomReel.classList.add("spinning");
    screenTimeTimer = setInterval(() => {
      recordingTime += 1;
      deciCounter += 1;
      const minutes = Math.floor(recordingTime / 600);
      const seconds = Math.floor((recordingTime - (minutes * 600)) / 10);
      const decisec = recordingTime - (minutes * 600) - (seconds * 10);
      if (deciCounter == 10) {
        deciCounter = 0;
        if (seconds >= 10) {
          screenTimerSec.textContent = `${minutes}:${seconds}`;
        } else {
          screenTimerSec.textContent = `${minutes}:${"0" + seconds}`;
        }
      }
      screenTimerDecisec.textContent = decisec;
    }, 100); // With such a short interval and screen updates the timer will
             // drift eventually, but oh well, such is life.
  }

  function animateScreenStopRecording() {
    screenNeedle.classList.remove("recording");
    const reelSpinTime = Date.now() - reelSpinStartTime - 100; // 100 ms delay before it starts spinning
    const degPerMs = 0.18; // One rotation in 2 seconds => 180º in 1 second => 0.18º in 1 millisecond
    screenTopReel.style = `--from-deg-cw: ${360 - (degPerMs * (reelSpinTime))}deg; animation: partial-spin-clockwise 0.4s linear;`;
    screenTopReel.classList.remove("spinning");
    screenBottomReel.style = `--from-deg-ccw: ${-360 + (degPerMs * (reelSpinTime))}deg; animation: partial-spin-counter-clockwise 0.4s linear;`;
    screenBottomReel.classList.remove("spinning");
    clearInterval(screenTimeTimer);
  }

  function animateScreenStartPlayback() {
    // Reset inline rewind styles
    screenTopReel.style = "";
    screenBottomReel.style = "";

    playbackTime = recordingTime;
    deciCounter = recordingTime % 10;

    screenNeedle.classList.add("playback");
    reelSpinStartTime = Date.now();
    screenTopReel.classList.add("spinning");
    screenBottomReel.classList.add("spinning");

    resetIntervals();
    screenTimeTimer = setInterval(() => {
      playbackTime -= 1;
      deciCounter -= 1;
      const minutes = Math.floor(playbackTime / 600);
      const seconds = Math.floor((playbackTime - (minutes * 600)) / 10);
      const decisec = playbackTime - (minutes * 600) - (seconds * 10);
      if (deciCounter == -1) {
        deciCounter = 9;
        if (seconds >= 10) {
          screenTimerSec.textContent = `${minutes}:${seconds}`;
        } else {
          screenTimerSec.textContent = `${minutes}:${"0" + seconds}`;
        }
      }
      screenTimerDecisec.textContent = decisec;
      if (playbackTime <= 0) {
        animateScreenStopPlayback();
        isPlayingBack = false;
        resetIntervals();
      }
    }, 100);
  }

  function animateScreenStopPlayback() {
    if (reelSpinStartTime === undefined) { return }

    screenNeedle.classList.remove("playback");
    const reelSpinTime = Date.now() - reelSpinStartTime - 100; // 100 ms delay before it starts spinning
    const degPerMs = 0.18; // One rotation in 2 seconds => 180º in 1 second => 0.18º in 1 millisecond
    screenTopReel.style = `--from-deg-cw: ${360 - (degPerMs * (reelSpinTime))}deg; animation: partial-spin-clockwise 0.4s linear;`;
    screenTopReel.classList.remove("spinning");
    screenBottomReel.style = `--from-deg-ccw: ${-360 + (degPerMs * (reelSpinTime))}deg; animation: partial-spin-counter-clockwise 0.4s linear;`;
    screenBottomReel.classList.remove("spinning");
    clearInterval(screenTimeTimer);

    const minutes = Math.floor(recordingTime / 600);
    const seconds = Math.floor((recordingTime - (minutes * 600)) / 10);
    const decisec = recordingTime - (minutes * 600) - (seconds * 10);
    if (seconds >= 10) {
      screenTimerSec.textContent = `${minutes}:${seconds}`;
    } else {
      screenTimerSec.textContent = `${minutes}:${"0" + seconds}`;
    }
    screenTimerDecisec.textContent = decisec;
  }
  // ==============================


  // Record/playback buttons ===
  // ===========================
  let continuePlayback  = false;
  let continueRecording = false;
  let delayKeyPointerUp = false;
  let delayKeyPointerUpTimer;
  let isRecording   = false;
  let isPlayingBack = false;

  function playbackButtonDown() {
    if (mediaRecorder && mediaRecorder.state === "recording") {
      mediaRecorder.stop();
      isRecording = false;
      delayKeyPointerUp = true;
      animateScreenStopRecording();

      // audio isn't ready to play immediately, so trigger playback on the canplay event
      audio.oncanplay = () => {
        audio.play();
        isPlayingBack = true;
        animateScreenStartPlayback();

        // Reset to prevent automatic playback on subsequent recordings
        audio.oncanplay = null;
      };
      // Has to be outside the canplay event handler so that the global value is affected
      continuePlayback = true;
    }

    if (audio.paused) {
      if (audio.readyState < 1) { return }

      audio.play();
      isPlayingBack = true;
      continuePlayback = true;
      animateScreenStartPlayback();
    }
  }

  function playbackButtonUp() {
    if (playbackToggleHold.checked || !continuePlayback) {
      audio.pause();
      isPlayingBack = false;
      audio.currentTime = 0.0;
      // Firefox doesn't understand setting the currentTime to zero (or to any value), but
      // seems to understand (re)setting the src, which acheives the same thing ¯\_(ツ)_/¯
      // Thanks to Daniel Garcia Sanchez <3 https://stackoverflow.com/a/16978083
      if (audio.currentTime !== 0.0) { audio.src = audio.src; }

      if (isRecording) { return }
      animateScreenStopPlayback();
    } else {
      // Same reason as for `recordButton`
      continuePlayback = false;
    }

    // In the specific case when the device is recording and the playback button
    // is in "HOLD TO PLAY BACK" mode, a quick tap of the playback button will
    // cause the pointer-/keyup event to fire before the audio's canplay fires.
    // This means audio starts playing after the pointer-/keyup happens, which
    // will not pause the audio as it should.
    if (delayKeyPointerUp) {
      let delay = 220;
      if (recordingTime >  300 /* 30 s */) { delay = 320; }
      if (recordingTime > 1800 /*  3 m */) { delay = 420; }
      if (recordingTime > 4200 /*  7 m */) { delay = 520; }
      if (recordingTime > 4800 /*  8 m */) { delay = 620; }
      if (recordingTime > 5400 /*  9 m */) { delay = 820; }

      delayKeyPointerUpTimer = setTimeout(() => {
        if (playbackToggleHold.checked) {
          audio.pause();
          isPlayingBack = false;
          audio.currentTime = 0.0;
          // Firefox doesn't understand setting the currentTime to zero (or to any value), but
          // seems to understand (re)setting the src, which acheives the same thing ¯\_(ツ)_/¯
          // Thanks to Daniel Garcia Sanchez <3 https://stackoverflow.com/a/16978083
          if (audio.currentTime !== 0.0) { audio.src = audio.src; }
          animateScreenStopPlayback();
        }
      }, delay);
      delayKeyPointerUp = false;
    }
  }

  function recordButtonDown() {
    if (!mediaRecorder) { return }

    if (isPlayingBack) {
      audio.pause();
      isPlayingBack = false;
      audio.currentTime = 0.0;
      // Firefox doesn't understand setting the currentTime to zero (or to any value), but
      // seems to understand (re)setting the src, which acheives the same thing ¯\_(ツ)_/¯
      // Thanks to Daniel Garcia Sanchez <3 https://stackoverflow.com/a/16978083
      if (audio.currentTime !== 0.0) { audio.src = audio.src; }
      animateScreenStopPlayback();
    }

    if (mediaRecorder.state !== "recording") {
      mediaRecorder.start();
      isRecording = true;
      continueRecording = true;
      animateScreenStartRecording();
    }
  }

  function recordButtonUp() {
    if (!mediaRecorder) { return }

    if (recordToggleHold.checked || !continueRecording) {
      mediaRecorder.stop();
      isRecording = false;

      if (isPlayingBack) { return }
      animateScreenStopRecording();
    } else {
      // When "PRESS TO START/STOP", recording starts on the `pointerdown` event and should end
      // on a `pointerup` event. However, in the first "full press" (`pointerdown` and `pointerup`)
      // of the record button, it should `continueRecording` after the `pointerup`, and only stop
      // on the second, "stop recording" press of the record button.
      continueRecording = false;
    }
  }

  playbackButton.onpointerdown = (_e) => { playbackButtonDown(); };
  playbackButton.onpointerup   = (_e) => { playbackButtonUp();   };

  recordButton.onpointerdown = (_e) => { recordButtonDown(); };
  recordButton.onpointerup   = (_e) => { recordButtonUp();   };
  // ===========================


  // Keyboard shortcuts for recording/playback ===
  // =============================================
  const recordShortcutEnabledPref   = localStorage.getItem("parrot.recordShortcutEnabled");
  const recordShortcutKeyPref       = localStorage.getItem("parrot.recordShortcutKey");
  const playbackShortcutEnabledPref = localStorage.getItem("parrot.playbackShortcutEnabled");
  const playbackShortcutKeyPref     = localStorage.getItem("parrot.playbackShortcutKey");

  const recKey = recordShortcutKeyPref   ? recordShortcutKeyPref.toLowerCase()   : "r";
  const plbKey = playbackShortcutKeyPref ? playbackShortcutKeyPref.toLowerCase() : "f";

  recordButton.setAttribute("aria-keyshortcuts", recKey.toUpperCase());
  playbackButton.setAttribute("aria-keyshortcuts", plbKey.toUpperCase());

  document.querySelector(".key.r > svg > text").textContent = recKey.toUpperCase();
  document.querySelector(".key.f > svg > text").textContent = plbKey.toUpperCase();

  let recordShortcutDisabled = false;
  if (recordShortcutEnabledPref && recordShortcutEnabledPref === "false") {
    recordShortcutDisabled = true;
    recordButton.removeChild(recordButton.children[2]);
    recordButton.setAttribute("aria-keyshortcuts", "");
  }

  let playbackShortcutDisabled = false;
  if (playbackShortcutEnabledPref && playbackShortcutEnabledPref === "false") {
    playbackShortcutDisabled = true;
    playbackButton.removeChild(playbackButton.children[1]);
    playbackButton.setAttribute("aria-keyshortcuts", "");
  }

  document.onkeydown = (e) => {
    if (e.repeat) { return }
    if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return }

    // `keydown` on "r" is equivalent to `pointerdown` on the record button, so ...
    if (e.key === recKey || (document.activeElement === recordButton && (e.key === " " || e.key === "Enter"))) {
      if (recordShortcutDisabled) { return }

      recordButton.classList.add("active");

      // ... same as `recordButton.onpointerdown`
      recordButtonDown();
    // `keydown` on "f" is equivalent to `pointerdown` on the playback button, so ...
    } else if (e.key === plbKey || (document.activeElement === playbackButton && (e.key === " " || e.key === "Enter"))) {
      if (playbackShortcutDisabled) { return }

      playbackButton.classList.add("active");

      // ... same as `playbackButton.onpointerdown`
      playbackButtonDown();
    } else if ((document.activeElement === document.getElementById("dmt-label")
                || document.activeElement === document.querySelector("footer a"))
               && (e.key === " " || e.key === "Enter")) {
      document.activeElement.click();
    } else {
      // Ignore all other keys
    }
  };
  document.onkeyup = (e) => {
    // `keyup` on "r" is equivalent to `pointerup` on the record button, so ...
    if (e.key === recKey || (document.activeElement === recordButton && (e.key === " " || e.key === "Enter"))) {
      if (recordShortcutDisabled) { return }

      recordButton.classList.remove("active");

      // ... same as `recordButton.onpointerup`
      recordButtonUp();
    // `keyup` on "f" is equivalent to `pointerup` on the playback button, so ...
    } else if (e.key === plbKey || (document.activeElement === playbackButton && (e.key === " " || e.key === "Enter"))) {
      if (playbackShortcutDisabled) { return }

      playbackButton.classList.remove("active");

      // ... same as `playbackButton.onpointerup`
      playbackButtonUp();
    } else {
      // Ignore all other keys
    }
  };
  // =============================================


  // Input cable ===
  // ===============
  let cableTouchStartY = 0;
  let cableTouchEndY   = 0;
  function handleCableTouchStart(e) { cableTouchStartY = e.changedTouches[0].screenY; }
  async function handleCableTouchEnd(e) {
    cableTouchEndY = e.changedTouches[0].screenY;
    const direction = checkDirection(cableTouchStartY, cableTouchEndY);
    if (direction === "down") {
      const accessGranted = await requestMicrophoneAccess(setMediaRecorder, null);
      if (accessGranted) {
        inputCable.classList.remove("unplugged");
        inputCable.setAttribute("aria-label", "input cable (currently plugged in)");

        microphoneSticker.classList.add("hidden");
        screenBacklight.classList.remove("off");
        screenBacklightGlow.classList.remove("off"); // For dark mode: screen glow on
        recordButton.children[0].classList.remove("off"); // For dark mode: turn button label backlight on
        playbackButton.children[0].classList.remove("off"); // For dark mode: turn button label backlight on
        audioCableIn.play();
      }
    } else if (direction === "up") {
      mediaRecorder = null;
      inputSelect.classList.add("hidden");
      inputSelect.disabled = true;
      inputCable.classList.add("unplugged");
      inputCable.setAttribute("aria-label", "input cable (currently unplugged)");
      audioCableOut.play();
    } else {
      // Ignore the "none" direction (see `onclick` instead)
    }
  }
  async function handleCableClick(e) {
    if (inputCable.classList.contains("unplugged")) {
      const accessGranted = await requestMicrophoneAccess(setMediaRecorder, null);
      if (accessGranted) {
          inputCable.classList.remove("unplugged");
          inputCable.setAttribute("aria-label", "input cable (currently plugged in)");

          microphoneSticker.classList.add("hidden");
          screenBacklight.classList.remove("off");
          screenBacklightGlow.classList.remove("off"); // For dark mode: screen glow on
          recordButton.children[0].classList.remove("off"); // For dark mode: turn button label backlight on
          playbackButton.children[0].classList.remove("off"); // For dark mode: turn button label backlight on
          audioCableIn.play();
      }
    } else {
      mediaRecorder = null;
      inputSelect.classList.add("hidden");
      inputSelect.disabled = true;
      inputCable.classList.add("unplugged");
      inputCable.setAttribute("aria-label", "input cable (currently unplugged)");
      audioCableOut.play();
    }
  }

  // For some reason `ontouch*` has to be on the interior <img> element
  inputCableImage.ontouchstart = handleCableTouchStart;
  inputCableImage.ontouchend   = handleCableTouchEnd;
  inputCable.onclick           = handleCableClick;
  // ===============


  // Input select ===
  // ================
  inputSelect.onchange = async (e) => {
    mediaRecorder.stop();
    isRecording = false;

    const selectedAudioSource = inputSelect.value;

    await requestMicrophoneAccess(setMediaRecorder, selectedAudioSource);

    // For Mac-and-iPhone users, a Mac can use the microphone of a paired iPhone. When the request
    // for a paired iPhone microphone happens, the <select>'s value does not change. This causes
    // problems switching back to the Mac's microphone, as it appears already <select>ed. So, an
    // easy fix by explicitly setting the <select>'s value; this shouldn't create other problems.
    inputSelect.value = selectedAudioSource;

    // After an input change, the user first has to click _somewhere_ on the document before the
    // record/playback buttons start working again (the toggle switches work, though); something
    // to do with `:focus` or `:active` on <select> and the buttons' `pointerup`/`pointerdown`
    // events, because the keyboard shortcuts (r/f) work (with `inputSelect.blur()`).
    // `dummyElement.click()` or whatever doesn't work; _the user_ has to click themselves.
    inputSelect.blur();
    alert("After an input change, the \"RECORD\" and \"PLAYBACK\" buttons don't work right away (unless " +
          "using the keyboard shortcuts \"r\"/\"f\"). Click or tap somewhere—anywhere—" +
          "first, then everything will work as expected.");
  };
  // ================


  // Responsive ===
  // ==============
  function getBottomOffset0(w, h) {
    const x1 = Math.floor(w / 0.578);
    const x2 = Math.ceil(w / 0.531);
    const m = (145.6 - 64) / (x2 - x1);
    const b = 145.6 - m * x2; // 9.1em * 16px = 145.6px
    const bottomOffset =  m * h + b;

    return bottomOffset;
  }

  function getBottomOffset1(h) {
    if (h > 1023) { return 12 * 16; } // 12em * 16px = 192px

    const m = (192 - 64) / (1023 - 894);
    const b = 64 - m * 894;
    const bottomOffset =  m * h + b;

    return bottomOffset;
  }

  function getBottomOffset2(w, h) {
    const at065 = Math.floor(w / 0.65);
    const high = getBottomOffset1(at065);

    const at060 = Math.ceil(w / 0.60);
    const low = 4 * 16; // 4em * 16px

    const m = (high - 64) / (at065 - at060);
    const b = high - m * at065;
    const bottomOffset = m * h + b;

    return bottomOffset;
  }

  function setMainDivBottomOffset() {
    const w = window.innerWidth;
    const h = window.innerHeight;
    const aspectRatio = w / h;

    if (0.531 <= aspectRatio && aspectRatio <= 0.578) {
      const bottomOffset = getBottomOffset0(w, h);
      mainDiv.style.bottom = `${bottomOffset}px`;
    } else if (aspectRatio >= 0.65 && 894 <= h && h <= 1023) {
      const bottomOffset = getBottomOffset1(h);
      mainDiv.style.bottom = `${bottomOffset}px`;
    } else if (0.60 <= aspectRatio && aspectRatio < 0.65 && w > 581) {
      const bottomOffset  = getBottomOffset2(w, h);
      mainDiv.style.bottom = `${bottomOffset}px`;
    } else {
      // Return to the styles defined in CSS
      mainDiv.style.bottom = null;
    }
  }

  setMainDivBottomOffset();
  window.onresize = (_e) => { setMainDivBottomOffset() };

  if (!CSS.supports("overscroll-behavior: none")) {
    document.body.addEventListener("touchmove", (e) => { e.preventDefault() }, { passive: false });
  }
  // ==============
});

// =================================================================================================

async function requestMicrophoneAccess(setMediaRecorder, deviceConstraint) {
  try {
    const audioConstraints = deviceConstraint === null ? { audio: { sampleRate: 48000 } } :
                                                         { audio: { sampleRate: 48000, deviceId: deviceConstraint } };
    const stream = await navigator.mediaDevices?.getUserMedia?.(audioConstraints);
    if (stream === undefined) {
      alert("It appears that `navigator.mediaDevices.getUserMedia` is not supported in your browser. " +
            "This means that recording audio is not possible. You can still look around and press " +
            "the buttons and flip the toggle switches, but this Web app is otherwise not functional.");
    }

    const mediaDevices = await navigator.mediaDevices?.enumerateDevices?.();
    addInputDevicesToInputSelect(mediaDevices);

    const mimeType = MediaRecorder.isTypeSupported("audio/mp4; codecs=mp4a") ?
                     "audio/mp4; codecs=mp4a" :
                     "audio/webm; codecs=opus";
    try {
      setMediaRecorder(new MediaRecorder(stream, { mimeType, audioBitsPerSecond: 256000 }));
    } catch (ex) {
      if (ex.name === "NotSupportedError") {
        alert("Oh no! Looks like your browser/system supports neither of the two requested MIME types " +
              "(`audio/mp4; codecs=mp4a` and `audio/webm; codecs=opus`). That's curious, what are you " +
              "accessing this Web app from? Please contact me to let me know!");
      } else {
        console.error(ex);
      }
    }

    return true;
  } catch (ex) {
    console.error(`The following getUserMedia error occurred: ${ex}`);

    if (ex.name === "NotAllowedError") {
      alert("Looks like you denied microphone access. That's fine, but this Web app " +
            "is pretty useless without it. Anyway, have a look around. Once you're " +
            "ready to try with the microphone, click or swipe the cable again.")
    } else if (ex.name === "AbortError" || ex.name === "NotReadableError") {
      alert(`Oh no! Unable to use the microphone for recording: ${ex}`);
    } else if (ex.name === "InvalidStateError") {
      alert(`Oh no! The document must be fully active to request microphone access: ${ex}`);
    } else if (ex.name === "SecurityError") {
      alert(`Oh no! Looks like you have media support disabled in your browser: ${ex}`);
    } else if (ex.name === "NotFoundError" || ex.name === "OverconstrainedError") {
      alert(`Oh no! Unable to find a suitable microphone: ${ex}`);
    } else {
      // TypeError
      alert(`This shouldn't happen, but it did (probably in an "insecure context"): ${ex}`);
    }

    return false;
  }
}

function addInputDevicesToInputSelect(mediaDevices) {
  const inputSelect = document.getElementById("input-select");
  inputSelect.replaceChildren();
  mediaDevices.forEach((device) => {
    if (device.kind === "audioinput") {
        const option = document.createElement("option");
        option.value = device.deviceId;
        option.text = device.label || `Microphone ${inputSelect.length + 1}`;
        inputSelect.appendChild(option);
    }
  })
  inputSelect.classList.remove("hidden");
  inputSelect.disabled = false;
}

function checkDirection(touchstartY, touchendY) {
  // Smallest change to consider `touchendY` != `touchstartY` is about 20px (on iPhone)
  if (touchendY < touchstartY) { return "up" }
  if (touchendY > touchstartY) { return "down" }
  // When `touchendY` == `touchstartY` (below the "swiped" threshold), the toggle switch `<button>`
  // is also considered `click`ed. To prevent a double flip of the switch (one from the
  // not-big-enough "swipe" and the other from the `click`), return a "none" to prevent triggering
  // a `toggleUp()` or `toggleDown()` from the not-big-enough "swipe" in `operateToggleSwitch(...)`.
  return "none";
}

function operateToggleSwitch(e, direction, quiet) {
  // Bubble up to the parent <div .toggle>
  let target = e instanceof Event ? e.target : e; // otherwise directly an HTMLElement
  while (target.localName != "button") {
    target = target.parentElement;
  }
  const toggleRoot = target.parentElement;

  let toggleType = null;
  if (toggleRoot.classList.contains("playback")) {
    toggleType = "playback";
  } else {
    toggleType = "record";
  }

  const pressCheckbox = toggleRoot.querySelector(`input[name="${toggleType}-toggle-press"]`);
  const holdCheckbox  = toggleRoot.querySelector(`input[name="${toggleType}-toggle-hold"]`);

  const currentState = pressCheckbox.checked && !holdCheckbox.checked ? "up" : "down";

  if ((direction === "up"   || direction === "flip") && currentState === "down") {
    toggleUp();
    pressCheckbox.checked = true;
    holdCheckbox.checked  = false;
  }
  if ((direction === "down" || direction === "flip") && currentState === "up") {
    toggleDown();
    pressCheckbox.checked = false;
    holdCheckbox.checked  = true;
  }

  async function toggleUp() {
    if (!quiet) {
      try {
        await document.getElementById("switch-up").play()
      } catch (ex) {
        if (ex.name === "NotAllowedError") {
          console.log("Toggle up: not allowed to play sound until the toggle has been clicked first.")
        }
      }
    }

    const ariaLabelText = `Toggle switch for ${toggleType} button mode. Currently in press to start/stop mode. Activate to switch to hold to ${toggleType} mode.`;

    const toggleButton  = toggleRoot.querySelector("button");
    toggleButton.classList.remove("down");
    toggleButton.classList.add("up");
    toggleButton.setAttribute("aria-label", ariaLabelText);

    const toggleShaftImage = toggleRoot.querySelector("img");
    toggleShaftImage.classList.remove("shaft-down");
    toggleShaftImage.classList.add("shaft-up");
    toggleShaftImage.src = "toggle-shaft-up.webp";

    const toggleSleeveShadow = toggleRoot.querySelector("div.sleeveshadow-down");
    toggleSleeveShadow.classList.remove("sleeveshadow-down");
    toggleSleeveShadow.classList.add("sleeveshadow-up");

    localStorage.setItem(`parrot.${toggleType}Toggle`, "up");
  }

  async function toggleDown() {
    if (!quiet) {
      try {
        await document.getElementById("switch-down").play()
      } catch (ex) {
        if (ex.name === "NotAllowedError") {
          console.log("Toggle down: not allowed to play sound until the toggle has been clicked first.")
        }
      }
    }

    const ariaLabelText = `Toggle switch for ${toggleType} button mode. Currently in hold to ${toggleType} mode. Activate to switch to press to start/stop mode.`;

    const toggleButton  = toggleRoot.querySelector("button");
    toggleButton.classList.remove("up");
    toggleButton.classList.add("down");
    toggleButton.setAttribute("aria-label", ariaLabelText);

    const toggleShaftImage = toggleRoot.querySelector("img");
    toggleShaftImage.classList.remove("shaft-up");
    toggleShaftImage.classList.add("shaft-down");
    toggleShaftImage.src = "toggle-shaft-down.webp";

    const toggleSleeveShadow = toggleRoot.querySelector("div.sleeveshadow-up");
    toggleSleeveShadow.classList.remove("sleeveshadow-up");
    toggleSleeveShadow.classList.add("sleeveshadow-down");

    localStorage.setItem(`parrot.${toggleType}Toggle`, "down");
  }
}

function loadToggleStates(htmlElements) {
  ["playback", "record"].forEach((value, i) => {
    const pressCheckbox =
      htmlElements[i].parentElement.querySelector(`input[name="${value}-toggle-press"]`);
    const holdCheckbox  =
      htmlElements[i].parentElement.querySelector(`input[name="${value}-toggle-hold"]`);

    const currentState = pressCheckbox.checked && !holdCheckbox.checked ? "up" : "down";
    const toggleStatePreference = localStorage.getItem(`parrot.${value}Toggle`);
    if (toggleStatePreference && toggleStatePreference !== currentState) {
      operateToggleSwitch(htmlElements[i], "flip", true);
    }
  })
}
