js/vercengen/engine/VercengenClass.js

if (!global.ve) global.ve = {};

/**
 * <span color = "yellow">{@link ve.Class}</span>: Classes that extend this are parsed by the reflection engine, with UIs treated as being a subset of state via <span color = "yellow">{@link ve.Component}</span> type declarations.
 *
 * UIs can be opened via super.<span color=00ffff>open</span>(arg0_mode:"class"/"instance", arg1_options:{@link Object}), and closed using <span color=00ffff>close</span>(arg0_mode:"class"/"instance").
 * 
 * ##### Instance:
 * - `.id=Class.generateRandomID(ve.Class)`
 * - 
 * - `.class_window`: {@link ve.Window} - The class window any static components are bound to.
 * - `.instance_window`: {@link ve.Window} - The instance window non-static components is bound to.
 * 
 * ##### Methods:
 * - <span color=00ffff>{@link ve.Class.close|close}</span>(arg0_mode:"class"/"instance")
 * - <span color=00ffff>{@link ve.Class.draw|draw}</span>(arg0_function:{@link function}(this:{@link ve.Class}), arg1_interval=0:{@link number}) - The draw function intended to make non-UI related draw calls to browser/process.
 * - <span color=00ffff>{@link ve.Class.getState|getState}</span>() | {@link Object}
 * - <span color=00ffff>{@link ve.Class.isClosed|isClosed}</span>(arg0_mode:"class"/"instance") | {@link boolean} - Whether the selected window is closed.
 * - <span color=00ffff>{@link ve.Class.isOpen|isOpen}</span>(arg0_mode:"class"/"instance") | {@link boolean} - Whether the selected window is open.
 * - <span color=00ffff>{@link ve.Class.open|open}</span>(arg0_mode:"class"/"instance") - Opens the UI bound to the current ve.Class.
 * - <span color=00ffff>{@link ve.Class.updateOwner|updateOwner}()</span> - Updates the `.owner`/`.owners` field attached to {@link ve.Component}s.
 * 
 * ##### Static Fields:
 * - `.instances`: {@link Array}<this:{@link ve.Class}>
 * 
 * @class
 * @memberof ve
 * @namespace ve.Class
 * @type {ve.Class}
 */
