const stashListener = new EventTarget();

const { fetch: originalFetch } = window;

const baseURL = document.querySelector("base")?.getAttribute("href") ?? "/";

window.fetch = async (...args) => {
  let [resource, config] = args;
  // request interceptor here
  const response = await originalFetch(resource, config);
  // response interceptor here
  const contentType = response.headers.get("content-type");
  if (
    contentType &&
    contentType.indexOf("application/json") !== -1 &&
    resource.endsWith("/graphql")
  ) {
    try {
      const data = await response.clone().json();
      stashListener.dispatchEvent(
        new CustomEvent("response", {
          detail: data,
        })
      );
    } catch (e) {}
  }
  return response;
};

class Logger {
  constructor(enabled) {
    this.enabled = enabled;
  }
  debug() {
    if (!this.enabled) return;
    console.debug(...arguments);
  }
}

class Stash extends EventTarget {
  constructor({
    pageUrlCheckInterval = 100,
    detectReRenders = true,
    logging = false,
  } = {}) {
    super();
    this.log = new Logger(logging);
    this._pageUrlCheckInterval = pageUrlCheckInterval;
    this._detectReRenders = detectReRenders;
    this._lastPathStr = "";
    this._lastQueryStr = "";
    this._lastHref = "";
    this._lastStashPageEvent = "";
    this.waitForElement(this._detectReRenders ? ".main > div" : "html").then(
      () => {
        this._pageURLCheckTimerId = setInterval(() => {
          // Loop every 100 ms
          if (
            this._lastPathStr !== location.pathname ||
            this._lastHref !== location.href ||
            this._lastQueryStr !== location.search ||
            (!document.querySelector(".main > div[stashUserscriptLibrary]") &&
              this._detectReRenders)
          ) {
            this._dispatchPageEvent("stash:page", false);

            this._handlePageChange({
              lastPathStr: this._lastPathStr,
              lastQueryStr: this._lastQueryStr,
              lastHref: this._lastHref,
              lastStashPageEvent: this._lastStashPageEvent,
            });

            this._lastPathStr = location.pathname;
            this._lastQueryStr = location.search;
            this._lastHref = location.href;

            if (this._detectReRenders) {
              this.waitForElement(".main > div", 10000).then((element) => {
                if (element) element.setAttribute("stashUserscriptLibrary", "");
              });
            }
          }
        }, this._pageUrlCheckInterval);
      }
    );
    stashListener.addEventListener("response", (evt) => {
      this.processRemoteScenes(evt.detail);
      this.processScene(evt.detail);
      this.processScenes(evt.detail);
      this.processStudios(evt.detail);
      this.processPerformers(evt.detail);
      this.dispatchEvent(
        new CustomEvent("stash:response", {
          detail: evt.detail,
        })
      );
    });
    this.version = [0, 0, 0];
    this.getVersion();
    this.remoteScenes = {};
    this.scenes = {};
    this.studios = {};
    this.performers = {};
    this.userscripts = [];
    this._pageListeners = {};
    this._initDefaultPageListeners();
  }
  async getVersion() {
    const reqData = {
      operationName: "",
      variables: {},
      query: `query version {
                version {
                    version
                }
            }`,
    };
    const data = await this.callGQL(reqData);
    const versionString = data.data.version.version;
    this.version = versionString
      .substring(1)
      .split(".")
      .map((o) => parseInt(o));
  }
  compareVersion(minVersion) {
    let [currMajor, currMinor, currPatch = 0] = this.version;
    let [minMajor, minMinor, minPatch = 0] = minVersion
      .split(".")
      .map((i) => parseInt(i));
    if (currMajor > minMajor) return 1;
    if (currMajor < minMajor) return -1;
    if (currMinor > minMinor) return 1;
    if (currMinor < minMinor) return -1;
    return 0;
  }
  async runPluginTask(pluginId, taskName, args = []) {
    const reqData = {
      operationName: "RunPluginTask",
      variables: {
        plugin_id: pluginId,
        task_name: taskName,
        args: args,
      },
      query:
        "mutation RunPluginTask($plugin_id: ID!, $task_name: String!, $args: [PluginArgInput!]) {\n  runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args: $args)\n}\n",
    };
    return this.callGQL(reqData);
  }
  async callGQL(reqData) {
    const options = {
      method: "POST",
      body: JSON.stringify(reqData),
      headers: {
        "Content-Type": "application/json",
      },
    };

    try {
      const res = await window.fetch(baseURL + "graphql", options);
      this.log.debug(res);
      return res.json();
    } catch (err) {
      console.error(err);
    }
  }
  async getFreeOnesStats(link) {
    try {
      const doc = await fetch(link)
        .then(function (response) {
          // When the page is loaded convert it to text
          return response.text();
        })
        .then(function (html) {
          // Initialize the DOM parser
          var parser = new DOMParser();

          // Parse the text
          var doc = parser.parseFromString(html, "text/html");

          // You can now even select part of that html as you would in the regular DOM
          // Example:
          // var docArticle = doc.querySelector('article').innerHTML;

          console.log(doc);
          return doc;
        })
        .catch(function (err) {
          console.log("Failed to fetch page: ", err);
        });

      var data = new Object();
      data.rank = doc.querySelector("rank-chart-button");
      console.log(data.rank);
      data.views = doc.querySelector(
        ".d-none.d-m-flex.flex-column.align-items-center.global-header > div.font-weight-bold"
      ).textContent;
      data.votes = "0";
      return JSON.stringify(data);
    } catch (err) {
      console.error(err);
    }
  }
  async getPlugins() {
    const reqData = {
      operationName: "Plugins",
      variables: {},
      query: `query Plugins {
              plugins {
                id
                name
                description
                url
                version
                tasks {
                  name
                  description
                  __typename
                }
                hooks {
                  name
                  description
                  hooks
                }
              }
            }
            `,
    };
    return this.callGQL(reqData);
  }
  async getStashBoxes() {
    const reqData = {
      operationName: "Configuration",
      variables: {},
      query: `query Configuration {
                        configuration {
                          general {
                            stashBoxes {
                              endpoint
                              api_key
                              name
                            }
                          }
                        }
                      }`,
    };
    return this.callGQL(reqData);
  }
  async getApiKey() {
    const reqData = {
      operationName: "Configuration",
      variables: {},
      query: `query Configuration {
                        configuration {
                          general {
                            apiKey
                          }
                        }
                      }`,
    };
    return this.callGQL(reqData);
  }
  matchUrl(href, fragment) {
    const regexp = concatRegexp(
      new RegExp(window.location.origin + baseURL),
      fragment
    );
    return href.match(regexp) != null;
  }
  get serverUrl() {
    return window.location.origin + baseURL;
  }
  async waitForElement(
    selector,
    timeout = null,
    location = document.body,
    disconnectOnPageChange = false
  ) {
    return new Promise((resolve) => {
      if (document.querySelector(selector)) {
        return resolve(document.querySelector(selector));
      }

      const observer = new MutationObserver(async () => {
        if (document.querySelector(selector)) {
          resolve(document.querySelector(selector));
          observer.disconnect();
        } else {
          if (timeout) {
            async function timeOver() {
              return new Promise((resolve) => {
                setTimeout(() => {
                  observer.disconnect();
                  resolve(false);
                }, timeout);
              });
            }
            resolve(await timeOver());
          }
        }
      });

      observer.observe(location, {
        childList: true,
        subtree: true,
      });

      if (disconnectOnPageChange) {
        const stash = this;
        function disconnect() {
          resolve(false);
          observer.disconnect();
          stash.removeEventListener("stash:page", disconnect);
        }
        stash.addEventListener("stash:page", disconnect);
      }
    });
  }
  async waitForElementDeath(
    selector,
    location = document.body,
    disconnectOnPageChange = false
  ) {
    return new Promise((resolve) => {
      const observer = new MutationObserver(async () => {
        if (!document.querySelector(selector)) {
          resolve(true);
          observer.disconnect();
        }
      });

      observer.observe(location, {
        childList: true,
        subtree: true,
      });

      if (disconnectOnPageChange) {
        const stash = this;
        function disconnect() {
          resolve(false);
          observer.disconnect();
          stash.removeEventListener("stash:page", disconnect);
        }
        stash.addEventListener("stash:page", disconnect);
      }
    });
  }
  async _listenForNonPageChanges({
    selector = "",
    location = document.body,
    listenType = "",
    event = "",
    recursive = false,
    reRunHandlePageChange = false,
    listenDefaultTab = true,
    callback = () => {},
  } = {}) {
    if (recursive) return;

    if (listenType === "tabs") {
      const tabsContainer = await this.waitForElement(
        selector,
        10000,
        location,
        true
      );
      const stash = this;
      let previousEvent = "";

      function listenForTabClicks(domEvent) {
        const clickedChild = domEvent.target ? domEvent.target : domEvent;

        if (!clickedChild.classList?.contains("nav-link")) return;

        const tagName = clickedChild.getAttribute("data-rb-event-key");
        const parentEvent = tagName.split("-")[0];
        const childEvent = tagName.split("-").slice(1, -1).join("-");

        event = `stash:page:${parentEvent}:${childEvent}`;

        if (previousEvent === event) return;
        previousEvent = event;

        stash._dispatchPageEvent(`stash:page:any:${childEvent}`, false);
        stash._dispatchPageEvent(event);
      }

      if (listenDefaultTab)
        listenForTabClicks(tabsContainer.querySelector(".nav-link.active"));

      tabsContainer.addEventListener("click", listenForTabClicks);

      function removeEventListenerOnPageChange() {
        tabsContainer.removeEventListener("click", listenForTabClicks);
        stash.removeEventListener(
          "stash:page",
          removeEventListenerOnPageChange
        );
      }
      stash.addEventListener("stash:page", removeEventListenerOnPageChange);
    } else if (await this.waitForElement(selector, null, location, true)) {
      this._dispatchPageEvent(event);

      if (await this.waitForElementDeath(selector, location, true)) {
        if (
          this._lastPathStr === window.location.pathname &&
          reRunHandlePageChange
        ) {
          // triggered after home, performer, studio, tag's edit page close
          this._handlePageChange({
            recursive: true,
            lastPathStr: this._lastPathStr,
            lastQueryStr: this._lastQueryStr,
            lastHref: this._lastHref,
            lastStashPageEvent: this._lastStashPageEvent,
          });
        }
      }
    }

    callback();
  }
  _dispatchPageEvent(event, addToHistory = true) {
    this.dispatchEvent(
      new CustomEvent(event, {
        detail: {
          event: event,
          lastEventState: {
            lastPathStr: this._lastPathStr,
            lastQueryStr: this._lastQueryStr,
            lastHref: this._lastHref,
            lastStashPageEvent: this._lastStashPageEvent,
          },
        },
      })
    );

    if (addToHistory) {
      this.log.debug(`[Navigation] ${event}`);
      if (event.startsWith("stash:")) {
        this._lastStashPageEvent = event;
      }
    }

    // if (event!=="stash:page" && !addToHistory) this.log.debug(`[Navigation] ${event}`); // log ":any:" events
  }
  addPageListener(eventData) {
    const {
      event,
      regex,
      callback = () => {},
      manuallyHandleDispatchEvent = false,
    } = eventData;
    if (
      event &&
      !event?.startsWith("stash:") &&
      regex &&
      this._pageListeners[event] === undefined
    ) {
      this._pageListeners[event] = {
        regex: regex,
        callback: callback,
        manuallyHandleDispatchEvent: manuallyHandleDispatchEvent,
      };

      return event;
    } else {
      if (this._pageListeners[event] !== undefined) {
        console.error(`Can't add page listener: Event ${event} already exists`);
      } else if (event?.startsWith("stash:")) {
        console.error(
          `Can't add page listener: Event name can't start with "stash:"`
        );
      } else {
        console.error(
          `Can't add page listener: Missing required argument(s) "event", "regex"`
        );
      }

      return false;
    }
  }
  removePageListener(event) {
    if (event && !event?.startsWith("stash:") && this._pageListeners[event]) {
      delete this._pageListeners[event];
      return event;
    } else {
      if (this._pageListeners[event] === undefined && event) {
        console.error(
          `Can't remove page listener: Event ${event} doesn't exists`
        );
      } else if (event?.startsWith("stash:")) {
        console.error(
          `Can't remove page listener: Event ${event} is a built in event`
        );
      } else {
        console.error(`Can't remove page listener: Missing "event" argument`);
      }

      return false;
    }
  }
  stopPageListener() {
    clearInterval(this._pageURLCheckTimerId);
  }
  _initDefaultPageListeners() {
    this._pageListeners = {
      // scenes tab
      "stash:page:scenes": {
        regex: /scenes\?/,
        manuallyHandleDispatchEvent: true,
        handleDisplayView: "ignoreDisplayViewCondition",
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
            this.processTagger();
          }
        },
      },
      "stash:page:scene:new": {
        regex: /scenes\/new/,
      },
      "stash:page:scene": {
        regex: /scenes\/\d+\?/,
        callback: ({ recursive = false }) =>
          this._listenForNonPageChanges({
            selector: ".scene-tabs .nav-tabs",
            listenType: "tabs",
            recursive: recursive,
          }),
      },

