import * as audio from "./../audio/audio.ts";
import { warning } from "../lang/console.js";
import state from "./../state/state.ts";
import {
	emit,
	LOADER_COMPLETE,
	LOADER_ERROR,
	LOADER_PROGRESS,
} from "../system/event.ts";
import { getBasename } from "../utils/file.ts";
import {
	binList,
	fontList,
	imgList,
	jsonList,
	mtlList,
	objList,
	tmxList,
	videoList,
} from "./cache.js";
import { preloadAseprite } from "./parsers/aseprite.js";
import { preloadBinary } from "./parsers/binary.js";
import { preloadFontFace } from "./parsers/fontface.js";
import { preloadImage } from "./parsers/image.js";
import { preloadJSON } from "./parsers/json.js";
import { preloadMTL } from "./parsers/mtl.js";
import { preloadOBJ } from "./parsers/obj.js";
import { preloadJavascript } from "./parsers/script.js";
import { preloadTMX } from "./parsers/tmx.js";
import { preloadVideo } from "./parsers/video.js";

/**
 * a small class to manage loading of stuff and manage resources
 * @namespace loader
 */

//  to enable/disable caching
export let nocache = "";

/**
 * @type {Object.<string, string>}
 */
export const baseURL = {};

/**
 * crossOrigin attribute to configure the CORS requests for Image and Video data element.
 * By default (that is, when the attribute is not specified), CORS is not used at all.
 * The "anonymous" keyword means that there will be no exchange of user credentials via cookies,
 * client-side SSL certificates or HTTP authentication as described in the Terminology section of the CORS specification.<br>
 * @type {string}
 * @name crossOrigin
 * @memberof loader
 * @default undefined
 * @see {@link setOptions}
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes}
 * @example
 *  // allow for cross-origin texture loading
 * me.loader.crossOrigin = "anonymous";
 *
 * // set all resources to be loaded
 * me.loader.preload(game.resources, () => this.loaded());
 */
export let crossOrigin;

/**
 * indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies,
 * authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests.
 * @public
 * @type {boolean}
 * @name withCredentials
 * @see {@link setOptions}
 * @default false
 * @memberof loader
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials}
 * @example
 *  // enable withCredentials
 * me.loader.withCredentials = true;
 *
 * // set all resources to be loaded
 * me.loader.preload(game.resources, () => this.loaded());
 */
export let withCredentials = false;

/**
 * enable the nocache mechanism
 * @ignore
 */
export function setNocache(enable = false) {
	nocache = enable ? "?" + ~~(Math.random() * 10000000) : "";
}

/**
 * Sets the options for the loader.
 * @memberof loader
 * @param {Object} options - The options to set.
 * @param {string} [options.crossOrigin] - The crossOrigin attribute to configure the CORS requests for Image and Video data element.
 * @param {boolean} [options.nocache] - Enable or disable the nocache mechanism.
 * @param {boolean} [options.withCredentials] - Indicates whether or not cross-site Access-Control requests should be made using credentials.
 * @example
 * // Set the crossOrigin attribute to "anonymous"
 * me.loader.setOptions({ crossOrigin: "anonymous" });
 *
 * // Enable the nocache mechanism
 * me.loader.setOptions({ nocache: true });
 *
 * // Enable withCredentials
 * me.loader.setOptions({ withCredentials: true });
 * @category Assets
 */
export function setOptions(options) {
	if (options.crossOrigin !== undefined) {
		crossOrigin = options.crossOrigin;
	}
	if (options.nocache !== undefined) {
		setNocache(options.nocache);
	}
	if (options.withCredentials !== undefined) {
		withCredentials = options.withCredentials;
	}
}

/**
 * change the default baseURL for the given asset type.<br>
 * (this will prepend the asset URL and must finish with a '/')
 * @name setBaseURL
 * @memberof loader
 * @public
 * @param {string} type  - "*", "audio", "video", "binary", "image", "json", "js", "tmx", "tsx", "fontface", "aseprite"
 * @param {string} [url="./"] - default base URL
 * @example
 * // change the base URL relative address for audio assets
 * me.loader.setBaseURL("audio", "data/audio/");
 * // change the base URL absolute address for all object types
 * me.loader.setBaseURL("*", "http://myurl.com/")
 * @category Assets
 */
