/**
 * @alternateClassName Ext.DomHelper
 * @alternateClassName Ext.core.DomHelper
 *
 * @singleton
 *
 * The DomHelper class provides a layer of abstraction from DOM and transparently supports creating
 * elements via DOM or using HTML fragments. It also has the ability to create HTML fragment
 * templates from your DOM building code.
 *
 * ## DomHelper element specification object
 *
 * A specification object is used when creating elements. Attributes of this object are assumed
 * to be element attributes, except for 4 special attributes:
 *
 * * **tag**: The tag name of the element
 * * **children (or cn)**: An array of the same kind of element definition objects to be created
 * and appended. These can be nested as deep as you want.
 * * **cls**: The class attribute of the element. This will end up being either the "class"
 * attribute on a HTML fragment or className for a DOM node, depending on whether DomHelper is using
 * fragments or DOM.
 * * **html**: The innerHTML for the element
 *
 * ## Insertion methods
 *
 * Commonly used insertion methods:
 *
 * * {@link #append}
 * * {@link #insertBefore}
 * * {@link #insertAfter}
 * * {@link #overwrite}
 * * {@link #insertHtml}
 *
 * ## Example
 *
 * This is an example, where an unordered list with 3 children items is appended to an existing
 * element with id 'my-div':
 *
 *     var dh = Ext.DomHelper; // create shorthand alias
 *     // specification object
 *     var spec = {
 *         id: 'my-ul',
 *         tag: 'ul',
 *         cls: 'my-list',
 *         // append children after creating
 *         children: [     // may also specify 'cn' instead of 'children'
 *             {tag: 'li', id: 'item0', html: 'List Item 0'},
 *             {tag: 'li', id: 'item1', html: 'List Item 1'},
 *             {tag: 'li', id: 'item2', html: 'List Item 2'}
 *         ]
 *     };
 *     var list = dh.append(
 *         'my-div', // the context element 'my-div' can either be the id or the actual node
 *         spec      // the specification object
 *     );
 *
 * Element creation specification parameters in this class may also be passed as an Array
 * of specification objects. This can be used to insert multiple sibling nodes into an existing
 * container very efficiently. For example, to add more list items to the example above:
 *
 *     dh.append('my-ul', [
 *         {tag: 'li', id: 'item3', html: 'List Item 3'},
 *         {tag: 'li', id: 'item4', html: 'List Item 4'}
 *     ]);
 *
 * ## Templating
 *
 * The real power is in the built-in templating. Instead of creating or appending any elements,
 * createTemplate returns a Template object which can be used over and over to insert new elements.
 * Revisiting the example above, we could utilize templating this time:
 *
 *     // create the node
 *     var list = dh.append('my-div', {tag: 'ul', cls: 'my-list'});
 *     // get template
 *     var tpl = dh.createTemplate({tag: 'li', id: 'item{0}', html: 'List Item {0}'});
 *
 *     for(var i = 0; i < 5; i++){
 *         tpl.append(list, i); // use template to append to the actual node
 *     }
 *
 * An example using a template:
 *
 *     var html = '"{0}" href="{1}" class="nav">{2}';
 *
 *     var tpl = new Ext.DomHelper.createTemplate(html);
 *     tpl.append('blog-roll', ['link1', 'http://www.foxmulder.com/', "Fox's Site"]);
 *     tpl.append('blog-roll', ['link2', 'http://www.danascully.org/', "Scully's Site"]);
 *
 * The same example using named parameters:
 *
 *     var html = '"{id}" href="{url}" class="nav">{text}';
 *
 *     var tpl = new Ext.DomHelper.createTemplate(html);
 *     tpl.append('blog-roll', {
 *         id: 'link1',
 *         url: 'http://www.danascully.org/',
 *         text: "Scully's Site"
 *     });
 *     tpl.append('blog-roll', {
 *         id: 'link2',
 *         url: 'http://www.foxmulder.com/',
 *         text: "Fox's Site"
 *     });
 *
 * ## Compiling Templates
 *
 * Templates are applied using regular expressions. The performance is great, but if you are adding
 * a bunch of DOM elements using the same template, you can increase performance even further by
 * "compiling" the template. The way "compile()" works is the template is parsed and broken up
 * at the different variable points and a dynamic function is created and eval'ed. The generated
 * function performs string concatenation of these parts and the passed variables instead of using
 * regular expressions.
 *
 *     var html = '"{id}" href="{url}" class="nav">{text}';
 *
 *     var tpl = new Ext.DomHelper.createTemplate(html);
 *     tpl.compile();
 *
 *     // ... use template like normal
 *
 * ## Performance Boost
 *
 * DomHelper will transparently create HTML fragments when it can. Using HTML fragments instead of
 * DOM can significantly boost performance.
 *
 * Element creation specification parameters may also be strings which are used as innerHTML.
 */