      // images tab
      "stash:page:images": {
        regex: /images\?/,
        handleDisplayView: true,
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:image": {
        regex: /images\/\d+/,
        callback: ({ recursive = false }) =>
          this._listenForNonPageChanges({
            selector: ".image-tabs .nav-tabs",
            listenType: "tabs",
            recursive: recursive,
          }),
      },

      // movies tab
      "stash:page:movies": {
        regex: /movies\?/,
      },
      "stash:page:movie": {
        regex: /movies\/\d+/,
      },
      "stash:page:movie:scenes": {
        regex: /movies\/\d+\?/,
        callback: () => this.processTagger(),
      },

      // markers tab
      "stash:page:markers": {
        regex: /scenes\/markers/,
      },

      // galleries tab
      "stash:page:galleries": {
        regex: /galleries\?/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:gallery:new": {
        regex: /galleries\/new/,
      },
      "stash:page:gallery:images": {
        regex: /galleries\/\d+\?/,
        manuallyHandleDispatchEvent: true,
        handleDisplayView: "ignoreDisplayViewCondition",
        callback: ({ lastHref, lastPathStr, recursive = false, event }) => {
          if (
            !this.matchUrl(lastHref, /\/galleries\/\d+\//) &&
            lastPathStr !== window.location.pathname
          ) {
            this._dispatchPageEvent("stash:page:gallery");
            this._listenForNonPageChanges({
              selector: ".gallery-tabs .nav-tabs .nav-link.active",
              event: "stash:page:gallery:details",
              recursive: recursive,
            });
          }

          this._dispatchPageEvent(event);

          this._listenForNonPageChanges({
            selector: ".gallery-tabs .nav-tabs",
            listenType: "tabs",
            recursive: recursive,
            listenDefaultTab: false,
          });
        },
      },
      "stash:page:gallery:add": {
        regex: /galleries\/\d+\/add/,
        manuallyHandleDispatchEvent: true,
        handleDisplayView: "ignoreDisplayViewCondition",
        callback: ({ lastHref, lastPathStr, recursive = false, event }) => {
          if (
            !this.matchUrl(lastHref, /\/galleries\/\d+/) &&
            lastPathStr !== window.location.pathname
          ) {
            this._dispatchPageEvent("stash:page:gallery");
            this._listenForNonPageChanges({
              selector: ".gallery-tabs .nav-tabs .nav-link.active",
              event: "stash:page:gallery:details",
              recursive: recursive,
            });
          }

          this._dispatchPageEvent(event);

          this._listenForNonPageChanges({
            selector: ".gallery-tabs .nav-tabs",
            listenType: "tabs",
            recursive: recursive,
            listenDefaultTab: false,
          });
        },
      },

      // performers tab
      "stash:page:performers": {
        regex: /performers\?/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex) || this._detectReRenders) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:performer:new": {
        regex: /performers\/new/,
      },
      "stash:page:performer": {
        regex: /performers\/\d+/,
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
            this.processTagger();
          }

          this._listenForNonPageChanges({
            selector: "#performer-edit",
            event: "stash:page:performer:edit",
            reRunHandlePageChange: true,
            callback: () =>
              this._detectReRenders ? this._dispatchPageEvent(event) : null,
          });
        },
      },
      "stash:page:performer:scenes": {
        regex: /performers\/\d+\?/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:performer:galleries": {
        regex: /performers\/\d+\/galleries/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:performer:images": {
        regex: /performers\/\d+\/images/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:performer:movies": {
        regex: /performers\/\d+\/movies/,
      },
      "stash:page:performer:appearswith": {
        regex: /performers\/\d+\/appearswith/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
            this.processTagger();
          }
        },
      },

      // studios tab
      "stash:page:studios": {
        regex: /studios\?/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:studio:new": {
        regex: /studios\/new/,
      },
      "stash:page:studio": {
        regex: /studios\/\d+/,
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
            this.processTagger();
          }

          this._listenForNonPageChanges({
            selector: "#studio-edit",
            event: "stash:page:studio:edit",
            reRunHandlePageChange: true,
            callback: () =>
              this._detectReRenders ? this._dispatchPageEvent(event) : null,
          });
        },
      },
      "stash:page:studio:scenes": {
        regex: /studios\/\d+\?/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:studio:galleries": {
        regex: /studios\/\d+\/galleries/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:studio:images": {
        regex: /studios\/\d+\/images/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:studio:performers": {
        regex: /studios\/\d+\/performers/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:studio:movies": {
        regex: /studios\/\d+\/movies/,
      },
      "stash:page:studio:childstudios": {
        regex: /studios\/\d+\/childstudios/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },

      // tags tab
      "stash:page:tags": {
        regex: /tags\?/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:tag:new": {
        regex: /tags\/new/,
      },
      "stash:page:tag": {
        regex: /tags\/\d+/,
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
            this.processTagger();
          }

          this._listenForNonPageChanges({
            selector: "#tag-edit",
            event: "stash:page:tag:edit",
            reRunHandlePageChange: true,
            callback: () =>
              this._detectReRenders ? this._dispatchPageEvent(event) : null,
          });
        },
      },
      "stash:page:tag:scenes": {
        regex: /tags\/\d+\?/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:tag:galleries": {
        regex: /tags\/\d+\/galleries/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:tag:images": {
        regex: /tags\/\d+\/images/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:tag:markers": {
        regex: /tags\/\d+\/markers/,
      },
      "stash:page:tag:performers": {
        regex: /tags\/\d+\/performers/,
        handleDisplayView: "ignoreDisplayViewCondition",
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },

      // settings page
      "stash:page:settings": {
        regex: /settings/,
        manuallyHandleDispatchEvent: true,
        callback: ({ lastHref, event, regex }) => {
          if (!this.matchUrl(lastHref, regex)) {
            this._dispatchPageEvent(event);
          }
        },
      },
      "stash:page:settings:tasks": {
        regex: /settings\?tab=tasks/,
      },
      "stash:page:settings:library": {
        regex: /settings\?tab=library/,
      },
      "stash:page:settings:interface": {
        regex: /settings\?tab=interface/,
      },
      "stash:page:settings:security": {
        regex: /settings\?tab=security/,
      },
      "stash:page:settings:metadata-providers": {
        regex: /settings\?tab=metadata-providers/,
      },
      "stash:page:settings:services": {
        regex: /settings\?tab=services/,
      },
      "stash:page:settings:system": {
        regex: /settings\?tab=system/,
      },
      "stash:page:settings:plugins": {
        regex: /settings\?tab=plugins/,
      },
      "stash:page:settings:logs": {
        regex: /settings\?tab=logs/,
      },
      "stash:page:settings:tools": {
        regex: /settings\?tab=tools/,
      },
      "stash:page:settings:changelog": {
        regex: /settings\?tab=changelog/,
      },
      "stash:page:settings:about": {
        regex: /settings\?tab=about/,
      },

      // stats page
      "stash:page:stats": {
        regex: /stats/,
      },

      // home page
      "stash:page:home": {
        regex: /$/,
        callback: () =>
          this._listenForNonPageChanges({
            selector: ".recommendations-container-edit",
            event: "stash:page:home:edit",
            reRunHandlePageChange: true,
          }),
      },
    };
  }
  _handlePageChange(args) {
    const events = Object.keys(this._pageListeners);

    for (const event of events) {
      const {
        regex,
        callback = () => {},
        manuallyHandleDispatchEvent = false,
        handleDisplayView = false,
      } = this._pageListeners[event];

      let isDisplayViewPage = false;
      let isGridPage, isListPage, isWallPage, isTaggerPage;

      const splitEvent = event.split(":");
      const tabPage = { page: "", tab: "" };
      let childAnyEventCondition = false;

      if (splitEvent.length === 4) {
        childAnyEventCondition = true;
        tabPage.page = splitEvent[2];
        tabPage.tab = splitEvent[3];
      }

      splitEvent.pop();

      if (handleDisplayView) {
        isGridPage = this.matchUrl(
          window.location.href,
          concatRegexp(regex, /(?!.*disp=)/)
        );
        isListPage = this.matchUrl(
          window.location.href,
          concatRegexp(regex, /.*disp=1/)
        );
        isWallPage = this.matchUrl(
          window.location.href,
          concatRegexp(regex, /.*disp=2/)
        );
        isTaggerPage = this.matchUrl(
          window.location.href,
          concatRegexp(regex, /.*disp=3/)
        );

        if (isListPage || isWallPage || isTaggerPage) isDisplayViewPage = true;
      }

      function dispatchViewEvent(view, stash) {
        stash._dispatchPageEvent(event + `:${view}`);
        if (childAnyEventCondition) {
          stash._dispatchPageEvent(
            "stash:page:" + tabPage.page + `:any:${view}`,
            false
          );
          stash._dispatchPageEvent(
            "stash:page:any:" + tabPage.tab + `:${view}`,
            false
          );
        } else {
          stash._dispatchPageEvent(`stash:page:any:${view}`, false);
        }
      }

      const handleDisplayViewCondition =
        handleDisplayView !== true ||
        (handleDisplayView && (!isDisplayViewPage || args.lastHref === ""));

      if (
        this.matchUrl(window.location.href, regex) &&
        handleDisplayViewCondition
      ) {
        if (!manuallyHandleDispatchEvent) this._dispatchPageEvent(event);
        callback({
          ...args,
          location: window.location,
          event: event,
          regex: regex,
        });

        if (isGridPage) dispatchViewEvent("grid", this);
      }

      if (handleDisplayView) {
        let view = "";
        switch (true) {
          case isListPage:
            view = "list";
            break;
          case isWallPage:
            view = "wall";
            break;
          case isTaggerPage:
            view = "tagger";
            break;
        }
        if (view) dispatchViewEvent(view, this);
      }
    }
  }
  addEventListeners(events, callback, ...options) {
    events.forEach((event) => {
      this.addEventListener(event, callback, ...options);
    });
  }
  async pollLogsForMessage(prefix) {
    const reqTime = Date.now();
    const reqData = {
      variables: {},
      query: `query Logs {
                        logs {
                            time
                            level
                            message
                        }
                    }`,
    };
    await new Promise((r) => setTimeout(r, 500));
    let retries = 0;
    while (true) {
      const delay = 2 ** retries * 100;
      await new Promise((r) => setTimeout(r, delay));
      retries++;

      const logs = await this.callGQL(reqData);
      for (const log of logs.data.logs) {
        const logTime = Date.parse(log.time);
        if (logTime > reqTime && log.message.startsWith(prefix)) {
          return log.message.replace(prefix, "").trim();
        }
      }

      if (retries >= 5) {
        throw `Poll logs failed for message: ${prefix}`;
      }
    }
  }
  processTagger() {
    waitForElementByXpath("//button[text()='Scrape All']", (xpath, el) => {
      this.dispatchEvent(
        new CustomEvent("tagger", {
          detail: el,
        })
      );

      const searchItemContainer =
        document.querySelector(".tagger-container").lastChild;

      const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          mutation.addedNodes.forEach((node) => {
            if (
              node?.classList?.contains("entity-name") &&
              node.innerText.startsWith("Performer:")
            ) {
              this.dispatchEvent(
                new CustomEvent("tagger:mutation:add:remoteperformer", {
                  detail: {
                    node,
                    mutation,
                  },
                })
              );
            } else if (
              node?.classList?.contains("entity-name") &&
              node.innerText.startsWith("Studio:")
            ) {
              this.dispatchEvent(
                new CustomEvent("tagger:mutation:add:remotestudio", {
                  detail: {
                    node,
                    mutation,
                  },
                })
              );
            } else if (
              node.tagName === "SPAN" &&
              node.innerText.startsWith("Matched:")
            ) {
              this.dispatchEvent(
                new CustomEvent("tagger:mutation:add:local", {
                  detail: {
                    node,
                    mutation,
                  },
                })
              );
            } else if (node.tagName === "UL") {
              this.dispatchEvent(
                new CustomEvent("tagger:mutation:add:container", {
                  detail: {
                    node,
                    mutation,
                  },
                })
              );
            } else if (node?.classList?.contains("col-lg-6")) {
              this.dispatchEvent(
                new CustomEvent("tagger:mutation:add:subcontainer", {
                  detail: {
                    node,
                    mutation,
                  },
                })
              );
            } else if (node.tagName === "H5") {
              // scene date
              this.dispatchEvent(
                new CustomEvent("tagger:mutation:add:date", {
                  detail: {
                    node,
                    mutation,
                  },
                })
              );
            } else if (
              node.tagName === "DIV" &&
              node?.classList?.contains("d-flex") &&
              node?.classList?.contains("flex-column")
            ) {
              // scene stashid, url, details
              this.dispatchEvent(
                new CustomEvent("tagger:mutation:add:detailscontainer", {
                  detail: {
                    node,
                    mutation,
                  },
                })
              );
            } else {
              this.dispatchEvent(
                new CustomEvent("tagger:mutation:add:other", {
                  detail: {
                    node,
                    mutation,
                  },
                })
              );
            }
          });
        });
        this.dispatchEvent(
          new CustomEvent("tagger:mutations:searchitems", {
            detail: mutations,
          })
        );
      });
      observer.observe(searchItemContainer, {
        childList: true,
        subtree: true,
      });

      const taggerContainerHeader = document.querySelector(
        ".tagger-container-header"
      );
      const taggerContainerHeaderObserver = new MutationObserver(
        (mutations) => {
          this.dispatchEvent(
            new CustomEvent("tagger:mutations:header", {
              detail: mutations,
            })
          );
        }
      );
      taggerContainerHeaderObserver.observe(taggerContainerHeader, {
        childList: true,
        subtree: true,
      });

      for (const searchItem of document.querySelectorAll(".search-item")) {
        this.dispatchEvent(
          new CustomEvent("tagger:searchitem", {
            detail: searchItem,
          })
        );
      }

      if (!document.getElementById("progress-bar")) {
        const progressBar = createElementFromHTML(
          `<div id="progress-bar" class="progress"><div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div>`
        );
        progressBar.classList.add("progress");
        progressBar.style.display = "none";
        taggerContainerHeader.appendChild(progressBar);
      }
    });
    waitForElementByXpath(
      "//div[@class='tagger-container-header']/div/div[@class='row']/h4[text()='Configuration']",
      (xpath, el) => {
        this.dispatchEvent(
          new CustomEvent("tagger:configuration", {
            detail: el,
          })
        );
      }
    );
  }
  setProgress(value) {
    const progressBar = document.getElementById("progress-bar");
    if (progressBar) {
      progressBar.firstChild.style.width = value + "%";
      progressBar.style.display = value <= 0 || value > 100 ? "none" : "flex";
    }
  }
  processRemoteScenes(data) {
    if (data.data?.scrapeMultiScenes) {
      for (const matchResults of data.data.scrapeMultiScenes) {
        for (const scene of matchResults) {
          this.remoteScenes[scene.remote_site_id] = scene;
        }
      }
    } else if (data.data?.scrapeSingleScene) {
      for (const scene of data.data.scrapeSingleScene) {
        this.remoteScenes[scene.remote_site_id] = scene;
      }
    }
  }
  processScene(data) {
    if (data.data.findScene) {
      this.scenes[data.data.findScene.id] = data.data.findScene;
    }
  }
  processScenes(data) {
    if (data.data.findScenes?.scenes) {
      for (const scene of data.data.findScenes.scenes) {
        this.scenes[scene.id] = scene;
      }
    }
  }
  processStudios(data) {
    if (data.data.findStudios?.studios) {
      for (const studio of data.data.findStudios.studios) {
        this.studios[studio.id] = studio;
      }
    }
  }
  processPerformers(data) {
    if (data.data.findPerformers?.performers) {
      for (const performer of data.data.findPerformers.performers) {
        this.performers[performer.id] = performer;
      }
    }
  }
  parseSearchItem(searchItem) {
    const urlNode = searchItem.querySelector("a.scene-link");
    const url = new URL(urlNode.href);
    const id = url.pathname.replace(baseURL + "scenes/", "");
    const data = this.scenes[id];
    const nameNode = searchItem.querySelector(
      "a.scene-link > div.TruncatedText"
    );
    const name = nameNode.innerText;
    const queryInput = searchItem.querySelector("input.text-input");
    const performerNodes = searchItem.querySelectorAll(
      ".performer-tag-container"
    );

    return {
      urlNode,
      url,
      id,
      data,
      nameNode,
      name,
      queryInput,
      performerNodes,
    };
  }
  parseSearchResultItem(searchResultItem) {
    const remoteUrlNode = searchResultItem.querySelector(
      ".scene-details .optional-field .optional-field-content a"
    );
    const remoteId = remoteUrlNode?.href.split("/").pop();
    const remoteUrl = remoteUrlNode?.href ? new URL(remoteUrlNode.href) : null;
    const remoteData = this.remoteScenes[remoteId];

    const sceneDetailNodes = searchResultItem.querySelectorAll(
      ".scene-details .optional-field .optional-field-content"
    );
    let urlNode = null;
    let detailsNode = null;
    for (const sceneDetailNode of sceneDetailNodes) {
      if (
        sceneDetailNode.innerText.startsWith("http") &&
        remoteUrlNode?.href !== sceneDetailNode.innerText
      ) {
        urlNode = sceneDetailNode;
      } else if (!sceneDetailNode.innerText.startsWith("http")) {
        detailsNode = sceneDetailNode;
      }
    }

    const imageNode = searchResultItem.querySelector(
      ".scene-image-container .optional-field .optional-field-content"
    );

    const metadataNode = searchResultItem.querySelector(".scene-metadata");
    const titleNode = metadataNode.querySelector(
      "h4 .optional-field .optional-field-content"
    );
    const codeAndDateNodes = metadataNode.querySelectorAll(
      "h5 .optional-field .optional-field-content"
    );
    let codeNode = null;
    let dateNode = null;
    for (const node of codeAndDateNodes) {
      if (node.textContent.includes("-")) {
        dateNode = node;
      } else {
        codeNode = node;
      }
    }

    const entityNodes = searchResultItem.querySelectorAll(".entity-name");
    let studioNode = null;
    const performerNodes = [];
    for (const entityNode of entityNodes) {
      if (entityNode.innerText.startsWith("Studio:")) {
        studioNode = entityNode;
      } else if (entityNode.innerText.startsWith("Performer:")) {
        performerNodes.push(entityNode);
      }
    }

    const matchNodes = searchResultItem.querySelectorAll(
      "div.col-lg-6 div.mt-2 div.row.no-gutters.my-2 span.ml-auto"
    );
    const matches = [];
    for (const matchNode of matchNodes) {
      let matchType = null;
      const entityNode = matchNode.parentElement.querySelector(".entity-name");

      const matchName = matchNode.querySelector(
        ".optional-field-content b"
      ).innerText;
      const remoteName = entityNode.querySelector("b").innerText;

      let data;
      if (entityNode.innerText.startsWith("Performer:")) {
        matchType = "performer";
        if (remoteData) {
          data = remoteData.performers.find(
            (performer) => performer.name === remoteName
          );
        }
      } else if (entityNode.innerText.startsWith("Studio:")) {
        matchType = "studio";
        if (remoteData) {
          data = remoteData.studio;
        }
      }

      matches.push({
        matchType,
        matchNode,
        entityNode,
        matchName,
        remoteName,
        data,
      });
    }

    return {
      remoteUrlNode,
      remoteId,
      remoteUrl,
      remoteData,
      urlNode,
      detailsNode,
      imageNode,
      titleNode,
      codeNode,
      dateNode,
      studioNode,
      performerNodes,
      matches,
    };
  }
}

