/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
define([
    'ko',
    'jquery',
    'underscore'
], function (ko, $, _) {
    'use strict';

    /**
     * Checks if provided  value is a dom element.
     *
     * @param {*} node - Value to be checked.
     * @returns {Boolean}
     */
    function isDomElement(node) {
        return typeof node === 'object' && node.tagName && node.nodeType;
    }

    /**
     * Removes from the provided array all non-root nodes located inside
     * of the comment element as long as the closing comment tags.
     *
     * @param {(Array|ArrayLike)} nodes - An array of nodes to be processed.
     * @returns {Array}
     */
    function normalize(nodes) {
        var result;

        nodes   = _.toArray(nodes);
        result  = nodes.slice();

        nodes.forEach(function (node) {
            if (node.nodeType === 8) {
                result = !ko.virtualElements.hasBindingValue(node) ?
                    _.without(result, node) :
                    _.difference(result, ko.virtualElements.childNodes(node));
            }
        });

        return result;
    }

    /**
     * Extends binding context of each item in the collection.
     *
     * @param {...Object} extenders - Multiple extender objects to be applied to the context.
     * @returns {jQueryCollection} Chainable.
     */
    $.fn.extendCtx = function () {
        var nodes       = normalize(this),
            extenders   = _.toArray(arguments);

        nodes.forEach(function (node) {
            var ctx  = ko.contextFor(node),
                data = [ctx].concat(extenders);

            _.extend.apply(_, data);
        });

        return this;
    };

    /**
     * Evaluates bindings specified in each DOM element of collection.
     *
     * @param {(HTMLElement|Object)} [ctx] - Context to use for bindings evaluation.
     *      If not specified then current context of a collections' item will be used.
     * @returns {jQueryCollection} Chainable.
     */
    $.fn.applyBindings = function (ctx) {
        var nodes = normalize(this),
            nodeCtx;

        if (isDomElement(ctx)) {
            ctx = ko.contextFor(ctx);
        }

        nodes.forEach(function (node) {
            nodeCtx = ctx || ko.contextFor(node);

            ko.applyBindings(nodeCtx, node);
        });

        return this;
    };

    /**
     * Adds specified bindings to each DOM element in
     * collection and evaluates them with provided context.
     *
     * @param {(Object|Function)} data - Either bindings object or a function
     *      which returns bindings data for each element in collection.
     * @param {(HTMLElement|Object)} [ctx] - Context to use for bindings evaluation.
     *      If not specified then current context of a collections' item will be used.
     * @returns {jQueryCollection} Chainable.
     */
    $.fn.bindings = function (data, ctx) {
        var nodes    = normalize(this),
            bindings = data,
            nodeCtx;

        if (isDomElement(ctx)) {
            ctx = ko.contextFor(ctx);
        }

        nodes.forEach(function (node) {
            nodeCtx = ctx || ko.contextFor(node);

            if (_.isFunction(data)) {
                bindings = data(nodeCtx, node);
            }

            ko.applyBindingsToNode(node, bindings, nodeCtx);
        });

        return this;
    };
});
