/**
 * @ignore
 * Add ul and ol command identifier for KISSY Editor.
 * @author yiminghe@gmail.com
 */
KISSY.add("editor/plugin/list-utils/cmd", function (S, Editor, ListUtils, undefined) {

    var insertUnorderedList = "insertUnorderedList",
        insertOrderedList = "insertOrderedList",
        listNodeNames = {"ol": insertOrderedList, "ul": insertUnorderedList},
        KER = Editor.RangeType,
        ElementPath = Editor.ElementPath,
        Walker = Editor.Walker,
        UA = S.UA,
        Node = S.Node,
        Dom = S.DOM,
        headerTagRegex = /^h[1-6]$/;

    function ListCommand(type) {
        this.type = type;
    }

    ListCommand.prototype = {

        constructor:ListCommand,

        changeListType: function (editor, groupObj, database, listsCreated, listStyleType) {
            // This case is easy...
            // 1. Convert the whole list into a one-dimensional array.
            // 2. Change the list type by modifying the array.
            // 3. Recreate the whole list by converting the array to a list.
            // 4. Replace the original list with the recreated list.
            var listArray = ListUtils.listToArray(groupObj.root, database,
                    undefined, undefined, undefined),
                selectedListItems = [];

            for (var i = 0; i < groupObj.contents.length; i++) {
                var itemNode = groupObj.contents[i];
                itemNode = itemNode.closest('li', undefined);
                if ((!itemNode || !itemNode[0]) ||
                    itemNode.data('list_item_processed'))
                    continue;
                selectedListItems.push(itemNode);
                itemNode._4e_setMarker(database, 'list_item_processed', true, undefined);
            }

            var fakeParent = new Node(groupObj.root[0].ownerDocument.createElement(this.type));
            fakeParent.css('list-style-type', listStyleType);
            for (i = 0; i < selectedListItems.length; i++) {
                var listIndex = selectedListItems[i].data('listarray_index');
                listArray[listIndex].parent = fakeParent;
            }
            var newList = ListUtils.arrayToList(listArray, database, null, "p");
            var child, length = newList.listNode.childNodes.length;
            for (i = 0; i < length &&
                ( child = new Node(newList.listNode.childNodes[i]) ); i++) {
                if (child.nodeName() == this.type)
                    listsCreated.push(child);
            }
            groupObj.root.before(newList.listNode);
            groupObj.root.remove();
        },

        createList: function (editor, groupObj, listsCreated, listStyleType) {
            var contents = groupObj.contents,
                doc = groupObj.root[0].ownerDocument,
                listContents = [];

            // It is possible to have the contents returned by DomRangeIterator to be the same as the root.
            // e.g. when we're running into table cells.
            // In such a case, enclose the childNodes of contents[0] into a <div>.
            if (contents.length == 1
                && contents[0][0] === groupObj.root[0]) {
                var divBlock = new Node(doc.createElement('div'));
                contents[0][0].nodeType != Dom.NodeType.TEXT_NODE &&
                contents[0]._4e_moveChildren(divBlock, undefined, undefined);
                contents[0][0].appendChild(divBlock[0]);
                contents[0] = divBlock;
            }

            // Calculate the common parent node of all content blocks.
            var commonParent = groupObj.contents[0].parent();

            for (var i = 0; i < contents.length; i++) {
                commonParent = commonParent._4e_commonAncestor(contents[i].parent(), undefined);
            }

            // We want to insert things that are in the same tree level only,
            // so calculate the contents again
            // by expanding the selected blocks to the same tree level.
            for (i = 0; i < contents.length; i++) {
                var contentNode = contents[i],
                    parentNode;
                while (( parentNode = contentNode.parent() )) {
                    if (parentNode[0] === commonParent[0]) {
                        listContents.push(contentNode);
                        break;
                    }
                    contentNode = parentNode;
                }
            }

            if (listContents.length < 1)
                return;

            // Insert the list to the Dom tree.
            var insertAnchor = new Node(listContents[ listContents.length - 1 ][0].nextSibling),
                listNode = new Node(doc.createElement(this.type));

            listNode.css('list-style-type', listStyleType);

            listsCreated.push(listNode);
            while (listContents.length) {
                var contentBlock = listContents.shift(),
                    listItem = new Node(doc.createElement('li'));

                // Preserve heading structure when converting to list item. (#5271)
                if (headerTagRegex.test(contentBlock.nodeName())) {
                    listItem[0].appendChild(contentBlock[0]);
                } else {
                    contentBlock._4e_copyAttributes(listItem, undefined, undefined);
                    contentBlock._4e_moveChildren(listItem, undefined, undefined);
                    contentBlock.remove();
                }
                listNode[0].appendChild(listItem[0]);

                // Append a bogus BR to force the LI to render at full height
                if (!UA['ie'])
                    listItem._4e_appendBogus(undefined);
            }
            if (insertAnchor[0]) {
                listNode.insertBefore(insertAnchor, undefined);
            } else {
                commonParent.append(listNode);
            }
        },

        removeList: function (editor, groupObj, database) {
            // This is very much like the change list type operation.
            // Except that we're changing the selected items' indent to -1 in the list array.
            var listArray = ListUtils.listToArray(groupObj.root, database,
                    undefined, undefined, undefined),
                selectedListItems = [];

            for (var i = 0; i < groupObj.contents.length; i++) {
                var itemNode = groupObj.contents[i];
                itemNode = itemNode.closest('li', undefined);
                if (!itemNode || itemNode.data('list_item_processed'))
                    continue;
                selectedListItems.push(itemNode);
                itemNode._4e_setMarker(database, 'list_item_processed', true, undefined);
            }

            var lastListIndex = null;

            for (i = 0; i < selectedListItems.length; i++) {
                var listIndex = selectedListItems[i].data('listarray_index');
                listArray[listIndex].indent = -1;
                lastListIndex = listIndex;
            }

            // After cutting parts of the list out with indent=-1, we still have to maintain the array list
            // model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the
            // list cannot be converted back to a real Dom list.
            for (i = lastListIndex + 1; i < listArray.length; i++) {
                //if (listArray[i].indent > listArray[i - 1].indent + 1) {
                //modified by yiminghe
                if (listArray[i].indent > Math.max(listArray[i - 1].indent, 0)) {
                    var indentOffset = listArray[i - 1].indent + 1 -
                        listArray[i].indent;
                    var oldIndent = listArray[i].indent;
                    while (listArray[i]
                        && listArray[i].indent >= oldIndent) {
                        listArray[i].indent += indentOffset;
                        i++;
                    }
                    i--;
                }
            }

            var newList = ListUtils.arrayToList(listArray, database, null, "p");

            // Compensate <br> before/after the list node if the surrounds are non-blocks.(#3836)
            var docFragment = newList.listNode, boundaryNode, siblingNode;

            function compensateBrs(isStart) {
                if (( boundaryNode = new Node(docFragment[ isStart ? 'firstChild' : 'lastChild' ]) )
                    && !( boundaryNode[0].nodeType == Dom.NodeType.ELEMENT_NODE &&
                    boundaryNode._4e_isBlockBoundary(undefined, undefined) )
                    && ( siblingNode = groupObj.root[ isStart ? 'prev' : 'next' ]
                    (Walker.whitespaces(true), 1) )
                    && !( boundaryNode[0].nodeType == Dom.NodeType.ELEMENT_NODE &&
                    siblingNode._4e_isBlockBoundary({ br: 1 }, undefined) )) {
                    boundaryNode[ isStart ? 'before' : 'after' ](editor.get("document")[0].createElement('br'));
                }
            }

            compensateBrs(true);
            compensateBrs(undefined);
            groupObj.root.before(docFragment);
            groupObj.root.remove();
        },

        exec: function (editor, listStyleType) {
            var selection = editor.getSelection(),
                ranges = selection && selection.getRanges();

            // There should be at least one selected range.
            if (!ranges || ranges.length < 1)
                return;


            var startElement = selection.getStartElement(),
                currentPath = new Editor.ElementPath(startElement);

            var state = queryActive(this.type, currentPath);

            var bookmarks = selection.createBookmarks(true);

            // Group the blocks up because there are many cases where multiple lists have to be created,
            // or multiple lists have to be cancelled.
            var listGroups = [],
                database = {};
            while (ranges.length > 0) {
                var range = ranges.shift();

                var boundaryNodes = range.getBoundaryNodes(),
                    startNode = boundaryNodes.startNode,
                    endNode = boundaryNodes.endNode;

                if (startNode[0].nodeType == Dom.NodeType.ELEMENT_NODE && startNode.nodeName() == 'td')
                    range.setStartAt(boundaryNodes.startNode, KER.POSITION_AFTER_START);

                if (endNode[0].nodeType == Dom.NodeType.ELEMENT_NODE && endNode.nodeName() == 'td')
                    range.setEndAt(boundaryNodes.endNode, KER.POSITION_BEFORE_END);

                var iterator = range.createIterator(),
                    block;

                iterator.forceBrBreak = false;

                while (( block = iterator.getNextParagraph() )) {

                    // Avoid duplicate blocks get processed across ranges.
                    if (block.data('list_block'))
                        continue;
                    else
                        block._4e_setMarker(database, 'list_block', 1, undefined);


                    var path = new ElementPath(block),
                        pathElements = path.elements,
                        pathElementsCount = pathElements.length,
                        listNode = null,
                        processedFlag = false,
                        blockLimit = path.blockLimit,
                        element;

                    // First, try to group by a list ancestor.
                    //2010-11-17 :
                    //注意从上往下,从body开始找到最早的list祖先,从那里开始重建!!!
                    for (var i = pathElementsCount - 1; i >= 0 &&
                        ( element = pathElements[ i ] ); i--) {
                        if (listNodeNames[ element.nodeName() ]
                            && blockLimit.contains(element))     // Don't leak outside block limit (#3940).
                        {
                            // If we've encountered a list inside a block limit
                            // The last group object of the block limit element should
                            // no longer be valid. Since paragraphs after the list
                            // should belong to a different group of paragraphs before
                            // the list. (Bug #1309)
                            blockLimit.removeData('list_group_object');

                            var groupObj = element.data('list_group_object');
                            if (groupObj)
                                groupObj.contents.push(block);
                            else {
                                groupObj = { root: element, contents: [ block ] };
                                listGroups.push(groupObj);
                                element._4e_setMarker(database, 'list_group_object', groupObj, undefined);
                            }
                            processedFlag = true;
                            break;
                        }
                    }

                    if (processedFlag) {
                        continue;
                    }

                    // No list ancestor? Group by block limit.
                    var root = blockLimit || path.block;
                    if (root.data('list_group_object')) {
                        root.data('list_group_object').contents.push(block);
                    } else {
                        groupObj = { root: root, contents: [ block ] };
                        root._4e_setMarker(database, 'list_group_object', groupObj, undefined);
                        listGroups.push(groupObj);
                    }
                }
            }

            // Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element.
            // We either have to build lists or remove lists, for removing a list does not makes sense when we are looking
            // at the group that's not rooted at lists. So we have three cases to handle.
            var listsCreated = [];
            while (listGroups.length > 0) {
                groupObj = listGroups.shift();
                if (!state) {
                    if (listNodeNames[ groupObj.root.nodeName() ]) {
                        this.changeListType(editor, groupObj, database, listsCreated, listStyleType);
                    } else {
                        //2010-11-17
                        //先将之前原来元素的 expando 去除,
                        //防止 ie li 复制原来标签属性带来的输出代码多余
                        Editor.Utils.clearAllMarkers(database);
                        this.createList(editor, groupObj, listsCreated, listStyleType);
                    }
                } else if (listNodeNames[ groupObj.root.nodeName() ]) {
                    if (groupObj.root.css('list-style-type') == listStyleType) {
                        this.removeList(editor, groupObj, database);
                    } else {
                        groupObj.root.css('list-style-type', listStyleType)
                    }
                }
            }

            var self = this;

            // For all new lists created, merge adjacent, same type lists.
            for (i = 0; i < listsCreated.length; i++) {
                listNode = listsCreated[i];

                // note by yiminghe,why not use merge sibling directly
                // listNode._4e_mergeSiblings();
                function mergeSibling(rtl, listNode) {
                    var sibling = listNode[ rtl ?
                        'prev' : 'next' ](Walker.whitespaces(true), 1);
                    if (sibling &&
                        sibling[0] &&
                        sibling.nodeName() == self.type &&
                        // consider list-style-type @ 2012-11-07
                        sibling.css('list-style-type') == listStyleType) {
                        sibling.remove();
                        // Move children order by merge direction.(#3820)
                        sibling._4e_moveChildren(listNode, rtl ? true : false, undefined);
                    }
                }

                mergeSibling(undefined, listNode);
                mergeSibling(true, listNode);
            }

            // Clean up, restore selection and update toolbar button states.
            Editor.Utils.clearAllMarkers(database);
            selection.selectBookmarks(bookmarks);
        }
    };

    function queryActive(type, elementPath) {
        var element,
            name,
            i,
            blockLimit = elementPath.blockLimit,
            elements = elementPath.elements;
        if (!blockLimit) {
            return false;
        }
        // Grouping should only happen under blockLimit.(#3940).
        if (elements) {
            for (i = 0; i < elements.length &&
                ( element = elements[ i ] ) &&
                element[0] !== blockLimit[0];
                 i++) {
                if (listNodeNames[name = element.nodeName()]) {
                    if (name == type) {
                        return element.css('list-style-type');
                    }
                }
            }
        }
        return false;
    }


    return {
        ListCommand: ListCommand,
        queryActive: queryActive
    };

}, {
    requires: ['editor', '../list-utils']
});