# src/Shader.js

    import { isSomething } from "akutils";
    import invariant from "invariant";

    import AutoShader from "./AutoShader";
    import noParentStateErrorMsg from "./noParentStateErrorMsg";
    import PropertyFactoryShader from "./PropertyFactoryShader";
    import shadowProperty from "./shadowProperty";

    const readonlyAutoShader = new AutoShader(true);
    const readWriteAutoShader = new AutoShader(false);

    /*
    	Mappings for property names that always have the same type, such as create_on dates.
    */
    const defaultShaders = {};

    // Instnace variable names
    const _automount = Symbol('_autoMount');
    const _automountAll = Symbol('automountAll');
    const _autoshadow = Symbol('autoShadow');
    const _elementShader = Symbol('elementShader');
    const _hasJitProperties = Symbol('hasJitProperties');
    const _elementShaderAutomount = Symbol('elementShaderAutomount');
    const _property = Symbol('property');
    const _shaders = Symbol('shaders');

    /**
    	Shaders implement the shadowing process by which the shadow state proxies the actual state.
    	Each {@link Property} maintains a shader used for shadowing its associated state property.

    	The {@link Shader#shadowProperty} method is responsible for setting up the `Shadow` proxy
    	for the state.

    	Shaders are setup through {@link StateType} instances associated with `Property` objects
    	and there is rarely a need to directly interact with them.

    	@see {@link Property}
    	@see {@link StateType}
    */
    export default class Shader {
    	constructor(property) {
    		const stateType = property.stateType();

    		this[_automount] = [];
    		this[_automountAll] = stateType._automountAll;
    		this[_autoshadow] = stateType._autoshadow;
    		this[_elementShader] = null;
    		this[_hasJitProperties] = stateType.hasJitProperties();
    		this[_property] = property;
    		this[_shaders] = { };
    	}

    	static addDefault(name, shader) {
    		defaultShaders[name] = shader;
    	}

    	add(name, shader, automount) {
    		this[_shaders][name] = shader;

    		if (automount) {
    			this[_automount].push(name);
    		}

    		return this;
    	}

    	automountAll() {
    		// leaving implementation for now but automount (experimental feature) is no longer being
    		// supported. Keeping in place for a bit to see if needed. (2/2/2017)
    		return false;

    		//return this[_automountAll];
    	}

    	addProperty(name, stateType) {
    		const property = this[_property];
    		const shader = stateType.factory(property);

    		this.add(name, shader);
    	}

    	autoShader() {
    		if (this[_autoshadow]) {
    			return this[_property].isReadonly() ?readonlyAutoShader :readWriteAutoShader;
    		} else {
    			return null;
    		}
    	}

    	autoShadow() {
    		return this[_autoshadow];
    	}

    	elementShader() {
    		return this[_elementShader];
    	}

    	get(name) {
    		return this[_shaders][name];
    	}

    	has(name) {
    		return !!this[_shaders][name];
    	}

    	isAutomount(name) {
    		// leaving implementation for now but automount (experimental feature) is no longer being
    		// supported. Keeping in place for a bit to see if needed. (2/2/2017)
    		return false;

    		// if (this[_automountAll]) {
    		// 	return true;
    		// } else if (this[_shaders][name]) {
    		// 	return this[_shaders][name].shouldAutomount() || this[_automount].indexOf(name) !== -1;
    		// } else if (this[_elementShader]) {
    		// 	return this[_elementShaderAutomount] || this[_elementShader].shouldAutomount();
    		// } else {
    		// 	return false;
    		// }
    	}

    	property() {
    		return this[_property];
    	}

    	remove(name) {
    		delete this[_shaders][name];

    		// remove the automount setting
    		const automountIdx = this[_automount].indexOf(name);

    		if (automountIdx != -1) {
    			this[_automount].splice(automountIdx, 1);
    		}

    		return this;
    	}

    	setAutomountAll(automount) {
    		this[_automountAll] = automount;
    	}

    	setAutoshadow(value) {
    		this[_autoshadow] = !!value;

    		return this;
    	}

    	setElementType(stateType) {
    		const property = this[_property];
    		const shader = stateType.factory(property);

    		return this.setElementShader(shader);
    	}

    	setElementShader(shader, automount) {
    		if (this[_elementShader]) {
    			console.warn(`Property already has a child shader set - ignoring setElementShader() call - shader=%O`, this);
    			return;
    		}

    		this[_elementShader] = shader;
    		this[_elementShaderAutomount] = automount;

    		return this;
    	}

    	/*
    		Gets the shader for a child property. This method returns the first shader generated by checking:
    			1) shaders registered using add() or addPropertyClass() - named shaders
    			2) jit shaders from state type
    			3) child shader (registered using setElementShader() )
    			4) default shaders (regustered using the static addDefault() method)
    			5) auto shader (applicable if autoShadow set to true)

    		Parameters:
    			name - the name of the child property
    			sate - the property's current state (not the child state)
    	*/
    	shaderFor(name, state) {
    		// priority order: named, child, default mapping, auto shading
    		return this[_shaders][name] ||
    			(this[_hasJitProperties] && this.stateType().jitShaderFor(name, state, this[_property])) ||
    			this[_elementShader] ||
    			defaultShaders[name] ||
    			this.autoShader();
    	}

    	/*
    		Creates the proxy (shadow) for a state property.
    	*/
    	shadowProperty(time, name, parentState, parentImpl) {
    		const IsolatedProperty = require("./IsolatedProperty").default;
    		const property = this[_property];
    		const isRoot = property.isRoot() || property instanceof IsolatedProperty;

    		invariant(parentState || isRoot, noParentStateErrorMsg(name, parentImpl));

    		// get the initial state for the property
    		const currState = isRoot ?parentState :parentState[name];
    		const state = property.getInitialState(currState);

    		if (!isRoot) {
    			parentState[name] = state;
    		}

    		// add mixins to the property (but only once)
    		if (!property.__hasMixins()) {
    			let proto = Object.getPrototypeOf(property);
    			let mixins = proto.constructor.mixins;
    			let mixin;

    			if (mixins) {
    				for (let i=0, len=mixins.length; i<len; i++) {
    					mixin = mixins[i](property);

    					property.__addMixin(mixin);
    				}
    			}
    		}

    		// create the backing implementation instance
    		const ImplClass = property.implementationClass();
    		const impl = shadowProperty(time, ImplClass, property, name, state, parentImpl, this);

    		return impl;
    	}

    	shadowUndefinedProperties(state, impl, define) {
    		const keys = Object.keys(this[_shaders]);
    		const shadow = impl.shadow();
    		const propNames = Object.getOwnPropertyNames(shadow);
    		var key, shader;

    		for (let i=0, len=keys.length; i<len; i++) {
    			key = keys[i];
    			shader = this[_shaders][key];

    			if (propNames.indexOf(key) !== -1) { continue }

    			define(key, shader);
    		}
    	}

    	/*
    		Automounting is an experimental, partially implemented feature.
    	*/
    	shouldAutomount() {
    		const proto = Object.getPrototypeOf(this[_property]);

    		return proto.constructor.shouldAutomount && proto.constructor.shouldAutomount();
    	}

    	stateType() {
    		return this[_property].stateType();
    	}
    }