import {
  CoreNode,
  type CoreNodeAnimateProps,
  type CoreNodeProps,
} from '../core/CoreNode.js';
import { type RendererMainSettings } from './Renderer.js';
import type { AnimationSettings } from '../core/animations/CoreAnimation.js';
import type {
  IAnimationController,
  AnimationControllerState,
} from '../common/IAnimationController.js';
import { isProductionEnvironment } from '../utils.js';
import { CoreTextNode, type CoreTextNodeProps } from '../core/CoreTextNode.js';
import type { Texture } from '../core/textures/Texture.js';
import { TextureType } from '../core/textures/Texture.js';

/**
 * Inspector Options
 *
 * Configuration options for the Inspector's performance monitoring features.
 */
export interface InspectorOptions {
  /**
   * Enable performance monitoring for setter calls
   *
   * @defaultValue true
   */
  enablePerformanceMonitoring: boolean;

  /**
   * Threshold for excessive setter calls before logging a warning
   *
   * @defaultValue 100
   */
  excessiveCallThreshold: number;

  /**
   * Time interval in milliseconds to reset the setter call counters
   *
   * @defaultValue 5000
   */
  resetInterval: number;

  /**
   * Enable animation monitoring and statistics tracking
   *
   * @defaultValue true
   */
  enableAnimationMonitoring: boolean;

  /**
   * Maximum number of animations to keep in history for statistics
   *
   * @defaultValue 1000
   */
  maxAnimationHistory: number;

  /**
   * Automatically print animation statistics every X seconds (0 to disable)
   *
   * @defaultValue 0
   */
  animationStatsInterval: number;
}

/**
 * Inspector
 *
 * The inspector is a tool that allows you to inspect the state of the renderer
 * and the nodes that are being rendered. It is a tool that is used for debugging
 * and development purposes.
 *
 * The inspector will generate a DOM tree that mirrors the state of the renderer
 */

/**
 * stylePropertyMap is a map of renderer properties that are mapped to CSS properties
 *
 * It can either return a string or an object with a prop and value property. Once a
 * property is found in the map, the value is set on the style of the div element.
 * Erik H made me do it.
 */
interface StyleResponse {
  prop: string;
  value: string;
}
const stylePropertyMap: {
  [key: string]: (
    value: string | number | boolean,
  ) => string | StyleResponse | null;
} = {
  alpha: (v) => {
    if (v === 1) {
      return null;
    }

    return { prop: 'opacity', value: `${v}` };
  },
  x: (x) => {
    return { prop: 'left', value: `${x}px` };
  },
  y: (y) => {
    return { prop: 'top', value: `${y}px` };
  },
  w: (w) => {
    if (w === 0) {
      return { prop: 'width', value: 'auto' };
    }

    return { prop: 'width', value: `${w}px` };
  },
  h: (h) => {
    if (h === 0) {
      return { prop: 'height', value: 'auto' };
    }

    return { prop: 'height', value: `${h}px` };
  },
  fontSize: (fs) => {
    if (fs === 0) {
      return null;
    }

    return { prop: 'font-size', value: `${fs}px` };
  },
  lineHeight: (lh) => {
    if (lh === 0) {
      return null;
    }

    return { prop: 'line-height', value: `${lh}px` };
  },
  zIndex: () => 'z-index',
  fontFamily: () => 'font-family',
  fontStyle: () => 'font-style',
  letterSpacing: () => 'letter-spacing',
  textAlign: () => 'text-align',
  overflowSuffix: () => 'overflow-suffix',
  maxLines: () => 'max-lines',
  contain: () => 'contain',
  verticalAlign: () => 'vertical-align',
  clipping: (v) => {
    if (v === false) {
      return null;
    }

    return { prop: 'overflow', value: v ? 'hidden' : 'visible' };
  },
  rotation: (v) => {
    if (v === 0) {
      return null;
    }

    return { prop: 'transform', value: `rotate(${v}rad)` };
  },
  scale: (v) => {
    if (v === 1) {
      return null;
    }

    return { prop: 'transform', value: `scale(${v})` };
  },
  scaleX: (v) => {
    if (v === 1) {
      return null;
    }

    return { prop: 'transform', value: `scaleX(${v})` };
  },
  scaleY: (v) => {
    if (v === 1) {
      return null;
    }

    return { prop: 'transform', value: `scaleY(${v})` };
  },
  color: (v) => {
    if (v === 0) {
      return null;
    }

    return { prop: 'color', value: convertColorToRgba(v as number) };
  },
};

