/**
 * Unified Asset Loader service that handles both image and audio loading
 * Uses a priority queue to prioritize assets based on type and proximity
 */

// Configuration
const MAX_CONCURRENT_IMAGE_LOADS = 5;
const MAX_CONCURRENT_AUDIO_LOADS = 1;
const RETRY_DELAY_MS = 200;

// Types
export enum AssetType {
  IMAGE = "image",
  AUDIO = "audio",
}

export interface ImageAsset {
  type: AssetType.IMAGE;
  element: HTMLImageElement;
  getDistance: () => number;
  onComplete: () => void;
}

export interface AudioAsset {
  type: AssetType.AUDIO;
  src: string;
  getDistance: () => number;
  onComplete: (blobUrl: string) => void;
  onError: (error: Error) => void;
}

type Asset = ImageAsset | AudioAsset;

// State
let activeImageLoads = 0;
let activeAudioLoads = 0;

const assetQueue: Asset[] = [];

const loadedAudioCache: Record<string, string> = {};
const pendingAudioLoads: Set<string> = new Set();

/**
 * Check if there are any assets queued or actively loading
 */
export function hasQueuedAssets(): boolean {
  return assetQueue.length > 0 || activeImageLoads > 0 || activeAudioLoads > 0;
}

/**
 * Check if there are any images queued or actively loading
 */
export function hasQueuedImages(): boolean {
  return (
    assetQueue.some((asset) => asset.type === AssetType.IMAGE) ||
    activeImageLoads > 0
  );
}

/**
 * Queue an image for loading, respecting the global concurrent loading limit
 */
export function queueImageForLoading(
  element: HTMLImageElement,
  getDistance: () => number,
  onComplete?: () => void
) {
  if (!element.dataset.src) {
    console.warn("[AssetLoader] Element has no data-src attribute");
    return;
  }

  // Add to queue
  assetQueue.push({
    type: AssetType.IMAGE,
    element,
    onComplete: onComplete || (() => {}),
    getDistance,
  });

  // Try to process queue
  processQueue();
}

/**
 * Queue an audio file for loading, respecting the global concurrent loading limit
 * Returns a promise that resolves with the blob URL when loading is complete
 */
export function queueAudioForLoading(
  src: string,
  getDistance: () => number,
  onComplete?: (blobUrl: string) => void,
  onError?: (error: Error) => void
): Promise<string> {
  // Return cached result immediately if available
  if (loadedAudioCache[src]) {
    console.debug(`[AssetLoader] Using cached audio for ${src}`);
    const blobUrl = loadedAudioCache[src];
    if (onComplete) setTimeout(() => onComplete(blobUrl), 0);
    return Promise.resolve(blobUrl);
  }

  // Create promise for this request
  return new Promise((resolve, reject) => {
    // If this source is already being loaded, add to queue with same source
    // It will be grouped during loading
    const asset: AudioAsset = {
      type: AssetType.AUDIO,
      src,
      onComplete: (blobUrl) => {
        if (onComplete) onComplete(blobUrl);
        resolve(blobUrl);
      },
      onError: (error) => {
        if (onError) onError(error);
        reject(error);
      },
      getDistance,
    };

    // Add to queue
    assetQueue.push(asset);

    // Track pending loads
    if (!pendingAudioLoads.has(src)) {
      pendingAudioLoads.add(src);
    }

    // Try to process queue
    processQueue();
  });
}

/**
 * Sort the queue based on priority
 */
function sortQueue(): void {
  assetQueue.sort((a, b) => {
    return a.getDistance() - b.getDistance();
  });
}

/**
 * Process the next items in the queue if we have capacity
 */