Ext.define('Ext.dom.Helper', function() {
    var afterbegin = 'afterbegin',
        afterend = 'afterend',
        beforebegin = 'beforebegin',
        beforeend = 'beforeend',
        bbValues = ['BeforeBegin', 'previousSibling'],
        aeValues = ['AfterEnd', 'nextSibling'],
        bb_ae_PositionHash = {
            beforebegin: bbValues,
            afterend: aeValues
        },
        fullPositionHash = {
            beforebegin: bbValues,
            afterend: aeValues,
            afterbegin: ['AfterBegin', 'firstChild'],
            beforeend: ['BeforeEnd', 'lastChild']
        };
 
    return {
        singleton: true,
 
        alternateClassName: [
            'Ext.DomHelper',
            'Ext.core.DomHelper'
        ],
 
        emptyTags: /^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i,
        confRe: /^(?:tag|children|cn|html|tpl|tplData)$/i,
        endRe: /end/i,
 
        // Since cls & for are reserved words, we need to transform them
        attributeTransform: { cls: 'class', htmlFor: 'for' },
 
        closeTags: {},
 
        detachedDiv: document.createElement('div'),
 
        decamelizeName: function() {
            var camelCaseRe = /([a-z])([A-Z])/g,
                cache = {};
 
            function decamel(match, p1, p2) {
                return p1 + '-' + p2.toLowerCase();
            }
 
            return function(s) {
                return cache[s] || (cache[s] = s.replace(camelCaseRe, decamel));
            };
        }(),
 
        generateMarkup: function(spec, buffer) {
            var me = this,
                specType = typeof spec,
                attr, val, tag, i, closeTags;
 
            if (specType === "string" || specType === "number") {
                buffer.push(spec);
            }
            else if (Ext.isArray(spec)) {
                for (= 0; i < spec.length; i++) {
                    if (spec[i]) {
                        me.generateMarkup(spec[i], buffer);
                    }
                }
            }
            else {
                tag = spec.tag || 'div';
                buffer.push('<', tag);
 
                for (attr in spec) {
                    if (spec.hasOwnProperty(attr)) {
                        val = spec[attr];
 
                        if (val !== undefined && !me.confRe.test(attr)) {
                            if (val && val.join) {
                                val = val.join(' ');
                            }
 
                            if (typeof val === "object") {
                                buffer.push(' ', attr, '="');
                                me.generateStyles(val, buffer, true).push('"');
                            }
                            else {
                                buffer.push(' ', me.attributeTransform[attr] || attr, '="', val,
                                            '"');
                            }
                        }
                    }
                }
 
                // Now either just close the tag or try to add children and close the tag.
                if (me.emptyTags.test(tag)) {
                    buffer.push('/>');
                }
                else {
                    buffer.push('>');
 
                    // Apply the tpl html, and cn specifications
                    if ((val = spec.tpl)) {
                        val.applyOut(spec.tplData, buffer);
                    }
 
                    if ((val = spec.html)) {
                        buffer.push(val);
                    }
 
                    if ((val = spec.cn || spec.children)) {
                        me.generateMarkup(val, buffer);
                    }
 
                    // we generate a lot of close tags, so cache them rather than push 3 parts
                    closeTags = me.closeTags;
                    buffer.push(closeTags[tag] || (closeTags[tag] = '</' + tag + '>'));
                }
            }
 
            return buffer;
        },
 
        /**
         * Converts the styles from the given object to text. The styles are CSS style names
         * with their associated value.
         * 
         * The basic form of this method returns a string:
         * 
         *      var s = Ext.DomHelper.generateStyles({
         *          backgroundColor: 'red'
         *      });
         *      
         *      // s = 'background-color:red;'
         *
         * Alternatively, this method can append to an output array.
         * 
         *      var buf = [];
         *
         *      ...
         *
         *      Ext.DomHelper.generateStyles({
         *          backgroundColor: 'red'
         *      }, buf);
         *
         * In this case, the style text is pushed on to the array and the array is returned.
         * 
         * @param {Object} styles The object describing the styles.
         * @param {String[]} [buffer] The output buffer.
         * @param {Boolean} [encode] `true` to {@link Ext.String#htmlEncode} property values if they
         * are going to be inserted as HTML attributes.
         * @return {String/String[]} If buffer is passed, it is returned. Otherwise the style
         * string is returned.
         */
        generateStyles: function(styles, buffer, encode) {
            var a = buffer || [],
                name, val;
 
            for (name in styles) {
                if (styles.hasOwnProperty(name)) {
                    val = styles[name];
                    // Since a majority of attributes won't have html characters (basically
                    // restricted to fonts), we'll check first before we try and encode it
                    // because it's less expensive and this method gets called a lot.
                    name = this.decamelizeName(name);
 
                    if (encode && Ext.String.hasHtmlCharacters(val)) {
                        val = Ext.String.htmlEncode(val);
                    }
 
                    a.push(name, ':', val, ';');
                }
            }
 
            return buffer || a.join('');
        },
 
        /**
         * Returns the markup for the passed Element(s) config.
         * @param {Object} spec The DOM object spec (and children).
         * @return {String} 
         */
        markup: function(spec) {
            var buf;
 
            if (typeof spec === "string") {
                return spec;
            }
 
            buf = this.generateMarkup(spec, []);
 
            return buf.join('');
        },
 
        /**
         * Applies a style specification to an element.
         * 
         * Styles in object form should be a valid DOM element style property.  
         * [Valid style property names](http://www.w3schools.com/jsref/dom_obj_style.asp
         * (_along with the supported CSS version for each_)
         * 
         *     // <div id="my-el">Phineas Flynn</div>
         *     
         *     var el = Ext.get('my-el'),
         *         dh = Ext.dom.Helper;
         *     
         *     dh.applyStyles(el, 'color: white;');
         *     
         *     dh.applyStyles(el, {
         *         fontWeight: 'bold',
         *         backgroundColor: 'gray',
         *         padding: '10px'
         *     });
         *     
         *     dh.applyStyles(el, function () {
         *         if (name.initialConfig.html === 'Phineas Flynn') {
         *             return 'font-style: italic;';
         *             // OR return { fontStyle: 'italic' };
         *         }
         *     });
         * 
         * @param {String/HTMLElement/Ext.dom.Element} el The element to apply styles to
         * @param {String/Object/Function} styles A style specification string e.g. 'width:100px',
         * or object in the form {width:'100px'}, or a function which returns such a specification.
         */
        applyStyles: function(el, styles) {
            Ext.fly(el).applyStyles(styles);
        },
 
        /**
         * @private
         * Fix for browsers which do not support createContextualFragment
         */
        createContextualFragment: function(html) {
            var div = this.detachedDiv,
                fragment = document.createDocumentFragment(),
                length, childNodes;
 
            div.innerHTML = html;
            childNodes = div.childNodes;
            length = childNodes.length;
 
            // Move nodes into fragment, don't clone: http://jsperf.com/create-fragment
            while (length--) {
                fragment.appendChild(childNodes[0]);
            }
 
            return fragment;
        },
 
        /**
         * Creates new DOM element(s) without inserting them to the document.
         * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
         * @return {HTMLElement} The new un-inserted node
         */
        createDom: function(o) {
            var me = this,
                markup = me.markup(o),
                div = me.detachedDiv,
                child;
 
            div.innerHTML = markup;
            child = div.firstChild;
 
            // Important to clone the node here, IE8 & 9 have an issue where the markup
            // in the first element will be lost.
            // var ct = document.createElement('div'),
            //     a, b;
            //     ct.innerHTML = '<div>markup1</div>';
            //     a = ct.firstChild;
            //     ct.innerHTML = '<div>markup2</div>';
            //     b = ct.firstChild;
            //     console.log(a.innerHTML, b.innerHTML);
 
            return Ext.supports.ChildContentClearedWhenSettingInnerHTML
                ? child.cloneNode(true)
                : child;
        },
 
        /**
         * Inserts an HTML fragment into the DOM.
         * @param {String} where Where to insert the html in relation to el - beforeBegin,
         * afterBegin, beforeEnd, afterEnd.
         *
         * For example take the following HTML: `<div>Contents</div>`
         *
         * Using different `where` values inserts element to the following places:
         *
         * - beforeBegin: `<HERE><div>Contents</div>`
         * - afterBegin: `<div><HERE>Contents</div>`
         * - beforeEnd: `<div>Contents<HERE></div>`
         * - afterEnd: `<div>Contents</div><HERE>`
         *
         * @param {HTMLElement/TextNode} el The context element
         * @param {String} html The HTML fragment
         * @return {HTMLElement} The new node
         */
        insertHtml: function(where, el, html) {
            var me = this,
                hashVal, range, rangeEl, setStart, frag;
 
            where = where.toLowerCase();
 
            // Has fast HTML insertion into existing DOM: http://www.w3.org/TR/html5/apis-in-html-documents.html#insertadjacenthtml
            if (el.insertAdjacentHTML) {
                if (me.ieInsertHtml) {
                    // hook for IE table hack - impl in ext package override
                    frag = me.ieInsertHtml(where, el, html);
 
                    if (frag) {
                        return frag;
                    }
                }
 
                hashVal = fullPositionHash[where];
 
                if (hashVal) {
                    el.insertAdjacentHTML(hashVal[0], html);
 
                    return el[hashVal[1]];
                }
                // if (not IE and context element is an HTMLElement) or TextNode
            }
            else {
                // we cannot insert anything inside a textnode so...
                if (el.nodeType === 3) {
                    where = where === afterbegin ? beforebegin : where;
                    where = where === beforeend ? afterend : where;
                }
 
                range = Ext.supports.CreateContextualFragment
                    ? el.ownerDocument.createRange()
                    : undefined;
 
                setStart = 'setStart' + (this.endRe.test(where) ? 'After' : 'Before');
 
                if (bb_ae_PositionHash[where]) {
                    if (range) {
                        range[setStart](el);
                        frag = range.createContextualFragment(html);
                    }
                    else {
                        frag = this.createContextualFragment(html);
                    }
 
                    el.parentNode.insertBefore(frag, where === beforebegin ? el : el.nextSibling);
 
                    return el[(where === beforebegin ? 'previous' : 'next') + 'Sibling'];
                }
                else {
                    rangeEl = (where === afterbegin ? 'first' : 'last') + 'Child';
 
                    if (el.firstChild) {
                        if (range) {
                            // Creating ranges on a hidden element throws an error, checking for
                            // visibility is expensive, so we'll catch the error and fall back to
                            // using the full fragment
                            try {
                                range[setStart](el[rangeEl]);
                                frag = range.createContextualFragment(html);
                            }
                            catch (e) {
                                frag = this.createContextualFragment(html);
                            }
                        }
                        else {
                            frag = this.createContextualFragment(html);
                        }
 
                        if (where === afterbegin) {
                            el.insertBefore(frag, el.firstChild);
                        }
                        else {
                            el.appendChild(frag);
                        }
                    }
                    else {
                        el.innerHTML = html;
                    }
 
                    return el[rangeEl];
                }
            }
 
            //<debug>
            Ext.raise({
                sourceClass: 'Ext.DomHelper',
                sourceMethod: 'insertHtml',
                htmlToInsert: html,
                targetElement: el,
                msg: 'Illegal insertion point reached: "' + where + '"'
            });
            //</debug>
        },
 
        /**
         * Creates new DOM element(s) and inserts them before el.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
         * @param {Boolean} [returnElement] true to return a Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        insertBefore: function(el, o, returnElement) {
            return this.doInsert(el, o, returnElement, beforebegin);
        },
 
        /**
         * Creates new DOM element(s) and inserts them after el.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object} o The DOM object spec (and children)
         * @param {Boolean} [returnElement] true to return a Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        insertAfter: function(el, o, returnElement) {
            return this.doInsert(el, o, returnElement, afterend);
        },
 
        /**
         * Creates new DOM element(s) and inserts them as the first child of el.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
         * @param {Boolean} [returnElement] true to return a Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        insertFirst: function(el, o, returnElement) {
            return this.doInsert(el, o, returnElement, afterbegin);
        },
 
        /**
         * Creates new DOM element(s) and appends them to el.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
         * @param {Boolean} [returnElement] true to return a Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        append: function(el, o, returnElement) {
            return this.doInsert(el, o, returnElement, beforeend);
        },
 
        /**
         * Creates new DOM element(s) and overwrites the contents of el with them.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object/String} html The DOM object spec (and children) or raw HTML blob
         * @param {Boolean} [returnElement=false] true to return an Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        overwrite: function(el, html, returnElement) {
            var me = this,
                newNode;
 
            el = Ext.getDom(el);
            html = me.markup(html);
 
            if (me.ieOverwrite) {
                // hook for IE table hack - impl in ext package override
                newNode = me.ieOverwrite(el, html);
            }
 
            if (!newNode) {
                el.innerHTML = html;
                newNode = el.firstChild;
            }
 
            return returnElement ? Ext.get(newNode) : newNode;
        },
 
        doInsert: function(el, o, returnElement, where) {
            var me = this,
                newNode;
 
            el = el.dom || Ext.getDom(el);
 
            if ('innerHTML' in el) {
                // regular dom node
                // For standard HTMLElements, we insert as innerHTML instead of
                // createElement/appenChild because it is much faster in all versions of
                // IE: https://fiddle.sencha.com/#fiddle/tj
                newNode = me.insertHtml(where, el, me.markup(o));
            }
            else {
                // document fragment does not support innerHTML
                newNode = me.createDom(o, null);
 
                // we cannot insert anything inside a textnode so...
                if (el.nodeType === 3) {
                    where = where === afterbegin ? beforebegin : where;
                    where = where === beforeend ? afterend : where;
                }
 
                if (bb_ae_PositionHash[where]) {
                    el.parentNode.insertBefore(newNode, where === beforebegin ? el : el.nextSibling); // eslint-disable-line max-len
                }
                else if (el.firstChild && where === afterbegin) {
                    el.insertBefore(newNode, el.firstChild);
                }
                else {
                    el.appendChild(newNode);
                }
            }
 
            return returnElement ? Ext.get(newNode) : newNode;
        },
 
        /**
         * Creates a new Ext.Template from the DOM object spec.
         * @param {Object} o The DOM object spec (and children)
         * @return {Ext.Template} The new template
         */
        createTemplate: function(o) {
            var html = this.markup(o);
 
            return new Ext.Template(html);
        },
 
        /**
         * @method createHtml
         * Alias for {@link #markup}.
         * @deprecated 5.0.0 Please use {@link #markup} instead.
         */
        createHtml: function(spec) {
            return this.markup(spec);
        }
    };
});