Source: lib/resource.js

import { EventEmitter } from "events";
import { extname } from "path";
import { EMPTY_RESOURCE } from "./constants.js";

let XEBRA_RESOURCE_ID = 0;

/**
 * @desc Represents some data that the remote Max instance has access to. The intended use is to support Max objects
 * like fpic and live.tab, which may want to display images. Can also be used to fetch data from files in Max's search
 * path. Setting `filename` (or setting `dimensions` in the case of .svg files) will query Max for that data in Max's
 * search path. Listen for the {@link Resource.event:data_received} event to receive the data as a data URI string.
 * Only images are currently supported.
 * @class
 * @extends EventEmitter
 * @example
 * // To use a resource without an ObjectNode, first create the resource.
 * const xebraState; // Instance of Xebra.State
 * const resource = xebraState.createResource();
 *
 * // The resource doesn't hold on to data from Max once it receives it, so be sure to listen for {@link Resource.event:data_received}
 * // events in order to handle resource data.
 * resource.on("data_received", (filename, data_uri) => {
 * // Do something with the data
 * });
 *
 * // Setting the filename property will cause the Resource object to fetch the data from Max. filename should be the
 * // name of a file in Max's search path. If Max is able to load the file successfully, it will send the data back
 * // to the Resource object, which will fire a {@link Resource.event:data_received} event with the data and filename.
 * resource.filename = "alex.png";
 *
 * // If the requested file is an .svg file, then Max will render the file before sending the data back to the Resource
 * // object. In this case, the dimensions property of the resource must be set as well as filename.
 * resource.filename = "maxelement.svg";
 * resource.dimensions = {width: 100, height: 50};
 */
class Resource extends EventEmitter {

	/**
	 * @constructor
	 * @param  {Xebra.NodeId} [0] parentObjectId - The id of the ObjectNode that owns this resource
	 */
	constructor(parentObjectId = 0) {
		super();
		this._id = ++XEBRA_RESOURCE_ID;
		this._width = 1;
		this._height = 1;
		this._filename = EMPTY_RESOURCE;
		this._objectContext = parentObjectId;
	}

	/**
	 * Unique identifier associated with each resource.
	 * @readonly
	 * @type {Xebra.NodeId}
	 */
	get id() {
		return this._id;
	}

	/**
	 * Name of a file in Max's search path. Setting this will query Max for data from the corresponding file. Listen to
	 * the {@link Resource.event:data_received} event for the data in the form of a data-uri string.
	 * @type {string}
	 */
	get filename() {
		return this._filename;
	}

	set filename(fn) {
		this._filename = fn;
		this._doFetch();
	}

	/**
	 * Id of the ObjectNode that owns the resource. If the resource is not bound to an ObjectNode, returns null. Max can
	 * use the object id to augment the search path with the parent patcher of the object, if the object id is supplied.
	 * @type {Xebra.NodeId}
	 */
	get objectContext() {
		return this._objectContext;
	}

	get isEmpty() {
		return this.filename === EMPTY_RESOURCE;
	}

	/**
	 * Whether the resource is a SVG image or not
	 * @readonly
	 * @type {boolean}
	 */
	get isSVG() {
		return this._filename ? extname(this._filename) === ".svg" : false;
	}

	/**
	 * @typedef {object} ResourceDimensions
	 * @property {number} height The height
	 * @property {number} width The width
	 */

	/**
	 * Dimensions of the resource. These are <strong>not</strong> updated automatically, and <strong>cannot</strong> be
	 * used to determine the dimensions of a raster image in Max's filepath. Instead, use the data URI returned with the
	 * {@link Resource.event:data_received} event to determine size. Setting these dimensions will trigger a new data
	 * fetch, if the resource is an .svg image. Max will be used to render the image and a .png data-uri will be
	 * returned.
	 * @type {ResourceDimensions}
	 */
	get dimensions() {
		return {
			width: this._width,
			height: this._height
		};
	}

	set dimensions(dim) {
		if (this._width !== dim.width || this._height !== dim.height) {
			this._width = dim.width;
			this._height = dim.height;
			if (this.isSVG) this._doFetch();
		}
	}

	/**
   * @private
   */
	on(event, fn) {
		super.on(event, fn);
		if (event === "data_received") this._doFetch();
	}

	/**
	 * Clears the resource content
	 * @private
	 */
	clear() {
		this._width = 1;
		this._height = 1;
		this._filename = EMPTY_RESOURCE;

		/**
		 * @event Resource.clear
		 * Called whenever the resource is cleared and its content is now empty
		 */
		this.emit("clear");
	}

	/**
	 * Be sure to call this when the Resource is no longer needed.
	 */
	destroy() {
		/**
		 * @event Resource.destroy
		 * Called whenever the resource is about to be destroyed
		 */
		this.emit("destroy");
		this.removeAllListeners();
	}

	/**
	 * Fetch the resource data
	 * @private
	 */
	_doFetch() {
		this.emit("needs_data", this);
	}

	/**
	 * Handle incoming resource data.
	 * @private
	 * @param {object} data - The resource data
	 */
	handleData(data) {
		let filetype = extname(data.request.name);
		if (filetype.length && filetype[0] === ".") filetype = filetype.substr(1);

		if (filetype === "svg") filetype = "png"; // Max will convert rendered svg surfaces to png for us
		let data_uri_string = "data:image/" + filetype + ";base64," + data.data;

		/**
		 * @event Resource.data_received
		 * @param {string} name - name of the resource
		 * @param {string} datauri - data-uri representation of the resource
		 */
		this.emit("data_received", data.request.name, data_uri_string);
	}
}

export default Resource;

class ResourceController extends EventEmitter {
	constructor() {
		super();
	}

	_fetchResourceData = (resource) => {
		this.emit("get_resource_info", resource);
	}

	createResource(parentObjectId = 0) {
		const resource = new Resource(parentObjectId);
		resource.on("needs_data", this._fetchResourceData);
		return resource;
	}
}

export { ResourceController };