(function () {
  "use strict";

  const IS_FIREFOX = navigator.userAgent.indexOf("Firefox") !== -1;

  class OwlDevtoolsGlobalHook {
    constructor() {
      // The set of apps exposed by owl
      this.apps = window.__OWL_DEVTOOLS__.apps;
      // Class definition of an owl Fiber
      this.Fiber = window.__OWL_DEVTOOLS__.Fiber;
      // Same but for RootFiber
      this.RootFiber = window.__OWL_DEVTOOLS__.RootFiber;
      // This is for retrocompatibility purposes since new versions of owl should always expose toRaw and reactive
      // in __OWL_DEVTOOLS__
      this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw;
      this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive;
      // Set to keep track of the HTML elements we added to the page
      this.addedElements = [];
      // To keep track of the succession order of the render events
      this.eventId = 0;
      // Set to keep track of the frame on which this script is loaded
      this.frame = "top";
      // Allows to launch a message each time an iframe html element is added to the page
      const iFrameObserver = new MutationObserver(function (mutationsList) {
        mutationsList.forEach(function (mutation) {
          mutation.addedNodes.forEach(function (addedNode) {
            if (addedNode.tagName == "IFRAME") {
              // Ensure it is not an empty iframe element
              if (addedNode.contentDocument) {
                /*
                 * This message is intercepted by the content script which relays it to the background script which relays it to the devtools app.
                 * This process may seem long and indirect but is necessary. This applies to all window.top.postMessage methods in this file.
                 * More information in the docs: https://developer.chrome.com/docs/extensions/mv3/devtools/#evaluated-scripts-to-devtools
                 */
                window.top.postMessage({
                  source: "owl-devtools",
                  type: "NewIFrame",
                  data: addedNode.contentDocument.location.href,
                });
              }
            }
          });
        });
      });
      iFrameObserver.observe(document.body, { subtree: true, childList: true });
      this.patchAppsSetMethods();
      this.recordEvents = false;
      this.traceRenderings = false;
      this.traceSubscriptions = false;
      this.requestedFrame = false;
      this.eventsBatch = [];
      // Object which defines how different types of data should be displayed when passed to the devtools
      this.serializer = {
        // Defines how leaf object nodes should be displayed in the extension when inside a bigger structure
        // This is also used by default when it is a standalone item (like functions, numbers, strings, etc)
        // The asConstructorName parameter can be passed to change the display of objects and functions to
        // be their constructor name (useful for prototype, map and set display)
        serializeItem(value, asConstructorName = false) {
          if (typeof value === "array") {
            return "Array(" + value.length + ")";
          } else if (typeof value === "object") {
            if (value == null) {
              return "null";
            }
            if (asConstructorName) {
              return value.constructor.name;
            }
            return "{...}";
          } else if (typeof value === "undefined") {
            return "undefined";
          } else if (typeof value === "string") {
            return JSON.stringify(value);
          } else if (typeof value === "function") {
            if (asConstructorName) {
              return value.constructor.name;
            }
            let functionString = value.toString();
            if (functionString.startsWith("class")) {
              return "class " + value.name;
            } else {
              // This replaces the body of any function with curly braces by ...
              const regex = /\{(?![^()]*\))(.*?)\}(?![^{}]*\})/s;
              return functionString.replace(regex, "{...}");
            }
          } else {
            let valueAsString = value.toString();
            if (asConstructorName && valueAsString.length > 10) {
              valueAsString = valueAsString.substring(0, 8) + "...";
            }
            return valueAsString;
          }
        },
        array(arr) {
          const result = [];
          let length = 0;
          for (const item of arr) {
            if (length > 25) {
              result.push("...");
              break;
            }
            const element = this.serializeItem(item);
            length += element.length;
            result.push(element);
          }
          return "[" + result.join(", ") + "]";
        },
        object(obj) {
          const result = [];
          let length = 0;
          for (const [key, value] of Object.entries(obj)) {
            if (length > 25) {
              result.push("...");
              break;
            }
            const element = key + ": " + this.serializeItem(value);
            length += element.length;
            result.push(element);
          }
          for (const key of Object.getOwnPropertySymbols(obj)) {
            if (length > 25) {
              result.push("...");
              break;
            }
            const element = key.toString() + ": " + this.serializeItem(obj[key]);
            length += element.length;
            result.push(element);
          }
          return "{" + result.join(", ") + "}";
        },
        map(obj) {
          const result = [];
          let length = 0;
          for (const [key, value] of obj.entries()) {
            if (length > 25) {
              result.push("...");
              break;
            }
            const element =
              this.serializeItem(key, true) + " => " + this.serializeItem(value, true);
            length += element.length;
            result.push(element);
          }
          return "Map(" + obj.size + "){" + result.join(", ") + "}";
        },
        ["map entry"](obj) {
          return (
            "{" + this.serializeItem(obj[0], true) + " => " + this.serializeItem(obj[1], true) + "}"
          );
        },
        set(obj) {
          const result = [];
          let length = 0;
          for (const value of obj) {
            if (length > 25) {
              result.push("...");
              break;
            }
            const element = this.serializeItem(value, false);
            length += element.length;
            result.push(element);
          }
          return "Set(" + obj.size + "){" + result.join(", ") + "}";
        },
        // Returns a shortened string version of a collection of properties for display purposes
        // Shortcut for serializeItem when it is a standalone item
        serializeContent(item, type) {
          if (this[type]) {
            return this[type](item);
          }
          return this.serializeItem(item);
        },
      };
    }

    initDevtools(frame = "top") {
      if (!this.devtoolsInit) {
        this.frame = frame;
        const self = this;
        // Flush the events batcher when a root render is completed
        const original_Complete = self.RootFiber.prototype.complete;
        self.RootFiber.prototype.complete = function () {
          original_Complete.call(this, ...arguments);
          const path = self.getComponentPath(this.node);
          //Add a functionnality to the complete function which sends a message to the window every time it is triggered.
          window.top.postMessage({
            source: "owl-devtools",
            type: "Complete",
            data: path,
            origin: { frame: self.frame },
          });
          if (self.recordEvents) {
            window.top.postMessage({
              source: "owl-devtools",
              type: "Event",
              data: self.eventsBatch,
            });
            self.eventsBatch = [];
          }
        };
        this.devtoolsInit = true;
      }
    }
    // Modify the methods of the apps set in order to send a message each time it is modified.
    patchAppsSetMethods() {
      const originalAdd = this.apps.add;
      const originalDelete = this.apps.delete;
      this.apps.add = function () {
        originalAdd.call(this, ...arguments);
        window.top.postMessage({
          source: "owl-devtools",
          type: "RefreshApps",
        });
      };
      this.apps.delete = function () {
        originalDelete.call(this, ...arguments);
        window.top.postMessage({
          source: "owl-devtools",
          type: "RefreshApps",
        });
      };
    }

    // Modify methods of each app so that it triggers messages on each flush and component render
    patchAppMethods() {
      let app;
      for (const appItem of this.apps) {
        if (appItem.root) {
          app = appItem;
        }
      }
      if (!app.root) {
        return;
      }
      const self = this;
      const originalFlush = app.scheduler.constructor.prototype.flush;
      let inFlush = false;
      let _render = false;
      app.scheduler.constructor.prototype.flush = function () {
        // Used to know when a render is triggered inside the flush method or not
        inFlush = true;
        originalFlush.call(this, ...arguments);
        inFlush = false;
      };
      const originalRender = self.Fiber.prototype.render;
      self.Fiber.prototype.render = function () {
        const id = self.eventId++;
        _render = false;
        let flushed = false;
        // We know if a render comes from flush before calling the render method
        if (this instanceof self.RootFiber && inFlush) {
          flushed = true;
        }

        // patch the renderFn function from the node to only measure the time
        // spent in the template, not in additional work, such as flushing scheduler
        const node = this.node;
        const nodeRenderFn = node.renderFn;
        let time = 0;
        this.node.renderFn = function () {
          const before = performance.now();
          const result = nodeRenderFn.call(this);
          time = performance.now() - before;
          // unpatch the node render function
          node.renderFn = nodeRenderFn;
          return result;
        };

        originalRender.call(this, ...arguments);
        if (_render && self.traceRenderings && this instanceof self.RootFiber) {
          console.groupCollapsed(`Rendering <${this.node.name}>`);
          console.trace();
          console.groupEnd();
        }
        if (self.recordEvents) {
          const path = self.getComponentPath(this.node);
          // if the render comes from flush
          if (flushed) {
            self.eventsBatch.push({
              type: "render (flushed)",
              component: this.node.name,
              key: this.node.parentKey ? this.node.parentKey : "",
              path: path,
              time: time,
              id: id,
            });
            // if _render is called, it is a proper render (and not a delayed one)
          } else if (_render) {
            // A render on a RootFiber is a root render and can propagate other renders to its children
            if (this instanceof self.RootFiber) {
              self.eventsBatch.push({
                type: this.deep ? "render (deep)" : "render",
                component: this.node.name,
                key: this.node.parentKey ? this.node.parentKey : "",
                path: path,
                time: time,
                id: id,
              });
              // if the node status is NEW, the node has been created just before rendering
            } else if (this.node.status === 0) {
              self.eventsBatch.push({
                type: "create",
                component: this.node.name,
                key: this.node.parentKey ? this.node.parentKey : "",
                path: path,
                time: time,
                id: id,
              });
              // else it is an update
            } else {
              self.eventsBatch.push({
                type: "update",
                component: this.node.name,
                key: this.node.parentKey ? this.node.parentKey : "",
                path: path,
                time: time,
                id: id,
              });
            }
            // _render has not been called so it is a delayed render that could be flushed later on
          } else {
            self.eventsBatch.push({
              type: "render (delayed)",
              component: this.node.name,
              key: this.node.parentKey ? this.node.parentKey : "",
              path: path,
              time: time,
              id: id,
            });
          }
        }
      };
      const original_Render = self.Fiber.prototype._render;
      self.Fiber.prototype._render = function () {
        _render = true;
        original_Render.call(this, ...arguments);
      };
      // Signals when a component is destroyed
      const originalDestroy = app.root.constructor.prototype._destroy;
      app.root.constructor.prototype._destroy = function () {
        if (self.recordEvents) {
          const path = self.getComponentPath(this);
          const event = {
            type: "destroy",
            component: this.name,
            key: this.parentKey,
            path: path,
            time: 0,
            id: self.eventId++,
          };
          self.eventsBatch.push(event);
          const before = performance.now();
          originalDestroy.call(this, ...arguments);
          event.time = performance.now() - before;
        } else {
          originalDestroy.call(this, ...arguments);
        }
      };
    }

    // patch reactivity system to activate subscription tracing
    patchReactivity() {
      // i am simultaneously proud and ashamed of this code...
      const WeakMapGet = WeakMap.prototype.get;
      const MapGet = Map.prototype.get;
      const SetAdd = Set.prototype.add;
      const self = this;

      let callbacksToTargets;
      let targetToKeysToCallbacks;

      // Step 1: extract internal values from owl
      const obj = self.reactive({}, () => {});
      let count = 0;
      WeakMap.prototype.get = function () {
        count++;
        if (count === 1) {
          targetToKeysToCallbacks = this;
        }
        if (count === 3) {
          callbacksToTargets = this;
          WeakMap.prototype.get = WeakMapGet;
        }
        return WeakMapGet.call(this, ...arguments);
      };
      obj.a;

      // Step 2: patch these objects
      let key;
      function getKey(_key) {
        key = _key;
        return MapGet.call(this, _key);
      }

      targetToKeysToCallbacks.get = function (target) {
        const keyToCallbacks = WeakMapGet.call(this, target);
        if (keyToCallbacks) {
          keyToCallbacks.get = getKey;
        }
        return keyToCallbacks;
      };

      function addTarget(target) {
        // HERE
        if (self.traceSubscriptions && !this.has(target)) {
          console.groupCollapsed(`Observing:`, key, target);
          console.trace();
          console.groupEnd();
        }
        return SetAdd.call(this, target);
      }
      callbacksToTargets.get = function (callback) {
        const targets = WeakMapGet.call(this, callback);
        if (targets) {
          targets.add = addTarget;
        }
        return targets;
      };
    }

    toggleTracing(value) {
      if (value) {
        this.patchAppMethods();
        this.patchAppMethods = () => {}; // to only patch once
      }
      this.traceRenderings = value;
      return this.traceRenderings;
    }

    toggleSubscriptionTracing(value) {
      if (value) {
        this.patchReactivity();
        this.patchReactivity = () => {}; // to only patch once
      }
      this.traceSubscriptions = value;
      return value;
    }
    // Enables/disables the recording of the render/destroy events based on value
    toggleEventsRecording(value, index) {
      if (value) {
        this.patchAppMethods();
        this.patchAppMethods = () => {}; // to only patch once
      }
      this.recordEvents = value;
      this.eventId = index;
      return this.recordEvents;
    }

    // Reset the event ids (the events on devtools side will be cleared at the same time)
    resetEvents() {
      this.eventId = 0;
    }

    // Get the urls of all iframes present on the page
    getIFrameUrls() {
      let frames = [];
      for (const frame of document.querySelectorAll("iframe")) {
        frames.push(frame.contentDocument.location.href);
      }
      return frames;
    }

    // Draws a highlighting rectangle on the specified html element and displays its dimensions and the specified name in a box
    highlightElements(elements, name) {
      this.removeHighlights();

      if (elements.length === 0) {
        return;
      }

      let minTop = Number.MAX_SAFE_INTEGER;
      let minLeft = Number.MAX_SAFE_INTEGER;
      let maxBottom = Number.MIN_SAFE_INTEGER;
      let maxRight = Number.MIN_SAFE_INTEGER;

      for (const element of elements) {
        let rect;
        // Since this function accepts text nodes, we need to create a range to get the position and dimensions
        if (element instanceof Text) {
          const range = document.createRange();
          range.selectNode(element);
          rect = range.getBoundingClientRect();
        } else {
          rect = element.getBoundingClientRect();
        }

        const top = rect.top;
        const left = rect.left;
        const bottom = top + rect.height;
        const right = left + rect.width;

        minTop = Math.min(minTop, top);
        minLeft = Math.min(minLeft, left);
        maxBottom = Math.max(maxBottom, bottom);
        maxRight = Math.max(maxRight, right);

        const width = right - left;
        const height = bottom - top;

        if (element instanceof HTMLElement) {
          const marginTop = parseInt(getComputedStyle(element).marginTop);
          const marginRight = parseInt(getComputedStyle(element).marginRight);
          const marginBottom = parseInt(getComputedStyle(element).marginBottom);
          const marginLeft = parseInt(getComputedStyle(element).marginLeft);

          const paddingTop = parseInt(getComputedStyle(element).paddingTop);
          const paddingRight = parseInt(getComputedStyle(element).paddingRight);
          const paddingBottom = parseInt(getComputedStyle(element).paddingBottom);
          const paddingLeft = parseInt(getComputedStyle(element).paddingLeft);

          const highlight = document.createElement("div");
          highlight.style.top = `${top}px`;
          highlight.style.left = `${left}px`;
          highlight.style.width = `${width}px`;
          highlight.style.height = `${height}px`;
          highlight.style.boxSizing = "border-box";
          highlight.style.position = "fixed";
          highlight.style.backgroundColor = "rgba(15, 139, 245, 0.4)";
          highlight.style.borderStyle = "solid";
          highlight.style.borderWidth = `${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px`;
          highlight.style.borderColor = "rgba(65, 196, 68, 0.4)";
          highlight.style.zIndex = "10000";
          highlight.style.pointerEvents = "none";
          document.body.appendChild(highlight);
          this.addedElements.push(highlight);

          const highlightMargins = document.createElement("div");
          highlightMargins.style.top = `${top - marginTop}px`;
          highlightMargins.style.left = `${left - marginLeft}px`;
          highlightMargins.style.width = `${width + marginLeft + marginRight}px`;
          highlightMargins.style.height = `${height + marginBottom + marginTop}px`;
          highlightMargins.style.position = "fixed";
          highlightMargins.style.borderStyle = "solid";
          highlightMargins.style.boxSizing = "border-box";
          highlightMargins.style.borderWidth = `${marginTop}px ${marginRight}px ${marginBottom}px ${marginLeft}px`;
          highlightMargins.style.borderColor = "rgba(241, 179, 121, 0.4)";
          highlightMargins.style.zIndex = "10000";
          highlightMargins.style.pointerEvents = "none";
          document.body.appendChild(highlightMargins);
          this.addedElements.push(highlightMargins);
          // In case the element is a range because a text node was passed
        } else {
          const highlight = document.createElement("div");
          highlight.style.top = `${top}px`;
          highlight.style.left = `${left}px`;
          highlight.style.width = `${width}px`;
          highlight.style.height = `${height}px`;
          highlight.style.boxSizing = "border-box";
          highlight.style.position = "fixed";
          highlight.style.backgroundColor = "rgba(15, 139, 245, 0.4)";
          highlight.style.zIndex = "10000";
          highlight.style.pointerEvents = "none";
          document.body.appendChild(highlight);
          this.addedElements.push(highlight);
        }
      }

      const width = maxRight - minLeft;
      const height = maxBottom - minTop;

      const detailsBox = document.createElement("div");
      detailsBox.className = "owl-devtools-detailsBox";
      detailsBox.innerHTML = `
      <div style="color: #ffc107; display: inline;">${name} </div><div style="color: white; display: inline;">${width.toFixed(
        2
      )}px x ${height.toFixed(2)}px</div>
      `;
      detailsBox.style.position = "fixed";
      detailsBox.style.backgroundColor = "black";
      detailsBox.style.padding = "5px";
      detailsBox.style.zIndex = "10000";
      detailsBox.style.pointerEvents = "none";
      detailsBox.style.display = "inline";
      document.body.appendChild(detailsBox);
      this.addedElements.push(detailsBox);

      const viewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
      const viewportHeight = Math.max(
        document.documentElement.clientHeight,
        window.innerHeight || 0
      );
      const detailsBoxRect = detailsBox.getBoundingClientRect();
      let detailsBoxTop = minTop + height + 5;
      let detailsBoxLeft = minLeft;
      if (detailsBoxTop + detailsBoxRect.height > viewportHeight) {
        detailsBoxTop = minTop - detailsBoxRect.height - 5;
        if (detailsBoxTop < 0) {
          detailsBoxTop = minTop;
        }
      }
      if (detailsBoxLeft + detailsBoxRect.width > viewportWidth) {
        detailsBoxLeft = minLeft - detailsBoxRect.width - 5;
      }
      detailsBox.style.top = `${detailsBoxTop}px`;
      detailsBox.style.left = `${detailsBoxLeft + 5}px`;
    }

    // Remove all elements drawn by the HighlightElement function
    removeHighlights = () => {
      for (const el of this.addedElements) {
        el.remove();
      }
      this.addedElements = [];
    };

    // Identify the hovered component based on the corresponding DOM element and send the Select message
    // when the target changes
    HTMLSelector = (ev) => {
      const target = ev.target;
      if (!this.currentSelectedElement || !target.isEqualNode(this.currentSelectedElement)) {
        const path = this.getElementPath(target);
        this.highlightComponent(path);
        this.currentSelectedElement = target;
        window.top.postMessage({
          source: "owl-devtools",
          type: "SelectElement",
          data: path,
        });
      }
    };

    // Activate the HTML selector tool
    enableHTMLSelector() {
      document.addEventListener("mouseover", this.HTMLSelector, { capture: true });
      document.addEventListener("click", this.disableHTMLSelector, { capture: true });
      document.addEventListener("mouseout", this.removeHighlights, { capture: true });
    }

    // Diasble the HTML selector tool
    disableHTMLSelector = (ev = undefined) => {
      if (ev) {
        if (!ev.isTrusted) {
          return;
        }
        ev.stopPropagation();
        ev.preventDefault();
      }
      this.removeHighlights();
      document.removeEventListener("mouseover", this.HTMLSelector, { capture: true });
      document.removeEventListener("click", this.disableHTMLSelector, { capture: true });
      document.removeEventListener("mouseout", this.removeHighlights, { capture: true });
      window.top.postMessage({
        source: "owl-devtools",
        type: "StopSelector",
      });
    };

    // Returns the object specified by the path starting from the topParent object
    getObject(topParent, path) {
      let obj = topParent;
      if (path.length === 0) {
        return obj;
      }
      for (const key of path) {
        switch (key.type) {
          case "prototype":
            obj = Object.getPrototypeOf(obj);
            break;
          // We will be handling maps and sets entries as arrays to keep things stable
          case "set entries":
          case "map entries":
            obj = [...obj];
            break;
          // Kind of tricky structure-wise but the set entry already is the object itself so this is
          // only useful for display purpose in the devtools.
          case "set value":
            obj = obj;
            break;
          // A map entry is a two items array with the key and the value so indexes 0 and 1
          case "map key":
            obj = obj[0];
            break;
          case "map value":
            obj = obj[1];
            break;
          case "getter":
            // When the item is a getter, get its value.
            try {
              obj = Object.getOwnPropertyDescriptor(obj, key.value).get.call(obj);
            } catch (e) {
              obj = e.toString();
            }
            break;
          case "prototype getter":
          case "set entry":
          case "map entry":
          case "item":
            // When the item is a symbol, we make sure it is the right one by using the index on the
            // Object.getOwnPropertySymbols array.
            if (key.hasOwnProperty("symbolIndex") && key.symbolIndex >= 0) {
              const symbol = Object.getOwnPropertySymbols(obj)[key.symbolIndex];
              if (symbol) {
                obj = obj[symbol];
              }
            } else if (Object.getOwnPropertyDescriptor(obj, key.value)?.hasOwnProperty("get")) {
              try {
                obj = Object.getOwnPropertyDescriptor(obj, key.value).get;
              } catch (e) {
                obj = e.toString();
              }
            } else {
              try {
                obj = obj[key.value];
              } catch (e) {
                obj = e.toString();
              }
            }
        }
        if (obj) {
          obj = this.toRaw(obj);
        }
      }
      return obj;
    }

    // Returns the asked property given its global path
    getObjectProperty(path) {
      // Just return the corresponding app if path is of length 1
      if (path.length === 1) {
        return [...this.apps][path[0]];
      }
      // Path to the component node is only strings, becomes objects for properties
      const index = path.findIndex((key) => typeof key !== "string");
      const componentNode = this.getComponentNode(path.slice(0, index));
      const obj = this.getObject(componentNode, path.slice(index));
      return obj;
    }

    // Returns a modified version of an object node that has compatible format with the devtools ObjectTreeElement component
    // parentObj is the parent of the node, key is the path key of the node, type is where the object belongs to (props, env, instance, ...),
    // oldBranch is supposedly the same final node in the previous version of the tree and oldTree is the complete old component details
    serializeObjectChild(parentObj, key, depth, type, path, oldBranch, oldTree) {
      let obj;
      let child = {
        depth: depth,
        toggled: false,
        objectType: type,
        hasChildren: false,
      };
      if (oldBranch?.toggled) {
        child.toggled = true;
      }
      child.path = path.concat([key]);
      child.children = [];
      switch (key.type) {
        case "prototype":
          child.name = "[[Prototype]]";
          child.contentType = "object";
          child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true);
          child.hasChildren = true;
          if (!oldTree && type === "env") {
            child.toggled = true;
          }
          break;
        case "set entries":
        case "map entries":
          child.name = "[[Entries]]";
          child.content = "";
          child.contentType = "array";
          child.hasChildren = true;
          break;
        case "set value":
          child.name = "value";
          obj = parentObj;
          break;
        case "map key":
          child.name = "key";
          obj = parentObj[0];
          break;
        case "map value":
          child.name = "value";
          obj = parentObj[1];
          break;
        case "set entry":
        case "map entry":
        case "item":
          if (Object.getOwnPropertyDescriptor(parentObj, key.value)?.hasOwnProperty("get")) {
            child.name = "get " + key.value;
            obj = Object.getOwnPropertyDescriptor(parentObj, key.value).get;
          } else {
            child.name = key.value;
            if (typeof key.value === "symbol") {
              child.name = key.value.toString();
              obj = parentObj[key.value];
              child.path[child.path.length - 1].symbolIndex = Object.getOwnPropertySymbols(
                parentObj
              ).findIndex((sym) => sym === key.value);
              child.path[child.path.length - 1].value = key.value.toString();
            } else {
              obj = parentObj[key.value];
            }
          }
          if (key.type === "map entry") {
            child.contentType = "array";
            child.hasChildren = obj.length > 0;
            child.content = this.serializer.serializeContent(obj, key.type);
          }
          break;
      }
      if (!child.contentType) {
        if (obj === null) {
          child.content = "null";
          child.contentType = "object";
          child.hasChildren = false;
        } else if (obj === undefined) {
          child.content = "undefined";
          child.contentType = "undefined";
          child.hasChildren = false;
        } else {
          obj = this.toRaw(obj);
          switch (true) {
            case obj instanceof Map:
              child.contentType = "map";
              child.hasChildren = true;
              break;
            case obj instanceof Set:
              child.contentType = "set";
              child.hasChildren = true;
              break;
            case obj instanceof Array:
              child.contentType = "array";
              child.hasChildren = obj.length > 0;
              break;
            case typeof obj === "function":
              child.contentType = "function";
              child.hasChildren = true;
              break;
            case obj instanceof Object:
              child.contentType = "object";
              child.hasChildren =
                Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length;
              break;
            default:
              child.contentType = typeof obj;
              child.hasChildren = false;
          }
          if (key.type === "set entry") {
            child.content = this.serializer.serializeItem(obj, true);
          } else {
            child.content = this.serializer.serializeContent(obj, child.contentType);
          }
        }
      }
      if (child.toggled) {
        child.children = this.loadObjectChildren(
          child.path,
          child.depth,
          child.contentType,
          child.objectType,
          oldTree
        );
      }
      this.addHighlightedKeys(child);
      return child;
    }

    // returns the serialized node corresponding to its path in the serialized tree
    getObjectInOldTree(oldTree, completePath, objType) {
      const objPathIndex = completePath.findIndex((key) => typeof key !== "string");
      let path = completePath.slice(objPathIndex);
      let obj;
      if (objType === "subscription") {
        const subscriptionPath = completePath.slice(0, objPathIndex + 3);
        obj = oldTree.subscriptions.children.find(
          (child) => JSON.stringify(child.target.path) === JSON.stringify(subscriptionPath)
        ).target;
        path = path.slice(3);
      } else {
        // Everything here is in component if it is not an app so remove this key of the path in the former case
        if (objPathIndex > 1) {
          path.shift();
        }
        // the value is either "props" or "env" here
        if (objType !== "instance") {
          obj = oldTree[path[0].value].children;
          path.shift();
          // there is nothing otherwise but extension side it is in instance
        } else {
          obj = oldTree.instance.children;
        }
        // the first element here is directly in an array instead of a children array
        obj = obj[path[0].childIndex];
        path.shift();
      }
      // just follow the indexes in the rest of the path
      for (const key of path) {
        obj = obj.children[key.childIndex];
      }
      return obj;
    }
    // Returns a serialized version of the children properties of the specified component's property given its path.
    loadObjectChildren(path, depth, contentType, objType, oldTree) {
      const children = [];
      depth = depth + 1;
      let obj = this.getObjectProperty(path);
      let oldBranch = oldTree && this.getObjectInOldTree(oldTree, path, objType);
      if (!obj) {
        return [];
      }
      let index = 0;
      const lastKey = path.at(-1);
      let prototype;
      if (lastKey.type.includes("entry")) {
        if (lastKey.type === "map entry") {
          const mapKey = this.serializeObjectChild(
            obj,
            { type: "map key", childIndex: 0 },
            depth,
            objType,
            path,
            oldBranch?.children[0],
            oldTree
          );
          children.push(mapKey);
          const mapValue = this.serializeObjectChild(
            obj,
            { type: "map value", childIndex: 1 },
            depth,
            objType,
            path,
            oldBranch?.children[1],
            oldTree
          );
          children.push(mapValue);
        } else if (lastKey.type === "set entry") {
          const setValue = this.serializeObjectChild(
            obj,
            { type: "set value", childIndex: 0 },
            depth,
            objType,
            path,
            oldBranch?.children[0],
            oldTree
          );
          children.push(setValue);
        }
        return children;
      }
      switch (contentType) {
        case "array":
          if (lastKey.type.includes("entries")) {
            for (; index < obj.length; index++) {
              const child = this.serializeObjectChild(
                obj,
                {
                  type: lastKey.type.replace("entries", "entry"),
                  value: index,
                  childIndex: children.length,
                },
                depth,
                objType,
                path,
                oldBranch?.children[index],
                oldTree
              );
              if (child) {
                children.push(child);
              }
            }
          } else {
            for (; index < obj.length; index++) {
              const child = this.serializeObjectChild(
                obj,
                { type: "item", value: index, childIndex: children.length },
                depth,
                objType,
                path,
                oldBranch?.children[index],
                oldTree
              );
              if (child) {
                children.push(child);
              }
            }
          }
          break;
        case "set":
        case "map":
          const entries = this.serializeObjectChild(
            obj,
            { type: contentType + " entries", value: "[[Entries]]", childIndex: children.length },
            depth,
            objType,
            path,
            oldBranch?.children[index],
            oldTree
          );
          if (entries) {
            children.push(entries);
            index++;
          }
          Reflect.ownKeys(obj).forEach((key) => {
            const child = this.serializeObjectChild(
              obj,
              { type: "item", value: key, childIndex: children.length },
              depth,
              objType,
              path,
              oldBranch?.children[index],
              oldTree
            );
            if (child) {
              children.push(child);
            }
            index++;
          });
          break;
        case "object":
        case "function":
          Reflect.ownKeys(obj).forEach((key) => {
            if (
              key !== "__proto__" &&
              Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
            ) {
              let child = {
                name: key,
                depth: depth,
                toggled: false,
                objectType: objType,
                path: [...path, { type: "getter", value: key, childIndex: children.length }],
                contentType: "getter",
                content: "(...)",
                hasChildren: false,
                children: [],
              };
              children.push(child);
            }
            const child = this.serializeObjectChild(
              obj,
              { type: "item", value: key, childIndex: children.length },
              depth,
              objType,
              path,
              oldBranch?.children[index],
              oldTree
            );
            if (child) children.push(child);
            index++;
          });
      }
      let proto = Object.getPrototypeOf(obj);
      while (proto) {
        Reflect.ownKeys(proto).forEach((key) => {
          if (
            key !== "__proto__" &&
            Object.getOwnPropertyDescriptor(proto, key).hasOwnProperty("get")
          ) {
            let child = {
              name: key,
              depth: depth,
              toggled: false,
              objectType: objType,
              path: [
                ...path,
                { type: "prototype getter", value: key, childIndex: children.length },
              ],
              contentType: "getter",
              content: "(...)",
              hasChildren: false,
              children: [],
            };
            children.push(child);
          }
        });
        proto = Object.getPrototypeOf(proto);
      }
      if (obj.__proto__) {
        prototype = this.serializeObjectChild(
          obj,
          { type: "prototype", childIndex: children.length },
          depth,
          objType,
          path,
          oldBranch?.children.at(-1),
          oldTree
        );
        children.push(prototype);
      }
      return children;
    }
    // Returns the Component node given its path and the root component node
    getComponentNode(path) {
      // The node is an app and not a component
      if (path.length === 1) {
        return [...this.apps][path[0]];
      }
      // The second element in the path will always be the root of the app
      let node = [...this.apps][path[0]]?.root;
      if (!node) {
        return null;
      }
      for (let i = 2; i < path.length; i++) {
        // From this point onwards, it is an object path inside the component node
        if (typeof path[i] !== "string") {
          break;
        }
        if (node.children.hasOwnProperty(path[i])) {
          node = node.children[path[i]];
        } else {
          return null;
        }
      }
      return node;
    }
    // Apply manual render to the specified component
    refreshComponent(path) {
      const componentNode = this.getComponentNode(path);
      componentNode.render();
    }
    // Returns the component's details given its path
    getComponentDetails(path = null, oldTree = null) {
      let component = {};
      if (!path) {
        path = this.getElementPath($0);
      }
      component.path = path;
      let node = this.getComponentNode(path) || [...this.apps].find((app) => app.root)?.root;
      if (!node) {
        return null;
      }
      // A path with only the app index indicates that the component is an App instead
      const isApp = path.length === 1;
      if (isApp) {
        component.version = node.__proto__.constructor?.version
          ? node.__proto__.constructor.version
          : "<2.0.8";
      }
      // Load props of the component
      const props = isApp ? node.props : node.component.props;
      component.props = { toggled: oldTree ? oldTree.props.toggled : true, children: [] };
      component.name = isApp
        ? node?.name
          ? `App (${node.name})`
          : "App " + (Number(path[0]) + 1)
        : node.component.constructor.name;
      const propsPath = isApp
        ? [...path, { type: "item", value: "props" }]
        : [...path, { type: "item", value: "component" }, { type: "item", value: "props" }];
      Reflect.ownKeys(props).forEach((key) => {
        let oldBranch = oldTree?.props.children[component.props.children.length];
        const property = this.serializeObjectChild(
          props,
          { type: "item", value: key, childIndex: component.props.children.length },
          0,
          "props",
          propsPath,
          oldBranch,
          oldTree
        );
        if (property) {
          component.props.children.push(property);
        }
      });
      // Load env of the component
      const env = isApp ? node.env : node.component.env;
      component.env = { toggled: oldTree ? oldTree.env.toggled : false, children: [] };
      const envPath = isApp
        ? [...path, { type: "item", value: "env" }]
        : [...path, { type: "item", value: "component" }, { type: "item", value: "env" }];
      Reflect.ownKeys(env).forEach((key) => {
        let oldBranch = oldTree?.env.children[component.env.children.length];
        const envElement = this.serializeObjectChild(
          env,
          { type: "item", value: key, childIndex: component.env.children.length },
          0,
          "env",
          envPath,
          oldBranch,
          oldTree
        );
        if (envElement) {
          component.env.children.push(envElement);
        }
      });
      // Load env getters
      let obj = Object.getPrototypeOf(env);
      Reflect.ownKeys(obj).forEach((key) => {
        if (
          key !== "__proto__" &&
          Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
        ) {
          let child = {
            name: key,
            depth: 0,
            toggled: false,
            objectType: "env",
            path: [
              ...envPath,
              { type: "prototype getter", value: key, childIndex: component.env.children.length },
            ],
            contentType: "getter",
            content: "(...)",
            hasChildren: false,
            children: [],
          };
          component.env.children.push(child);
        }
      });
      const envPrototype = this.serializeObjectChild(
        env,
        { type: "prototype", childIndex: component.env.children.length },
        0,
        "env",
        envPath,
        oldTree?.env[component.env.children.length],
        oldTree
      );
      component.env.children.push(envPrototype);
      // Load instance of the component
      const instance = isApp ? node : node.component;
      component.instance = { toggled: oldTree ? oldTree.instance.toggled : true, children: [] };
      const instancePath = isApp ? path : [...path, { type: "item", value: "component" }];
      Reflect.ownKeys(instance).forEach((key) => {
        if (!["env", "props"].includes(key)) {
          let oldBranch = oldTree?.instance.children[component.instance.children.length];
          const instanceElement = this.serializeObjectChild(
            instance,
            { type: "item", value: key, childIndex: component.instance.children.length },
            0,
            "instance",
            instancePath,
            oldBranch,
            oldTree
          );
          if (instanceElement) {
            component.instance.children.push(instanceElement);
          }
        }
      });
      // Load instance getters
      obj = Object.getPrototypeOf(instance);
      while (obj) {
        Reflect.ownKeys(obj).forEach((key) => {
          if (
            key !== "__proto__" &&
            Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
          ) {
            let child = {
              name: key,
              depth: 0,
              toggled: false,
              objectType: "instance",
              path: [
                ...instancePath,
                {
                  type: "prototype getter",
                  value: key,
                  childIndex: component.instance.children.length,
                },
              ],
              contentType: "getter",
              content: "(...)",
              hasChildren: false,
              children: [],
            };
            component.instance.children.push(child);
          }
        });
        obj = Object.getPrototypeOf(obj);
      }
      const instancePrototype = this.serializeObjectChild(
        instance,
        { type: "prototype", childIndex: component.instance.children.length },
        0,
        "instance",
        instancePath,
        oldTree?.instance[component.instance.children.length],
        oldTree
      );
      component.instance.children.push(instancePrototype);

      // Load subscriptions of the component
      if (isApp) {
        component.subscriptions = {
          toggled: oldTree ? oldTree.subscriptions.toggled : true,
          children: [],
        };
      } else {
        const rawSubscriptions = this.topLevelSubscriptions(node);
        component.subscriptions = {
          toggled: oldTree ? oldTree.subscriptions.toggled : true,
          children: [],
        };
        rawSubscriptions.forEach((rawSubscription) => {
          let subscription = {
            target: {
              name: this.targetName(rawSubscription.target, node),
              contentType:
                typeof rawSubscription.target === "object"
                  ? Array.isArray(rawSubscription.target)
                    ? "array"
                    : "object"
                  : rawSubscription.target,
              depth: 0,
              path: [
                ...path,
                { type: "item", value: "subscriptions" },
                { type: "item", value: rawSubscription.index },
                { type: "item", value: "target" },
              ],
              toggled: false,
              objectType: "subscription",
            },
          };
          if (
            oldTree &&
            oldTree.subscriptions.children[rawSubscription.index] &&
            oldTree.subscriptions.children[rawSubscription.index].target.toggled
          ) {
            subscription.target.toggled = true;
          }
          if (rawSubscription.target == null) {
            if (subscription.target.contentType === "undefined") {
              subscription.target.content = "undefined";
            } else {
              subscription.target.content = "null";
            }
            subscription.target.hasChildren = false;
          } else {
            subscription.target.content = this.serializer.serializeContent(
              rawSubscription.target,
              subscription.target.contentType
            );
            subscription.target.hasChildren =
              subscription.target.contentType === "object"
                ? Object.keys(rawSubscription.target).length > 0
                : subscription.target.contentType === "array"
                ? rawSubscription.target.length > 0
                : false;
          }
          subscription.target.children = [];
          if (subscription.target.toggled) {
            subscription.target.children = this.loadObjectChildren(
              subscription.target.path,
              subscription.target.depth,
              subscription.target.contentType,
              subscription.target.objectType,
              oldTree
            );
          }
          this.addHighlightedKeys(subscription.target);
          component.subscriptions.children.push(subscription);
        });
      }
      return component;
    }
    // Replace the content of a parsed getter object with the result of the corresponding get method
    loadGetterContent(getter) {
      let obj = this.getObjectProperty(getter.path);
      if (obj == null) {
        if (typeof obj === "undefined") {
          getter.content = "undefined";
          getter.contentType = "undefined";
        } else {
          getter.content = "null";
          getter.contentType = "object";
        }
        getter.hasChildren = false;
      } else {
        obj = this.toRaw(obj);
        switch (true) {
          case obj instanceof Map:
            getter.contentType = "map";
            getter.hasChildren = obj.size > 0;
            break;
          case obj instanceof Set:
            getter.contentType = "set";
            getter.hasChildren = obj.size > 0;
            break;
          case obj instanceof Array:
            getter.contentType = "array";
            getter.hasChildren = obj.length > 0;
            break;
          case typeof obj === "function":
            getter.contentType = "function";
            getter.hasChildren = Reflect.ownKeys(obj).length > 0;
            break;
          case obj instanceof Object:
            getter.contentType = "object";
            getter.hasChildren = Reflect.ownKeys(obj).length > 0;
            break;
          default:
            getter.contentType = typeof obj;
            getter.hasChildren = false;
        }
        getter.content = this.serializer.serializeContent(obj, getter.contentType);
      }
      return getter;
    }
    // Gives the DOM elements which correspond to the given component node
    getDOMElementsRecursive(node) {
      if (node.hasOwnProperty("bdom")) {
        return this.getDOMElementsRecursive(node.bdom);
      }
      if (node.hasOwnProperty("content")) {
        return this.getDOMElementsRecursive(node.content);
      }
      if (node.hasOwnProperty("el")) {
        if (node.el instanceof Element || node.el instanceof Text) {
          return [node.el];
        }
      }
      if (node.hasOwnProperty("child")) {
        return this.getDOMElementsRecursive(node.child);
      }
      if (node.hasOwnProperty("children") && node.children.length > 0) {
        let elements = [];
        for (const child of node.children) {
          if (child) {
            elements = elements.concat(this.getDOMElementsRecursive(child));
          }
        }
        if (elements.length > 0) {
          return elements;
        }
      }
      if (node.hasOwnProperty("parentEl")) {
        if (node.parentEl instanceof Element) {
          return [node.parentEl];
        }
      }
      return [];
    }
    // Triggers the highlight effect around the specified component.
    highlightComponent(path) {
      // Try to highlight the root component of the app if function called on an app
      if (path.length === 1) {
        path.push("root");
      }
      let component = this.getComponentNode(path);
      if (!component) {
        return;
      }
      const elements = this.getDOMElementsRecursive(component);
      this.highlightElements(elements, component.component.constructor.name);
    }
    // Edit a reactive state property with the provided value of the given component (path) and the subscription path
    editObject(path, value, objectType) {
      if (value === "undefined") {
        value = undefined;
      } else {
        try {
          value = JSON.parse(value);
        } catch (e) {
          console.warn("Could not evaluate user property\n", e);
          return;
        }
      }
      const item = path.pop();
      const obj = this.getObjectProperty(path);
      const key = item.hasOwnProperty("symbolIndex")
        ? Object.getOwnPropertySymbols(obj)[item.symbolIndex]
        : item.value;
      if (!obj) {
        return;
      }
      if (objectType === "subscription") {
        this.reactive(obj)[key] = value;
      } else {
        obj[key] = value;
        if (objectType === "props" || objectType === "instance") {
          const component = this.getComponentNode(path);
          if (component.__proto__.hasOwnProperty("render")) {
            this.getComponentNode(path).render();
          } else {
            component.root.render();
          }
        } else if (objectType === "env") {
          [...this.apps][path[0]].root.render(true);
        }
      }
    }
    // Recursively checks if the given html element corresponds to a component in the components tree.
    // Immediatly returns the path of the first component which matches the element
    searchElement(node, path, element) {
      if (!node?.bdom) {
        return null;
      }
      const results = this.getDOMElementsRecursive(node);
      let hasElementInChildren = false;
      for (const result of results) {
        if (result.isEqualNode(element)) {
          return path;
        }
        if (result.contains(element)) {
          hasElementInChildren = true;
        }
      }
      if (hasElementInChildren) {
        for (const [key, child] of Object.entries(node.children)) {
          const result = this.searchElement(child, path.concat([key]), element);
          if (result) {
            return result;
          }
        }
      }
      return null;
    }
    // Returns the path to the component which is currently being inspected
    getElementPath(element) {
      if (element) {
        // Create an array with the html element and all its successive parents
        const parentsList = [element];
        if (element.tagName !== "BODY") {
          while (element && element?.parentElement?.tagName !== "BODY") {
            element = element.parentElement;
            parentsList.push(element);
          }
        }
        const appsArray = [...this.apps];
        // Try to find a correspondance between the elements in the array and the owl component, stops at first result found
        for (const elem of parentsList) {
          for (const [index, app] of appsArray.entries()) {
            const inspectedPath = this.searchElement(app.root, ["root"], elem);
            if (inspectedPath) {
              inspectedPath.unshift(index.toString());
              return inspectedPath;
            }
          }
        }
      }
      // If nothing was found, return the path of the first root component found in the apps
      const appIndex = [...this.apps].findIndex((app) => app.root);
      return [appIndex.toString(), "root"];
    }
    // Returns the tree of components of the inspected page in a parsed format
    // Use inspectedPath to specify the path of the selected component
    getComponentsTree(inspectedPath = null, oldTrees = null, oldDetails = null) {
      const appsArray = [...this.apps];
      if (inspectedPath && !this.getComponentNode(inspectedPath)) {
        inspectedPath = null;
      }
      const trees = appsArray.map((app, index) => {
        let oldTree;
        if (oldTrees) {
          oldTree = oldTrees[index];
        }
        let appNode = {};
        appNode = {
          name: app?.name ? `App (${app.name})` : "App " + (index + 1),
          path: [index.toString()],
          key: "",
          depth: 0,
          toggled: true,
          selected: false,
          highlighted: false,
          version: app.__proto__.constructor?.version
            ? app.__proto__.constructor.version
            : "<2.0.8",
          children: [],
        };
        if (app.root) {
          const root = {
            name: app.root.component.constructor.name,
            path: [index.toString(), "root"],
            key: "",
            depth: 1,
            toggled: true,
            selected: false,
            highlighted: false,
          };
          if (oldTree) {
            appNode.toggled = oldTree.toggled;
            oldTree = oldTree.children[0];
          }
          if (oldTree) {
            root.toggled = oldTree.toggled;
          }
          // If no path is provided, it defaults to the target of the inspect element action
          if (!inspectedPath) {
            inspectedPath = this.getElementPath($0);
          }
          if (inspectedPath.join("/") === index.toString()) {
            appNode.selected = true;
            root.highlighted = true;
          } else if (inspectedPath.join("/") === index.toString() + "/root") {
            root.selected = true;
          }
          root.children = this.fillTree(app.root, root, inspectedPath.join("/"), oldTree);
          appNode.children.push(root);
        }
        return appNode;
      });
      const component = this.getComponentDetails(inspectedPath, oldDetails);
      return trees ? [trees, component] : [];
    }
    // Recursively fills the components tree as a parsed version
    fillTree(appNode, treeNode, inspectedPathString, oldBranch) {
      const children = [];
      for (const [key, appChild] of Object.entries(appNode.children)) {
        let child = {
          name: appChild.component.constructor.name,
          key: key,
          depth: treeNode.depth + 1,
          toggled: true,
          selected: false,
          highlighted: false,
        };
        child.path = treeNode.path.concat([child.key]);
        let oldChild = null;
        if (oldBranch) {
          const searchResult = oldBranch.children.find((o) => o.key === key);
          if (searchResult) {
            oldChild = searchResult;
            child.toggled = oldChild.toggled;
          }
        }
        const childPathString = child.path.join("/");
        if (childPathString === inspectedPathString) {
          child.selected = true;
        } else if (childPathString.includes(inspectedPathString)) {
          child.highlighted = true;
        }
        child.children = this.fillTree(appChild, child, inspectedPathString, oldChild);
        children.push(child);
      }
      return children;
    }
    // Returns the path of the given component node
    getComponentPath(componentNode) {
      let path = [];
      if (componentNode.parentKey) {
        path = [componentNode.parentKey];
        while (componentNode.parent && componentNode.parent.parentKey) {
          componentNode = componentNode.parent;
          path.unshift(componentNode.parentKey);
        }
      }
      path.unshift("root");
      const appsArray = [...this.apps];
      let index = appsArray.findIndex((app) => app === componentNode.app);
      path.unshift(index.toString());
      return path;
    }
    // Store the object into a temp window variable and log it to the console
    sendObjectToConsole(path) {
      const obj = this.getObjectProperty(path);
      let index = 1;
      while (window["temp" + index] !== undefined) {
        index++;
      }
      window["temp" + index] = obj;
      console.log("temp" + index + " = ", window["temp" + index]);
    }
    // Inspect the DOM of the component in the elements tab of the devtools
    inspectComponentDOM(path) {
      const componentNode = this.getComponentNode(path);
      const elements = this.getDOMElementsRecursive(componentNode);
      if (IS_FIREFOX) {
        window.$temp = elements[0];
      } else {
        inspect(elements[0]);
      }
    }
    // Inspect the DOM of the component in the elements tab of the devtools
    inspectComponentCompiledTemplate(path) {
      const componentNode = this.getComponentNode(path);
      const template = componentNode.component.constructor.template;
      if (IS_FIREFOX) {
        window.$temp = componentNode.app.templates[template];
      } else {
        inspect(componentNode.app.templates[template]);
      }
    }
    // Inspect source code of the component (corresponds to inspecting its constructor)
    inspectComponentRawTemplate(path) {
      const componentNode = this.getComponentNode(path);
      const template = componentNode.component.constructor.template;
      const templateNode = componentNode.app.rawTemplates[template];
      console.log(templateNode);
    }
    // Inspect source code of the function given by its path
    inspectFunctionSource(path) {
      const obj = this.getObjectProperty(path);
      if (IS_FIREFOX) {
        window.$temp = obj;
      } else {
        inspect(obj);
      }
    }

    targetName(target, node) {
      // check on component
      const { component } = node;
      for (const [key, value] of Object.entries(component)) {
        if (target === this.toRaw(value)) {
          return key;
        }
      }
      // check on props
      for (const [key, value] of Object.entries(component.props)) {
        if (target === this.toRaw(value)) {
          return `props.${key}`;
        }
      }
      return "[unknown]";
    }

    /**
     * Removes subscriptions that are a direct child of another subscription:
     * they will be reachable from the top level by expanding observed keys.
     *
     * @param {ComponentNode} node
     * @returns {{ keys: PropertyKey[], target: unknown}[]} the top level
     *  subscriptions of the node
     */
    topLevelSubscriptions(node) {
      const subscriptions = node.subscriptions.map((s, index) => ({ ...s, index }));
      const topLevelValues = new Set(Object.values(node.component).map((o) => this.toRaw(o)));
      const toOmit = new Set(
        subscriptions
          .flatMap(({ keys, target }) => keys.map((k) => this.toRaw(target[k])))
          .filter((obj) => !topLevelValues.has(obj))
      );
      return subscriptions.filter(({ target }) => !toOmit.has(target));
    }

    addHighlightedKeys(child) {
      const { path } = child;
      const subscriptionIndex = path.findIndex((item) => typeof item !== "string");
      if (path[subscriptionIndex]?.value === "subscriptions") {
        const node = this.getComponentNode(path.slice(0, subscriptionIndex));
        // Add observed keys
        const targetToKeys = new Map(node.subscriptions.map(({ keys, target }) => [target, keys]));
        const target = this.getObjectProperty(child.path);
        child.keys = targetToKeys.get(target)?.map((k) => String(k));
      }
    }
  }

  function checkOwlStatus() {
    let owlStatus = 2;
    if (!window.__OWL__DEVTOOLS_GLOBAL_HOOK__) {
      if (window.__OWL_DEVTOOLS__ || window.owl?.App) {
        owlStatus = 1;
      } else {
        // It seems that sending a 0 results in undefined at the other end for some reason
        owlStatus = -1;
      }
    }
    window.postMessage({ source: "owl-devtools", type: "owlStatus", data: owlStatus });
  }

  if (!window.__OWL_DEVTOOLS__) {
    let val;
    const descriptor = Object.getOwnPropertyDescriptor(window, "__OWL_DEVTOOLS__") || {
      get() {
        return val;
      },
      set(value) {
        val = value;
      },
    };
    Object.defineProperty(window, "__OWL_DEVTOOLS__", {
      get() {
        return descriptor.get.call(this);
      },
      set(value) {
        descriptor.set.call(this, value);
        if (value?.Fiber !== undefined) {
          window.__OWL__DEVTOOLS_GLOBAL_HOOK__ = new OwlDevtoolsGlobalHook();
        }
        window.top.postMessage({ source: "owl-devtools", type: "FrameReady" });
        checkOwlStatus();
      },
    });
    // Do note that the reload message is not sent on the top window so that it is not intercepted when originating
    // from an iframe
    window.postMessage({ source: "owl-devtools", type: "Reload" });
  } else if (
    window.__OWL_DEVTOOLS__?.Fiber !== undefined &&
    !window.__OWL__DEVTOOLS_GLOBAL_HOOK__
  ) {
    window.__OWL__DEVTOOLS_GLOBAL_HOOK__ = new OwlDevtoolsGlobalHook();
    window.postMessage({ source: "owl-devtools", type: "Reload" });
  }
  // Listener that checks whether owl is available on the page and if it has the right version
  window.addEventListener(
    "message",
    function (event) {
      if (event.data.source === "owl-devtools-background" && event.data.type === "checkOwlStatus") {
        checkOwlStatus();
      }
    },
    false
  );
  checkOwlStatus();
  // Indicates whether the scripts loaded successfully or not (only useful when loaded with eval in iframes)
  return window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;
})();