export function setBaseURL(type, url = "./") {
	if (type !== "*") {
		baseURL[type] = url;
	} else {
		// "wildcards"
		baseURL["audio"] = url;
		baseURL["video"] = url;
		baseURL["binary"] = url;
		baseURL["image"] = url;
		baseURL["json"] = url;
		baseURL["js"] = url;
		baseURL["tmx"] = url;
		baseURL["tsx"] = url;
		baseURL["fontface"] = url;
		baseURL["aseprite"] = url;
	}
}

/**
 * onload callback
 * @default undefined
 * @memberof loader
 * @type {function}
 * @deprecated since 18.2.0 - Use the {@link event.LOADER_COMPLETE} event or the `onloadcb` parameter of {@link loader.preload} instead.
 * @example
 * // use the LOADER_COMPLETE event instead
 * me.event.on(me.event.LOADER_COMPLETE, this.loaded.bind(this));
 */
export let onload;

/**
 * onProgress callback<br>
 * each time a resource is loaded, the loader will fire the specified function,
 * giving the actual progress [0 ... 1], as argument, and an object describing the resource loaded
 * @default undefined
 * @memberof loader
 * @type {function}
 * @deprecated since 18.2.0 - Use the {@link event.LOADER_PROGRESS} event instead.
 * @example
 * // use the LOADER_PROGRESS event instead
 * me.event.on(me.event.LOADER_PROGRESS, (progress, resource) => this.updateProgress(progress, resource));
 */
export let onProgress; // eslint-disable-line no-unassigned-vars

/**
 * onError callback<br>
 * each time a resource loading is failed, the loader will fire the specified function giving the actual asset as argument.
 * @default undefined
 * @memberof loader
 * @type {function}
 * @deprecated since 18.2.0 - Use the {@link event.LOADER_ERROR} event instead.
 * @example
 * // use the LOADER_ERROR event instead
 * me.event.on(me.event.LOADER_ERROR, (resource) => this.loaderError(resource));
 */
export let onError;

/**
 * list of parser function for supported format type
 */
const parsers = new Map();

/**
 * keep track if parsers were registered
 */
let parserInitialized = false;

// flag to check loading status
let resourceCount = 0;
let loadCount = 0;

/**
 * Assets uploaded with an error
 */
const failureLoadedAssets = {};

/**
 * init all supported parsers
 * @ignore
 */
function initParsers() {
	setParser("binary", preloadBinary);
	setParser("image", preloadImage);
	setParser("json", preloadJSON);
	setParser("js", preloadJavascript);
	setParser("tmx", preloadTMX);
	setParser("tsx", preloadTMX);
	setParser("audio", audio.load);
	setParser("fontface", preloadFontFace);
	setParser("video", preloadVideo);
	setParser("obj", preloadOBJ);
	setParser("mtl", preloadMTL);
	setParser("aseprite", preloadAseprite);
	parserInitialized = true;
}

/**
 * Complete loading: invoke the callback and emit the LOADER_COMPLETE event.
 * @param {Function} onloadcb - the completion callback
 * @ignore
 */
function completeLoading(onloadcb) {
	const callback = onloadcb || onload;
	if (typeof callback === "function") {
		callback();
		emit(LOADER_COMPLETE);
	} else {
		throw new Error("no load callback defined");
	}
}

/**
 * just increment the number of already loaded resources
 * @ignore
 */
function onResourceLoaded(res) {
	delete failureLoadedAssets[res.src];
	// increment the loading counter
	loadCount++;

	// currrent progress
	const progress = loadCount / resourceCount;

	// call callback if defined
	if (typeof onProgress === "function") {
		// pass the load progress in percent, as parameter
		onProgress(progress, res);
	}
	emit(LOADER_PROGRESS, progress, res);
}

