/*
 * If not stated otherwise in this file or this component's LICENSE file the
 * following copyright and licenses apply:
 *
 * Copyright 2026 Comcast Cable Communications Management, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the License);
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Platform, type PlatformSettings } from '../Platform.js';
import {
  ImageWorkerManager,
  type ImageWorkerFactory,
} from './lib/ImageWorker.js';
import { createImageWorker } from './lib/ImageWorkerDefault.js';
import type { Stage } from '../../Stage.js';
import {
  dataURIToBlob,
  isBase64Image,
  convertUrlToAbsolute,
  createWebGLContext,
} from './lib/utils.js';

import type { ImageResponse } from '../../textures/ImageTexture.js';
import { loadSvg } from './lib/textureSvg.js';
import { loadCompressedTexture } from './lib/textureCompression.js';
import { WebGlContextWrapper } from './WebGlContextWrapper.js';
import type { GlContextWrapper } from '../GlContextWrapper.js';

/**
 * make fontface add not show errors
 */
interface FontFaceSetWithAdd extends FontFaceSet {
  add(font: FontFace): this;
}

export class WebPlatform extends Platform {
  private useImageWorker: boolean;
  private imageWorkerManager: ImageWorkerManager | null = null;
  private hasWorker = !!self.Worker;
  private stopped = false;

  constructor(settings: PlatformSettings = {}) {
    super(settings);

    const numImageWorkers = settings.numImageWorkers ?? 0;
    this.useImageWorker = numImageWorkers > 0 && this.hasWorker;

    if (this.useImageWorker === true) {
      this.imageWorkerManager = this.createImageWorkerManager(numImageWorkers);
    }
  }

  protected createImageWorkerManager(
    numImageWorkers: number,
  ): ImageWorkerManager {
    return new ImageWorkerManager(
      numImageWorkers,
      this.getImageWorkerFactory(),
    );
  }

  protected getImageWorkerFactory(): ImageWorkerFactory {
    return createImageWorker;
  }

  ////////////////////////
  // Platform-specific methods
  ////////////////////////

  override createCanvas(): HTMLCanvasElement {
    return document.createElement('canvas');
  }

  override createContext(): GlContextWrapper {
    if (this.canvas === null) {
      throw new Error('Canvas has not been created yet.');
    }

    const gl = createWebGLContext(this.canvas, this.settings.forceWebGL2);
    this.glw = new WebGlContextWrapper(gl);
    return this.glw;
  }

  override getElementById(id: string): HTMLElement | null {
    return document.getElementById(id);
  }

  ////////////////////////
  // Update loop
  ////////////////////////

  override startLoop(stage: Stage): void {
    this.stopped = false;
    let isIdle = false;
    let lastFrameTime = 0;

    const runLoop = (currentTime: number = 0) => {
      if (this.stopped) return;
      const targetFrameTime = stage.targetFrameTime;

      // Check if we should throttle this frame
      if (
        targetFrameTime > 0 &&
        currentTime - lastFrameTime < targetFrameTime
      ) {
        // Too early for next frame, schedule with setTimeout for precise timing
        const delay = targetFrameTime - (currentTime - lastFrameTime);
        setTimeout(() => requestAnimationFrame(runLoop), delay);
        return;
      }

      stage.updateFrameTime();
      stage.updateAnimations();

      if (!stage.hasSceneUpdates()) {
        // We still need to calculate the fps else it looks like the app is frozen
        stage.calculateFps();

        if (targetFrameTime > 0) {
          // Use setTimeout for throttled idle frames
          setTimeout(
            () => requestAnimationFrame(runLoop),
            Math.max(targetFrameTime, 16.666666666666668),
          );
        } else {
          // Use standard idle timeout when not throttling
          setTimeout(() => requestAnimationFrame(runLoop), 16.666666666666668);
        }

        if (isIdle === false) {
          stage.shManager.cleanup();
          stage.eventBus.emit('idle');
          isIdle = true;
        }

        if (stage.txMemManager.checkCleanup() === true) {
          stage.txMemManager.cleanup();
        }

        stage.flushFrameEvents();
        return;
      }

      if (isIdle === true) {
        stage.eventBus.emit('active');
        isIdle = false;
      }

      stage.drawFrame();
      stage.flushFrameEvents();

      // Schedule next frame
      if (targetFrameTime > 0) {
        // Use setTimeout + rAF combination for precise FPS control
        const nextFrameDelay = Math.max(
          0,
          targetFrameTime - (performance.now() - currentTime),
        );
        setTimeout(() => requestAnimationFrame(runLoop), nextFrameDelay);
      } else {
        // Use standard rAF when not throttling
        requestAnimationFrame(runLoop);
      }
    };
    requestAnimationFrame(runLoop);
  }

