export type LayoutShiftSourceRect = {
  x: number;
  y: number;
  width: number;
  height: number;
};

export type LayoutShiftSource = {
  locator: string;
  previousRect: LayoutShiftSourceRect;
  currentRect: LayoutShiftSourceRect;
};

export type LayoutShiftEntry = {
  value: number;
  hadRecentInput: boolean;
  sources: LayoutShiftSource[];
};

const LAYOUT_SHIFT_OBSERVER_SCRIPT = `(() => {
  const globalWindow = window;
  globalWindow.__invLayoutShifts = [];
  if (typeof PerformanceObserver === "undefined") {
    return;
  }
  try {
    function locatorFor(element) {
      const segments = [];
      let current = element;
      for (;;) {
        if (current.id.length > 0) {
          segments.unshift("#" + current.id);
          return segments.join(" > ");
        }
        const testId = current.getAttribute("data-testid");
        const parent = current.parentElement;
        const tag = current.tagName.toLowerCase();
        if (testId !== null && testId.length > 0) {
          segments.unshift('[data-testid="' + testId + '"]');
        } else if (parent === null) {
          segments.unshift(tag);
        } else {
          const self = current;
          const twins = Array.from(parent.children).filter(
            (sibling) => sibling.tagName === self.tagName,
          );
          segments.unshift(
            twins.length < 2
              ? tag
              : tag + ":nth-of-type(" + String(twins.indexOf(self) + 1) + ")",
          );
        }
        if (parent === null) {
          return segments.join(" > ");
        }
        current = parent;
      }
    }

    function roundRect(rect, scrollX, scrollY) {
      return {
        x: Math.round(rect.x + scrollX),
        y: Math.round(rect.y + scrollY),
        width: Math.round(rect.width),
        height: Math.round(rect.height),
      };
    }

    function hasRect(rect) {
      return (
        rect !== undefined &&
        rect !== null &&
        typeof rect.x === "number" &&
        typeof rect.y === "number" &&
        typeof rect.width === "number" &&
        typeof rect.height === "number"
      );
    }

    function sourceFor(rawSource, scrollX, scrollY) {
      if (!hasRect(rawSource.previousRect) || !hasRect(rawSource.currentRect)) {
        return null;
      }
      const node = rawSource.node;
      let locator = "<detached>";
      if (node instanceof Element && node.isConnected) {
        try {
          locator = locatorFor(node);
        } catch {
          locator = "<detached>";
        }
      }
      return {
        locator,
        previousRect: roundRect(rawSource.previousRect, scrollX, scrollY),
        currentRect: roundRect(rawSource.currentRect, scrollX, scrollY),
      };
    }

    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        try {
          const layoutShift = entry;
          if (typeof layoutShift.value !== "number") {
            continue;
          }
          const scrollX = window.scrollX;
          const scrollY = window.scrollY;
          const sources = [];
          const rawSources = layoutShift.sources;
          if (rawSources !== undefined && rawSources.length > 0) {
            const limit = Math.min(5, rawSources.length);
            for (let index = 0; index < limit; index += 1) {
              try {
                const source = sourceFor(rawSources[index], scrollX, scrollY);
                if (source !== null) {
                  sources.push(source);
                }
              } catch {
                // Skip one malformed source without aborting the entry.
              }
            }
          }
          if (!Array.isArray(window.__invLayoutShifts)) {
            window.__invLayoutShifts = [];
          }
          window.__invLayoutShifts.push({
            value: layoutShift.value,
            hadRecentInput: layoutShift.hadRecentInput === true,
            sources,
          });
        } catch {
          // Skip one malformed entry without escaping to the page.
        }
      }
    });
    observer.observe({ type: "layout-shift", buffered: true });
  } catch {
    // Layout-shift observation is best-effort when unsupported.
  }
})()`;

export function layoutShiftObserverScript(): string {
  return LAYOUT_SHIFT_OBSERVER_SCRIPT;
}
