Helper utilities for working with DOM nodes and collections.

var query = {

    htmlCollectionToArray: function (foundNodes) {
        var nodes = [],
            index;

        if (!foundNodes || !foundNodes.length) {
            return nodes;
        }

        for (index = 0; index < foundNodes.length; index++) {
            nodes.push(foundNodes[index]);
        }

        return nodes;
    },

    find: function (selector) {
        // we use querySelectorAll only on document, not on nodes because of its
        // unexpected behavior. See for instance
        // http://stackoverflow.com/questions/11503534/jquery-vs-document-
        // queryselectorall and http://jsfiddle.net/QdMc5/ and
        // http://ejohn.org/blog/thoughts-on-queryselectorall
        if (!document.querySelectorAll || !selector) {
            return []; // not supported on all browsers
        }

        var foundNodes = document.querySelectorAll(selector);

        return this.htmlCollectionToArray(foundNodes);
    },

    findMultiple: function (selectors) {
        if (!selectors || !selectors.length) {
            return [];
        }

        var index, foundNodes;
        var nodes = [];

        for (index = 0; index < selectors.length; index++) {
            foundNodes = this.find(selectors[index]);
            nodes = nodes.concat(foundNodes);
        }

        nodes = this.makeNodesUnique(nodes);

        return nodes;
    },

    findNodesByTagName: function (node, tagName) {
        if (!node || !tagName || !node.getElementsByTagName) {
            return [];
        }

        var foundNodes = node.getElementsByTagName(tagName);

        return this.htmlCollectionToArray(foundNodes);
    },

    makeNodesUnique: function (nodes) {
        var copy = [].concat(nodes);

        nodes.sort(function (n1, n2) {
            if (n1 === n2) {
                return 0;
            }

            var index1 = indexOfArray(copy, n1);
            var index2 = indexOfArray(copy, n2);

            if (index1 === index2) {
                return 0;
            }

            return index1 > index2 ? -1 : 1;
        });

        if (nodes.length <= 1) {
            return nodes;
        }

        var index = 0;
        var numDuplicates = 0;
        var duplicates = [];
        var node;

        node = nodes[index++];
        while (node) {
            if (node === nodes[index]) {
                numDuplicates = duplicates.push(index);
            }
            node = nodes[index++] || null;
        }

        while (numDuplicates--) {
            nodes.splice(duplicates[numDuplicates], 1);
        }

        return nodes;
    },

    getAttributeValueFromNode: function (node, attributeName) {
        if (!this.hasNodeAttribute(node, attributeName)) {
            return;
        }

        if (node && node.getAttribute) {
            return node.getAttribute(attributeName);
        }

        if (!node || !node.attributes) {
            return;
        }

        var typeOfAttr = (typeof node.attributes[attributeName]);
        if ('undefined' === typeOfAttr) {
            return;
        }

        if (node.attributes[attributeName].value) {
            // nodeValue is deprecated ie Chrome
            return node.attributes[attributeName].value;
        }

        if (node.attributes[attributeName].nodeValue) {
            return node.attributes[attributeName].nodeValue;
        }

        var index;
        var attrs = node.attributes;

        if (!attrs) {
            return;
        }

        for (index = 0; index < attrs.length; index++) {
            if (attrs[index].nodeName === attributeName) {
                return attrs[index].nodeValue;
            }
        }

        return null;
    },

    hasNodeAttributeWithValue: function (node, attributeName) {
        var value = this.getAttributeValueFromNode(node, attributeName);
        return !!value;
    },

    hasNodeAttribute: function (node, attributeName) {
        if (node && node.hasAttribute) {
            return node.hasAttribute(attributeName);
        }

        if (node && node.attributes) {
            var typeOfAttr = (typeof node.attributes[attributeName]);
            return 'undefined' !== typeOfAttr;
        }

        return false;
    },

    hasNodeCssClass: function (node, className) {
        if (node && className && node.className) {
            var classes = node.className.split(' ');
            if (-1 !== indexOfArray(classes, className)) {
                return true;
            }
        }

        return false;
    },

    findNodesHavingAttribute: function (nodeToSearch, attributeName, nodes) {
        if (!nodes) {
            nodes = [];
        }

        if (!nodeToSearch || !attributeName) {
            return nodes;
        }

        var children = getChildrenFromNode(nodeToSearch);
        if (!children || !children.length) {
            return nodes;
        }

        var index, child;
        for (index = 0; index < children.length; index++) {
            child = children[index];
            if (this.hasNodeAttribute(child, attributeName)) {
                nodes.push(child);
            }

            nodes = this.findNodesHavingAttribute(child, attributeName, nodes);
        }

        return nodes;
    },

    findFirstNodeHavingAttribute: function (node, attributeName) {
        if (!node || !attributeName) {
            return;
        }

        if (this.hasNodeAttribute(node, attributeName)) {
            return node;
        }

        var nodes = this.findNodesHavingAttribute(node, attributeName);
        if (nodes && nodes.length) {
            return nodes[0];
        }
    },

    findFirstNodeHavingAttributeWithValue: function (node, attributeName) {
        if (!node || !attributeName) {
            return;
        }

        if (this.hasNodeAttributeWithValue(node, attributeName)) {
            return node;
        }

        var nodes = this.findNodesHavingAttribute(node, attributeName);
        if (!nodes || !nodes.length) {
            return;
        }

        var index;
        for (index = 0; index < nodes.length; index++) {
            if (this.getAttributeValueFromNode(nodes[index], attributeName)) {
                return nodes[index];
            }
        }
    },

    findNodesHavingCssClass: function (nodeToSearch, className, nodes) {
        if (!nodes) {
            nodes = [];
        }

        if (!nodeToSearch || !className) {
            return nodes;
        }

        if (nodeToSearch.getElementsByClassName) {
            var foundNodes = nodeToSearch.getElementsByClassName(className);
            return this.htmlCollectionToArray(foundNodes);
        }

        var children = getChildrenFromNode(nodeToSearch);
        if (!children || !children.length) {
            return [];
        }

        var index, child;
        for (index = 0; index < children.length; index++) {
            child = children[index];
            if (this.hasNodeCssClass(child, className)) {
                nodes.push(child);
            }

            nodes = this.findNodesHavingCssClass(child, className, nodes);
        }

        return nodes;
    },

    findFirstNodeHavingClass: function (node, className) {
        if (!node || !className) {
            return;
        }

        if (this.hasNodeCssClass(node, className)) {
            return node;
        }

        var nodes = this.findNodesHavingCssClass(node, className);
        if (nodes && nodes.length) {
            return nodes[0];
        }
    },

    isLinkElement: function (node) {
        if (!node) {
            return false;
        }

        var elementName = String(node.nodeName).toLowerCase();
        var linkElementNames = ['a', 'area'];
        var pos = indexOfArray(linkElementNames, elementName);

        return pos !== -1;
    },

    setAnyAttribute: function (node, attrName, attrValue) {
        if (!node || !attrName) {
            return;
        }

        if (node.setAttribute) {
            node.setAttribute(attrName, attrValue);
        }
        else {
            node[attrName] = attrValue;
        }
    }

};