/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/**
 * Bug 1713710 - Shim Vidible video player
 *
 * Sites relying on Vidible's video player may experience broken videos if that
 * script is blocked. This shim allows users to opt into viewing those videos
 * regardless of any tracking consequences, by providing placeholders for each.
 */

if (!window.vidible?.version) {
  const PlayIconURL = "https://smartblock.firefox.etp/play.svg";

  const originalScript = document.currentScript.src;

  const getGUID = () => {
    const v = crypto.getRandomValues(new Uint8Array(20));
    return Array.from(v, c => c.toString(16)).join("");
  };

  const sendMessageToAddon = (function() {
    const shimId = "Vidible";
    const pendingMessages = new Map();
    const channel = new MessageChannel();
    channel.port1.onerror = console.error;
    channel.port1.onmessage = event => {
      const { messageId, response } = event.data;
      const resolve = pendingMessages.get(messageId);
      if (resolve) {
        pendingMessages.delete(messageId);
        resolve(response);
      }
    };
    function reconnect() {
      const detail = {
        pendingMessages: [...pendingMessages.values()],
        port: channel.port2,
        shimId,
      };
      window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
    }
    window.addEventListener("ShimHelperReady", reconnect);
    reconnect();
    return function(message) {
      const messageId = getGUID();
      return new Promise(resolve => {
        const payload = { message, messageId, shimId };
        pendingMessages.set(messageId, resolve);
        channel.port1.postMessage(payload);
      });
    };
  })();

  const Shimmer = (function() {
    // If a page might store references to an object before we replace it,
    // ensure that it only receives proxies to that object created by
    // `Shimmer.proxy(obj)`. Later when the unshimmed object is created,
    // call `Shimmer.unshim(proxy, unshimmed)`. This way the references
    // will automatically "become" the unshimmed object when appropriate.

    const shimmedObjects = new WeakMap();
    const unshimmedObjects = new Map();

    function proxy(shim) {
      if (shimmedObjects.has(shim)) {
        return shimmedObjects.get(shim);
      }

      const prox = new Proxy(shim, {
        get: (target, k) => {
          if (unshimmedObjects.has(prox)) {
            return unshimmedObjects.get(prox)[k];
          }
          return target[k];
        },
        apply: (target, thisArg, args) => {
          if (unshimmedObjects.has(prox)) {
            return unshimmedObjects.get(prox)(...args);
          }
          return target.apply(thisArg, args);
        },
        construct: (target, args) => {
          if (unshimmedObjects.has(prox)) {
            return new unshimmedObjects.get(prox)(...args);
          }
          return new target(...args);
        },
      });
      shimmedObjects.set(shim, prox);
      shimmedObjects.set(prox, prox);

      for (const key in shim) {
        const value = shim[key];
        if (typeof value === "function") {
          shim[key] = function() {
            const unshimmed = unshimmedObjects.get(prox);
            if (unshimmed) {
              return unshimmed[key].apply(unshimmed, arguments);
            }
            return value.apply(this, arguments);
          };
        } else if (typeof value !== "object" || value === null) {
          shim[key] = value;
        } else {
          shim[key] = Shimmer.proxy(value);
        }
      }

      return prox;
    }

    function unshim(shim, unshimmed) {
      unshimmedObjects.set(shim, unshimmed);

      for (const prop in shim) {
        if (prop in unshimmed) {
          const un = unshimmed[prop];
          if (typeof un === "object" && un !== null) {
            unshim(shim[prop], un);
          }
        } else {
          unshimmedObjects.set(shim[prop], undefined);
        }
      }
    }

    return { proxy, unshim };
  })();

  const extras = [];
  const playersByNode = new WeakMap();
  const playerData = new Map();

  const getJSONPVideoPlacements = () => {
    return document.querySelectorAll(
      `script[src*="delivery.vidible.tv/jsonp"]`
    );
  };

  const allowVidible = () => {
    if (allowVidible.promise) {
      return allowVidible.promise;
    }

    const shim = window.vidible;
    window.vidible = undefined;

    allowVidible.promise = sendMessageToAddon("optIn")
      .then(() => {
        return new Promise((resolve, reject) => {
          const script = document.createElement("script");
          script.src = originalScript;
          script.addEventListener("load", () => {
            Shimmer.unshim(shim, window.vidible);

            for (const args of extras) {
              window.visible.registerExtra(...args);
            }

            for (const jsonp of getJSONPVideoPlacements()) {
              const { src } = jsonp;
              const jscript = document.createElement("script");
              jscript.onload = resolve;
              jscript.src = src;
              jsonp.replaceWith(jscript);
            }

            for (const [playerShim, data] of playerData.entries()) {
              const { loadCalled, on, parent, placeholder, setup } = data;

              placeholder?.remove();

              const player = window.vidible.player(parent);
              Shimmer.unshim(playerShim, player);

              for (const [type, fns] of on.entries()) {
                for (const fn of fns) {
                  try {
                    player.on(type, fn);
                  } catch (e) {
                    console.error(e);
                  }
                }
              }

              if (setup) {
                player.setup(setup);
              }

              if (loadCalled) {
                player.load();
              }
            }

            resolve();
          });

          script.addEventListener("error", () => {
            script.remove();
            reject();
          });

          document.head.appendChild(script);
        });
      })
      .catch(() => {
        window.vidible = shim;
        delete allowVidible.promise;
      });

    return allowVidible.promise;
  };

  const createVideoPlaceholder = (service, callback) => {
    const placeholder = document.createElement("div");
    placeholder.style = `
      position: absolute;
      width: 100%;
      height: 100%;
      min-width: 160px;
      min-height: 100px;
      top: 0px;
      left: 0px;
      background: #000;
      color: #fff;
      text-align: center;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      background-image: url(${PlayIconURL});
      background-position: 50% 47.5%;
      background-repeat: no-repeat;
      background-size: 25% 25%;
      -moz-text-size-adjust: none;
      -moz-user-select: none;
      color: #fff;
      align-items: center;
      padding-top: 200px;
      font-size: 14pt;
    `;
    placeholder.textContent = `Click to allow blocked ${service} content`;
    placeholder.addEventListener("click", evt => {
      evt.isTrusted && callback();
    });
    return placeholder;
  };

  const Player = function(parent) {
    const existing = playersByNode.get(parent);
    if (existing) {
      return existing;
    }

    const player = Shimmer.proxy(this);
    playersByNode.set(parent, player);

    const placeholder = createVideoPlaceholder("Vidible", allowVidible);
    parent.parentNode.insertBefore(placeholder, parent);

    playerData.set(player, {
      on: new Map(),
      parent,
      placeholder,
    });
    return player;
  };

  const changeData = function(fn) {
    const data = playerData.get(this);
    if (data) {
      fn(data);
      playerData.set(this, data);
    }
  };

  Player.prototype = {
    addEventListener() {},
    destroy() {
      const { placeholder } = playerData.get(this);
      placeholder?.remove();
      playerData.delete(this);
    },
    dispatchEvent() {},
    getAdsPassedTime() {},
    getAllMacros() {},
    getCurrentTime() {},
    getDuration() {},
    getHeight() {},
    getPixelsLog() {},
    getPlayerContainer() {},
    getPlayerInfo() {},
    getPlayerStatus() {},
    getRequestsLog() {},
    getStripUrl() {},
    getVolume() {},
    getWidth() {},
    hidePlayReplayControls() {},
    isMuted() {},
    isPlaying() {},
    load() {
      changeData(data => (data.loadCalled = true));
    },
    mute() {},
    on(type, fn) {
      changeData(({ on }) => {
        if (!on.has(type)) {
          on.set(type, new Set());
        }
        on.get(type).add(fn);
      });
    },
    off(type, fn) {
      changeData(({ on }) => {
        on.get(type)?.delete(fn);
      });
    },
    overrideMacro() {},
    pause() {},
    play() {},
    playVideoByIndex() {},
    removeEventListener() {},
    seekTo() {},
    sendBirthDate() {},
    sendKey() {},
    setup(s) {
      changeData(data => (data.setup = s));
      return this;
    },
    setVideosToPlay() {},
    setVolume() {},
    showPlayReplayControls() {},
    toggleFullscreen() {},
    toggleMute() {},
    togglePlay() {},
    updateBid() {},
    version() {},
    volume() {},
  };

  const vidible = {
    ADVERT_CLOSED: "advertClosed",
    AD_END: "adend",
    AD_META: "admeta",
    AD_PAUSED: "adpaused",
    AD_PLAY: "adplay",
    AD_START: "adstart",
    AD_TIMEUPDATE: "adtimeupdate",
    AD_WAITING: "adwaiting",
    AGE_GATE_DISPLAYED: "agegatedisplayed",
    BID_UPDATED: "BidUpdated",
    CAROUSEL_CLICK: "CarouselClick",
    CONTEXT_ENDED: "contextended",
    CONTEXT_STARTED: "contextstarted",
    ENTER_FULLSCREEN: "playerenterfullscreen",
    EXIT_FULLSCREEN: "playerexitfullscreen",
    FALLBACK: "fallback",
    FLOAT_END_ACTION: "floatended",
    FLOAT_START_ACTION: "floatstarted",
    HIDE_PLAY_REPLAY_BUTTON: "hideplayreplaybutton",
    LIGHTBOX_ACTIVATED: "lightboxactivated",
    LIGHTBOX_DEACTIVATED: "lightboxdeactivated",
    MUTE: "Mute",
    PLAYER_CONTROLS_STATE_CHANGE: "playercontrolsstatechaned",
    PLAYER_DOCKED: "playerDocked",
    PLAYER_ERROR: "playererror",
    PLAYER_FLOATING: "playerFloating",
    PLAYER_READY: "playerready",
    PLAYER_RESIZE: "playerresize",
    PLAYLIST_END: "playlistend",
    SEEK_END: "SeekEnd",
    SEEK_START: "SeekStart",
    SHARE_SCREEN_CLOSED: "sharescreenclosed",
    SHARE_SCREEN_OPENED: "sharescreenopened",
    SHOW_PLAY_REPLAY_BUTTON: "showplayreplaybutton",
    SUBTITLES_DISABLED: "subtitlesdisabled",
    SUBTITLES_ENABLED: "subtitlesenabled",
    SUBTITLES_READY: "subtitlesready",
    UNMUTE: "Unmute",
    VIDEO_DATA_LOADED: "videodataloaded",
    VIDEO_END: "videoend",
    VIDEO_META: "videometadata",
    VIDEO_MODULE_CREATED: "videomodulecreated",
    VIDEO_PAUSE: "videopause",
    VIDEO_PLAY: "videoplay",
    VIDEO_SEEKEND: "videoseekend",
    VIDEO_SELECTED: "videoselected",
    VIDEO_START: "videostart",
    VIDEO_TIMEUPDATE: "videotimeupdate",
    VIDEO_VOLUME_CHANGED: "videovolumechanged",
    VOLUME: "Volume",
    _getContexts: () => [],
    "content.CLICK": "content.click",
    "content.IMPRESSION": "content.impression",
    "content.QUARTILE": "content.quartile",
    "content.VIEW": "content.view",
    createPlayer: parent => new Player(parent),
    createPlayerAsync: parent => new Player(parent),
    createVPAIDPlayer: parent => new Player(parent),
    destroyAll() {},
    extension() {},
    getContext() {},
    player: parent => new Player(parent),
    playerInceptionTime() {
      return { undefined: 1620149827713 };
    },
    registerExtra(a, b, c) {
      extras.push([a, b, c]);
    },
    version: () => "21.1.313",
  };

  window.vidible = Shimmer.proxy(vidible);

  for (const jsonp of getJSONPVideoPlacements()) {
    const player = new Player(jsonp);
    const { placeholder } = playerData.get(player);
    jsonp.parentNode.insertBefore(placeholder, jsonp);
  }
}