/**
 * on error callback for image loading
 * @param {Asset} asset - asset that loaded with failure
 * @ignore
 */
function onLoadingError(res) {
	failureLoadedAssets[res.src] = res;
	if (this.onError) {
		this.onError(res);
	}
	emit(LOADER_ERROR, res);
	throw new Error("Failed loading resource " + res.src);
}

/**
 * an asset definition to be used with the loader
 * @typedef {object} Asset
 * @memberof loader
 * @property {string} name - name of the asset
 * @property {string} type  - the type of the asset ("audio"|"binary"|"image"|"json"|"js"|"tmx"|"tmj"|"tsx"|"tsj"|"fontface"|"video"|"aseprite")
 * @property {string|string[]} [src]  - path and/or file name of the resource (for audio assets only the path is required).
 * For image assets, an array of sources can be provided as a fallback chain (e.g. compressed texture formats by priority, with a PNG fallback).
 * The loader will try each source in order and use the first one that loads successfully.
 * @property {string} [data]  - TMX data if not provided through a src url
 * @property {boolean} [stream=false] - Set to true to not to wait for large audio or video file to be downloaded before playing.
 * @property {boolean} [autoplay=false] - Set to true to automatically start playing audio or video when loaded or added to a scene (using autoplay might require user iteraction to enable it)
 * @property {boolean} [loop=false] - Set to true to automatically loop the audio or video when playing
 * @see {@link preload}
 * @see {@link load}
 * @example
 *   // PNG tileset
 *   {name: "tileset-platformer", type: "image",  src: "data/map/tileset.png"}
 *   // PNG packed texture
 *   {name: "texture", type:"image", src: "data/gfx/texture.png"}
 *   // PNG base64 encoded image
 *   {name: "texture", type:"image", src: "data:image/png;base64,iVBORw0KAAAQAAAAEACA..."}
 *   // compressed texture with fallback chain (tries each source in order until one succeeds)
 *   {name: "terrain", type:"image", src: ["data/gfx/terrain.astc.ktx", "data/gfx/terrain.dds", "data/gfx/terrain.png"]}
 *   // TSX file
 *   {name: "meta_tiles", type: "tsx", src: "data/map/meta_tiles.tsx"}
 *   // TMX level (XML & JSON)
 *   {name: "map1", type: "tmx", src: "data/map/map1.json"}
 *   {name: "map2", type: "tmx", src: "data/map/map2.tmx"}
 *   {name: "map3", type: "tmx", format: "json", data: {"height":15,"layers":[...],"tilewidth":32,"version":1,"width":20}}
 *   {name: "map4", type: "tmx", format: "xml", data: {xml representation of tmx}}
 *   // audio resources
 *   {name: "bgmusic", type: "audio",  src: "data/audio/"}
 *   {name: "cling",   type: "audio",  src: "data/audio/"}
 *   // base64 encoded audio resources
 *   {name: "band",   type: "audio",  src: "data:audio/wav;base64,..."}
 *   // binary file
 *   {name: "ymTrack", type: "binary", src: "data/audio/main.ym"}
 *   // JSON file (used for texturePacker)
 *   {name: "texture", type: "json", src: "data/gfx/texture.json"}
 *   // JavaScript file
 *   {name: "plugin", type: "js", src: "data/js/plugin.js"}
 *   // Font Face
 *   { name: "'kenpixel'", type: "fontface",  src: "data/font/kenvector_future.woff2" }
 *   // video resources
 *   {name: "intro", type: "video",  src: "data/video/"}
 */

/**
 * specify a parser/preload function for the given asset type
 * @memberof loader
 * @param {string} type - asset type
 * @param {function} parserFn - parser function
 * @see {@link Asset.type}
 * @example
 * // specify a custom function for "abc" format
 * function customAbcParser(data, onload, onerror) {
 *    // preload and do something with the data
 *    let parsedData = doSomething(data);
 *    // when done, call the onload callback with the parsed data
 *    onload(parsedData);
 *    // in case of error, call the onerror callback
 *    onerror();
 *    // return the amount of asset parsed
 *    return 1
 * }
 * // set the parser for the custom format
 * loader.setParser("abc", customAbcParser);
 * @category Assets
 */
