/**
 * The outline of a ignite property which is a managed property that
 * can be used to invoke call back functions when the value of the property changes.
 */
class IgniteProperty {
    constructor(val) {
        this.onChangeCallbacks = [];
        this.onPushCallbacks = [];
        this.onPopCallbacks = [];
        this.arrayCallbacks = [];
        this.reflected = [];
        this._value = val;
        this.ignoreValueChange = false;

        //Attempt to patch the value if it's a list.
        this.patchArray();
    }

    get value() {
        return this._value;
    }

    set value(val) {
        this.setValue(val, true);
    }

    setValue(val, reflect) {
        //If the ignore value change flag is set exit.
        if (this.ignoreValueChange) {
            return;
        }

        var old = this._value;
        this._value = val;

        //Attempt to patch the value if it's an array.
        this.patchArray();

        //Invoke any callbacks letting them know the value changed.
        this.invokeOnChange(old, val);

        if (reflect) {
            this.ignoreValueChange = true; //Ignore changes incase we are connected to any reflected properties.
            this.reflected.forEach(reflect => reflect.value = val);
            this.ignoreValueChange = false;
        }
    }

    patchArray() {
        //Disconnect any existing array callbacks
        if (this.arrayCallbacks.length > 0) {
            this.arrayCallbacks.forEach(callback => callback.disconnect());
            this.arrayCallbacks = [];
        }

        //If our value is an array and it hasn't been patched, then patch it.
        if (Array.isArray(this._value) && this._value.onPushCallbacks == undefined) {
            this._value.onPushCallbacks = [];
            this._value.onPopCallbacks = [];

            this._value.push = function () {
                var len = Array.prototype.push.apply(this, arguments);
                this.onPushCallbacks.forEach((callback) => callback.invoke(arguments[0]));
                return len;
            };

            this._value.pop = function () {
                var len = Array.prototype.pop.apply(this, arguments);
                this.onPopCallbacks.forEach((callback) => callback.invoke());
                return len;
            }

            this._value.attachOnPush = function (func) {
                var callback = new IgniteCallback(func, (detach) => this.detachOnPush(detach));
                this.onPushCallbacks.push(callback);
                return callback;
            }

            this._value.attachOnPop = function (func) {
                var callback = new IgniteCallback(func, (detach) => this.detachOnPop(detach));
                this.onPopCallbacks.push(callback);
                return callback;
            }

            this._value.detachOnPush = function (callback) {
                this.onPushCallbacks = this.onPushCallbacks.filter(push => push != callback);
            }

            this._value.detachOnPop = function (callback) {
                this.onPopCallbacks = this.onPopCallbacks.filter(pop => pop != callback);
            }

            this.arrayCallbacks.push(this._value.attachOnPush(() => this.invokeOnPush()));
            this.arrayCallbacks.push(this._value.attachOnPop(() => this.invokeOnPop()));
        } else if (Array.isArray(this._value) && this._value.onPushCallbacks) {
            //This array has already been patched but attach to it so we get callbacks.
            this.arrayCallbacks.push(this._value.attachOnPush(() => this.invokeOnPush()));
            this.arrayCallbacks.push(this._value.attachOnPop(() => this.invokeOnPop()));
        }
    }

    invokeOnChange(oldValue, newValue) {
        for (var i = 0; i < this.onChangeCallbacks.length; i++) {
            this.onChangeCallbacks[i].invoke(oldValue, newValue);
        }
    }

    invokeOnPush() {
        for (var i = 0; i < this.onPushCallbacks.length; i++) {
            this.onPushCallbacks[i].invoke(null, this._value);
        }
    }

    invokeOnPop() {
        for (var i = 0; i < this.onPopCallbacks.length; i++) {
            this.onPopCallbacks[i].invoke(null, this._value);
        }
    }

    attachOnChange(onChange) {
        var callback = new IgniteCallback(onChange, (detach) => this.detachOnChange(detach));
        this.onChangeCallbacks.push(callback);
        return callback;
    }

    attachOnPush(onPush) {
        var callback = new IgniteCallback(onPush, (detach) => this.detachOnPush(detach));
        this.onPushCallbacks.push(callback);
        return callback;
    }

    attachOnPop(onPop) {
        var callback = new IgniteCallback(onPop, (detach) => this.detachOnPop(detach));
        this.onPopCallbacks.push(callback);
        return callback;
    }

    detachOnChange(callback) {
        this.onChangeCallbacks = this.onChangeCallbacks.filter(change => change != callback);
    }

    detachOnPush(callback) {
        this.onPushCallbacks = this.onPushCallbacks.filter(push => push != callback);
    }

    detachOnPop(callback) {
        this.onPopCallbacks = this.onPopCallbacks.filter(pop => pop != callback);
    }
}

/**
 * Return the value of the property if we try to convert
 * the property to a string.
 */
IgniteProperty.prototype.toString = function () {
    return this.value.toString();
}

/**
 * The outline of a ignite callback that can be invoked and disconnected
 * to help maintain and cleanup callbacks.
 */
class IgniteCallback {
    constructor(callback, detach) {
        this.callback = callback;
        this.detach = detach;
    }

    invoke(...params) {
        if (this.callback) {
            this.callback(...params);
        }
    }

    disconnect() {
        this.callback = null;

        if (this.detach) {
            this.detach(this);
        }
    }
}

/**
 * The outline of a simple rendering context which allows us to 
 * know if we are currently rendering anything ignite related. This works
 * because Javascript is single threaded, if events could break the current execution
 * this would fail. But it's safe since events cannot do that.
 */
class IgniteRenderingContext {
    static enter() {
        if (!IgniteRenderingContext.RenderCount) {
            IgniteRenderingContext.RenderCount = 0;
        }

        IgniteRenderingContext.RenderCount++;
    }

    static leave() {
        if (IgniteRenderingContext.RenderCount) {
            IgniteRenderingContext.RenderCount--;
        }
    }

    static get rendering() {
        if (IgniteRenderingContext.RenderCount && IgniteRenderingContext.RenderCount > 0) {
            return true;
        }

        return false;
    }
}

window.IgniteRenderingContext = IgniteRenderingContext;

export {
    IgniteProperty,
    IgniteRenderingContext,
    IgniteCallback
};