// REPEAT binding for Knockout http://knockoutjs.com/
// (c) Michael Best
// License: MIT (http://www.opensource.org/licenses/mit-license.php)
// Version 2.1.0



    if (!ko.virtualElements)
        throw Error('Repeat requires at least Knockout 2.1');

    var ko_bindingFlags = ko.bindingFlags || {};
    var ko_unwrap = ko.utils.unwrapObservable;

    var koProtoName = '__ko_proto__';

    if (ko.version >= "3.0.0") {
        // In Knockout 3.0.0, use the node preprocessor to replace a node with a repeat binding with a virtual element
        var provider = ko.bindingProvider.instance, previousPreprocessFn = provider.preprocessNode;
        provider.preprocessNode = function(node) {
            var newNodes, nodeBinding;
            if (!previousPreprocessFn || !(newNodes = previousPreprocessFn.call(this, node))) {
                if (node.nodeType === 1 && (nodeBinding = node.getAttribute('data-bind'))) {
                    if (/^\s*repeat\s*:/.test(nodeBinding)) {
                        var leadingComment = node.ownerDocument.createComment('ko ' + nodeBinding),
                            trailingComment = node.ownerDocument.createComment('/ko');
                        node.parentNode.insertBefore(leadingComment, node);
                        node.parentNode.insertBefore(trailingComment, node.nextSibling);
                        node.removeAttribute('data-bind');
                        newNodes = [leadingComment, node, trailingComment];
                    }
                }
            }
            return newNodes;
        };
    }

    ko.virtualElements.allowedBindings.repeat = true;
    ko.bindingHandlers.repeat = {
        flags: ko_bindingFlags.contentBind | ko_bindingFlags.canUseVirtual,
        init: function(element, valueAccessor, allBindingsAccessor, xxx, bindingContext) {

            // Read and set fixed options--these options cannot be changed
            var repeatParam = ko_unwrap(valueAccessor());
            if (repeatParam && typeof repeatParam == 'object' && !('length' in repeatParam)) {
                var repeatIndex = repeatParam.index,
                    repeatData = repeatParam.item,
                    repeatStep = repeatParam.step,
                    repeatReversed = repeatParam.reverse,
                    repeatBind = repeatParam.bind,
                    repeatInit = repeatParam.init,
                    repeatUpdate = repeatParam.update;
            }
            // Set default values for options that need it
            repeatIndex = repeatIndex || '$index';
            repeatData = repeatData || ko.bindingHandlers.repeat.itemName || '$item';
            repeatStep = repeatStep || 1;
            repeatReversed = repeatReversed || false;

            var parent = element.parentNode, placeholder;
            if (element.nodeType == 8) {    // virtual element
                // Extract the "children" and find the single element node
                var childNodes = ko.utils.arrayFilter(ko.virtualElements.childNodes(element), function(node) { return node.nodeType == 1;});
                if (childNodes.length !== 1) {
                    throw Error("Repeat binding requires a single element to repeat");
                }
                ko.virtualElements.emptyNode(element);

                // The placeholder is the closing comment normally, or the opening comment if reversed
                placeholder = repeatReversed ? element : element.nextSibling;
                // The element to repeat is the contained element
                element = childNodes[0];
            } else {    // regular element
                // First clean the element node and remove node's binding
                var origBindString = element.getAttribute('data-bind');
                ko.cleanNode(element);
                element.removeAttribute('data-bind');

                // Original element is no longer needed: delete it and create a placeholder comment
                placeholder = element.ownerDocument.createComment('ko_repeatplaceholder ' + origBindString);
                parent.replaceChild(placeholder, element);
            }

            // extract and remove a data-repeat-bind attribute, if present
            if (!repeatBind) {
                repeatBind = element.getAttribute('data-repeat-bind');
                if (repeatBind) {
                    element.removeAttribute('data-repeat-bind');
                }
            }

            // Make a copy of the element node to be copied for each repetition
            var cleanNode = element.cloneNode(true);
            if (typeof repeatBind == "string") {
                cleanNode.setAttribute('data-bind', repeatBind);
                repeatBind = null;
            }

            // Set up persistent data
            var lastRepeatCount = 0,
                notificationObservable = ko.observable(),
                repeatArray, arrayObservable;

            if (repeatInit) {
                repeatInit(parent);
            }

            var subscribable = ko.computed(function() {
                function makeArrayItemAccessor(index) {
                    var f = function(newValue) {
                        var item = repeatArray[index];
                        // Reading the value of the item
                        if (!arguments.length) {
                            notificationObservable();   // for dependency tracking
                            return ko_unwrap(item);
                        }
                        // Writing a value to the item
                        if (ko.isObservable(item)) {
                            item(newValue);
                        } else if (arrayObservable && arrayObservable.splice) {
                            arrayObservable.splice(index, 1, newValue);
                        } else {
                            repeatArray[index] = newValue;
                        }
                        return this;
                    };
                    // Pretend that our accessor function is an observable
                    f[koProtoName] = ko.observable;
                    return f;
                }

                function makeBinding(item, index, context) {
                    return repeatArray
                        ? function() { return repeatBind.call(bindingContext.$data, item, index, context); }
                        : function() { return repeatBind.call(bindingContext.$data, index, context); }
                }

                // Read and set up variable options--these options can change and will update the binding
                var paramObservable = valueAccessor(), repeatParam = ko_unwrap(paramObservable), repeatCount = 0;
                if (repeatParam && typeof repeatParam == 'object') {
                    if ('length' in repeatParam) {
                        repeatArray = repeatParam;
                        repeatCount = repeatArray.length;
                    } else {
                        if ('foreach' in repeatParam) {
                            repeatArray = ko_unwrap(paramObservable = repeatParam.foreach);
                            if (repeatArray && typeof repeatArray == 'object' && 'length' in repeatArray) {
                                repeatCount = repeatArray.length || 0;
                            } else {
                                repeatCount = repeatArray || 0;
                                repeatArray = null;
                            }
                        }
                        // If a count value is provided (>0), always output that number of items
                        if ('count' in repeatParam)
                            repeatCount = ko_unwrap(repeatParam.count) || repeatCount;
                        // If a limit is provided, don't output more than the limit
                        if ('limit' in repeatParam)
                            repeatCount = Math.min(repeatCount, ko_unwrap(repeatParam.limit)) || repeatCount;
                    }
                    arrayObservable = repeatArray && ko.isObservable(paramObservable) ? paramObservable : null;
                } else {
                    repeatCount = repeatParam || 0;
                }

                // Remove nodes from end if array is shorter
                for (; lastRepeatCount > repeatCount; lastRepeatCount-=repeatStep) {
                    ko.removeNode(repeatReversed ? placeholder.nextSibling : placeholder.previousSibling);
                }

                // Notify existing nodes of change
                notificationObservable.notifySubscribers();

                // Add nodes to end if array is longer (also initially populates nodes)
                for (; lastRepeatCount < repeatCount; lastRepeatCount+=repeatStep) {
                    // Clone node and add to document
                    var newNode = cleanNode.cloneNode(true);
                    parent.insertBefore(newNode, repeatReversed ? placeholder.nextSibling : placeholder);
                    newNode.setAttribute('data-repeat-index', lastRepeatCount);

                    // Apply bindings to inserted node
                    if (repeatArray && repeatData == '$data') {
                        var newContext = bindingContext.createChildContext(makeArrayItemAccessor(lastRepeatCount));
                    } else {
                        var newContext = bindingContext.extend();
                        if (repeatArray)
                            newContext[repeatData] = makeArrayItemAccessor(lastRepeatCount);
                    }
                    newContext[repeatIndex] = lastRepeatCount;
                    if (repeatBind) {
                        var result = ko.applyBindingsToNode(newNode, makeBinding(newContext[repeatData], lastRepeatCount, newContext), newContext, true),
                            shouldBindDescendants = result && result.shouldBindDescendants;
                    }
                    if (!repeatBind || (result && shouldBindDescendants !== false)) {
                        ko.applyBindings(newContext, newNode);
                    }
                }
                if (repeatUpdate) {
                    repeatUpdate(parent);
                }
            }, null, {disposeWhenNodeIsRemoved: placeholder});

            return { controlsDescendantBindings: true, subscribable: subscribable };
        }
    };

