date/History.js

/**
 * Creates a {@link History} Object which stores {@link HistoryKeyframe|HistoryKeyframes}.
 * 
 * ##### Constructor:
 * - `arg0_keyframes_obj`: {@link Object} - Map of keyframes.
 * - `arg1_options`: {@link Object}
 *   - `.components_obj`: {@link Object}
 *   - `.draw_keyframes_function`: {@link function}(arg0_history:this, { components_obj:{@link Object}, key:{@link string}, value:{@link Array}})
 *   - `.localisation_function`: {@link function}(arg0_history:this, arg1_new_keyframe:{@link HistoryKeyframe}, arg2_old_keyframe:{@link HistoryKeyframe}) | {@link string} - Localisation function to generate descriptions per keyframe. 
 *   
 * ##### Instance:
 * - `.do_not_draw=false`: {@link boolean}
 * - `.keyframes`: {@link Object} - Map of keyframes.
 * - `.options`: {@link Object}
 *   - `.components_obj`: {@link Object}
 * 
 * ##### Methods:
 * - <span color=00ffff>{@link ve.History._getUniqueKeyframes|_getUniqueKeyframes}</span>(arg0_options:{@link Object}) | {@link Array}<{@link string}>
 * - <span color=00ffff>{@link ve.History._hasTimestampAfter|_hasTimestampAfter}</span>(arg0_timestamp:{@link number|Object}) | {@link boolean}
 * - <span color=00ffff>{@link ve.History.addKeyframe|addKeyframe}</span>(arg0_date:{@link number|Object}, ...argn_arguments) | {@link HistoryKeyframe}</span>
 * - <span color=00ffff>{@link ve.History.callFunctionInDateRange|callFunctionInDateRange}</span>(arg0_date_range:{@link Array}<{@link number}>|{@link Array}<{@link Object}>, arg1_function:{@link function}(arg0_local_keyframe:{@link HistoryKeyframe}))
 * - <span color=00ffff>{@link ve.History.cleanKeyframes|cleanKeyframes}</span>()
 * - <span color=00ffff>{@link ve.History.draw|draw}</span>(arg0_interface_obj:{@link ve.Interface}) | {@link ve.Interface}
 * - <span color=00ffff>{@link ve.History.fromJSON|fromJSON}</span>(arg0_json:{@link Object}|{@link string})
 * - <span color=00ffff>{@link ve.History.getFirstKeyframe|getFirstKeyframe}</span>() | {@link HistoryKeyframe}
 * - <span color=00ffff>{@link ve.History.getLastKeyframe|getLastKeyframe}</span>() | {@link HistoryKeyframe}
 * - <span color=00ffff>{@link ve.History.getKeyframe|getKeyframe}</span>(arg0_options:{@link Object}) | {@link HistoryKeyframe}
 * - <span color=00ffff>{@link ve.History.getTimestamps|getTimestamps}</span>() | {@link Array}<{@link number}>
 * - <span color=00ffff>{@link ve.History.moveKeyframe|moveKeyframe}</span>(arg0_date:{@link number|Object}, arg1_new_date:{@link number|Object})
 * - <span color=00ffff>{@link ve.History.removeKeyframe|removeKeyframe}</span>(arg0_date:{@link number|Object})
 * - <span color=00ffff>{@link ve.History.replaceKeyframe|replaceKeyframe}</span>(arg0_date:{@link number|Object}, ...argn_arguments:{@link any}) | {@link HistoryKeyframe}
 * - <span color=00ffff>{@link ve.History.toJSON|toJSON}</span>() | {@link string}
 * 
 * @augments ve.Class
 * @class
 * @type {History}
 */
