// @ts-nocheck

/**
 * TokenVideoOverlay — Optionally overlays player webcam video on their character
 * tokens on the active scene. 100% optional, toggled via world setting.
 *
 * Architecture:
 *   - Replaces token.mesh texture with a video texture from the user's webcam
 *   - Patches Token.prototype.refresh to re-apply after redraws
 *   - Subscribes to canvasReady, createToken, deleteToken, updateToken hooks
 *   - When disabled, restores original texture on all meshes
 *
 * @module ui/shared/TokenVideoOverlay
 */

export class TokenVideoOverlay {
  /**
   * @param {object} adapter - FoundryAdapter instance
   */
  constructor(adapter) {
    this._adapter = adapter;
    /** @type {Map<string, { videoEl: HTMLVideoElement, origTexture: PIXI.Texture, userId: string, canvas?: HTMLCanvasElement, ctx?: CanvasRenderingContext2D, pixiTexture?: PIXI.Texture, rafId?: number }>} */
    this._overlays = new Map();
    /** @type {Set<string>} */
    this._pending = new Set();
    /** @type {Function|null} */
    this._origRefresh = null;
    /** @type {boolean} */
    this._enabled = false;
  }

  init() {
    this._enabled = this._adapter.settings.get('showVideoOnTokens') ?? false;

    const TokenClass = foundry?.canvas?.placeables?.Token ?? globalThis.Token;
    if (!TokenClass?.prototype?.refresh) {
      console.warn('[ScryingPool] TokenVideoOverlay: Token class not found');
    }
    if (TokenClass?.prototype?.refresh) {
      this._origRefresh = TokenClass.prototype.refresh;
      const self = this;
      TokenClass.prototype.refresh = function (...args) {
        const result = self._origRefresh.apply(this, args);
        if (self._enabled) self._onTokenRefreshed(this);
        return result;
      };
    }

    Hooks.on('canvasReady', () => { if (this._enabled) this.syncAll(); });
    Hooks.on('createToken', (doc) => { if (this._enabled) this._onCreateToken(doc); });
    Hooks.on('deleteToken', (doc) => { if (this._enabled) this._detach(doc.id); });
    Hooks.on('updateToken', (doc) => { if (this._enabled) this._onUpdateToken(doc); });

    if (this._enabled) this.syncAll();
  }

  enable() {
    this._enabled = true;
    this.syncAll();
  }

  disable() {
    this._enabled = false;
    this._cleanupAll();
  }

  syncAll() {
    if (!canvas?.tokens?.placeables) {
      return;
    }
    const processed = new Set();
    for (const token of canvas.tokens.placeables) {
      const key = this._tokenKey(token);
      processed.add(key);
      if (!this._overlays.has(key) && !this._pending.has(key)) this._attach(token);
    }
    for (const [key] of this._overlays) {
      if (!processed.has(key)) this._detachByKey(key);
    }
  }

  _getOwningUserId(token) {
    const actor = token.document?.actor;
    if (!actor) return null;

    const mapping = this._adapter.settings.get('userActorMapping') ?? {};
    for (const [userId, actorId] of Object.entries(mapping)) {
      if (actorId === actor.id) {
        const user = this._adapter.users.get(userId);
        if (user && user.active) return userId;
      }
    }

    for (const user of this._adapter.users.all()) {
      if (user.isGM || !user.active) continue;
      if (typeof actor.testUserPermission === 'function') {
        if (actor.testUserPermission(user, CONST.DOCUMENT_PERMISSION_LEVELS.OWNER)) {
          return user.id;
        }
      }
    }
    return null;
  }

  _tokenKey(token) {
    return `${canvas.scene?.id ?? '?'}.${token.id}`;
  }

