// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import type * as Protocol from '../../generated/protocol.js';

/**
 * Functions in this file are serialized via `.toString()` and evaluated in the
 * inspected page context over CDP (e.g. `Runtime.callFunctionOn`). Because they
 * execute in the inspected browser environment, they use standard DOM types.
 */

export function scrollListenerInPage(this: HTMLElement|Document, id: number, reportScrollPositionBindingName: string,
                                     scrollListenerNameInPage: string): void {
  if ('scrollingElement' in this && !this.scrollingElement) {
    return;
  }

  const scrollingElement = ('scrollingElement' in this ? this.scrollingElement : this) as HTMLElement;
  // @ts-expect-error We're setting a custom field on `Element` or `Document` for retaining the function on the page.
  this[scrollListenerNameInPage] = () => {
    // @ts-expect-error `reportScrollPosition` binding is injected to the page before calling the function.
    globalThis[reportScrollPositionBindingName](
        JSON.stringify({scrollTop: scrollingElement.scrollTop, scrollLeft: scrollingElement.scrollLeft, id}));
  };

  // @ts-expect-error We've already defined the function used below.
  this.addEventListener('scroll', this[scrollListenerNameInPage], true);
}

export function removeScrollListenerInPage(this: HTMLElement|Document, scrollListenerNameInPage: string): void {
  // @ts-expect-error We've already set this custom field while adding scroll listener.
  this.removeEventListener('scroll', this[scrollListenerNameInPage]);
  // @ts-expect-error We've already set this custom field while adding scroll listener.
  delete this[scrollListenerNameInPage];
}

export function scrollTopInPage(this: Element|Document): number {
  if ('scrollingElement' in this) {
    if (!this.scrollingElement) {
      return 0;
    }

    return this.scrollingElement.scrollTop;
  }
  return this.scrollTop;
}

export function scrollLeftInPage(this: Element|Document): number {
  if ('scrollingElement' in this) {
    if (!this.scrollingElement) {
      return 0;
    }

    return this.scrollingElement.scrollLeft;
  }
  return this.scrollLeft;
}

export function setScrollTopInPage(this: Element|Document, offsetInPage: number): void {
  if ('scrollingElement' in this) {
    if (!this.scrollingElement) {
      return;
    }

    this.scrollingElement.scrollTop = offsetInPage;
  } else {
    this.scrollTop = offsetInPage;
  }
}

export function setScrollLeftInPage(this: Element|Document, offsetInPage: number): void {
  if ('scrollingElement' in this) {
    if (!this.scrollingElement) {
      return;
    }

    this.scrollingElement.scrollLeft = offsetInPage;
  } else {
    this.scrollLeft = offsetInPage;
  }
}

export function verticalScrollRangeInPage(this: Element|Document): number {
  if ('scrollingElement' in this) {
    if (!this.scrollingElement) {
      return 0;
    }

    return this.scrollingElement.scrollHeight - this.scrollingElement.clientHeight;
  }

  return this.scrollHeight - this.clientHeight;
}

export function horizontalScrollRangeInPage(this: Element|Document): number {
  if ('scrollingElement' in this) {
    if (!this.scrollingElement) {
      return 0;
    }

    return this.scrollingElement.scrollWidth - this.scrollingElement.clientWidth;
  }

  return this.scrollWidth - this.clientWidth;
}

export function toggleClassAndInjectStyleRule(this: Element, pseudoElementName: string|null, hidden: boolean): void {
  const classNamePrefix = '__web-inspector-hide';
  const classNameSuffix = '-shortcut__';
  const styleTagId = '__web-inspector-hide-shortcut-style__';
  const pseudoElementNameEscaped = pseudoElementName ? pseudoElementName.replace(/[\(\)\:]/g, '_') : '';
  const className = classNamePrefix + pseudoElementNameEscaped + classNameSuffix;
  this.classList.toggle(className, hidden);

  let localRoot: Element|HTMLHeadElement = this;
  while (localRoot.parentNode) {
    localRoot = (localRoot.parentNode as Element);
  }
  if (localRoot.nodeType === Node.DOCUMENT_NODE) {
    localRoot = document.head;
  }

  let style = localRoot.querySelector('style#' + styleTagId);
  if (!style) {
    const selectors = [];
    selectors.push('.__web-inspector-hide-shortcut__');
    selectors.push('.__web-inspector-hide-shortcut__ *');
    const selector = selectors.join(', ');
    const ruleBody = '    visibility: hidden !important;';
    const rule = '\n' + selector + '\n{\n' + ruleBody + '\n}\n';

    style = document.createElement('style');
    style.id = styleTagId;
    style.textContent = rule;

    localRoot.appendChild(style);
  }

  // In addition to putting them on the element we want to hide, we will
  // also add pseudo element classes to the style element to keep track of
  // which pseudo elements we have style rules for.
  if (pseudoElementName && !style.classList.contains(className)) {
    style.classList.add(className);
    style.textContent = `.${className}${pseudoElementName}, ${style.textContent}`;
  }
}

export function scrollIntoViewInPage(this: Element): void {
  this.scrollIntoViewIfNeeded(true);
}

export function focusInPage(this: HTMLElement): void {
  this.focus();
}

export function toStringForClipboard(this: Object, data: {
  subtype: string|undefined,
  indent: string,
}): string|undefined {
  const subtype = data.subtype;
  const indent = data.indent;

  if (subtype === 'node') {
    return this instanceof Element ? this.outerHTML : undefined;
  }
  if (subtype && typeof this === 'undefined') {
    return String(subtype);
  }
  try {
    return JSON.stringify(this, null, indent);
  } catch {
    return String(this);
  }
}

export function saveVariable(this: Window, value: Protocol.Runtime.CallArgument): string {
  const prefix = 'temp';
  let index = 1;
  while ((prefix + index) in this) {
    ++index;
  }
  const name = prefix + index;
  // @ts-expect-error Assignment to global object
  this[name] = value;
  return name;
}
