/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2010, Ajax.org B.V.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

define(function (require, exports, module) {
    "use strict";

    var oop = require("../../lib/oop");
    var lang = require("../../lib/lang");
    var Range = require("../../range").Range;
    var BaseFoldMode = require("./fold_mode").FoldMode;
    var TokenIterator = require("../../token_iterator").TokenIterator;

    var FoldMode = exports.FoldMode = function (voidElements, optionalEndTags) {
        BaseFoldMode.call(this);
        this.voidElements = voidElements || {};
        this.optionalEndTags = oop.mixin({}, this.voidElements);
        if (optionalEndTags)
            oop.mixin(this.optionalEndTags, optionalEndTags);

    };
    oop.inherits(FoldMode, BaseFoldMode);

    var Tag = function () {
        this.tagName = "";
        this.closing = false;
        this.selfClosing = false;
        this.start = {row: 0, column: 0};
        this.end = {row: 0, column: 0};
    };

    function is(token, type) {
        return token.type.lastIndexOf(type + ".xml") > -1;
    }

    (function () {

        this.getFoldWidget = function (session, foldStyle, row) {
            var tag = this._getFirstTagInLine(session, row);

            if (!tag)
                return "";

            if (tag.closing || (!tag.tagName && tag.selfClosing))
                return foldStyle == "markbeginend" ? "end" : "";

            if (!tag.tagName || tag.selfClosing || this.voidElements.hasOwnProperty(tag.tagName.toLowerCase()))
                return "";

            if (this._findEndTagInLine(session, row, tag.tagName, tag.end.column))
                return "";

            return "start";
        };

        /*
         * returns a first tag (or a fragment) in a line
         */
        this._getFirstTagInLine = function (session, row) {
            var tokens = session.getTokens(row);
            var tag = new Tag();

            for (var i = 0; i < tokens.length; i++) {
                var token = tokens[i];
                if (is(token, "tag-open")) {
                    tag.end.column = tag.start.column + token.value.length;
                    tag.closing = is(token, "end-tag-open");
                    token = tokens[++i];
                    if (!token)
                        return null;
                    tag.tagName = token.value;
                    tag.end.column += token.value.length;
                    for (i++; i < tokens.length; i++) {
                        token = tokens[i];
                        tag.end.column += token.value.length;
                        if (is(token, "tag-close")) {
                            tag.selfClosing = token.value == '/>';
                            break;
                        }
                    }
                    return tag;
                } else if (is(token, "tag-close")) {
                    tag.selfClosing = token.value == '/>';
                    return tag;
                }
                tag.start.column += token.value.length;
            }

            return null;
        };

        this._findEndTagInLine = function (session, row, tagName, startColumn) {
            var tokens = session.getTokens(row);
            var column = 0;
            for (var i = 0; i < tokens.length; i++) {
                var token = tokens[i];
                column += token.value.length;
                if (column < startColumn)
                    continue;
                if (is(token, "end-tag-open")) {
                    token = tokens[i + 1];
                    if (token && token.value == tagName)
                        return true;
                }
            }
            return false;
        };

        /*
         * reads a full tag and places the iterator after the tag
         */
        this._readTagForward = function (iterator) {
            var token = iterator.getCurrentToken();
            if (!token)
                return null;

            var tag = new Tag();
            do {
                if (is(token, "tag-open")) {
                    tag.closing = is(token, "end-tag-open");
                    tag.start.row = iterator.getCurrentTokenRow();
                    tag.start.column = iterator.getCurrentTokenColumn();
                } else if (is(token, "tag-name")) {
                    tag.tagName = token.value;
                } else if (is(token, "tag-close")) {
                    tag.selfClosing = token.value == "/>";
                    tag.end.row = iterator.getCurrentTokenRow();
                    tag.end.column = iterator.getCurrentTokenColumn() + token.value.length;
                    iterator.stepForward();
                    return tag;
                }
            } while (token = iterator.stepForward());

            return null;
        };

        this._readTagBackward = function (iterator) {
            var token = iterator.getCurrentToken();
            if (!token)
                return null;

            var tag = new Tag();
            do {
                if (is(token, "tag-open")) {
                    tag.closing = is(token, "end-tag-open");
                    tag.start.row = iterator.getCurrentTokenRow();
                    tag.start.column = iterator.getCurrentTokenColumn();
                    iterator.stepBackward();
                    return tag;
                } else if (is(token, "tag-name")) {
                    tag.tagName = token.value;
                } else if (is(token, "tag-close")) {
                    tag.selfClosing = token.value == "/>";
                    tag.end.row = iterator.getCurrentTokenRow();
                    tag.end.column = iterator.getCurrentTokenColumn() + token.value.length;
                }
            } while (token = iterator.stepBackward());

            return null;
        };

        this._pop = function (stack, tag) {
            while (stack.length) {

                var top = stack[stack.length - 1];
                if (!tag || top.tagName == tag.tagName) {
                    return stack.pop();
                }
                else if (this.optionalEndTags.hasOwnProperty(top.tagName)) {
                    stack.pop();
                    continue;
                } else {
                    return null;
                }
            }
        };

        this.getFoldWidgetRange = function (session, foldStyle, row) {
            var firstTag = this._getFirstTagInLine(session, row);

            if (!firstTag)
                return null;

            var isBackward = firstTag.closing || firstTag.selfClosing;
            var stack = [];
            var tag;

            if (!isBackward) {
                var iterator = new TokenIterator(session, row, firstTag.start.column);
                var start = {
                    row: row,
                    column: firstTag.start.column + firstTag.tagName.length + 2
                };
                if (firstTag.start.row == firstTag.end.row)
                    start.column = firstTag.end.column;
                while (tag = this._readTagForward(iterator)) {
                    if (tag.selfClosing) {
                        if (!stack.length) {
                            tag.start.column += tag.tagName.length + 2;
                            tag.end.column -= 2;
                            return Range.fromPoints(tag.start, tag.end);
                        } else
                            continue;
                    }

                    if (tag.closing) {
                        this._pop(stack, tag);
                        if (stack.length == 0)
                            return Range.fromPoints(start, tag.start);
                    }
                    else {
                        stack.push(tag);
                    }
                }
            }
            else {
                var iterator = new TokenIterator(session, row, firstTag.end.column);
                var end = {
                    row: row,
                    column: firstTag.start.column
                };

                while (tag = this._readTagBackward(iterator)) {
                    if (tag.selfClosing) {
                        if (!stack.length) {
                            tag.start.column += tag.tagName.length + 2;
                            tag.end.column -= 2;
                            return Range.fromPoints(tag.start, tag.end);
                        } else
                            continue;
                    }

                    if (!tag.closing) {
                        this._pop(stack, tag);
                        if (stack.length == 0) {
                            tag.start.column += tag.tagName.length + 2;
                            if (tag.start.row == tag.end.row && tag.start.column < tag.end.column)
                                tag.start.column = tag.end.column;
                            return Range.fromPoints(tag.start, end);
                        }
                    }
                    else {
                        stack.push(tag);
                    }
                }
            }

        };

    }).call(FoldMode.prototype);

});
