core/loader.js

import { instanceOfGlobal } from './runtime.js';

/**
 * @fileoverview LuminaJS Core - Image Loader
 * Handles ingestion of image sources (URL strings or File objects)
 * and resolves them to HTMLImageElement instances.
 */

/**
 * Loads an image from a URL string.
 *
 * @param {string} url - A fully-qualified image URL or a data URL.
 * @returns {Promise<HTMLImageElement>} Resolves with a fully loaded HTMLImageElement.
 * @throws {Error} Rejects if the image fails to load (e.g. 404, CORS block).
 */
function loadFromURL(url) {
  if (typeof globalThis.Image !== 'function') {
    throw new Error(
      'LuminaJS [runtime]: "load image from URL" is browser-only. ' +
        'Run this on the client side (window/document/Image available).',
    );
  }

  return new Promise((resolve, reject) => {
    const img = new Image();

    // Required for cross-origin images to allow canvas pixel access.
    // The server must respond with appropriate CORS headers.
    img.crossOrigin = 'Anonymous';

    img.onload = () => resolve(img);
    img.onerror = () =>
      reject(
        new Error(
          `LuminaJS [loader]: Failed to load image from URL — "${url}"`,
        ),
      );

    img.src = url;
  });
}

/**
 * Loads an image from a File object by creating a temporary object URL.
 * The object URL is revoked automatically after the image has loaded
 * to prevent memory leaks.
 *
 * @param {File} file - A File object, typically from an <input type="file"> element.
 * @returns {Promise<HTMLImageElement>} Resolves with a fully loaded HTMLImageElement.
 * @throws {TypeError} Rejects if the provided File is not a valid image MIME type.
 * @throws {Error} Rejects if the image fails to load from the generated object URL.
 */
function loadFromFile(file) {
  if (!file.type.startsWith('image/')) {
    return Promise.reject(
      new TypeError(
        `LuminaJS [loader]: Expected an image File, but received MIME type "${file.type}".`,
      ),
    );
  }

  if (typeof globalThis.Image !== 'function') {
    throw new Error(
      'LuminaJS [runtime]: "load image from File" is browser-only. ' +
        'Run this on the client side (window/document/Image available).',
    );
  }

  if (
    typeof globalThis.URL === 'undefined' ||
    typeof globalThis.URL.createObjectURL !== 'function' ||
    typeof globalThis.URL.revokeObjectURL !== 'function'
  ) {
    throw new Error(
      'LuminaJS [runtime]: "load image from File" requires URL.createObjectURL support in the browser.',
    );
  }

  const objectURL = URL.createObjectURL(file);

  return new Promise((resolve, reject) => {
    const img = new Image();

    img.onload = () => {
      URL.revokeObjectURL(objectURL); // Release memory immediately after load
      resolve(img);
    };

    img.onerror = () => {
      URL.revokeObjectURL(objectURL); // Also release on failure
      reject(
        new Error(
          `LuminaJS [loader]: Failed to load image from File — "${file.name}".`,
        ),
      );
    };

    img.src = objectURL;
  });
}

/**
 * Loads an image from either a URL string or a File object,
 * returning a Promise that resolves to an HTMLImageElement.
 *
 * This is the primary entry point for image ingestion in LuminaJS.
 *
 * @param {string | File} source - The image source. Accepts:
 *   - `string`: A URL (absolute, relative, or data URL).
 *   - `File`: A File object from the browser File API.
 * @returns {Promise<HTMLImageElement>} A promise that resolves to
 *   a fully loaded `HTMLImageElement`, ready for canvas drawing.
 * @throws {TypeError} Rejects if `source` is neither a string nor a File.
 *
 * @example
 * // Load from URL
 * const img = await loadImage('<image url string>');
 *
 * @example
 * // Load from File input
 * const [file] = event.target.files;
 * const img = await loadImage(file);
 */
export async function loadImage(source) {
  if (typeof source === 'string') {
    return loadFromURL(source);
  }

  if (instanceOfGlobal(source, 'File')) {
    return loadFromFile(source);
  }

  return Promise.reject(
    new TypeError(
      `LuminaJS [loader]: Invalid source type "${typeof source}". ` +
        `Expected a URL string or a File object.`,
    ),
  );
}