function processQueue(): void {
  sortQueue();

  // First, attempt to process image assets
  while (
    activeImageLoads < MAX_CONCURRENT_IMAGE_LOADS &&
    assetQueue.length > 0
  ) {
    // Look for the highest priority image
    const imageIndex = assetQueue.findIndex(
      (asset) => asset.type === AssetType.IMAGE
    );
    if (imageIndex === -1) break; // No more images in the queue

    const asset = assetQueue.splice(imageIndex, 1)[0] as ImageAsset;
    loadImage(asset.element, asset.onComplete);
  }

  // Only process audio if we have capacity and no images are prioritized
  if (hasQueuedImages()) {
    // Schedule a retry after a delay to check again for audio processing
    setTimeout(processQueue, RETRY_DELAY_MS);
    return;
  }

  // Process audio assets
  const pendingAudios: Record<
    string,
    Array<{
      onComplete: (blobUrl: string) => void;
      onError: (error: Error) => void;
    }>
  > = {};

  // Continue loading audio while respecting MAX_CONCURRENT_AUDIO_LOADS
  while (
    activeAudioLoads < MAX_CONCURRENT_AUDIO_LOADS &&
    assetQueue.length > 0
  ) {
    const audioIndex = assetQueue.findIndex(
      (asset) => asset.type === AssetType.AUDIO
    );
    if (audioIndex === -1) break; // No more audio in the queue

    const asset = assetQueue.splice(audioIndex, 1)[0] as AudioAsset;

    // If already cached, complete immediately without consuming a slot
    if (loadedAudioCache[asset.src]) {
      asset.onComplete(loadedAudioCache[asset.src]);
      continue;
    }

    // Group by src to avoid duplicate loads
    if (!pendingAudios[asset.src]) {
      pendingAudios[asset.src] = [];
      activeAudioLoads++;
    }

    pendingAudios[asset.src].push({
      onComplete: asset.onComplete,
      onError: asset.onError,
    });
  }

  // Start loading each unique audio file
  Object.entries(pendingAudios).forEach(([src, handlers]) => {
    loadAudio(src, handlers);
  });

  // If we have more items in the queue, schedule another check
  if (assetQueue.length > 0) {
    setTimeout(processQueue, RETRY_DELAY_MS);
  }
}

/**
 * Internal function to handle the actual image loading
 */
function loadImage(element: HTMLImageElement, onComplete: () => void) {
  // Increment active loads counter
  activeImageLoads++;

  const src = element.dataset.src;
  console.debug(`[AssetLoader] Loading image ${src}`);

  // Start loading the image
  element.src = src!;

  // Handle load completion
  const handleCompletion = () => {
    activeImageLoads--;
    onComplete();
    processQueue();
  };

  // Set handlers
  element.onload = () => {
    console.debug(`[AssetLoader] Loaded image ${src}`);
    handleCompletion();
  };

  element.onerror = () => {
    console.error(`[AssetLoader] Failed to load image ${src}`);
    handleCompletion();
  };
}

/**
 * Internal function to handle the actual audio loading using fetch API
 * This ensures the entire audio file is downloaded
 * @param src The source URL to load
 * @param handlers Array of handlers to call when loading completes or fails
 */
async function loadAudio(
  src: string,
  handlers: Array<{
    onComplete: (blobUrl: string) => void;
    onError: (error: Error) => void;
  }>
) {
  console.debug(
    `[AssetLoader] Loading audio ${src} (${handlers.length} listeners)`
  );

  try {
    // Fetch the entire audio file
    const response = await fetch(src);

    if (!response.ok) {
      throw new Error(`Failed to load audio: ${response.statusText}`);
    }

    // Convert to blob to ensure full download
    const blob = await response.blob();

    // Create a blob URL to use as the audio source
    const blobUrl = URL.createObjectURL(blob);

    // Store in cache
    loadedAudioCache[src] = blobUrl;

    console.debug(`[AssetLoader] Successfully loaded audio ${src}`);

    // Call all completion handlers
    handlers.forEach((handler) => handler.onComplete(blobUrl));
  } catch (error) {
    console.error(`[AssetLoader] Error loading audio: ${error}`);

    // Call all error handlers
    handlers.forEach((handler) => handler.onError(error as Error));
  } finally {
    // Remove from pending loads
    pendingAudioLoads.delete(src);

    // Decrement counter when audio is loaded (or failed)
    activeAudioLoads--;

    // Process next asset in queue if available
    processQueue();
  }
}