export function setParser(type, parserFn) {
	if (typeof parserFn !== "function") {
		throw new Error("invalid parser function for " + type);
	}

	if (typeof parsers.get(type) !== "undefined") {
		warning("overriding parser for " + type + " format");
	}

	parsers.set(type, parserFn);
}

/**
 * set all the specified game assets to be preloaded.
 * @memberof loader
 * @param {Asset[]} assets - list of assets to load
 * @param {Function} [onloadcb=loader.onload] - function to be called when all resources are loaded
 * @param {boolean} [switchToLoadState=true] - automatically switch to the loading screen
 * @example
 * game.assets = [
 *   // PNG tileset
 *   {name: "tileset-platformer", type: "image",  src: "data/map/tileset.png"},
 *   // PNG packed texture
 *   {name: "texture", type:"image", src: "data/gfx/texture.png"}
 *   // PNG base64 encoded image
 *   {name: "texture", type:"image", src: "data:image/png;base64,iVBORw0KAAAQAAAAEACA..."},
 *   // compressed texture with fallback chain (tries each source in order until one succeeds)
 *   {name: "terrain", type:"image", src: ["data/gfx/terrain.astc.ktx", "data/gfx/terrain.dds", "data/gfx/terrain.png"]},
 *   // TSX file
 *   {name: "meta_tiles", type: "tsx", src: "data/map/meta_tiles.tsx"},
 *   // TMX level (XML & JSON)
 *   {name: "map1", type: "tmx", src: "data/map/map1.json"},
 *   {name: "map2", type: "tmx", src: "data/map/map2.tmx"},
 *   {name: "map3", type: "tmx", format: "json", data: {"height":15,"layers":[...],"tilewidth":32,"version":1,"width":20}},
 *   {name: "map4", type: "tmx", format: "xml", data: {xml representation of tmx}},
 *   // audio resources
 *   {name: "bgmusic", type: "audio",  src: "data/audio/"},
 *   {name: "cling",   type: "audio",  src: "data/audio/"},
 *   // base64 encoded audio resources
 *   {name: "band",   type: "audio",  src: "data:audio/wav;base64,..."},
 *   // binary file
 *   {name: "ymTrack", type: "binary", src: "data/audio/main.ym"},
 *   // JSON file (used for texturePacker)
 *   {name: "texture", type: "json", src: "data/gfx/texture.json"},
 *   // JavaScript file
 *   {name: "plugin", type: "js", src: "data/js/plugin.js"},
 *   // Font Face
 *   {name: "'kenpixel'", type: "fontface",  src: "data/font/kenvector_future.woff2"},
 *   // video resources
 *   {name: "intro", type: "video",  src: "data/video/"},
 *   // base64 encoded video asset
 *   me.loader.load({name: "avatar", type:"video", src: "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZ..."};
 * ];
 * ...
 * // set all resources to be loaded
 * me.loader.preload(game.assets, () => this.loaded());
 * @category Assets
 */
export function preload(assets, onloadcb, switchToLoadState = true) {
	// set the onload callback if defined
	if (typeof onloadcb !== "undefined") {
		onload = onloadcb;
	}

	// parse the resources and collect promises for each asset
	const promises = [];
	for (let i = 0; i < assets.length; i++) {
		const asset = assets[i];
		const promise = new Promise((resolve, reject) => {
			const count = load(
				asset,
				() => {
					onResourceLoaded(asset);
					resolve();
				},
				(err) => {
					onLoadingError.call(this, asset);
					reject(err);
				},
			);
			resourceCount += count;
			if (count === 0) {
				// asset already loaded, resolve immediately
				resolve();
			}
		});
		promises.push(promise);
	}

	if (switchToLoadState === true) {
		// switch to the loading screen
		state.change(state.LOADING);
	}

	// call the completion callback as soon as all assets are loaded
	Promise.all(promises).then(() => {
		completeLoading(onload);
	});
}