window.stash = new Stash();

function waitForElementQuerySelector(query, callback, time) {
  time = typeof time !== "undefined" ? time : 100;
  window.setTimeout(() => {
    const element = document.querySelector(query);
    if (element) {
      callback(query, element);
    } else {
      waitForElementQuerySelector(query, callback, time);
    }
  }, time);
}

function waitForElementClass(elementId, callback, time) {
  time = typeof time !== "undefined" ? time : 100;
  window.setTimeout(() => {
    const element = document.getElementsByClassName(elementId);
    if (element.length > 0) {
      callback(elementId, element);
    } else {
      waitForElementClass(elementId, callback, time);
    }
  }, time);
}

function waitForElementId(elementId, callback, time) {
  time = typeof time !== "undefined" ? time : 100;
  window.setTimeout(() => {
    const element = document.getElementById(elementId);
    if (element != null) {
      callback(elementId, element);
    } else {
      waitForElementId(elementId, callback, time);
    }
  }, time);
}

function waitForElementByXpath(xpath, callback, time) {
  time = typeof time !== "undefined" ? time : 100;
  window.setTimeout(() => {
    const element = getElementByXpath(xpath);
    if (element) {
      callback(xpath, element);
    } else {
      waitForElementByXpath(xpath, callback, time);
    }
  }, time);
}