ve.Class = class {
	//Declare local static variables
	
	/**
	 * @type ve.Class[]
	 */
	static instances = [];
	
	//Constructor/getter/setter
	constructor () {
		//Declare local instance variables
		this.id = Class.generateRandomID(ve.Class); //Non-Vercengen objects can be used freely
		
		this.class_window = undefined;
		this.instance_window = undefined;
		
		//Call updateOwner() upon initialisation
		this.updateOwner();
	}
	
	//Class methods
	
	/**
	 * Closes any open UI currently associated with ve.Class, either its class (static) or instance.
	 * - Method of: {@link ve.Class}
	 * 
	 * @param {string} arg0_mode - Either 'class'/'instance'.
	 */
	close (arg0_mode) {
		//Convert from parameters
		let mode = arg0_mode;
		
		//Close class_window/instance_window
		clearInterval(this.draw_loop);
		delete this.draw_loop;
		this[`${mode}_window`].remove();
		delete this[`${mode}_window`];
	}
	
	/**
	 * Draws the current element on the screen based on a constant loop that can be cleared by the process.
	 * - Method of: {@link ve.Class}
	 * 
	 * @param {function|undefined} arg0_function
	 * @param {number} [arg1_interval=0]
	 */
	draw (arg0_function, arg1_interval) {
		//Convert from parameters
		let local_function = arg0_function;
		let interval = (arg1_interval) ? parseInt(arg1_interval) : 0;
		
		//Set draw loop
		this.draw_loop = setInterval(() => { local_function(this); }, interval);
	}
	
	/**
	 * Whether the bound UI for the current mode is closed.
	 * - Method of: {@link ve.Class}
	 * 
	 * @param {string} arg0_mode - Either 'class'/'instance'.
	 * 
	 * @returns {boolean}
	 */
	isClosed (arg0_mode) {
		//Return statement
		return (!(this.class_window || this.instance_window));
	}
	
	/**
	 * Whether the bound UI for the current mode is open.
	 * - Method of: {@link ve.Class}
	 *
	 * @param {string} arg0_mode - Either 'class'/'instance'.
	 *
	 * @returns {boolean}
	 */
	isOpen (arg0_mode) {
		//Return statement
		return (this.class_window || this.instance_window);
	}
	
	/**
	 * Opens the relevant {@link ve.Window} or other Feature responsible for containing either class or instance variables.
	 * - Method of: {@link ve.Class}
	 *
	 * @param {string} [arg0_mode="instance"] - Whether the UI is bound to 'class'/'instance'. If 'class', it displays all static Vercengen fields.
	 * @param {Object} [arg1_options]
	 *  @param {boolean} [arg1_options.do_not_close_if_open=false] - Whether to close the Window already bound to class_window or instance_window
	 *  @param {string} [arg1_options.name]
	 *  @param {string} [arg1_options.type="window"] - Either 'static'/'window'. 'static' by default if `.anchor`/`.x`/`.y` are specified.
	 *
	 *  @param {string} [arg1_options.anchor="top_left"] - Either 'bottom_left'/'bottom_right'/'top_left'/'top_right'. If neither this nor .x/.y are defined, the UI is spawned at the cursor position.
	 *  @param {number|string} [arg1_options.height]
	 *  @param {number|string} [arg1_options.width]
	 *  @param {number|string} [arg1_options.x=HTML.mouse_x] - Mouse coordinates if undefined.
	 *  @param {number|string} [arg1_options.y=HTML.mouse_y] - Mouse coordinates if undefined.
	 */
	open (arg0_mode, arg1_options) {
		//Convert from parameters
		let mode = (arg0_mode) ? arg0_mode : "instance";
		let options = (arg1_options) ? arg1_options : {};
		
		//Initialise options
		if (options.type === undefined &&
			(options.anchor !== undefined || options.x !== undefined || options.y !== undefined)
		) options.type = "static";
		
		//Declare local instance variables
		let state_obj = this.updateOwner();
		
		let class_components_obj = state_obj.class_components_obj;
		let instance_components_obj = state_obj.instance_components_obj;
		
		//Close window if open first
		if (this.isOpen(mode) && !options.do_not_close_if_open) this.close(mode);
		
		//Open ve.Window if either 'static'/'window'
		let components_obj = (mode === "class") ? class_components_obj : instance_components_obj;
		
		if (options.type === "static") {
			this[`${mode}_window`] = new ve.Window(components_obj, { is_static: true, ...options });
		} else {
			this[`${mode}_window`] = new ve.Window(components_obj, {
				draggable: true,
				is_static: false,
				resizeable: true,
				...options
			});
		}
	}
	
	//State methods
	
	/**
	 * Returns the values of all bound {@link ve.Component}s as a destructured object.
	 * 
	 * @returns {{"<variable_key>": ve.Component}}
	 */
	getState () {
		//Declare local instance variables
		let child_class = this.constructor;
		let parent_class = Object.getPrototypeOf(child_class);
		let state_obj = {};
		
		//Fetch instance fields in child class
		let instance_fields = {};
			Object.iterate(this, (local_key, local_value) => {
				instance_fields[local_key] = local_value
			});
		
		//Fetch static fields unique to child class, mutate their keys to have 'static-' prepended to them.
		let static_fields = Object.getOwnPropertyNames(child_class).filter((key) => (
			!["length", "prototype", "name"].includes(key) && //Filter out any default keys
			!Object.getOwnPropertyNames(parent_class).includes(key) //Filter out any parent keys
		)).reduce((local_obj, key) => {
			local_obj[`static-${key}`] = child_class[key];
			
			//Return statement
			return local_obj;
		}, {});
		
		//Filter both static_fields and instance_fields for .vercengen_component and append them to state_obj
		let temp_state_obj = { ...static_fields, ...instance_fields };
		
		Object.iterate(temp_state_obj, (local_key, local_value) => {
			try {
				if (local_value.is_vercengen_component)
					state_obj[local_key] = local_value;
			} catch (e) {}
		});
		
		//Return statement
		return state_obj;
	}
	
	/**
	 * Updates the `.owner`/`.owners` field(s) for any attached {@link ve.Component}s to the current ve.Class.
	 * - Method of: {@link ve.Class}
	 * 
	 * @returns {{class_components_obj: {"variable_key": ve.Component}, instance_components_obj: {"variable_key": ve.Component}}}
	 */
	updateOwner () {
		//Declare local instance variables
		let state_obj = this.getState();
		
		let class_components_obj = {};
		let instance_components_obj = {};
		
		//Iterate over all components in state_obj to update their owners
		Object.iterate(state_obj, (local_key, local_value) => {
			if (local_key.startsWith("static-")) {
				class_components_obj[local_key] = local_value;
				class_components_obj[local_key].setOwner(this, [this]);
			} else {
				instance_components_obj[local_key] = local_value;
				instance_components_obj[local_key].setOwner(this, [this]);
			}
		});
		
		//Return statement
		return {
			class_components_obj: class_components_obj,
			instance_components_obj: instance_components_obj
		};
	}
};