/**
 * retry loading assets after a loading failure
 * @memberof loader
 * @param {string} src - src of asset to reload
 * @example
 *  event.on(
 *      event.LOADER_ERROR,
 *      (res) => {
 *          // custom function
 *          showErrorNotification({
 *              text: `Error during loading content: ${res.name}`,
 *              done: loader.reload(res.src);
 *          })
 *      }
 *  );
 **/
export function reload(src) {
	const assetToReload = failureLoadedAssets[src];
	unload(assetToReload);
	resourceCount -= 1;

	new Promise((resolve, reject) => {
		const count = load(
			assetToReload,
			() => {
				onResourceLoaded(assetToReload);
				resolve();
			},
			(err) => {
				onLoadingError.call(this, assetToReload);
				reject(err);
			},
		);
		resourceCount += count;
		if (count === 0) {
			resolve();
		}
	}).then(() => {
		completeLoading(onload);
	});
}

/**
 * Load a single asset (to be used if you need to load additional asset(s) during the game)
 * @memberof loader
 * @param {Asset} asset
 * @param {Function} [onload] - function to be called when the asset is loaded
 * @param {Function} [onerror] - function to be called in case of error
 * @returns {number} the amount of corresponding resource to be preloaded
 * @example
 * // load an image asset
 * me.loader.load({name: "avatar",  type:"image",  src: "data/avatar.png"}, () => this.onload(), () => this.onerror());
 * // load a compressed texture with fallback chain
 * me.loader.load({name: "terrain", type:"image", src: ["data/gfx/terrain.astc.ktx", "data/gfx/terrain.dds", "data/gfx/terrain.png"]}, () => this.onload());
 * // load a base64 image asset
 *  me.loader.load({name: "avatar", type:"image", src: "data:image/png;base64,iVBORw0KAAAQAAAAEACA..."};
 *  // load a base64 video asset
 *  me.loader.load({
 *     name: "avatar",
 *     type:"video",
 *     src: "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZ.."
 *  };
 * // start loading music
 * me.loader.load({
 *     name   : "bgmusic",
 *     type   : "audio",
 *     src    : "data/audio/"
 * }, function () {
 *     me.audio.play("bgmusic");
 * });
 * @category Assets
 */
