var HL = HL || {};

HL.utils = (function() {
    var utils = {
        /**
         * get the next span relative to el within parent
         * @param {Node} el - an element to use for context of search
         * @param {Node} [parent] - containing element for search
         * @returns {Node|boolean}
         */
        getNextSpan: function(el, parent) {
            parent = parent || this.content;

            if (el.nodeType === 1 && el.children.length) {
                if (this.isHighlightable(el.firstChild)) {
                    return el.firstChild;
                }
                else {
                    return this.getNextSpan(el.firstChild);
                }
            }

            if (el.nextSibling) {
                if (this.isHighlightable(el.nextSibling)) {
                    return el.nextSibling;
                }
                else {
                    return this.getNextSpan(el.nextSibling);
                }
            }

            if (el.parentNode == parent) {
                return false;
            }

            if (el.parentNode.nextSibling) {
                if (this.isHighlightable(el.parentNode.nextSibling)) {
                    return el.parentNode.nextSibling;
                }
                else {
                    return this.getNextSpan(el.parentNode.nextSibling);
                }
            }

            return false;
        },

        /**
         * get the previous span relative to el within parent
         * @param {Node} el - an element to use for context of search
         * @param {Node} [parent] - containing element for search
         * @returns {Node|boolean}
         */
        getPrevSpan: function(el, parent) {
            parent = parent || this.content;

            if (el == parent) {
                return false;
            }

            if (el.previousSibling) {
                if (el.previousSibling.lastChild) {
                    if (this.isHighlightable(el.previousSibling.lastChild)) {
                        return el.previousSibling.lastChild;
                    }
                    else {
                        return this.getPrevSpan(el.previousSibling.lastChild);
                    }
                }
                else {
                    if (this.isHighlightable(el.previousSibling)) {
                        return el.previousSibling;
                    }
                    else {
                        return this.getPrevSpan(el.previousSibling);
                    }
                }
            }

            if (el.parentNode) {
                if (this.isHighlightable(el.parentNode)) {
                    return el.parentNode;
                }
                else {
                    return this.getPrevSpan(el.parentNode);
                }
            }

            return false;
        },

        /**
         * check if element is of type name and return result
         * @param el
         * @param name
         * @returns {boolean}
         */
        /*isElement: function(el, name) {
            return el.nodeName.toLowerCase() == name;
        },*/

        /**
         * unwraps el in place
         * @param {Element} el - element to unwrap
         */
        unwrapElement: function(el) {
            var parent = el.parentNode,
                prev = el.previousSibling,
                next = el.nextSibling;

            while (el.lastChild) {
                //join lastChild to next if both text nodes
                if (el.lastChild.nodeType == 3 && next && next.nodeType == 3) {
                    next.textContent = el.lastChild.textContent + next.textContent;
                    el.removeChild(el.lastChild);
                    continue;
                }

                parent.insertBefore(el.lastChild, next);
                next = el.nextSibling;
            }

            el.parentNode.removeChild(el);

            //join prev and unwrapped firstChild if both text nodes
            if (prev && prev.nodeType == 3 && prev.nextSibling && prev.nextSibling.nodeType == 3) {
                prev.textContent += prev.nextSibling.textContent;
                parent.removeChild(prev.nextSibling);
            }
        },

        /**
         * unwraps el and appends content to target
         * @param {Element} el
         * @param {Element} target - target element to unwrap content to
         */
        unwrapElementTo: function(el, target) {
            while (el.firstChild) {
                if (el.firstChild.nodeType == 3 && target.lastChild && target.lastChild.nodeType == 3) {
                    target.lastChild.textContent += el.firstChild.textContent;
                    el.removeChild(el.firstChild);
                }
                else {
                    target.appendChild(el.firstChild);
                }
            }

            if (el.parentNode) {
                el.parentNode.removeChild(el);
            }
        },

        unwrapSpan: function(el) {
            //check if prev is a text node
            console.log(el.previousSibling && el.previousSibling.nodeType === 3);

            if (el.previousSibling && el.previousSibling.nodeType === 3) {
                el.previousSibling.textContent += el.textContent;
            }
            else {
                el.insertAdjacentHTML('beforebegin', el.textContent);
            }

            this.removeSpan(el);
        },

        removeSpan: function(el) {
            var parent = el.parentNode;

            parent.removeChild(el);

            if (parent.parentNode && parent.childNodes.length === 0) {
                parent.parentNode.removeChild(parent);
            }
        },

        /**
         * chunks highlighted text (span data) and stores in an array
         * referenced in the returned object by the relevant colour
         * @param {object[]} array - span data
         * @returns {{}}
         */
        getBlocks: function(array) {
            var prev, related, ref;

            return array.reduce(function(blocks, data) {
                ref = (data.colour ? data.colour : (data.static ? 'static' : false));

                if (ref) {
                    if (prev) {
                        related = OU.utils.firstBlockParent(data.el) == OU.utils.firstBlockParent(prev.el);
                    }

                    if (!prev || ref != prev.ref || !related) { //new block
                        blocks[ref] = blocks[ref] || [];
                        blocks[ref].push([]);
                    }

                    blocks[ref][blocks[ref].length - 1].push(data);
                }

                prev = {
                    el: data.el,
                    ref: ref
                };

                return blocks;
            }, {});
        },

        getHighlight: function(el) {
            return el.getAttribute('data-hl');
        },

        isStatic: function(el) {
            return el.nodeType == 1 && OU.utils.hasClass(el, 'static');
        },

        isPrefill: function(el) {
            return el.nodeType == 1 && OU.utils.hasClass(el, 'pre-highlight');
        },

        /**
         * check if el is (potentially) highlightable
         * @param el
         * @returns {boolean}
         */
        isHighlightable: function(el) {
            return el.nodeType == 1 && el.hasAttribute('data-hl');
        }
    };

    /**
     * holds all logic for wrapping content in spans, defined by type
     */
    utils.process = {
        import: false,
        aliases: {
            32: 'space'
        },
        regex: {
            space: /\s/,
            punctuation: /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/,
            end: /$/
        },

        //TODO look at running answer through here?
        run: function(node, activity) {
            var type;

            this.activity = activity;
            this.inTool = activity.inTool;
            type = (this.inTool && activity.cfg.type == 'custom') ? 'character' : activity.cfg.type;
            this[type](node, activity.cfg);
        },

        character: function(node, cfg, colour, isStatic) {
            if (node.nodeType == 1) {
                //workaround for 'empty' elements, e.g. br
                if (node.childNodes.length === 0) {
                    node.parentNode.insertBefore(this.createSpan(), node).appendChild(node);
                    return;
                }

                OU.utils.toArray(node.childNodes).forEach(function(node) {
                    if (utils.isHighlightable(node) || utils.isStatic(node)) {
                        isStatic = utils.isStatic(node);

                        if (!this.inTool && isStatic) {
                            return;
                        }

                        if (node.textContent.length > 1) {
                            //span has been edited so process contents and unwrap
                            this.character(node, cfg, this.activity.getHighlight(node), isStatic);
                            utils.unwrapElement(node);
                        }
                    }
                    else {
                        this.character(node, cfg, colour, isStatic);
                    }
                }.bind(this));
            }
            else {
                //match anything that's not a line break etc. to exclude space between tags
                if (/[^\t\n\r]/.test(node.textContent)) {
                    node.nodeValue
                        .split('')
                        .forEach(function(c) {
                            this.spanContent(c, node, colour, isStatic);
                        }.bind(this));

                    node.parentNode.removeChild(node);
                }
            }
        },

        custom: function(el, cfg) {
            OU.utils.toArray(el.querySelectorAll('span[data-hl]')).forEach(function(el) {
                if (!OU.utils.hasClass(el, 'static')) {
                    if (cfg.type == 'custom') {
                        el.setAttribute('data-hl', '');
                        el.setAttribute('tabindex', '0');
                        OU.utils.addClass(el, 'custom-highlight');
                    }
                }
            });
        },

        paragraph: function(el) {
            if (el.children.length === 0) {
                this.spanContent(el);
            }
            else {
                OU.utils.toArray(el.children).some(function(node) {
                    if (getComputedStyle(node).display !== 'inline') {
                        this.paragraph(node);
                    }
                    else {
                        this.spanContent(el);
                        return true;
                    }
                }.bind(this));
            }
        },

        //find block level elements - use para logic
        //create span/fragment?
        //for each children and children's children etc
        //push to span until '.'
        //push remaining child elements back to block level children
        //span content (or whatever) and start again
        sentence: function(node) {
            var regex = /[\s.]/, //space or full stop
                str = '',
                next, replacement;

            if (node.nodeType == 1) {
                OU.utils.toArray(node.childNodes).forEach(function (node) {
                    if (!utils.isHighlightable(node)) {
                        this.sentence(node);
                    }
                    else {
                        if (node.textContent.length > 1 && regex.test(node.textContent)) {
                            //span has been edited so replace with text node and process el again
                            replacement = document.createTextNode(text);
                            node.parentNode.replaceChild(replacement, node);
                            this.sentence(node);
                        }
                    }
                }.bind(this));
            }
            else {
                //match anything that's not a line break etc. to exclude space between tags
                if (/[^\t\n\r]/.test(node.nodeValue)) {
                    node.nodeValue
                        .split('')
                        .forEach(function(c, i, array) {
                            next = array[i + 1];

                            //console.log(c, regex.test(c));
                            if (regex.test(c)) {
                                if (str.length) {
                                    this.spanContent(str, node);
                                }

                                this.spanContent(c, node);
                                str = '';
                            }
                            else {
                                str += c;
                            }

                            //last character in block so append
                            if (i === node.nodeValue.length - 1) {
                                this.spanContent(str, node);
                            }

                        }.bind(this));

                    node.parentNode.removeChild(node);
                }
            }
        },

        /**
         * Span words
         * @param {Node|Element} node
         * @param {object} cfg - activity cfg
         * @param {string} [colour]
         * @param {string} [isStatic]
         */
        word: function(node, cfg, colour, isStatic) {
            var str = '',
                next, text;

            if (node.nodeType == 1) {
                //workaround for 'empty' elements, e.g. br
                if (node.childNodes.length === 0) {
                    node.parentNode.insertBefore(this.createSpan(), node).appendChild(node);
                    return;
                }

                OU.utils.toArray(node.childNodes).forEach(function(node) {
                    if (utils.isHighlightable(node) || utils.isStatic(node)) {
                        isStatic = utils.isStatic(node);

                        if (!this.inTool && isStatic) {
                            return;
                        }

                        text = node.textContent;

                        if (isStatic || text.length > 1 && (/\s/.test(text) || cfg.splitWord && this.regex.punctuation.test(text))) {
                            //span has been edited so process contents and unwrap
                            this.word(node, cfg, this.activity.getHighlight(node), isStatic);
                            //console.log(node, node.parentNode);
                            utils.unwrapElement(node);
                        }
                    }
                    else {
                        this.word(node, cfg, colour, isStatic);
                    }
                }.bind(this));
            }
            else {
                //match anything that's not a line break etc. to exclude space between tags
                if (/[^\t\n\r]/.test(node.nodeValue)) {
                    node.nodeValue
                        .split('')
                        .forEach(function(c, i, array) {
                            next = array[i + 1];

                            //split on space/punctuation if split or next is undefined (end of input)/a space
                            if (/\s/.test(c) || this.regex.punctuation.test(c) && (cfg.splitWord || /[\u2026'!"(),.:;?\[\]`{}]/.test(c))) {
                                if (str.length) {
                                    this.spanContent(str, node, colour, isStatic);
                                }

                                this.spanContent(c, node, colour, isStatic);
                                str = '';
                            }
                            else {
                                str += c;
                            }

                            //last character in block so append if str
                            if (str.length && i === array.length - 1) {
                                this.spanContent(str, node, colour, isStatic);
                            }
                        }.bind(this));

                    node.parentNode.removeChild(node);
                }
            }
        },

        /**
         * create highlightable span
         * @param {string} [colour]
         * @param {string} [isStatic]
         * @returns {Element}
         */
        createSpan: function(colour, isStatic) {
            var span = document.createElement('span');

            span.setAttribute('data-hl', '');

            if (colour) {
                OU.utils.addClass(span, colour);
            }

            if (isStatic) {
                OU.utils.addClass(span, 'static');
            }

            return span;
        },

        /**
         *
         * @param {string|Element} content - content to be wrapped in a span
         * @param {Node} [ref]
         * @param {string} [colour]
         * @param {string} [isStatic]
         * @returns {*}
         */
        spanContent: function(content, ref, colour, isStatic) {
            var span, code;

            if (!content) {
                return;
            }

            switch (true) {
                case content instanceof Element:
                    span = this.createSpan(colour, isStatic);

                    while (content.firstChild) {
                        span.appendChild(content.firstChild);
                    }
                    break;
                case typeof content == 'string':
                    code = content.charCodeAt(content.length - 1);

                    if (code == 9 || code == 10) { //tab or linefeed
                        span = document.createTextNode(content);
                    }
                    else {
                        span = this.createSpan(colour, isStatic);
                        span.textContent = content;

                        if (this.aliases[code]) {
                            OU.utils.addClass(span, this.aliases[code]);
                        }
                    }
            }

            if (ref) {
                ref.parentNode.insertBefore(span, ref);
            }
            else {
                content.appendChild(span);
            }

            return span;
        }
    };

    return utils;
})();