const convertColorToRgba = (color: number) => {
  const a = (color & 0xff) / 255;
  const b = (color >> 8) & 0xff;
  const g = (color >> 16) & 0xff;
  const r = (color >> 24) & 0xff;
  return `rgba(${r},${g},${b},${a})`;
};

const domPropertyMap: { [key: string]: string } = {
  id: 'test-id',
};

const gradientColorPropertyMap = [
  'colorTop',
  'colorBottom',
  'colorLeft',
  'colorRight',
  'colorTl',
  'colorTr',
  'colorBl',
  'colorBr',
];

const textureTypeNames: Record<number, string> = {
  [TextureType.generic]: 'generic',
  [TextureType.color]: 'color',
  [TextureType.image]: 'image',
  [TextureType.noise]: 'noise',
  [TextureType.renderToTexture]: 'renderToTexture',
  [TextureType.subTexture]: 'subTexture',
};

interface TextureMetrics {
  previousState: string;
  loadedCount: number;
  failedCount: number;
  freedCount: number;
}

const knownProperties = new Set<string>([
  ...Object.keys(stylePropertyMap),
  ...Object.keys(domPropertyMap),
  // ...gradientColorPropertyMap,
  'src',
  'parent',
  'data',
  'text',
]);

export class Inspector {
  private root: HTMLElement | null = null;
  private canvas: HTMLCanvasElement | null = null;
  private mutationObserver: MutationObserver = new MutationObserver(() => {});
  private resizeObserver: ResizeObserver = new ResizeObserver(() => {});
  private height = 1080;
  private width = 1920;
  private scaleX = 1;
  private scaleY = 1;
  private textureMetrics = new Map<Texture, TextureMetrics>();

  // Performance monitoring for frequent setter calls
  private static setterCallCount = new Map<
    string,
    { count: number; lastReset: number; nodeId: number }
  >();

  // Animation monitoring structures
  private static activeAnimations = new Map<
    string,
    {
      nodeId: number;
      animationId: string;
      startTime: number;
      props: CoreNodeAnimateProps;
      settings: AnimationSettings;
      controller: IAnimationController;
      state: AnimationControllerState;
    }
  >();

  private static animationHistory: Array<{
    nodeId: number;
    animationId: string;
    startTime: number;
    endTime: number;
    duration: number;
    actualDuration: number;
    props: CoreNodeAnimateProps;
    settings: AnimationSettings;
    completionType: 'finished' | 'stopped' | 'cancelled';
  }> = [];

  // Performance monitoring settings (configured via constructor)
  private performanceSettings: InspectorOptions = {
    enablePerformanceMonitoring: false,
    excessiveCallThreshold: 100,
    resetInterval: 5000,
    enableAnimationMonitoring: false,
    maxAnimationHistory: 1000,
    animationStatsInterval: 0,
  };

  // Animation stats printing timer
  private animationStatsTimer: number | null = null;

  constructor(canvas: HTMLCanvasElement, settings: RendererMainSettings) {
    if (isProductionEnvironment === true) return;

    if (!settings) {
      throw new Error('settings is required');
    }

    // Initialize performance monitoring settings with defaults
    this.performanceSettings = {
      enablePerformanceMonitoring:
        settings.inspectorOptions?.enablePerformanceMonitoring ?? false,
      excessiveCallThreshold:
        settings.inspectorOptions?.excessiveCallThreshold ?? 100,
      resetInterval: settings.inspectorOptions?.resetInterval ?? 5000,
      enableAnimationMonitoring:
        settings.inspectorOptions?.enableAnimationMonitoring ?? false,
      maxAnimationHistory:
        settings.inspectorOptions?.maxAnimationHistory ?? 1000,
      animationStatsInterval:
        settings.inspectorOptions?.animationStatsInterval ?? 0,
    };

    // calc dimensions based on the devicePixelRatio
    this.height = Math.ceil(
      settings.appHeight ?? 1080 / (settings.deviceLogicalPixelRatio ?? 1),
    );

    this.width = Math.ceil(
      settings.appWidth ?? 1920 / (settings.deviceLogicalPixelRatio ?? 1),
    );

    this.scaleX = settings.deviceLogicalPixelRatio ?? 1;
    this.scaleY = settings.deviceLogicalPixelRatio ?? 1;

    this.canvas = canvas;
    this.root = document.createElement('div');
    this.setRootPosition();
    document.body.appendChild(this.root);

    //listen for changes on canvas
    this.mutationObserver = new MutationObserver(
      this.setRootPosition.bind(this),
    );
    this.mutationObserver.observe(canvas, {
      attributes: true,
      childList: false,
      subtree: false,
    });

    // Create a ResizeObserver to watch for changes in the element's size
    this.resizeObserver = new ResizeObserver(this.setRootPosition.bind(this));
    this.resizeObserver.observe(canvas);

    //listen for changes on window
    window.addEventListener('resize', this.setRootPosition.bind(this));

    // Start animation stats timer if enabled
    this.startAnimationStatsTimer();

    console.warn('Inspector is enabled, this will impact performance');
  }

