# src/IndexedShadowImpl.js

    import flatten from "lodash.flatten";
    import range from "lodash.range";

    import ShadowImpl from "./ShadowImpl";
    import modelizeArray from "./modelizeArray";
    import reshadow from "./reshadow";

    const _automounted = Symbol('automounted');
    const _impls = Symbol('impls');
    const _mapped = Symbol('mapped');
    const _length = Symbol('length');
    const _nextMapping = Symbol('nextMapping');
    const _shadow = Symbol('shadow');

    export default class IndexedShadowImpl extends ShadowImpl {
    	constructor(time, property, name, state, parent, shader, prev) {
    		super(time, property, name, state, parent, shader, prev);

    		this[_mapped] = false;
    		this[_impls] = [];

    		// setup the length property
    		this[_length] = state.length;

    		// go ahead and map properties so can reuse valid properties without creating a closure reference.
    		// Reusing properties allows for React components to do '===' to see if a property has changed.
    		// if (prev) {
    		// 	if (prev && prev[_mapped]) {
    		// 		this.defineChildProperties(prev, true);
    		// 	} else if (prev && Object.keys(prev[_impls]).length) {
    		// 		this.automountChildren(prev);
    		// 	}

    		// 	// Kill invalid() nodes that are still active
    		// 	prev._killInvalidButActiveChildren();
    		// }
    	}

    	/*
    		Map properties so can reuse valid properties. Reusing properties allows for React components
    		to do '===' to see if a property has changed.
    	*/
    	onReshadow(prev) {
    		// Kill invalid() nodes that are still active
    		prev._killInvalidButActiveChildren();
    	}

    	isMapped() {
    		return this[_mapped];
    	}

    	addChildren(start, num) {
    		this[_nextMapping] = this[_nextMapping] || [...this[_impls]];

    		const mapping = this[_nextMapping];
    		const blanks = [];
    		var count = num;

    		while(count--) blanks.push(null);

    		// rename children after newly added blanks
    		for (let i=start, len=mapping.length; i<len; i++) {
    			let idx = i + num;
    			let child = mapping[i];

    			if (child) {
    				child.updateName(idx);
    			}
    		}

    		mapping.splice(start, 0, ...blanks)
    	}

    	/*
    		Its possible for children to move when array items are inserted or removed in update actions.
    	*/
    	childMapping() {
    		return this[_nextMapping] ?this[_nextMapping] :this[_impls];
    	}

    	moveChildren(start, deleteCount, addCount) {
    		this.removeChildren(start, deleteCount);
    		this.addChildren(start, addCount);
    	}

    	removeChildren(start, num) {
    		this[_nextMapping] = this[_nextMapping] || [...this[_impls]];

    		const mapping = this[_nextMapping];

    		// prevent changes in removed children
    		for (let i=0; i<num; i++) {
    			let child = mapping[start+i];

    			if (child) {
    				child.blockFurtherUpdates(true);
    			}
    		}

    		// remove children from mapping
    		mapping.splice(start, num);

    		// rename remaining children
    		for (let i=start, len=mapping.length; i<len; i++) {
    			let child = mapping[i];

    			if (child) {
    				child.updateName(i);
    			}
    		}
    	}

    	toJSON() {
    		return {
    			...super.toJSON(),
    			type: 'ArrayImpl',
    			length: this.length,
    			mapped: this[_mapped],
    		};
    	}

    	//------------------------------------------------------------------------------------------------------
    	//	Methods for access and manipulate subproperties - based on Array methods
    	//------------------------------------------------------------------------------------------------------

    	get length() {
    		return this[_length];
    	}

    	/*
    		Removes all subproperties.
    	*/
    	clear() {
    		this.assign([], "clear()");
    	}

    	childAt(idx) {
    		return this[_impls][i];
    	}

    	// This method does not change this object's state.
    	concat(...values) {
    		return flatten(this.values(), values);
    	}

    	pop() {
    		var result;

    		this.update( state => {
    				const lastIdx = state.length - 1;

    				state = [...state];
    				result = state.pop();

    				if (lastIdx != -1) {
    					this.removeChildren(lastIdx, 1);
    				}

    				return { name: "pop()", nextState: state };
    			});

    		// value is no longer being managed so ok to return without cloning
    		return result;
    	}

    	push(...values) {
    		var result;

    		modelizeArray(values);

    		this.update( state => {
    				state = [...state];
    				result = state.push(...values);

    				// no need to update children mapping only affecting tail

    				return { name: "push()", nextState: state };
    			});

    		return result;
    	}

    	remove(idx) {
    		var result;

    		this.update( state => {
    			state = [...state];
    			result = state.splice(idx, 1);

    			this.removeChildren(idx, 1);

    			return { name: `remove(${idx})`, nextState: state }
    		});

    		// value is no longer being managed so ok to return without cloning
    		return result[0];
    	}

    	shift() {
    		var result;

    		this.update( state => {
    				state = [...state];
    				result = state.shift();

    				this.removeChildren(0, 1);

    				return { name: "shift()", nextState: state };
    			});

    		// value is no longer being managed so ok to return without cloning
    		return result;
    	}

    	splice(start, deleteCount, ...newItems) {
    		var result;

    		modelizeArray(newItems);

    		this.update( state => {
    				state = [...state];

    				result = state.splice(start, deleteCount, ...newItems);
    				this.moveChildren(start, deleteCount, newItems.length);

    				return { name: `splice(${start}, ${deleteCount}, ...)`, nextState: state };
    			});

    		// values is no longer being managed so ok to return without cloning
    		return result;
    	}

    	unshift(...values) {
    		var result;

    		modelizeArray(values);

    		this.update( state => {
    				state = [...state];
    				result = state.unshift(...values);

    				this.addChildren(0, values.length);

    				return { name: "unshift()", nextState: state };
    			});

    		// values are no longer being managed so ok to return without cloning
    		return result;
    	}

    	values() {
    		return [ ...this[_impls] ];
    	}

    	//------------------------------------------------------------------------------------------------------
    	//	Methods that must be implemented by subclasses
    	//------------------------------------------------------------------------------------------------------

    	defaults(newItems) {
    		if (!Array.isArray(newItems)) {
    			// nothing to do
    			return;
    		}

    		const impls = this.childMapping();
    		var child, value;

    		for (let i=0, len=newItems.length; i<len; i++) {
    			child = impls[i];
    			value = newItems[i];

    			if (child) {
    				child.defaults(value);
    			} else {
    				this.update( state => {
    						state = [...state];

    						state.splice(i, 0, value);
    						this.addChildren(i, newItems.length);

    						return { nextState: state };
    					});
    			}
    		}
    	}

    	merge(newItems) {
    		// original algorithm (commented out below) is adding duplicates where in most
    		// cases what is really wanted is one of two things:
    		//    1) add if no duplicate (exact deep compare)
    		//    2) merge if object's 'ID' is matched and add otherwise
    		//
    		// So probably need multiple algorithms driven by a property setting. Would need to worry
    		// about ordering, inserts, deletions,...
    		//
    		// In the meantime just going to do a full replace
    		return this.assign(newItems);

    		// Original algorithm (broken)
    		// if (!Array.isArray(newItems)) {
    		// 	return this.assign(newItems);
    		// }

    		// const impls = this.childMapping();
    		// var child, value;

    		// for (let i=0, len=newItems.length; i<len; i++) {
    		// 	child = impls[i];
    		// 	value = newItems[i];

    		// 	if (child) {
    		// 		child.merge(value);
    		// 	} else {
    		// 		this.update( state => {
    		// 				state = [...state];

    		// 				state.splice(i, 0, value);
    		// 				this.addChildren(i, newItems.length);

    		// 				return { nextState: state };
    		// 			});
    		// 	}
    		// }
    	}

    	//------------------------------------------------------------------------------------------------------
    	//	Methods that subclasses with children must implement
    	//------------------------------------------------------------------------------------------------------

    	automountChildren(prev) {
    		if (this[_mapped] || this[_automounted]) { return }

    		this[_automounted] = true;

    		const state = this.state();
    		const shader = this.shader(state);

    		for (let i=0, len=state.length; i<len; i++) {
    			if (shader.isAutomount(i)) {
    				this.defineChildProperty(i, shader, state, prev, true);
    			}
    		}
    	}

    	/*
    		Subclasses should implement this method in such a way as not to trigger a mapping.
    	*/
    	childCount() {
    		return this[_length];
    	}

    	/*
    		Gets the implementation objects managed by this property.
    	*/
    	children() {
    		return [ ...this[_impls] ];
    	}

    	getChild(idx) {
    		return this.childAt(idx);
    	}

    	/*
    		Gets the keys/indices for this property.

    		Implementation note: Subclasses should implement this method in such a way as not to trigger a mapping.
    	*/
    	keys() {
    		if (!this._keys) {
    			this._keys = range(this.length);
    		}

    		return this._keys;
    	}

    	/*
    		Maps all child properties onto this property using Object.defineProperty().
    	*/
    	defineChildProperties(prev, inCtor) {
    		if (this[_mapped]) { return }

    		this[_mapped] = true;

    		const shader = this.shader(state);
    		const state = this.state();
    		var elementShader;

    		for (let i=0, len=state.length; i<len; i++) {
    			elementShader = shader.shaderFor(i, state);

    			this.defineChildProperty(i, elementShader, state, prev, inCtor);
    		}

    		if (state) {
    			shader.shadowUndefinedProperties(state, this, (name, shader) => {
    					this.defineChildProperty(name, shader, state, prev, inCtor);
    				});
    		}
    	}

    	defineChildProperty(idx, elementShader, state, prev, inCtor=false) {
    		// ensure not already defined
    		if (this[_impls][idx]) { return }

    		const prevMapping = prev && prev.childMapping();
    		const prevChild = prevMapping && prevMapping[idx];
    		var child;

    		if (prevChild && !prevChild.replaced()) {
    			prevChild.switchName();

    			child = reshadow(this.time(), state, prevChild, this);
    		} else {
    			child = elementShader.shadowProperty(this.time(), idx, state, this, this.store());
    		}

    		if (child) {
    			this[_impls][idx] = child;

    			if (!prevChild && !inCtor) {
    				child.didShadow(this.time());
    			}
    		}
    	}

    	_killInvalidButActiveChildren() {
    		// kill any unvisited children
    			const children = this.children();
    			var child;

    			for (let i=0, len=children.length; i<len; i++) {
    				child = children[i];

    				if (!child.isValid() && child.isActive()) {
    					child.obsoleteTree();
    				}
    			}
    	}
    }