function getElementByXpath(xpath, contextNode) {
  return document.evaluate(
    xpath,
    contextNode || document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;
}

function createElementFromHTML(htmlString) {
  const div = document.createElement("div");
  div.innerHTML = htmlString.trim();

  // Change this to div.childNodes to support multiple top-level nodes.
  return div.firstChild;
}

function getElementByXpath(xpath, contextNode) {
  return document.evaluate(
    xpath,
    contextNode || document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;
}

function getElementsByXpath(xpath, contextNode) {
  return document.evaluate(
    xpath,
    contextNode || document,
    null,
    XPathResult.ORDERED_NODE_ITERATOR_TYPE,
    null
  );
}

function getClosestAncestor(el, selector, stopSelector) {
  let retval = null;
  while (el) {
    if (el.matches(selector)) {
      retval = el;
      break;
    } else if (stopSelector && el.matches(stopSelector)) {
      break;
    }
    el = el.parentElement;
  }
  return retval;
}

function setNativeValue(element, value) {
  const valueSetter = Object.getOwnPropertyDescriptor(element, "value").set;
  const prototype = Object.getPrototypeOf(element);
  const prototypeValueSetter = Object.getOwnPropertyDescriptor(
    prototype,
    "value"
  ).set;

  if (valueSetter && valueSetter !== prototypeValueSetter) {
    prototypeValueSetter.call(element, value);
  } else {
    valueSetter.call(element, value);
  }
}

function updateTextInput(element, value) {
  setNativeValue(element, value);
  element.dispatchEvent(
    new Event("input", {
      bubbles: true,
    })
  );
}

function concatRegexp(reg, exp) {
  let flags = reg.flags + exp.flags;
  flags = Array.from(new Set(flags.split(""))).join();
  return new RegExp(reg.source + exp.source, flags);
}

function sortElementChildren(node) {
  const items = node.childNodes;
  const itemsArr = [];
  for (const i in items) {
    if (items[i].nodeType == Node.ELEMENT_NODE) {
      // get rid of the whitespace text nodes
      itemsArr.push(items[i]);
    }
  }

  itemsArr.sort((a, b) => {
    return a.innerHTML == b.innerHTML ? 0 : a.innerHTML > b.innerHTML ? 1 : -1;
  });

  for (let i = 0; i < itemsArr.length; i++) {
    node.appendChild(itemsArr[i]);
  }
}

function xPathResultToArray(result) {
  let node = null;
  const nodes = [];
  while ((node = result.iterateNext())) {
    nodes.push(node);
  }
  return nodes;
}

function createStatElement(container, title, heading) {
  const statEl = document.createElement("div");
  statEl.classList.add("stats-element");
  container.appendChild(statEl);

  const statTitle = document.createElement("p");
  statTitle.classList.add("title");
  statTitle.innerText = title;
  statEl.appendChild(statTitle);

  const statHeading = document.createElement("p");
  statHeading.classList.add("heading");
  statHeading.innerText = heading;
  statEl.appendChild(statHeading);
}

const reloadImg = (url) =>
  fetch(url, {
    cache: "reload",
    mode: "no-cors",
  }).then(() =>
    document.body
      .querySelectorAll(`img[src='${url}']`)
      .forEach((img) => (img.src = url))
  );