export function load(asset, onload, onerror) {
	// make sure all parsers have been initialized
	if (parserInitialized === false) {
		initParsers();
	}

	// strip url() wrapper for fontface assets so baseURL can be prepended to the raw path
	if (asset.type === "fontface" && typeof asset.src === "string") {
		const urlMatch = asset.src.match(/^url\(\s*['"]?(.*?)['"]?\s*\)$/);
		if (urlMatch) {
			asset.src = urlMatch[1];
		}
	}

	// transform the url if necessary (skip for local() font sources and data URIs)
	if (
		typeof baseURL[asset.type] !== "undefined" &&
		typeof asset.src === "string" &&
		!asset.src.startsWith("local(") &&
		!asset.src.startsWith("data:")
	) {
		asset.src = baseURL[asset.type] + asset.src;
	}

	const parser = parsers.get(asset.type);

	if (typeof parser === "undefined") {
		throw new Error("load : unknown or invalid resource type : " + asset.type);
	}

	// parser returns the amount of asset to be loaded (usually 1 unless an asset is splitted into several ones)
	return parser.call(this, asset, onload, onerror, {
		nocache: nocache,
		crossOrigin: crossOrigin,
		withCredentials: withCredentials,
	});
}

/**
 * unload the specified asset to free memory
 * @memberof loader
 * @param {Asset} asset
 * @returns {boolean} true if unloaded
 * @example me.loader.unload({name: "avatar",  type:"image"});
 * @category Assets
 */
export function unload(asset) {
	switch (asset.type) {
		case "binary":
			if (!(asset.name in binList)) {
				return false;
			}

			delete binList[asset.name];
			return true;

		case "image":
			if (!(asset.name in imgList)) {
				return false;
			}
			delete imgList[asset.name];
			return true;

		case "json":
			if (!(asset.name in jsonList)) {
				return false;
			}

			delete jsonList[asset.name];
			return true;

		case "js":
			// ??
			return true;

		case "fontface":
			if (
				typeof typeof globalThis.document !== "undefined" &&
				typeof globalThis.document.fonts !== "undefined"
			) {
				globalThis.document.fonts.delete(fontList[asset.name]);
				delete fontList[asset.name];
				return true;
			}
			return false;

		case "tmx":
		case "tsx":
			if (!(asset.name in tmxList)) {
				return false;
			}

			delete tmxList[asset.name];
			return true;

		case "audio":
			return audio.unload(asset.name);

		case "video":
			if (!(asset.name in videoList)) {
				return false;
			}

			delete videoList[asset.name];
			return true;

		case "obj":
			if (!(asset.name in objList)) {
				return false;
			}

			delete objList[asset.name];
			return true;

		case "mtl":
			if (!(asset.name in mtlList)) {
				return false;
			}

			delete mtlList[asset.name];
			return true;

		case "aseprite": {
			// aseprite asset populates both imgList and jsonList under the same key
			const hadImage = asset.name in imgList;
			const hadJson = asset.name in jsonList;
			if (!hadImage && !hadJson) {
				return false;
			}
			delete imgList[asset.name];
			delete jsonList[asset.name];
			return true;
		}

		default:
			throw new Error(
				"unload : unknown or invalid resource type : " + asset.type,
			);
	}
}

/**
 * unload all resources to free memory
 * @memberof loader
 * @example me.loader.unloadAll();
 * @category Assets
 */
export function unloadAll() {
	let name;

	// unload all binary resources
	for (name in binList) {
		if (binList.hasOwnProperty(name)) {
			unload({
				name: name,
				type: "binary",
			});
		}
	}

	// unload all image resources
	for (name in imgList) {
		if (imgList.hasOwnProperty(name)) {
			unload({
				name: name,
				type: "image",
			});
		}
	}

	// unload all tmx resources
	for (name in tmxList) {
		if (tmxList.hasOwnProperty(name)) {
			unload({
				name: name,
				type: "tmx",
			});
		}
	}

	// unload all json resources
	for (name in jsonList) {
		if (jsonList.hasOwnProperty(name)) {
			unload({
				name: name,
				type: "json",
			});
		}
	}

	// unload all video resources
	for (name in videoList) {
		if (videoList.hasOwnProperty(name)) {
			unload({
				name: name,
				type: "json",
			});
		}
	}

	// unload all video resources
	for (name in fontList) {
		if (fontList.hasOwnProperty(name)) {
			unload({
				name: name,
				type: "font",
			});
		}
	}

	// unload all OBJ resources
	for (name in objList) {
		if (objList.hasOwnProperty(name)) {
			unload({
				name: name,
				type: "obj",
			});
		}
	}

	// unload all MTL resources
	for (name in mtlList) {
		if (mtlList.hasOwnProperty(name)) {
			unload({
				name: name,
				type: "mtl",
			});
		}
	}

	// unload all audio resources
	audio.unloadAll();
}

/**
 * return the specified TMX/TSX object
 * @memberof loader
 * @param {string} elt - name of the tmx/tsx element ("map1");
 * @returns {object} requested element or null if not found
 * @category Assets
 */
export function getTMX(elt) {
	// force as string
	elt = "" + elt;
	if (elt in tmxList) {
		return tmxList[elt];
	}
	return null;
}

/**
 * return the specified Binary object
 * @memberof loader
 * @param {string} elt - name of the binary object ("ymTrack");
 * @returns {object} requested element or null if not found
 * @category Assets
 */
export function getBinary(elt) {
	// force as string
	elt = "" + elt;
	if (elt in binList) {
		return binList[elt];
	}
	return null;
}

/**
 * return the specified Image Object
 * @memberof loader
 * @param {string} image - name of the Image element ("tileset-platformer");
 * @returns {HTMLImageElement|CompressedImage|null} requested element or null if not found
 * @category Assets
 */
export function getImage(image) {
	// force as string and extract the base name
	image = getBasename("" + image);
	if (image in imgList) {
		// return the corresponding Image object
		return imgList[image];
	}
	return null;
}

/**
 * return the specified JSON Object
 * @memberof loader
 * @param {string} elt - name of the json file
 * @returns {JSON}
 * @category Assets
 */
export function getJSON(elt) {
	// force as string
	elt = "" + elt;
	if (elt in jsonList) {
		return jsonList[elt];
	}
	return null;
}

/**
 * return the specified OBJ model data
 * @memberof loader
 * @param {string} elt - name of the OBJ file (as specified in the preload list)
 * @returns {object} parsed OBJ data with `vertices` (Float32Array), `uvs` (Float32Array), `indices` (Uint16Array), and `vertexCount` (number), or null if not found
 * @category Assets
 * @example
 * // 1. preload the OBJ model and its texture
 * me.loader.preload([
 *     { name: "cube", type: "obj", src: "models/cube.obj" },
 *     { name: "cube", type: "image", src: "models/cube_texture.png" },
 * ], () => {
 *     // 2. create a Mesh using the preloaded model name
 *     const mesh = new me.Mesh(400, 300, {
 *         model: "cube",        // references the preloaded OBJ
 *         texture: "cube",      // references the preloaded image
 *         width: 200,
 *         height: 200,
 *     });
 *     me.game.world.addChild(mesh);
 *
 *     // 3. or access the raw parsed data directly
 *     const data = me.loader.getOBJ("cube");
 *     // data.vertices — Float32Array of x,y,z positions
 *     // data.uvs — Float32Array of u,v texture coordinates
 *     // data.indices — Uint16Array of triangle vertex indices
 *     // data.vertexCount — number of unique vertices
 * });
 */
export function getOBJ(elt) {
	// force as string
	elt = "" + elt;
	if (elt in objList) {
		return objList[elt];
	}
	return null;
}

/**
 * return the specified MTL material data
 * @memberof loader
 * @param {string} elt - name of the MTL file (as specified in the preload list)
 * @returns {object} map of material names to properties (`Kd`, `d`, `map_Kd`), or null if not found
 * @category Assets
 * @example
 * // 1. preload OBJ + MTL + texture
 * me.loader.preload([
 *     { name: "fox", type: "obj", src: "models/fox.obj" },
 *     { name: "fox", type: "mtl", src: "models/fox.mtl" },
 *     { name: "colormap", type: "image", src: "models/colormap.png" },
 * ], () => {
 *     // 2. create a Mesh with material — texture, tint, opacity auto-applied
 *     const mesh = new me.Mesh(400, 300, {
 *         model: "fox",
 *         material: "fox",
 *         texture: "colormap",
 *         width: 200,
 *         height: 200,
 *     });
 *
 *     // 3. or access the raw material data directly
 *     const materials = me.loader.getMTL("fox");
 *     // materials["colormap"].Kd — [r, g, b] diffuse color (0-1 range)
 *     // materials["colormap"].d — opacity (0-1)
 *     // materials["colormap"].map_Kd — resolved texture URL
 * });
 */
export function getMTL(elt) {
	elt = "" + elt;
	if (elt in mtlList) {
		return mtlList[elt];
	}
	return null;
}

/**
 * return the specified Video Object
 * @memberof loader
 * @param {string} elt - name of the video file
 * @returns {HTMLVideoElement}
 * @category Assets
 */
export function getVideo(elt) {
	// force as string
	elt = "" + elt;
	if (elt in videoList) {
		return videoList[elt];
	}
	return null;
}

/**
 * return the specified FontFace Object
 * @memberof loader
 * @param {string} elt - name of the font file
 * @returns {FontFace}
 * @category Assets
 */
export function getFont(elt) {
	// force as string
	elt = "" + elt;
	if (elt in fontList) {
		return fontList[elt];
	}
	return null;
}