  /**
   * Track setter calls for performance monitoring
   * Only active when Inspector is loaded
   */
  private trackSetterCall(nodeId: number, setterName: string): void {
    if (!this.performanceSettings.enablePerformanceMonitoring) {
      return;
    }

    const key = `${nodeId}_${setterName}`;
    const now = Date.now();
    const existing = Inspector.setterCallCount.get(key);

    if (!existing) {
      Inspector.setterCallCount.set(key, { count: 1, lastReset: now, nodeId });
      return;
    }

    // Reset counter if enough time has passed
    if (now - existing.lastReset > this.performanceSettings.resetInterval) {
      existing.count = 1;
      existing.lastReset = now;
      return;
    }

    existing.count++;

    // Log if threshold exceeded
    if (existing.count === this.performanceSettings.excessiveCallThreshold) {
      console.warn(
        `🚨 Inspector Performance Warning: Setter '${setterName}' called ${existing.count} times in ${this.performanceSettings.resetInterval}ms on node ${nodeId}`,
      );
    } else if (
      existing.count > this.performanceSettings.excessiveCallThreshold &&
      existing.count % 50 === 0
    ) {
      console.warn(
        `🚨 Inspector Performance Warning: Setter '${setterName}' called ${existing.count} times in ${this.performanceSettings.resetInterval}ms on node ${nodeId} (continuing...)`,
      );
    }
  }

  /**
   * Get current performance monitoring statistics
   */
  public static getPerformanceStats(): Array<{
    nodeId: number;
    setterName: string;
    count: number;
    timeWindow: number;
  }> {
    const stats: Array<{
      nodeId: number;
      setterName: string;
      count: number;
      timeWindow: number;
    }> = [];
    const now = Date.now();

    Inspector.setterCallCount.forEach((data, key) => {
      const parts = key.split('_');
      const nodeIdStr = parts[0];
      const setterName = parts[1];

      if (nodeIdStr && setterName) {
        const timeWindow = now - data.lastReset;

        stats.push({
          nodeId: parseInt(nodeIdStr, 10),
          setterName,
          count: data.count,
          timeWindow,
        });
      }
    });

    return stats.sort((a, b) => b.count - a.count);
  }

  /**
   * Clear performance monitoring statistics
   */
  public static clearPerformanceStats(): void {
    Inspector.setterCallCount.clear();
  }