global.History = class extends ve.Class {
	constructor (arg0_keyframes_obj, arg1_options) {
		//Convert from parameters
		super();
		this.do_not_draw = false;
		this.keyframes = (arg0_keyframes_obj) ? arg0_keyframes_obj : {};
		
		//Declare local instance variables
		this.options = {
			components_obj: {},
			...arg1_options
		};
	}
	
	/**
	 * Returns all unique keyframes in the current History object.
	 * - Private method of: {@link History}
	 *
	 * @param {Object} [arg0_options]
	 *  @param {number[]} [arg0_options.indexes]
	 *  @param {boolean} [arg0_options.return_timestamps=false]
	 *  
	 * @returns {string[]}
	 * @private
	 */
	_getUniqueKeyframes (arg0_options) {
		//Convert from parameters
		let options = (arg0_options) ? arg0_options : {};
		
		//Declare local instance variables
		let unique_timestamps = [];
		
		Object.iterate(this.keyframes, (local_key, local_keyframe) => {
			let has_value;
			
			//Iterate over all options.indexes
			for (let i = 0; i < options.indexes.length; i++)
				if (local_keyframe.value[options.indexes[i]] !== undefined) {
					has_value = true;
					break;
				}
			
			if (has_value) unique_timestamps.push(Date.convertTimestampToInt(local_key));
		});
		
		if (!options.return_timestamps) {
			let unique_dates = [];
			
			//Return statement
			for (let i = 0; i < unique_timestamps.length; i++)
				unique_dates.push(Date.convertTimestampToDate(unique_timestamps[i]));
			return unique_dates;
		}
		
		//Return statement
		return unique_timestamps;
	}
	
	/**
	 * Checks whether the History has an entry after a given timestamp.
	 * - Private method of: {@link History}
	 * 
	 * @param {number|Object} arg0_timestamp
	 * 
	 * @returns {boolean}
	 * @private
	 */
	_hasTimestampAfter (arg0_timestamp) {
		//Convert from parameters
		let timestamp = Date.getTimestamp(arg0_timestamp);
		
		//Declare local instance variables
		let all_keyframes = Object.keys(this.keyframes);
		
		for (let i = 0; i < all_keyframes.length; i++)
			if (timestamp >= parseInt(all_keyframes[i]))
				//Return statement
				return true;
		return false;
	}
	
	/**
	 * Adds a keyframe at the given timestamp with data fields.
	 * - Method of: {@link History}
	 * 
	 * @param {number|Object} arg0_date
	 * @param {any} argn_arguments
	 * 
	 * @returns {HistoryKeyframe}
	 */
	addKeyframe (arg0_date, ...argn_arguments) {
		//Convert from parameters
		let date = (arg0_date !== undefined) ? Date.convertTimestampToDate(arg0_date) : main.date;
		
		//Declare local instance variables
		let timestamp = Date.getTimestamp(date);
		
		//Create a new keyframe, otherwise concatenate with existing options if history is already defined
		if (this.keyframes[timestamp] === undefined) {
			this.keyframes[timestamp] = new HistoryKeyframe(date, ...argn_arguments);
		} else {
			let local_keyframe = this.keyframes[timestamp];
			local_keyframe.addData(...argn_arguments);
		}
		
		//Return statement
		return this.keyframes[timestamp];
	}
	
	/**
	 * Calls a function within a given Date range.
	 * - Method of: {@link History}
	 * 
	 * @param {number[]|Object[]} arg0_date_range
	 * @param {function} arg1_function - (arg0_local_keyframe:{@link HistoryKeyframe})
	 */
	callFunctionInDateRange (arg0_date_range, arg1_function) {
		//Convert from parameters
		let date_range = arg0_date_range;
		let special_function = arg1_function;
		
		//Declare local instance variables
		date_range =  [Date.getTimestamp(date_range[0]), Date.getTimestamp(date_range[1])];
		let keyframes = this._getUniqueKeyframes({ indexes: [0], return_timestamps: true });
		
		//Keyframes are look-forwards; create the keyframe at start_date; then for .value[0] changes until end_date
		if (!keyframes.includes(date_range[0]))
			keyframes.unshift(date_range[0]);
		
		//Iterate over all polygon_keyframes and apply the changes at the given date
		for (let i = 0; i < keyframes.length; i++)
			if (keyframes[i] >= date_range[0] && keyframes[i] <= date_range[1])
				special_function(keyframes[i]);
	}
	
	/**
	 * Cleans keyframes within the given History object.
	 * - Method of: {@link History}
	 */
	cleanKeyframes () {
		//Declare local instance variables
		let all_timestamps = Object.keys(this.keyframes).sort((a, b) => {
			return Date.convertTimestampToInt(a) - Date.convertTimestampToInt(b);
		});
		let running_state = []; //Tracks the accumulated values to find redundancies
		
		//Iterate over all_timestamps in the current history
		for (let i = 0; i < all_timestamps.length; i++) {
			let timestamp = all_timestamps[i];
			let local_keyframe = this.keyframes[timestamp];
			let has_meaningful_change = false;
			
			//Don't clean the very first keyframe, as it serves as the baseline
			if (i === 0) {
				//Update running state with the first keyframe's data
				running_state = JSON.parse(JSON.stringify(local_keyframe.value));
				continue;
			}
			
			for (let x = 0; x < local_keyframe.value.length; x++) {
				let current_val = local_keyframe.value[x];
				let prev_val = running_state[x];
				
				//Skip only if undefined; null is treated as a meaningful value change
				if (current_val === undefined) continue;
				
				if (typeof current_val === "object" && current_val !== null) {
					// Handle object merging and variable delta checks
					let is_redundant_obj = true;
					let cleaned_obj = { ...current_val };
					if (current_val.variables)
						cleaned_obj.variables = { ...current_val.variables };
					
					//Check nested variables
					if (current_val.variables && prev_val && prev_val.variables) {
						for (let key in current_val.variables)
							if (Boolean.isDeepEqual(current_val.variables[key], prev_val.variables[key])) {
								delete cleaned_obj.variables[key];
							} else {
								is_redundant_obj = false;
							}
						//If variables becomes empty, remove the key
						if (Object.keys(cleaned_obj.variables).length === 0) delete cleaned_obj.variables;
					} else if (current_val.variables) {
						is_redundant_obj = false;
					}
					
					//Check other properties of the object (excluding variables which we just handled)
					for (let key in current_val) {
						if (key === "variables") continue;
						if (prev_val && Boolean.isDeepEqual(current_val[key], prev_val[key])) {
							delete cleaned_obj[key];
						} else {
							is_redundant_obj = false;
						}
					}
					
					if (is_redundant_obj) {
						//Remove this index from keyframe if it changes nothing
						local_keyframe.value[x] = undefined;
					} else {
						//Update the keyframe with cleaned object and update running state
						local_keyframe.value[x] = cleaned_obj;
						has_meaningful_change = true;
						
						//Update running state for next iteration; ensure state is an object if merging
						if (typeof running_state[x] !== "object" || running_state[x] === null)
							running_state[x] = { variables: {} };
						
						if (cleaned_obj.variables)
							running_state[x].variables = {
								...running_state[x].variables,
								...cleaned_obj.variables
							};
						running_state[x] = { ...running_state[x], ...cleaned_obj };
					}
				} else {
					//Handle primitive values and null
					if (current_val === prev_val) {
						local_keyframe.value[x] = undefined;
					} else {
						running_state[x] = current_val;
						has_meaningful_change = true;
					}
				}
			}
			
			//If the keyframe now contains no unique data, delete the keyframe entirely
			let is_empty = local_keyframe.value.every(val => val === undefined);
			if (is_empty || !has_meaningful_change) {
				delete this.keyframes[timestamp];
			}
		}
	}
	
	/**
	 * Draws the current History interface and places the end component in `this.interface`.
	 * - Method of: {@link History}
	 * 
	 * @param {ve.Interface} [arg0_interface_obj]
	 * 
	 * @returns {ve.Interface}
	 */
	draw (arg0_interface_obj) {
		//Convert from parameter
		let interface_obj = arg0_interface_obj;
		
		//Declare local instance variables
		let components_obj = {};
		if (this.interface && typeof this.interface.remove === "function") this.interface.remove();
		this.getKeyframe({ refresh_localisation: true });
		
		//Iterate over all_keyframes and push it to components_obj
		Object.iterate(this.keyframes, (local_key, local_value) => {
			if (this.options.draw_keyframe_function)
				this.options.draw_keyframe_function(this, {
					components_obj,
					key: local_key,
					value: local_value
				});
		}, { sort_mode: "date_descending" });
		
		//Set interface_obj.v
		if (interface_obj) {
			interface_obj.v = components_obj;
		} else {
			this.interface = new ve.Interface(components_obj, { name: "Keyframes", width: 99 });
		}
		
		//Return statement
		return this.interface;
	}
	
	/**
	 * Initialises a History timeline from JSON.
	 * - Method of: {@link History}
	 * 
	 * @param {Object|string} arg0_json
	 */
	fromJSON (arg0_json) {
		//Convert from parameters
		let json = (typeof arg0_json === "string") ? JSON.parse(arg0_json) : arg0_json;
		
		//Declare local instance variables
		let keyframes_obj = (json.keyframes) ? json.keyframes : json;
		
		//Iterate over all_json_keys and assume them as keyframes
		if (keyframes_obj) {
			let all_keyframes = Object.keys(keyframes_obj).sort();
			this.do_not_draw = true;
			this.keyframes = {};
			
			for (let i = 0; i < all_keyframes.length; i++) {
				let local_key = all_keyframes[i];
				let local_keyframe = keyframes_obj[local_key];
				
				if (Array.isArray(local_keyframe)) {
					this.addKeyframe(local_key, ...local_keyframe);
				} else {
					//Legacy nested logic
					this.addKeyframe(local_key, ...local_keyframe.value);
				}
			}
			this.do_not_draw = false;
		} else {
			console.error(`naissance.History.fromJSON() requires arg0_json to have a .keyframes Array<Object>.`, json);
		}
	}
	
	/**
	 * Returns the first keyframe in `.keyframes`.
	 * - Method of: {@link History}
	 * 
	 * @returns {HistoryKeyframe|undefined}
	 */
	getFirstKeyframe () {
		//Declare local instance variables
		let all_timestamps = this.getTimestamps();
		
		//Return statement
		return (all_timestamps.length > 0) ? this.keyframes[all_timestamps[0]] : undefined;
	}
	
	/**
	 * Returns the last keyframe in `.keyframes`.
	 * - Method of: {@link History}
	 * 
	 * @returns {HistoryKeyframe|undefined}
	 */
	getLastKeyframe () {
		//Declare local instance variables
		let all_timestamps = this.getTimestamps();
		
		//Return statement
		return (all_timestamps.length > 0) ? this.keyframes[all_timestamps.length - 1] : undefined;
	}
	
	/**
	 * Resolves a keyframe at the given date.
	 * - Method of: {@link History}
	 * 
	 * @param {Object} [arg0_options]
	 *  @param {boolean} [arg0_options.bake_keyframes=false]
	 *  @param {number|Object} [arg0_options.date=main.date]
	 *  @param {number[]} [arg0_options.guaranteed_indexes]
	 *  @param {boolean} [arg0_options.refresh_localisation=false]
	 *  
	 * @return {Object}
	 */
	getKeyframe (arg0_options) {
		//Convert from parameters
		let options = (arg0_options) ? arg0_options : {};
		
		//Initialise options
		if (options.date === undefined) options.date = main.date;
		
		//Declare local instance variables
		let return_keyframes = {};
		let return_keyframe = {};
		let timestamp = Date.getTimestamp(options.date);
		
		//1. If options.absolute_keyframe = true, iterate over all keyframes in this.keyframes, and return the most recent one
		if (options.absolute_keyframe) {
			Object.iterate(this.keyframes, (local_key, local_keyframe) => {
				if (Date.convertTimestampToInt(local_key) <= Date.convertTimestampToInt(timestamp))
					return_keyframe = this.keyframes[local_key];
			}, { sort_mode: "date_ascending" });
			
			//Return statement
			return return_keyframe;
		}
		
		//2. If options.absolute_keyframe = false, iterate over all keyframes in this.keyframes, and concatenate the .value of the relative keyframe
		if (!options.absolute_keyframe) {
			return_keyframe = {
				date: options.date,
				timestamp: timestamp,
				value: [],
			};
			
			let all_keyframes = this.getTimestamps();
			let remaining_guarantees = (options.guaranteed_indexes) ?
				[...options.guaranteed_indexes] : [];
			let last_geometry;
			
			for (let i = 0; i < all_keyframes.length; i++) {
				let local_keyframe = this.keyframes[all_keyframes[i]];
				let is_past_timestamp = (Date.convertTimestampToInt(all_keyframes[i]) > Date.convertTimestampToInt(timestamp));
				
				//Parse localisation first, then concatenate
				if (options.refresh_localisation)
					local_keyframe.localisation = (this.options.localisation_function) ?
						this.options.localisation_function(this, local_keyframe, return_keyframe) : "";
				
				if (!is_past_timestamp || remaining_guarantees.length > 0 || options.bake_keyframes) {
					for (let x = 0; x < local_keyframe.value.length; x++) {
						let is_guaranteed = (options.guaranteed_indexes) ?
							options.guaranteed_indexes.includes(x) : false;
						let is_resolved = (return_keyframe.value[x] !== undefined && return_keyframe.value[x] !== null);
						
						//Update value if before original timestamp cutoff, or if index is a guarantee that hasn't been fixed yet
						if (!is_past_timestamp || (is_guaranteed && !is_resolved)) {
							if (typeof local_keyframe.value[x] === "object" && local_keyframe.value[x] !== null) {
								let old_variables = (return_keyframe.value[x]?.variables) ?
									return_keyframe.value[x].variables : {};
								
								//Return keyframe
								return_keyframe.value[x] = {
									...(return_keyframe.value[x] ? return_keyframe.value[x] : {}),
									...local_keyframe.value[x],
								};
								
								//Handle nested .variables
								if (local_keyframe.value[x] && local_keyframe.value[x].variables)
									return_keyframe.value[x].variables = {
										...old_variables,
										...local_keyframe.value[x].variables,
									};
							} else if (local_keyframe.value[x] !== undefined) {
								if (local_keyframe.value[x] === "undefined") continue; //Overwrite undefined strings
								if (x !== 0 && local_keyframe.value[x] === null) continue; //Null should be overridden for [1] symbols, [2] properties
								//If the value is null or a primitive, it overwrites the previous accumulated state
								return_keyframe.value[x] = local_keyframe.value[x];
							}
							
							//Remove from guarantees if now truthy
							if (is_guaranteed && return_keyframe.value[x] !== undefined && return_keyframe.value[x] !== null)
								remaining_guarantees = remaining_guarantees.filter(idx => idx !== x);
						}
					}
					
					//options.bake_keyframes handler
					if (options.bake_keyframes) {
						return_keyframes[all_keyframes[i]] = structuredClone(return_keyframe);
						let baked_keyframe = return_keyframes[all_keyframes[i]];
						
						if (baked_keyframe?.value?.[2]?.hidden === true) {
							if (baked_keyframe.value[0])
								last_geometry = structuredClone(baked_keyframe.value[0]);
							baked_keyframe.value[0] = null;
						} else if (
							baked_keyframe?.value?.[2]?.hidden === false &&
							!(return_keyframe.value[0] || return_keyframe.value[0] === null)
						) {
							if (last_geometry) baked_keyframe.value[0] = structuredClone(last_geometry);
						}
					}
				} else {
					if (!options.refresh_localisation) break;
				}
			}
			
			//Return statement
			return (!options.bake_keyframes) ? return_keyframe : return_keyframes;
		}
	}
	
	/**
	 * Sorts and returns all timestamp keys in chronological order.
	 * - Method of: {@link History}
	 * 
	 * @returns {string[]}
	 */
	getTimestamps () {
		//Return statement
		return Object.keys(this.keyframes).sort((a, b) => {
			return Date.convertTimestampToInt(a) - Date.convertTimestampToInt(b);
		});
	}
	
	/**
	 * Moves a keyframe from one date to another.
	 * - Method of: {@link History}
	 * 
	 * @param {number|Object} arg0_date - The date to move from.
	 * @param {number|Object} arg1_date - The date to move to.
	 */
	moveKeyframe (arg0_date, arg1_date) {
		//Convert from parameters
		let timestamp = Date.getTimestamp(arg0_date);
		let ot_timestamp = Date.getTimestamp(arg1_date);
		
		//Internal guard clause if timestamps are the same
		if (timestamp === ot_timestamp) return;
		
		//Check if keyframe_obj exists; if it does, move it
		let keyframe_obj = this.keyframes[timestamp];
		
		if (keyframe_obj) {
			keyframe_obj.date = Date.getDate(ot_timestamp);
			keyframe_obj.timestamp = ot_timestamp;
			this.keyframes[ot_timestamp] = this.keyframes[timestamp];
			
			delete this.keyframes[timestamp];
		}
	}
	
	/**
	 * Prunes a keyframe at the specified date.
	 * - Method of: {@link History}
	 * 
	 * @param {number|Object} arg0_date
	 */
	removeKeyframe (arg0_date) {
		//Convert from parameters
		let date = (arg0_date !== undefined) ? Date.convertTimestampToDate(arg0_date) : main.date;
		
		//Declare local instance variables
		let timestamp = Date.getTimestamp(date);
		
		//Delete target keyframe 
		delete this.keyframes[timestamp];
	}
	
	/**
	 * Replaces a keyframe at the specified date with another.
	 * - Method of: {@link History}
	 * 
	 * @param {HistoryKeyframe} arg0_keyframe
	 * @param {HistoryKeyframe} arg1_keyframe
	 * @param {Object} [arg2_options]
	 *  @param {boolean} [arg2_options.refresh_localisation=false]
	 */
	replaceKeyframe (arg0_keyframe, arg1_keyframe, arg2_options) {
		//Convert from parameters
		let keyframe = arg0_keyframe;
		let ot_keyframe = arg1_keyframe;
		let options = (arg2_options) ? arg2_options : {};
		
		//Declare local instance variables
		let timestamp = JSON.parse(JSON.stringify(keyframe.timestamp));
		
		//Swap out keyframe; refresh localisation
		this.removeKeyframe(timestamp);
		this.addKeyframe(timestamp, ...ot_keyframe.value);
		
		if (options.refresh_localisation !== false) this.getKeyframe();
	}
	
	/**
	 * Outputs the current History as a JSON string.
	 * - Method of: {@link History}
	 * 
	 * @returns {Object}
	 */
	toJSON () {
		//Convert from parameters
		let json_obj = {};
		
		//Iterate over all this.keyframes and parse them to a minimal JSON contract
		let all_keyframes = Object.keys(this.keyframes).sort();
		
		for (let i = 0; i < all_keyframes.length; i++) {
			let local_keyframe = this.keyframes[all_keyframes[i]];
			
			if (local_keyframe.value[0] === undefined) local_keyframe.value[0] = "undefined";
			json_obj[all_keyframes[i]] = local_keyframe.value;
		}
		
		//Return statement
		return json_obj;
	}
};