  _attach(token) {
    if (!token?.mesh) return;

    const key = this._tokenKey(token);
    if (this._pending.has(key)) return;
    this._pending.add(key);

    const userId = this._getOwningUserId(token);
    if (!userId) {
      this._pending.delete(key);
      return;
    }

    const stream = this._adapter.webrtc?.getMediaStreamForUser?.(userId);
    if (!stream?.getVideoTracks().length) {
      this._pending.delete(key);
      return;
    }

    const videoEl = document.createElement('video');
    videoEl.srcObject = stream;
    videoEl.autoplay = true;
    videoEl.muted = true;
    videoEl.playsInline = true;
    videoEl.setAttribute('aria-hidden', 'true');
    videoEl.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;';
    document.body.appendChild(videoEl);
    videoEl.play().catch(() => {});

    requestAnimationFrame(() => {
      this._pending.delete(key);
      if (!this._enabled || !token?.mesh) {
        videoEl.remove();
        return;
      }

      const origTexture = token.mesh.texture;
      const w = Math.max(1, Math.round(token.mesh.width));
      const h = Math.max(1, Math.round(token.mesh.height));

      const canvas = document.createElement('canvas');
      canvas.width = w;
      canvas.height = h;
      const ctx = canvas.getContext('2d');

      if (ctx) {
        const radius = Math.min(w, h) / 2;
        const cx = w / 2;
        const cy = h / 2;

        const pixiTexture = PIXI.Texture.from(canvas);
        token.mesh.texture = pixiTexture;

        const renderFrame = () => {
          const overlay = this._overlays.get(key);
          if (!overlay || !this._enabled) return;

          ctx.clearRect(0, 0, w, h);
          ctx.save();
          ctx.beginPath();
          ctx.arc(cx, cy, radius, 0, Math.PI * 2);
          ctx.clip();
          ctx.drawImage(videoEl, 0, 0, w, h);
          ctx.restore();

          pixiTexture.baseTexture.update();
          overlay.rafId = requestAnimationFrame(renderFrame);
        };

        const rafId = requestAnimationFrame(renderFrame);
        this._overlays.set(key, { videoEl, origTexture, userId, canvas, ctx, pixiTexture, rafId });
      } else {
        // Fallback: canvas 2D unavailable (e.g. test env), use direct video texture
        token.mesh.texture = PIXI.Texture.from(videoEl);
        this._overlays.set(key, { videoEl, origTexture, userId });
      }
    });
  }

  _detach(tokenId) {
    const sceneId = canvas.scene?.id ?? '?';
    this._detachByKey(`${sceneId}.${tokenId}`);
  }

  _detachByKey(key) {
    const overlay = this._overlays.get(key);
    if (!overlay) return;

    if (overlay.rafId) cancelAnimationFrame(overlay.rafId);

    const tokenId = key.split('.').pop();
    const token = canvas.tokens?.get(tokenId);
    if (token?.mesh && overlay.origTexture) {
      token.mesh.texture = overlay.origTexture;
    }

    overlay.videoEl?.pause();
    overlay.videoEl?.remove();
    this._overlays.delete(key);
  }

  _onTokenRefreshed(token) {
    const key = this._tokenKey(token);
    const overlay = this._overlays.get(key);

    if (overlay && token?.mesh) {
      if (overlay.canvas) {
        const w = Math.max(1, Math.round(token.mesh.width));
        const h = Math.max(1, Math.round(token.mesh.height));
        if (overlay.canvas.width !== w || overlay.canvas.height !== h) {
          this._detachByKey(key);
          this._attach(token);
          return;
        }
        if (token.mesh.texture !== overlay.pixiTexture) {
          token.mesh.texture = overlay.pixiTexture;
        }
      } else {
        const tex = token.mesh.texture;
        const src = tex?.baseTexture?.resource?.source;
        if (!src || src.tagName !== 'VIDEO' || src !== overlay.videoEl) {
          token.mesh.texture = PIXI.Texture.from(overlay.videoEl);
        }
      }
    } else if (!overlay && !this._pending.has(key)) {
      this._attach(token);
    }
  }

  _onCreateToken(doc) {
    requestAnimationFrame(() => {
      const token = canvas.tokens?.get(doc.id);
      if (token) this._attach(token);
    });
  }

  _onUpdateToken(doc) {
    const token = canvas.tokens?.get(doc.id);
    if (!token) return;

    const key = this._tokenKey(token);
    const existing = this._overlays.get(key);
    const userId = this._getOwningUserId(token);

    if (!userId) {
      if (existing) this._detachByKey(key);
      return;
    }

    if (existing && existing.userId === userId) {
      if (token?.mesh) {
        if (existing.pixiTexture) {
          if (token.mesh.texture !== existing.pixiTexture) {
            token.mesh.texture = existing.pixiTexture;
          }
        } else {
          const tex = token.mesh.texture;
          const src = tex?.baseTexture?.resource?.source;
          if (!src || src.tagName !== 'VIDEO' || src !== existing.videoEl) {
            token.mesh.texture = PIXI.Texture.from(existing.videoEl);
          }
        }
      }
      return;
    }

    if (existing) this._detachByKey(key);
    this._attach(token);
  }

  _cleanupAll() {
    for (const [key] of this._overlays) this._detachByKey(key);
  }
}