  override stopLoop(): void {
    this.stopped = true;
    if (this.imageWorkerManager !== null) {
      for (const worker of this.imageWorkerManager.workers) {
        worker.terminate();
      }
      this.imageWorkerManager = null;
    }
  }

  ////////////////////////
  // Image handling
  ////////////////////////

  override fetch(url: string): Promise<Blob> {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.responseType = 'blob';
      xhr.onreadystatechange = function () {
        if (xhr.readyState == XMLHttpRequest.DONE) {
          // On most devices like WebOS and Tizen, the file protocol returns 0 while http(s) protocol returns 200
          if (xhr.status === 0 || xhr.status === 200) {
            if (xhr.response instanceof Blob) {
              resolve(xhr.response);
            } else {
              reject(new Error('Expected blob response while loading image.'));
            }
          } else {
            reject(xhr.statusText);
          }
        }
      };
      xhr.open('GET', url, true);
      xhr.send(null);
    });
  }

  override async createImage(
    blob: Blob,
    premultiplyAlpha: boolean | null,
    sx: number | null,
    sy: number | null,
    sw: number | null,
    sh: number | null,
  ): Promise<ImageResponse> {
    const hasAlphaChannel = premultiplyAlpha ?? blob.type.includes('image/png');

    if (sw !== null && sh !== null) {
      // createImageBitmap with crop
      const bitmap = await createImageBitmap(blob, sx || 0, sy || 0, sw, sh, {
        premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none',
        colorSpaceConversion: 'none',
        imageOrientation: 'none',
      });
      return { data: bitmap, premultiplyAlpha: hasAlphaChannel };
    }

    // default createImageBitmap without crop but with options
    const bitmap = await createImageBitmap(blob, {
      premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none',
      colorSpaceConversion: 'none',
      imageOrientation: 'none',
    });

    return { data: bitmap, premultiplyAlpha: hasAlphaChannel };
  }

  override async loadImage(
    src: string,
    premultiplyAlpha: boolean | null,
    sx?: number | null,
    sy?: number | null,
    sw?: number | null,
    sh?: number | null,
  ): Promise<ImageResponse> {
    const isBase64 = isBase64Image(src);
    const absoluteSrc = convertUrlToAbsolute(src);
    const x = sx ?? null;
    const y = sy ?? null;
    const width = sw ?? null;
    const height = sh ?? null;

    // check if image worker is enabled
    if (this.imageWorkerManager !== null && isBase64 === false) {
      return this.imageWorkerManager.getImage(
        absoluteSrc,
        premultiplyAlpha,
        x,
        y,
        width,
        height,
      );
    }

    // fallback to main thread loading
    let blob: Blob;
    if (isBase64 === true) {
      blob = dataURIToBlob(src);
    } else {
      blob = await this.fetch(absoluteSrc);
    }

    return this.createImage(blob, premultiplyAlpha, x, y, width, height);
  }

  override async loadSvg(
    src: string,
    width: number | null,
    height: number | null,
    sx?: number | null,
    sy?: number | null,
    sw?: number | null,
    sh?: number | null,
  ): Promise<ImageResponse> {
    return loadSvg(
      convertUrlToAbsolute(src),
      width,
      height,
      sx ?? null,
      sy ?? null,
      sw ?? null,
      sh ?? null,
    );
  }

  override async loadCompressedTexture(src: string): Promise<ImageResponse> {
    return loadCompressedTexture(convertUrlToAbsolute(src));
  }

  ////////////////////////
  // Utilities
  ////////////////////////

  getTimeStamp(): number {
    return performance ? performance.now() : Date.now();
  }

  override addFont(font: FontFace): void {
    (document.fonts as FontFaceSetWithAdd).add(font);
  }
}