  /**
   * Generate a unique animation ID
   */
  private static generateAnimationId(): string {
    return `anim_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
  }

  /**
   * Wrap animation controller with monitoring capabilities
   */
  private wrapAnimationController(
    controller: IAnimationController,
    nodeId: number,
    props: CoreNodeAnimateProps,
    settings: AnimationSettings,
    div: HTMLElement,
  ): IAnimationController {
    if (!this.performanceSettings.enableAnimationMonitoring) {
      // Just add the basic DOM animation without tracking
      const originalStart = controller.start.bind(controller);
      controller.start = () => {
        this.animateNode(div, props, settings);
        return originalStart();
      };
      return controller;
    }

    const animationId = Inspector.generateAnimationId();

    // Create wrapper controller
    const wrappedController: IAnimationController = {
      start: () => {
        this.trackAnimationStart(
          animationId,
          nodeId,
          props,
          settings,
          controller,
        );
        this.animateNode(div, props, settings);
        return controller.start();
      },

      stop: () => {
        this.trackAnimationEnd(animationId, 'stopped');
        return controller.stop();
      },

      pause: () => {
        this.updateAnimationState(animationId, 'paused');
        return controller.pause();
      },

      restore: () => {
        this.trackAnimationEnd(animationId, 'cancelled');
        return controller.restore();
      },

      waitUntilStopped: () => {
        return controller.waitUntilStopped().then(() => {
          this.trackAnimationEnd(animationId, 'finished');
        });
      },

      get state() {
        return controller.state;
      },

      // Event emitter methods
      on: controller.on.bind(controller),
      off: controller.off.bind(controller),
      once: controller.once.bind(controller),
      emit: controller.emit.bind(controller),
    };

    // Track animation events
    controller.on('animating', () => {
      this.updateAnimationState(animationId, 'running');
    });

    controller.on('stopped', () => {
      this.trackAnimationEnd(animationId, 'finished');
    });

    return wrappedController;
  }

  /**
   * Track animation start
   */
  private trackAnimationStart(
    animationId: string,
    nodeId: number,
    props: CoreNodeAnimateProps,
    settings: AnimationSettings,
    controller: IAnimationController,
  ): void {
    const startTime = Date.now();

    Inspector.activeAnimations.set(animationId, {
      nodeId,
      animationId,
      startTime,
      props,
      settings,
      controller,
      state: 'scheduled',
    });
  }

  /**
   * Update animation state
   */
  private updateAnimationState(
    animationId: string,
    state: AnimationControllerState,
  ): void {
    const animation = Inspector.activeAnimations.get(animationId);
    if (animation) {
      animation.state = state;
    }
  }

  /**
   * Track animation end
   */
  private trackAnimationEnd(
    animationId: string,
    completionType: 'finished' | 'stopped' | 'cancelled',
  ): void {
    const animation = Inspector.activeAnimations.get(animationId);
    if (!animation) return;

    const endTime = Date.now();
    const actualDuration = endTime - animation.startTime;
    const expectedDuration = animation.settings.duration || 1000;

    // Move to history
    Inspector.animationHistory.unshift({
      nodeId: animation.nodeId,
      animationId: animation.animationId,
      startTime: animation.startTime,
      endTime,
      duration: expectedDuration,
      actualDuration,
      props: animation.props,
      settings: animation.settings,
      completionType,
    });

    // Limit history size for performance
    if (
      Inspector.animationHistory.length >
      this.performanceSettings.maxAnimationHistory
    ) {
      Inspector.animationHistory.splice(
        this.performanceSettings.maxAnimationHistory,
      );
    }

    // Remove from active animations
    Inspector.activeAnimations.delete(animationId);
  }

  /**
   * Get currently active animations
   */
  public static getActiveAnimations(): Array<{
    nodeId: number;
    animationId: string;
    startTime: number;
    duration: number;
    elapsedTime: number;
    props: CoreNodeAnimateProps;
    settings: AnimationSettings;
    state: AnimationControllerState;
  }> {
    const now = Date.now();
    const activeAnimations: Array<{
      nodeId: number;
      animationId: string;
      startTime: number;
      duration: number;
      elapsedTime: number;
      props: CoreNodeAnimateProps;
      settings: AnimationSettings;
      state: AnimationControllerState;
    }> = [];

    Inspector.activeAnimations.forEach((animation) => {
      activeAnimations.push({
        nodeId: animation.nodeId,
        animationId: animation.animationId,
        startTime: animation.startTime,
        duration: animation.settings.duration || 1000,
        elapsedTime: now - animation.startTime,
        props: animation.props,
        settings: animation.settings,
        state: animation.state,
      });
    });

    return activeAnimations.sort((a, b) => b.startTime - a.startTime);
  }

  /**
   * Get animation statistics
   */
  public static getAnimationStats(): {
    totalAnimations: number;
    activeCount: number;
    averageDuration: number;
  } {
    const totalAnimations = Inspector.animationHistory.length;
    const activeCount = Inspector.activeAnimations.size;

    // Calculate average duration from finished animations only
    const finishedAnimations = Inspector.animationHistory.filter(
      (anim) => anim.completionType === 'finished',
    );

    const averageDuration =
      finishedAnimations.length > 0
        ? finishedAnimations.reduce(
            (sum, anim) => sum + anim.actualDuration,
            0,
          ) / finishedAnimations.length
        : 0;

    return {
      totalAnimations,
      activeCount,
      averageDuration,
    };
  }

  /**
   * Clear animation monitoring data
   */
  public static clearAnimationStats(): void {
    Inspector.activeAnimations.clear();
    Inspector.animationHistory.length = 0;
  }

  /**
   * Start the animation stats timer if enabled
   */
  private startAnimationStatsTimer(): void {
    console.log(
      `Starting animation stats timer with interval: ${this.performanceSettings.animationStatsInterval} seconds`,
    );

    if (this.performanceSettings.animationStatsInterval > 0) {
      this.animationStatsTimer = setInterval(() => {
        this.printAnimationStats();
      }, this.performanceSettings.animationStatsInterval * 1000) as unknown as number;
    }
  }

  /**
   * Stop the animation stats timer
   */
  private stopAnimationStatsTimer(): void {
    if (this.animationStatsTimer) {
      clearInterval(this.animationStatsTimer);
      this.animationStatsTimer = null;
    }
  }

  /**
   * Print current animation statistics to console
   */
  private printAnimationStats(): void {
    const stats = Inspector.getAnimationStats();

    console.log(
      `🎬 Animation Stats: ${stats.activeCount} active, ${
        stats.totalAnimations
      } completed, ${Math.round(stats.averageDuration)}ms avg duration`,
    );
  }
  setRootPosition() {
    if (this.root === null || this.canvas === null) {
      return;
    }

    // get the world position of the canvas object, so we can match the inspector to it
    const rect = this.canvas.getBoundingClientRect();
    const top = document.documentElement.scrollTop + rect.top;
    const left = document.documentElement.scrollLeft + rect.left;

    this.root.id = 'root';
    this.root.style.left = `${left}px`;
    this.root.style.top = `${top}px`;
    this.root.style.width = `${this.width}px`;
    this.root.style.height = `${this.height}px`;
    this.root.style.position = 'absolute';
    this.root.style.transformOrigin = '0 0 0';
    this.root.style.transform = `scale(${this.scaleX}, ${this.scaleY})`;
    this.root.style.overflow = 'hidden';
    this.root.style.zIndex = '65534';
  }

  createDiv(
    node: CoreNode,
    properties: CoreNodeProps | CoreTextNodeProps,
  ): HTMLElement {
    const div = document.createElement('div');
    div.style.position = 'absolute';
    div.id = node.id.toString();
    div.setAttribute('type', node.constructor.name);

    // set initial properties
    for (const key in properties) {
      this.updateNodeProperty(
        div,
        // really typescript? really?
        key as keyof CoreNodeProps,
        properties[key as keyof CoreNodeProps],
        properties,
      );
    }

    return div;
  }

  createNodes(node: CoreNode): boolean {
    if (this.root === null) {
      return false;
    }

    const div = this.root.querySelector(`[id="${node.id}"]`);
    if (div === null && node instanceof CoreTextNode) {
      this.createTextNode(node);
    } else if (div === null && node instanceof CoreNode) {
      this.createNode(node);
    }

    for (const child of node.children) {
      this.createNodes(child);
    }
    return true;
  }

  createNode(node: CoreNode): CoreNode {
    const div = this.createDiv(node, node.props);
    (div as HTMLElement & { node: CoreNode }).node = node;
    (node as CoreNode & { div: HTMLElement }).div = div;

    node.on('inViewport', () => div.setAttribute('state', 'inViewport'));
    node.on('inBounds', () => div.setAttribute('state', 'inBounds'));
    node.on('outOfBounds', () => div.setAttribute('state', 'outOfBounds'));

    // Monitor only relevant properties by trapping with selective assignment
    return this.createProxy(node, div);
  }

  createTextNode(node: CoreTextNode): CoreTextNode {
    // eslint-disable-next-line
    // @ts-ignore - textProps is a private property and keeping it that way
    // but we need it from the inspector to set the initial properties on the div element
    const div = this.createDiv(node, node.textProps);
    (div as HTMLElement & { node: CoreNode }).node = node;
    (node as CoreTextNode & { div: HTMLElement }).div = div;

    return this.createProxy(node, div) as CoreTextNode;
  }

  createProxy(
    node: CoreNode | CoreTextNode,
    div: HTMLElement,
  ): CoreNode | CoreTextNode {
    // Store texture event listeners for cleanup
    const textureListeners = new Map<
      Texture,
      {
        onLoaded: () => void;
        onFailed: () => void;
        onFreed: () => void;
      }
    >();

    const coreNodeListeners = new Map<
      CoreNode,
      {
        onLoaded: () => void;
      }
    >();

    const setupCoreNodeListeners = (coreNode: CoreNode) => {
      const onLoaded = () => {
        this.updateTextNodeDimensions(div, coreNode as CoreTextNode);
      };
      coreNode.on('loaded', onLoaded);
      coreNodeListeners.set(coreNode, { onLoaded });
    };

    // Helper function to setup texture event listeners
    const setupTextureListeners = (texture: Texture | null) => {
      // Clean up existing listeners first
      textureListeners.forEach((listeners, oldTexture) => {
        oldTexture.off('loaded', listeners.onLoaded);
        oldTexture.off('failed', listeners.onFailed);
        oldTexture.off('freed', listeners.onFreed);
      });
      textureListeners.clear();

      // Setup new listeners if texture exists
      if (texture) {
        // Initialize metrics if not exists
        if (!this.textureMetrics.has(texture)) {
          this.textureMetrics.set(texture, {
            previousState: texture.state,
            loadedCount: 0,
            failedCount: 0,
            freedCount: 0,
          });
        }

        const onLoaded = () => {
          const metrics = this.textureMetrics.get(texture);
          if (metrics) {
            metrics.previousState =
              metrics.previousState !== texture.state
                ? metrics.previousState
                : 'loading';
            metrics.loadedCount++;
          }
          this.updateTextureAttributes(div, texture);
        };
        const onFailed = () => {
          const metrics = this.textureMetrics.get(texture);
          if (metrics) {
            metrics.previousState =
              metrics.previousState !== texture.state
                ? metrics.previousState
                : 'loading';
            metrics.failedCount++;
          }
          this.updateTextureAttributes(div, texture);
        };
        const onFreed = () => {
          const metrics = this.textureMetrics.get(texture);
          if (metrics) {
            metrics.previousState =
              metrics.previousState !== texture.state
                ? metrics.previousState
                : texture.state;
            metrics.freedCount++;
          }
          this.updateTextureAttributes(div, texture);
        };

        texture.on('loaded', onLoaded);
        texture.on('failed', onFailed);
        texture.on('freed', onFreed);

        textureListeners.set(texture, { onLoaded, onFailed, onFreed });
      }
    };
    // Define traps for each property in knownProperties
    knownProperties.forEach((property) => {
      let proto: CoreNode | ObjectConstructor | null = node;
      let originalProp: PropertyDescriptor | undefined | null = Object.getOwnPropertyDescriptor(proto, property);

      // Search the prototype chain for the property descriptor
      while(originalProp === undefined) {
          proto = Object.getPrototypeOf(proto) as ObjectConstructor;
          if (proto === null) {
            return;
          }
          originalProp = Object.getOwnPropertyDescriptor(proto, property);
      }

      if (property === 'text') {
        setupCoreNodeListeners(node);
      }

      Object.defineProperty(node, property, {
        get() {
          // eslint-disable-next-line @typescript-eslint/no-unsafe-return
          return originalProp?.get?.call(node);
        },
        set: (value) => {
          // Track setter call for performance monitoring
          this.trackSetterCall(node.id, property);

          originalProp?.set?.call(node, value);
          this.updateNodeProperty(
            div,
            property as keyof CoreNodeProps | keyof CoreTextNodeProps,
            value,
            node.props,
          );

          // Setup texture event listeners if this is a texture property
          if (property === 'texture') {
            const textureValue =
              value && typeof value === 'object' && 'state' in value
                ? (value as Texture)
                : null;
            setupTextureListeners(textureValue);
          }
        },
        configurable: true,
        enumerable: true,
      });
    });

    const originalDestroy = node.destroy;
    Object.defineProperty(node, 'destroy', {
      value: () => {
        // Clean up texture event listeners and metrics
        textureListeners.forEach((listeners, texture) => {
          texture.off('loaded', listeners.onLoaded);
          texture.off('failed', listeners.onFailed);
          texture.off('freed', listeners.onFreed);
          // Clean up metrics for this texture
          this.textureMetrics.delete(texture);
        });
        textureListeners.clear();

        coreNodeListeners.forEach((listeners, coreNode) => {
          coreNode.off('loaded', listeners.onLoaded);
        });
        coreNodeListeners.clear();

        this.destroyNode(node.id);
        originalDestroy.call(node);
      },
      configurable: true,
    });

    const originalAnimate = node.animate;
    Object.defineProperty(node, 'animate', {
      value: (
        props: CoreNodeAnimateProps,
        settings: AnimationSettings,
      ): IAnimationController => {
        const animationController = originalAnimate.call(node, props, settings);

        // Wrap animation controller with monitoring
        return this.wrapAnimationController(
          animationController,
          node.id,
          props,
          settings,
          div,
        );
      },
      configurable: true,
    });

    return node;
  }

  updateTextNodeDimensions(div: HTMLElement, node: CoreTextNode) {
    const textMetrics = node.renderInfo;
    if (textMetrics) {
      div.style.width = `${textMetrics.width}px`;
      div.style.height = `${textMetrics.height}px`;
    } else {
      div.style.removeProperty('width');
      div.style.removeProperty('height');
    }
  }

  updateTextureAttributes(div: HTMLElement, texture: Texture) {
    // Update texture state
    div.setAttribute('data-texture-state', texture.state);

    // Update texture type
    div.setAttribute(
      'data-texture-type',
      textureTypeNames[texture.type] || 'unknown',
    );

    // Update texture dimensions if available
    if (texture.dimensions) {
      div.setAttribute('data-texture-width', String(texture.dimensions.w));
      div.setAttribute('data-texture-height', String(texture.dimensions.h));
    } else {
      div.removeAttribute('data-texture-width');
      div.removeAttribute('data-texture-height');
    }

    // Update renderable owners count
    div.setAttribute(
      'data-texture-owners',
      String(texture.renderableOwners.length),
    );

    // Update retry count
    div.setAttribute('data-texture-retry-count', String(texture.retryCount));

    // Update max retry count if available
    if (texture.maxRetryCount !== null) {
      div.setAttribute(
        'data-texture-max-retry-count',
        String(texture.maxRetryCount),
      );
    } else {
      div.removeAttribute('data-texture-max-retry-count');
    }

    // Update metrics if available
    const metrics = this.textureMetrics.get(texture);
    if (metrics) {
      div.setAttribute('data-texture-previous-state', metrics.previousState);
      div.setAttribute(
        'data-texture-loaded-count',
        String(metrics.loadedCount),
      );
      div.setAttribute(
        'data-texture-failed-count',
        String(metrics.failedCount),
      );
      div.setAttribute('data-texture-freed-count', String(metrics.freedCount));
    } else {
      div.removeAttribute('data-texture-previous-state');
      div.removeAttribute('data-texture-loaded-count');
      div.removeAttribute('data-texture-failed-count');
      div.removeAttribute('data-texture-freed-count');
    }

    // Update error information if present
    if (texture.error) {
      div.setAttribute(
        'data-texture-error',
        texture.error.code || texture.error.message,
      );
    } else {
      div.removeAttribute('data-texture-error');
    }
  }

  public destroy() {
    // Stop animation stats timer
    this.stopAnimationStatsTimer();

    // Remove DOM observers
    this.mutationObserver.disconnect();
    this.resizeObserver.disconnect();

    // Remove resize listener
    window.removeEventListener('resize', this.setRootPosition.bind(this));
    if (this.root && this.root.parentNode) {
      this.root.remove();
    }

    // Clean up animation monitoring data
    Inspector.clearAnimationStats();
  }

  destroyNode(id: number) {
    const div = document.getElementById(id.toString());
    div?.remove();
  }

  updateNodeProperty(
    div: HTMLElement,
    property: keyof CoreNodeProps | keyof CoreTextNodeProps,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    value: any,
    props: CoreNodeProps | CoreTextNodeProps,
  ) {
    if (this.root === null || value === undefined) {
      return;
    }

    /**
     * Special case for parent property
     */
    if (property === 'parent') {
      if (value) {
        // detect if the parent is the root node
        if (value.id === value.stage.root.id) {
          this.root.appendChild(div);
        } else {
          value.div.appendChild(div);
        }
      } else {
        div.parentNode?.removeChild(div);
      }
      return;
    }

    // special case for text
    if (property === 'text') {
      div.innerHTML = String(value);

      // Keep DOM text invisible without breaking visibility checks
      // Use very low opacity (0.001) instead of 0 so Playwright still detects it
      div.style.opacity = '0.001';
      div.style.pointerEvents = 'none';
      div.style.userSelect = 'none';
      return;
    }

    // special case for images
    // we're not setting any CSS properties to avoid images getting loaded twice
    // as the renderer will handle the loading of the image. Setting it to `data-src`
    if (property === 'src' && value) {
      div.setAttribute(`data-src`, String(value));
      return;
    }

    // special case for color gradients (normal colors are handled by the stylePropertyMap)
    // FIXME the renderer seems to return the same number for all colors
    // if (gradientColorPropertyMap.includes(property as string)) {
    //   const color = convertColorToRgba(value as number);
    //   div.setAttribute(`data-${property}`, color);
    //   return;
    // }

    if (property === 'rtt' && value) {
      div.setAttribute('data-rtt', String(value));
      return;
    }

    // CSS mappable attribute
    if (stylePropertyMap[property]) {
      const mappedStyleResponse = stylePropertyMap[property]?.(value);

      if (mappedStyleResponse === null) {
        return;
      }

      if (typeof mappedStyleResponse === 'string') {
        div.style.setProperty(mappedStyleResponse, String(value));
        return;
      }

      if (typeof mappedStyleResponse === 'object') {
        let value = mappedStyleResponse.value;
        if (property === 'x') {
          const mount = props.mountX;
          const width = props.w;

          if (mount) {
            value = `${parseInt(value) - width * mount}px`;
          }
        } else if (property === 'y') {
          const mount = props.mountY;
          const height = props.h;

          if (mount) {
            value = `${parseInt(value) - height * mount}px`;
          }
        }
        div.style.setProperty(mappedStyleResponse.prop, value);
      }

      return;
    }

    // DOM properties
    if (domPropertyMap[property]) {
      const domProperty = domPropertyMap[property];
      if (!domProperty) {
        return;
      }

      div.setAttribute(String(domProperty), String(value));
      return;
    }

    // custom data properties
    if (property === 'data') {
      for (const key in value) {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        const keyValue: unknown = value[key];
        if (keyValue === undefined) {
          div.removeAttribute(`data-${key}`);
        } else {
          div.setAttribute(`data-${key}`, String(keyValue));
        }
      }
      return;
    }
  }

  updateViewport(
    width: number,
    height: number,
    deviceLogicalPixelRatio: number,
  ) {
    this.scaleX = deviceLogicalPixelRatio ?? 1;
    this.scaleY = deviceLogicalPixelRatio ?? 1;

    this.width = width;
    this.height = height;
    this.setRootPosition();
  }

  // simple animation handler
  animateNode(
    div: HTMLElement,
    props: CoreNodeAnimateProps,
    settings: AnimationSettings,
  ) {
    const {
      duration = 1000,
      delay = 0,
      // easing = 'linear',
      // repeat = 0,
      // loop = false,
      // stopMethod = false,
    } = settings;

    const {
      x,
      y,
      w,
      h,
      alpha = 1,
      rotation = 0,
      scale = 1,
      color,
      mountX,
      mountY,
    } = props;

    // ignoring loops and repeats for now, as that might be a bit too much for the inspector
    function animate() {
      setTimeout(() => {
        div.style.top = `${y - h * mountY}px`;
        div.style.left = `${x - w * mountX}px`;
        div.style.width = `${w}px`;
        div.style.height = `${h}px`;
        div.style.opacity = `${alpha}`;
        div.style.rotate = `${rotation}rad`;
        div.style.scale = `${scale}`;
        div.style.color = convertColorToRgba(color);
      }, duration);
    }

    setTimeout(animate, delay);
  }
}
