var gloo = (function () {
    "use strict";

    function DataContext(data) {
        this.data = data;
        this.root = data;
        this.index = null;
    }

    var bindingContextFactory,
        createDomElementSelector,
        createHandlerOptionsFactory,
        createHandlerOptionsFactoryBody,
        createProperty,
        gloo,
        nextId = 1;

    createDomElementSelector = function (domElementId) {
        return function () {
            return document.getElementById(domElementId);
        };
    };

    createHandlerOptionsFactoryBody = "return (function ($data, $root, $index) {" +
        "   with ($data) {" +
        "       return {%};" +
        "   }" +
        "})(dataContext.data, dataContext.root, dataContext.index);";

    createHandlerOptionsFactory = function (handlerOptionsDeclaration) {
        var functionBody = createHandlerOptionsFactoryBody.replace("%", handlerOptionsDeclaration);

        return new Function("dataContext", functionBody);
    };

    createProperty = function (initialValue) {
        var value = initialValue;

        return function (newValue) {
            if (arguments.length === 1) {
                value = newValue;
            }

            return value;
        };
    };

    bindingContextFactory = function () {
        function bind(domElement, dataOrDataContext) {
            var binding,
                childElement,
                dataContext,
                dataBindValue,
                domElementId,
                handler,
                handlerArguments,
                handlerOptions,
                handlerOptionsFactory,
                handlerName,
                i;

            if (dataOrDataContext instanceof DataContext) {
                dataContext = dataOrDataContext;
            } else {
                dataContext = new DataContext(dataOrDataContext);
            }

            dataBindValue = domElement.getAttribute("data-bind");

            if (dataBindValue != null) {
                domElementId = domElement.id;

                if (domElementId == null || domElementId.length === 0) {
                    domElement.id = domElementId = ("gloo-" + nextId++);
                }

                handlerOptionsFactory = createHandlerOptionsFactory(dataBindValue);

                binding = {
                    "dataContext": dataContext,
                    "domElementSelector": createDomElementSelector(domElementId),
                    "handlerOptionsFactory": handlerOptionsFactory,
                    "previousHandlerOptions": null
                };

                bindings.push(binding);

                handlerOptions = handlerOptionsFactory(dataContext);

                for (handlerName in handlerOptions) {
                    //noinspection JSUnfilteredForInLoop
                    handler = handlers[handlerName];

                    if (handler != null && handler.initialize) {
                        //noinspection JSUnfilteredForInLoop
                        handlerArguments = handlerOptions[handlerName];
                        handler.initialize(domElement, handlerArguments, dataContext);
                    }
                }
            }

            for (i = 0; i < domElement.children.length; i++) {
                childElement = domElement.children[i];

                bind(childElement, dataContext);
            }
        }

        function refresh() {
            var binding,
                domElement,
                handler,
                handlerArguments,
                handlerOptions,
                handlerName,
                i;

            for (i = 0; i < bindings.length; i++) {
                binding = bindings[i];

                domElement = binding.domElementSelector();

                if (domElement == null) {
                    bindings.splice(i, 1);
                } else {
                    handlerOptions = binding.handlerOptionsFactory(binding.dataContext);

                    for (handlerName in handlerOptions) {
                        //noinspection JSUnfilteredForInLoop
                        handler = handlers[handlerName];

                        if (handler != null && handler.refresh) {
                            //noinspection JSUnfilteredForInLoop
                            handlerArguments = handlerOptions[handlerName];
                            handler.refresh(domElement, handlerArguments);
                        }
                    }
                }
            }
        }

        var bindings = [],
            handlers = {};

        return {
            bind: bind,
            handlers: handlers,
            property: createProperty,
            refresh: refresh
        };
    };

    gloo = bindingContextFactory();
    gloo.createBindingContext = bindingContextFactory;
    gloo.stringify = function (object) {
        return JSON.stringify(object, function (key, value) {
            if (this[key] instanceof Function) value = value();

            return value;
        });
    };

    return gloo;
})();