/*
 * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint regexp: true */

/**
 * DOMHelpers is a collection of functions used by the DOMAgent exports `eachNode(src, callback)`
 */
define(function DOMHelpersModule(require, exports, module) {
    "use strict";

    /** Test if the given character is a quote character
     * {char} source character
     * {char} escape (previous) character
     * {char} quote character
     */
    function _isQuote(c, escape, quote) {
        if (escape === "\\") {
            return false;
        }
        if (quote !== undefined) {
            return c === quote;
        }
        return c === "\"" || c === "'";
    }

    /** Remove quotes from the string and adjust escaped quotes
     * @param {string} source string
     */
    function _removeQuotes(src) {
        if (_isQuote(src[0]) && src[src.length - 1] === src[0]) {
            var q = src[0];
            src = src.substr(1, src.length - 2);
            src = src.replace("\\" + q, q);
        }
        return src;
    }

    /** Find the next match using several constraints
     * @param {string} source string
     * @param {string} or [{regex}, {length}] the match definition
     * @param {integer} ignore characters before this offset
     * @param {boolean} watch for quotes
     * @param [{string},{string}] watch for comments
     */
    function _find(src, match, skip, quotes, comments) {
        if (typeof match === "string") {
            match = [match, match.length];
        }
        if (skip === undefined) {
            skip = 0;
        }
        var i, activeQuote, isComment = false;
        for (i = skip; i < src.length; i++) {
            if (quotes && _isQuote(src[i], src[i - 1], activeQuote)) {
                // starting quote
                activeQuote = activeQuote ? undefined : src[i];
            } else if (!activeQuote) {
                if (comments && !isComment && src.substr(i, comments[0].length) === comments[0]) {
                    // opening comment
                    isComment = true;
                    i += comments[0].length - 1;
                } else if (isComment) {
                    // we are commented
                    if (src.substr(i, comments[1].length) === comments[1]) {
                        isComment = false;
                        i += comments[1].length - 1;
                    }
                } else if (src.substr(i, match[1]).search(match[0]) === 0) {
                    // match
                    return i;
                }
            }
        }
        return -1;
    }

    /** Callback iterator using `_find` */
    function _findEach(src, match, quotes, comments, callback) {
        var from = 0;
        var to;
        while (from < src.length) {
            to = _find(src, match, from, quotes, comments);
            if (to < 0) {
                to = src.length;
            }
            callback(src.substr(from, to - from));
            from = to + 1;
        }
    }

    /** Find the next tag
     * @param {string} source string
     * @param {integer} ignore characters before this offset
     */
    function _findTag(src, skip) {
        var from, to, inc;
        from = _find(src, [/<[a-z!\/]/i, 2], skip);
        if (from < 0) {
            return null;
        }
        if (src.substr(from, 4) === "<!--") {
            // html comments
            to = _find(src, "-->", from + 4);
            inc = 3;
        } else if (src.substr(from, 7).toLowerCase() === "<script") {
            // script tag
            to = _find(src.toLowerCase(), "</script>", from + 7);
            inc = 9;
        } else if (src.substr(from, 6).toLowerCase() === "<style") {
            // style tag
            to = _find(src.toLowerCase(), "</style>", from + 6);
            inc = 8;
        } else {
            to = _find(src, ">", from + 1, true);
            inc = 1;
        }
        if (to < 0) {
            return null;
        }
        return {from: from, length: to + inc - from};
    }

    /** Extract tag attributes from the given source of a single tag
     * @param {string} source content
     */
    function _extractAttributes(content) {

        // remove the node name and the closing bracket and optional slash
        content = content.replace(/^<\S+\s*/, "");
        content = content.replace(/\s*\/?>$/, "");
        if (content.length === 0) {
            return;
        }

        // go through the items and identify key value pairs split by =
        var index, key, value;
        var attributes = {};
        _findEach(content, [/\s/, 1], true, undefined, function each(item) {
            index = item.search("=");
            if (index < 0) {
                return;
            }

            // get the key
            key = item.substr(0, index).trim();
            if (key.length === 0) {
                return;
            }

            // get the value
            value = item.substr(index + 1).trim();
            value = _removeQuotes(value);
            attributes[key] = value;
        });

        return attributes;
    }

    /** Extract the node payload
     * @param {string} source content
     */
    function extractPayload(content) {
        var payload = {};

        if (content[0] !== "<") {
            // text
            payload.nodeType = 3;
            payload.nodeValue = content;
        } else if (content.substr(0, 4) === "<!--") {
            // comment
            payload.nodeType = 8;
            payload.nodeValue = content.substr(4, content.length - 7);
        } else if (content[1] === "!") {
            // doctype
            payload.nodeType = 10;
        } else {
            // regular element
            payload.nodeType = 1;
            payload.nodeName = /^<([^>\s]+)/.exec(content)[1].toUpperCase();
            payload.attributes = _extractAttributes(content);

            // closing node (/ at the beginning)
            if (payload.nodeName[0] === "/") {
                payload.nodeName = payload.nodeName.substr(1);
                payload.closing = true;
            }

            // closed node (/ at the end)
            if (content[content.length - 2] === "/") {
                payload.closed = true;
            }

            // Special handling for script/style tag since we've already collected
            // everything up to the end tag.
            if (payload.nodeName === "SCRIPT" || payload.nodeName === "STYLE") {
                payload.closed = true;
            }
        }
        return payload;
    }

    /** Split the source string into payloads representing individual nodes
     * @param {string} source
     * @param {function(payload)} callback
     */
    // split a string into individual node contents
    function eachNode(src, callback) {
        var index = 0;
        var text, range, length, payload;
        while (index < src.length) {

            // find the next tag
            range = _findTag(src, index);
            if (!range) {
                range = { from: src.length, length: 0 };
            }

            // add the text before the tag
            length = range.from - index;
            if (length > 0) {
                text = src.substr(index, length);
                if (/\S/.test(text)) {
                    payload = extractPayload(text);
                    payload.sourceOffset = index;
                    payload.sourceLength = length;
                    callback(payload);
                }
            }

            // add the tag
            if (range.length > 0) {
                payload = extractPayload(src.substr(range.from, range.length));
                payload.sourceOffset = range.from;
                payload.sourceLength = range.length;
                callback(payload);
            }

            // advance
            index = range.from + range.length;
        }
    }

    // Export public functions
    exports.extractPayload = extractPayload;
    exports.eachNode = eachNode;
});
