/*
 * Copyright (C) 2005 - 2011 Jaspersoft Corporation. All rights reserved.
 * http://www.jaspersoft.com.
 *
 * Unless you have purchased  a commercial license agreement from Jaspersoft,
 * the following license terms  apply:
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License  as
 * published by the Free Software Foundation, either version 3 of  the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero  General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public  License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

var baseList = {
    isResponsive: function(item) { return $(item.up(0)).hasClassName(layoutModule.RESPONSIVE_CLASS); },

    isCollapsible: function(item) { return $(item.up(0)).hasClassName(layoutModule.COLLAPSIBLE_CLASS); },

    selectItem: function(item) {
        if (this.isItemDisabled(item)) { return; }
        $(item).addClassName(layoutModule.SELECTED_CLASS);
    },

    deselectItem: function(item) {
        $(item).removeClassName(layoutModule.SELECTED_CLASS);
    },

    isItemSelected: function(item) {
        return $(item).hasClassName(layoutModule.SELECTED_CLASS);
    },

    disableItem: function(item) { buttonManager.disable(item); },

    enableItem: function(item) { buttonManager.enable(item); },

    isItemDisabled: function(item) { buttonManager.isDisabled(item); },

    openItem: function(item) {
        $(item).removeClassName(layoutModule.CLOSED_CLASS).addClassName(layoutModule.OPEN_CLASS).isOpen = true;
    },

    isItemOpen: function(item) {
        return ($(item).hasClassName(layoutModule.OPEN_CLASS) || ($(item).isOpen && !$(item).hasClassName(layoutModule.CLOSED_CLASS)));
    },

    closeItem: function(item) {
        $(item).removeClassName(layoutModule.OPEN_CLASS).addClassName(layoutModule.CLOSED_CLASS).isOpen = false;
    }

};

///////////////////////////////////////////////////////
// Global module for all list related code
///////////////////////////////////////////////////////
var dynamicList = {

    /**
     * Map of all created lists on current page
     */
    lists: {},

    /**
     * Id of last active list
     */
    activeListId: null,

    _templateHash: {},

    messages: {
        'listNItemsSelected': "#{count} items selected"
    }
};

///////////////////////////////////////////////////////
// ListItem
///////////////////////////////////////////////////////
/**
 * Respond on events: dblclick, click, mouseover, mousedown, mouseup, key:down and key:up
 * Options:
 *      respondOnItemEvents - when false all event will be ignored
 *      excludeFromEventHandling - [selectorPattern1, selectorPattern2]
 *      excludeFromSelectionTriggers - []
 *
 * @param options
 */
dynamicList.ListItem = function(options) {
    this._itemId = undefined;
    this._list = undefined;

    this.first = false;
    this.last = false;
    if (options) {
        this._value = (options.value) ? options.value : {};
        this._label = (options.label) ? options.label : "";
        this._subList = options.subList;
        this._cssClassName = (options.cssClassName) ? options.cssClassName : undefined;
        this._templateDomId = (options.templateDomId) ? options.templateDomId : undefined;
        this._respondOnItemEvents = !Object.isUndefined(options.respondOnItemEvents) ? options.respondOnItemEvents : true;
        this._excludeFromEventHandling = options.excludeFromEventHandling ? options.excludeFromEventHandling : undefined;
        this._excludeFromSelectionTriggers = options.excludeFromSelectionTriggers ? options.excludeFromSelectionTriggers : undefined;
    }
};
/**
 *
 */
dynamicList.ListItem.addVar('DEFAULT_TEMPLATE_DOM_ID', "dynamicListItemTemplate");
dynamicList.ListItem.addVar('DEFAULT_ITEM_ID_PREFIX', "item");
dynamicList.ListItem.addVar('DEFAULT_SUB_LIST_ID_SUFFIX', "SubList");

///////////////////////////////////////////////////////
// Public ListItem methods
///////////////////////////////////////////////////////

/**
 * Gets ID of the item, which is being generated by the list.
 * @return {Number} ID of the item
 */
dynamicList.ListItem.addMethod('getId', function() { return this._itemId; });

/**
 * Sets the list and generates new id in that list.
 * This method is used by the list to set reference on itself.
 *
 * @param list {{@see dynamicList.List}}
 */
dynamicList.ListItem.addMethod('setList', function(list) {
    this._list = list;

    if (this.getList()) {
        this._itemId = this.getList().getNextItemId();
    }
});

/**
 * Gets the list where this item is listed.
 *
 * @return {{@see dynamicList.List}}
 */
dynamicList.ListItem.addMethod('getList', function() { return this._list; });

/**
 * Sets value of the item.
 *
 * @param {String}
 */
dynamicList.ListItem.addMethod('setValue', function(value) { return this._value = value; });

/**
 * Gets value of the item.
 *
 * @return {String}
 */
dynamicList.ListItem.addMethod('getValue', function() { return this._value; });

/**
 * Sets label defined by user.
 *
 * @param {Object}
 */
dynamicList.ListItem.addMethod('setLabel', function(label) { return this._label = label; });


/**
 * Gets label of the item defined by user.
 *
 * @return {Object}
 */
dynamicList.ListItem.addMethod('getLabel', function() { return this._label; });

/**
 * Setter and getter for the style of the item.
 * Changes of the style will be applied after call of method {@see dynamicList.ListItem#refresh}
 */
dynamicList.ListItem.
        addMethod('setCssClassName', function(cssClassName) { this._cssClassName = cssClassName; }).
        addMethod('getCssClassName', function() { return this._cssClassName; });

/**
 * Sets the ID of DOM element which will be used for rendering of mark up.
 * Changes of the style will be applied after call of method {@see dynamicList.ListItem#refresh}
 *
 * @param templateDomId {String}
 */
dynamicList.ListItem.addMethod('setTemplateDomId', function(templateDomId) { this._templateDomId = templateDomId; });

/**
 * Gets the ID of the current template of the item.
 *                                  Attr
 * @return {String}
 */
dynamicList.ListItem.addMethod('getTemplateDomId', function() { return this._templateDomId; });

/**
 * Render the item specified container.
 * Use this method when you need re-render item.
 *
 * @param container {DOMElement}
 */
dynamicList.ListItem.addMethod('show', function(container) {
    if (!container) { return; }

    this._element = this.processTemplate(this._getTemplate());

    this._getElement().setAttribute('id', this._generateId());
    this._getElement().setAttribute('tabindex', -1);
    this.first && this.getList().tabindex && this._getElement().writeAttribute("tabindex", this.getList().tabindex);

    this._getElement().listItem = this;

    this.refreshStyle();

    var siblings = container.childElements();
    var itemIndex = this.index();
    var afterIndex = itemIndex - 1;

    (afterIndex > -1 && afterIndex < siblings.length)
            ? this._getElement().insert({after: siblings[afterIndex]})
            : $(container).insert(this._getElement());
});

/**
 *
 */
dynamicList.ListItem.addMethod('refresh', function() {
    if(!isNotNullORUndefined(this._getElement())) {
        return;
    }

    if(this.getList()) {

        this._element = this.processTemplate(this._getElement());
        this.refreshStyle();

    } else {
        this._getElement().remove();
        this._element = null;
    }
});
/**
 *
 */
dynamicList.ListItem.addMethod('refreshStyle', function() {
    var element = this._getElement();

    if(element.templateClassName) { element.className = element.templateClassName; }

    if (this.first) { element.addClassName(layoutModule.FIRST_CLASS); }
    if (this.last) { element.addClassName(layoutModule.LAST_CLASS); }
    if (this.isSelected()) { element.addClassName(layoutModule.SELECTED_CLASS); }
    if (this.isDisabled()) { element.addClassName(layoutModule.DISABLED_CLASS); }

    if (this.getCssClassName()) { element.addClassName(this.getCssClassName()); }
});
/**
 *
 */
dynamicList.ListItem.addMethod('isRendered', function() {
    return isNotNullORUndefined(this._getElement());
});
/**
 *
 */
dynamicList.ListItem.
        addMethod('disable', function() { baseList.disableItem(this._getElement()); }).
        addMethod('enable', function() { baseList.enableItem(this._getElement()); }).
        addMethod('isDisabled', function() { return baseList.isItemDisabled(this._getElement()); });

/**
 * Prepare mark up template of the item before render or process existing mark up when refresh the item.
 * If template was changed method should be overridden.
 *
 * @param {DOMElement} - current item element
 * @return {DOMElement} - element ready for use
 */
dynamicList.ListItem.addMethod('processTemplate', function(element) {
    var wrapper = element.childElements()[0];

    wrapper.cleanWhitespace();

    var elementsCount = wrapper.childElements().length;
    if (elementsCount == wrapper.childNodes.length) {
        wrapper.insert(this.getLabel().escapeHTML());
    } else {
        wrapper.childNodes[elementsCount].data = this.getLabel();
    }
//	$(wrapper.parentNode).writeAttribute("tabIndex",-1);

    return element;
});
/**
 * focus on this node's element
 */
dynamicList.ListItem.addMethod('focus', function() {
    this._getElement().focus(); 
});
/**
 * Removes it self from the list.
 */
dynamicList.ListItem.addMethod('remove', function() {
    this.getList().removeItems([this]);
});
/**
 *
 */
dynamicList.ListItem.addMethod('isSelected', function() {
    return this.getList().isItemSelected(this);
});
/**
 *
 */
dynamicList.ListItem.addMethod('select', function() {
    this.getList().selectItem(this, true);
});
/**
 *
 */
dynamicList.ListItem.addMethod('deselect', function() {
    this.getList().deselectItem(this);
});

dynamicList.ListItem.addMethod('index', function() {
    this.getList().getItems().indexOf(this);
});

///////////////////////////////////////////////////////
// Private ListItem methods
///////////////////////////////////////////////////////

/**
 * Gets or looking for DOM element of this item.
 *
 * @return {DOMElement}
 */
dynamicList.ListItem.addMethod('_getElement', function() {
    if (!this._element) {
        var e = $(this._generateId());
        this._element = (Object.isElement(e) ? e : undefined);
    }

    return this._element;
});

dynamicList.ListItem.addMethod('_getTemplate', function() {
    var id = this._templateDomId;

    if (!dynamicList._templateHash[id]) {
        dynamicList._templateHash[id] = id;
    }

    var clone = $(dynamicList._templateHash[id]).cloneNode(true);
    clone.templateId = id;
    clone.templateClassName = clone.className;

    return clone;
});

dynamicList.ListItem.addMethod('_generateId', function() {
    return this.getList().getId() + "_" + this.DEFAULT_ITEM_ID_PREFIX + this.getId();
});

dynamicList.ListItem.addMethod('_isElementInExcluded', function(event, item) {
    var element = event.element();

    return this._excludeFromEventHandling && matchAny(element, this._excludeFromEventHandling) != null;
});

dynamicList.ListItem.addMethod('_isExcludedFromSelectionTriggers', function(event) {
    var element = event.element();

    return this._excludeFromSelectionTriggers && matchAny(element, this._excludeFromSelectionTriggers) != null;
});

///////////////////////////////////////////////////////
// Composite List Item
///////////////////////////////////////////////////////

dynamicList.CompositeItem = function (options) {
    dynamicList.ListItem.call(this, options);

    this.isComposite = true;

    this._items = options.items;
    this._openUp = options.openUp;
    this._subList = null;
    this._subListOptions = (options.listOptions) ? options.listOptions : {};
    this._listTagName = 'ul';

    this.OPEN_HANDLER_PATTERN = (options.openHandlerPattern) ? options.openHandlerPattern : this.OPEN_HANDLER_PATTERN;
    this.CLOSE_HANDLER_PATTERN = (options.closeHandlerPattern) ? options.closeHandlerPattern : this.CLOSE_HANDLER_PATTERN;
};

dynamicList.CompositeItem.prototype = deepClone(dynamicList.ListItem.prototype);

dynamicList.CompositeItem.addVar('OPEN_HANDLER_PATTERN', "[openHandler=openHandler]");
dynamicList.CompositeItem.addVar('CLOSE_HANDLER_PATTERN', "[closeHandler=closeHandler]");

dynamicList.CompositeItem.addMethod('getItems', function() { return this._items; });
dynamicList.CompositeItem.addMethod('setItems', function(items) { this._items = items; });
dynamicList.CompositeItem.addMethod('addItem', function(item) { this._items.push(item); });
dynamicList.CompositeItem.addMethod('removeItems', function(items) {
    this._items = this._items.reject(function(item) {
        return items.include(item);
    });

    this._subList.removeItems(items);
});

dynamicList.CompositeItem.addMethod('show', function(container) {
    this._listTagName = container.tagName;

    dynamicList.ListItem.prototype.show.call(this, container);
    baseList.closeItem(this._getElement());

    if(!this._items) { return; }
    this._showSubList();
});

dynamicList.CompositeItem.addMethod('_showSubList', function() {

    var id = this._getSubListId();
    var subListElement = new Element(this._listTagName, { id: id });

    this._getElement().insert(this._openUp ? {top: subListElement} : {bottom: subListElement});

    var opts = this._subListOptions;
    this._subList = new dynamicList.List(id, {
        responsive: (opts.responsive) ? opts.responsive : this.getList()._responsive,
        collapsible: (opts.collapsible) ? opts.collapsible : this.getList()._collapsible,
        multiSelect: (opts.multiSelect) ? opts.multiSelect : this.getList()._multiSelect,
        cssClassName: (opts.cssClassName) ? opts.cssClassName : this.getList()._cssClassName,
        listTemplateDomId: (opts.listTemplateDomId) ? opts.listTemplateDomId : this.getList()._listTemplateDomId,
        itemTemplateDomId: (opts.itemTemplateDomId) ? opts.itemTemplateDomId : this.getList()._itemTemplateDomId,
        itemCssClassName: (opts.itemCssClassName) ? opts.itemCssClassName : this.getList()._itemCssClassName,
        comparator: (opts.comparator) ? opts.comparator : this.getList()._comparator,
        items: this._items
    });

    this._subList._initEvents = function() {};
    this._subList.show();
    this._subList._parentList = this.getList();
    this._subList.getItems().each(function(item) {
        item.parentItem = this;
    }.bind(this));
});

dynamicList.CompositeItem.addMethod('refresh', function() {
    dynamicList.ListItem.prototype.refresh.call(this);

    if(!this._items) { return; }

    if (this._subList) {
        this._subList.refresh();
    } else {
        this._showSubList();
    }
});

dynamicList.CompositeItem.addMethod('getFirstChild', function() {
    return this._subList.getItems()[0];
});

dynamicList.CompositeItem.addMethod('refreshStyle', function() {
    dynamicList.ListItem.prototype.refreshStyle.call(this);

    if (baseList.isItemOpen(this._getElement())) {
        baseList.openItem(this._getElement());
    } else {
        baseList.closeItem(this._getElement());
    }

    if(!this._subList) { return; }
    this._subList.refreshStyle();
});

dynamicList.CompositeItem.
        addMethod('_isOpenHandler', function(element) { return element.match(this.OPEN_HANDLER_PATTERN); }).
        addMethod('_isCloseHandler', function(element) {return element.match(this.CLOSE_HANDLER_PATTERN); });

dynamicList.CompositeItem.addMethod('_getSubListId', function() {
    return this._generateId() + "_" + this.DEFAULT_SUB_LIST_ID_SUFFIX;
});


///////////////////////////////////////////////////////
// List Component
///////////////////////////////////////////////////////

/**
 * Dynamically creates items on specified UL element. Supports sorting and ...
 *
 * @param id {String} - ID of the UL element, this id will be used as ID of the list.
 * @param options {JSON Object}
 * <ul>
 * <li>items {Array} - array of {@link dynamicList.ListItem}, which need be shown when object is created</li>
 * </ul>
 */
dynamicList.List = function(id, options) {
    this._id = id;
    this._items = [];
    this._selectedItems = [];
    this._lastSelectedItem = null;

    this._nextId = 1; // private static var
    this.draggables = [];
    this._parentList = null;

    if (options) {
        this._multiSelect = (options.multiSelect) ? options.multiSelect : false;
        this._cssClassName = (options.cssClassName) ? options.cssClassName : "";
        this._listTemplateDomId = options.listTemplateDomId;
        this._itemTemplateDomId = options.itemTemplateDomId;
        this._itemCssClassName = options.itemCssClassName;
        this._comparator = options.comparator;
        this._excludeFromEventHandling = options.excludeFromEventHandling;
        this._excludeFromSelectionTriggers = options.excludeFromSelectionTriggers;
        this.dragPattern = options.dragPattern;
        this.selectOnMousedown = options.selectOnMousedown;
        this.scroll = options.scroll;
        this.setItems(options.items);
    }

    this._createFromTemplate();
    this._registerCustomScroll();
    dynamicList.activeListId = this.getId();
    this._msgNItemsSelected = new Template(dynamicList.messages['listNItemsSelected']);
    dynamicList.lists[this._id] = this;
};

dynamicList.List.addVar('Event', {
    ITEM_SELECTED: "item:selected",
    ITEM_UNSELECTED: "item:unselected",

    ITEM_MOUSEUP: "item:mouseup",
    ITEM_MOUSEDOWN: "item:mousedown",
    ITEM_CLICK: "item:click",
    ITEM_DBLCLICK: "item:dblclick",
    ITEM_OPEN: "item:open",
    ITEM_CLOSED: "item:closed",
    ITEM_CONTEXTMENU: "item:contextmenu",
    ITEM_BEFORE_SELECT_OR_UNSELECT: "item:beforeSelectOrUnselect"
});

dynamicList.List.addVar('DND_WRAPPER_TEMPLATE', "column_two");
dynamicList.List.addVar('DND_ITEM_TEMPLATE', "column_two:resourceName");

///////////////////////////////////////////////////////
// List public methods
///////////////////////////////////////////////////////

dynamicList.List.addMethod('getNextItemId', function() {
    return this._nextId ++;
});

/**
 * Gets ID of the list which can be used to find this list in map {@see dynamicList.lists}
 *
 * @return {String} - ID for the list and DOM element
 */
dynamicList.List.addMethod('getId', function() { return this._id; });

/**
 * @return {Array}
 */
dynamicList.List.addMethod('getItems', function() { return this._items; });

/**
 * Resets list items with new items and sorts items, if comparator is defined.
 * @param items {Array} - new array of {@link dynamicList.ListItem}
 */
dynamicList.List.addMethod('setItems', function(items) {
    if (!items) { return; }

    this._items = [];
    this.resetSelected();
    this.addItems(items);
});

/**
 * Adds items to array items of list and sorts it, if comparator is defined.
 * @param items {Array} - array of {@link dynamicList.ListItem}
 */
dynamicList.List.addMethod('addItems', function(items) {
    if (!items) { return; }

    items.compact().each(function(item) {
        this._prepareListItem(item);
        this._items.push(item);
    }.bind(this));

    if (this._comparator) {
        this._items = this._items.sort(this._comparator);
    }
});

/**
 * Inserts items to appropriate position in array items of list.
 *
 * WARNING: Using this function with sortable lists may cause unexpected results
 *
 * @param pos {int} - position of element after which new items will be inserted.
 * @param items {Array} - array of {@link dynamicList.ListItem}
 */
dynamicList.List.addMethod('insertItems', function(pos, items) {
    if (!items) { return; }
    items = items.compact();
    items.each(function(item) {
        this._prepareListItem(item);
    }.bind(this));
    this._items.splice.apply(this._items, [pos, 0].concat(items));

    if (this._comparator) {
        this._items = this._items.sort(this._comparator);
    }
});

/**
 * Prepare List Item for adding to list. Set List reference, template DOM ID and css class name.
 */
dynamicList.List.addMethod('_prepareListItem', function(item) {
    if (!item) {
        return;
    }
    item.setList(this);

    if (this._itemTemplateDomId && !item.getTemplateDomId()) {
        // If list has specified template for all items and there is no other template
        item.setTemplateDomId(this._itemTemplateDomId);
    }
    if (this._itemCssClassName && !item.getCssClassName()) {
        // If list has specified CSS class for all items and the item don't has CSS class
        item.setCssClassName(this._itemCssClassName);
    }
    if (this._excludeFromEventHandling && !item._excludeFromEventHandling) {
        item._excludeFromEventHandling = this._excludeFromEventHandling;
    }
    if (this._excludeFromSelectionTriggers && !item._excludeFromSelectionTriggers) {
        item._excludeFromSelectionTriggers = this._excludeFromSelectionTriggers;
    }
});

/**
 * Removes specified items from list
 * @param items {Array} - array of {@link dynamicList.ListItem}
 */
dynamicList.List.addMethod('removeItems', function(items) {
    if (!items || !isArray(items)) { return; }

    this._items = this._items.reject(function(item) {
        return items.include(item);
    });

    items.each(function(item) {
        this.deselectItem(item);
    }.bind(this));

    items.each(function(item) {
        item.setList(null);
        item.refresh();
    });
});

/**
 * @param comparator {Function} -
 */
dynamicList.List.addMethod('sort', function(comparator) {
    comparator && (this._comparator = comparator);

    if (this._comparator) {
        this.getItems().sort(this._comparator);
    }
});

/**
 * @return {Array}
 */
dynamicList.List.addMethod('getSelectedItems', function() { return this._selectedItems; });

/**
 * @param item {dynamicList.ListItem} -
 */
dynamicList.List.addMethod('isItemSelected', function(item) {
    return this.getSelectedItems().include(item);
});

/**
 * @param item {dynamicList.ListItem} -
 */
dynamicList.List.addMethod('selectItem', function(item, isCtrlHeld, isShiftHeld, isContextMenu) {
    var event = this.fire(this.Event.ITEM_BEFORE_SELECT_OR_UNSELECT, {item: item});
    if (event.stopSelectOrUnselect) {
        return;
    }

    // Fix for multiple DnD.
    // If couple items selected and selectOnMousedown enabled
    // we need deselect items on mouse up to be able Drag them.
    if (this._multiSelect && this._selectedItems.length > 1 && this.isItemSelected(item)
            && !(isCtrlHeld || isShiftHeld || isContextMenu)) {
        return;
    }

    var isContextMenuOnSelected = this.isItemSelected(item) && isContextMenu;
    var reset = !(this._multiSelect && isCtrlHeld) && !isContextMenuOnSelected;
    var deselect = this.isItemSelected(item) && isCtrlHeld && !isContextMenuOnSelected;
    var selectRange = !deselect && isNotNullORUndefined(this._lastSelectedItem) && isShiftHeld;
    var select = !deselect && !selectRange;

    if (reset) {
        this.resetSelected();
    }

    if (deselect && !reset) {
        this._removeItemFromSelected(item);
    }

    if (selectRange) {

        var start = this._items.indexOf(this._lastSelectedItem);
        var end = this._items.indexOf(item);
        var min = Math.min(start, end), max = Math.max(start, end);

        if (min > -1) {
            for (var i = min; i <= max; i++) {
                this._addItemToSelected(this._items[i], false);
            }
        } else {
            this._addItemToSelected(this._items[max], false);
        }
    }

    if (select) {
        this._addItemToSelected(item, !isShiftHeld);
    }
});

/**
 * @param item {dynamicList.ListItem} -
 */
dynamicList.List.addMethod('deselectItem', function(item) {
    this._removeItemFromSelected(item);
});

dynamicList.List.addMethod('deselectOthers', function(item, isCtrlHeld, isShiftHeld, isContextMenu) {
    var event = this.fire(this.Event.ITEM_BEFORE_SELECT_OR_UNSELECT, {item: item});
    if (event.stopSelectOrUnselect) {
        return;
    }

    // Fix for multiple DnD.
    // If couple items selected and selectOnMousedown enabled
    // we need deselect items on mouse up to be able Drag them.
    if (this._multiSelect && this._selectedItems.length > 1 && this.isItemSelected(item)
            && !(isCtrlHeld || isShiftHeld || isContextMenu)) {

        var items = this._selectedItems.findAll(function(i) { return i != item});

        items.each(function(i) {
            this._removeItemFromSelected(i);
        }.bind(this));
    }
});

/**
 *
 */
dynamicList.List.addMethod('resetSelected', function(skipParent) {
    var items = this._selectedItems;
    this._selectedItems = [];

    items.each(function(item) {
        if (item.getList() !== this) {
            item.getList().resetSelected(true);
        }

        item.refreshStyle();
        this.fire(this.Event.ITEM_UNSELECTED, {item: item});
    }.bind(this));

    if (this._parentList && !skipParent) {
        this._parentList.resetSelected();
    }
});

/**
 * @param item - reference item 
 */
dynamicList.List.addMethod('getNextItem', function(item) {
	var items = this.getItems();
	var currentIndex = items.indexOf(item);
	return ~currentIndex ? this.getItems()[currentIndex + 1] : null;
});

/**
 * @param item - reference item 
 */
dynamicList.List.addMethod('getPreviousItem', function(item) {
	var items = this.getItems();
	var currentIndex = items.indexOf(item);
	return ~currentIndex ? this.getItems()[currentIndex - 1] : null;
});


/**
 * @param event
 */
dynamicList.List.addMethod('selectNext', function(event) {
    var item = this.getSelectedItems()[0];
    var nextItem = item.getList().getNextItem(item);
    if (nextItem) {
        item.getList().deselectItem(item);
        item.getList().selectItem(nextItem);
//        item.getList().fire(this.Event.ITEM_MOUSEDOWN, {
//            targetEvent: event.memo.targetEvent,
//            item: nextItem
//        });
//		nextItem._getElement().onfocus = 
//			function() {
//				console.log('focused')
//			}
        nextItem._getElement().focus();
    }
});

/**
 * @param event 
 */
dynamicList.List.addMethod('selectPrevious', function(event) {
    var item = this.getSelectedItems()[0];
    var previousItem = item.getList().getPreviousItem(item);
    if (previousItem) {
        item.getList().deselectItem(item);
        item.getList().selectItem(previousItem);
        item.getList().fire(this.Event.ITEM_MOUSEDOWN, {
            targetEvent: event.memo.targetEvent,
            item: previousItem
        });
        previousItem._getElement().focus();
    }
});

/**
 * @param event
 */
dynamicList.List.addMethod('selectOutwards', function(event) {
    var item = this.getSelectedItems()[0];

    var element = item._getElement();
    var outItem = !baseList.isItemOpen(element) && item.parentItem;
    if (outItem) {
        item.deselect();
        outItem.select();
        outItem._getElement().focus();
    } else {
        baseList.closeItem(element);
        this.fire(this.Event.ITEM_CLOSED, { targetEvent: event, item: item });
    }
});

/**
 * @param event
 */
dynamicList.List.addMethod('selectInwards', function(event) {
    var item = this.getSelectedItems()[0];

    if(item.isComposite) {
        var element = item._getElement();
        var inItem = baseList.isItemOpen(element) && item.getFirstChild();
        if (inItem){
            item.deselect();
            inItem.select();
            element.focus();
        } else {
            baseList.openItem(element);
            this.fire(this.Event.ITEM_OPEN, { targetEvent: event, item: item });
        }
    }
});

/**
 * Render list items. Use this method when you need to re-render list.
 */
dynamicList.List.addMethod('show', function() {
    dynamicList.activeListId = this.getId();

    this._getElement().update();

    this.getItems().each(function(item, index) {
        item.first = index === 0;
        item.last = index === (this.getItems().length - 1);

        item.show(this._getElement());
    }.bind(this));

    this.draggables = [];
    this.scroll && this.scroll.refresh();
    this._initEvents();
});

/**
 * Updates UI from value of the item and remove unused DOM elements (li elements)
 */
dynamicList.List.addMethod('refresh', function() {
    this.refreshStyle();

    var elements = this._getElement().childElements();
    var itemElements = [];

    this.getItems().each(function(item, index) {
        item.first = index === 0;
        item.last = index === (this.getItems().length - 1);

        if (item.isRendered() ) {

            if (item.index() != elements.indexOf(item._getElement())) {
                item._getElement().remove();
                item.show(this._getElement());
            } else {
                item.refresh();
            }
        } else {
            item.show(this._getElement());
        }
        itemElements.push(item._getElement());
    }.bind(this));

    elements.each(function(e){
        if (!itemElements.include(e) && e.parentNode) {
            e.remove();
        }
    });
    //this.scroll && this.scroll.refresh();
});
/**
 * Refreshing classname of the list
 */
dynamicList.List.addMethod('refreshStyle', function(clean) {
    var element = this._getElement();

    if(element.templateClassName) { element.className = element.templateClassName; }
    if(this._cssClassName) { element.addClassName(this._cssClassName); }
});
/**
 * Generates custom event of the list.
 *
 * @param eventName
 */
dynamicList.List.addMethod('fire', function(eventName, memo) {
    return this._getElement().fire(eventName, memo);
});
/**
 * @param eventName
 */
dynamicList.List.addMethod('observe', function(eventName, handler) {
    this._getElement().observe(eventName, handler);
});
/**
 * @param eventName
 */
dynamicList.List.addMethod('stopObserving', function(eventName, memo) {
    this._getElement().stopObserving(eventName, handler);
});

///////////////////////////////////////////////////////
// List private methods
///////////////////////////////////////////////////////

/**
 * Gets list container
 * @return {DOMElement}
 */
dynamicList.List.addMethod('_getElement', function() {
    if (!this._element) {
        this._element = $(this.getId());
    }

    return this._element;
});

/**
 * Gets the item from the event in list
 *
 * @paran {Event}
 */
dynamicList.List.addMethod('getItemByEvent', function(event) {
    if (event) {

        var element = Event.element(event); //event.originalTarget || event.srcElement;

        while(element && element.readAttribute && element.readAttribute('id') !== this.getId()) {
            var item = element.listItem;
//            if (item && item.getList() != null && item.getList().getId() == this.getId()) {
            if (item && item.getList() != null) {
                return item;
            } else {
                element = $(element.parentNode);
            }
        }
    }

    return null;
});

dynamicList.List.addMethod('_createFromTemplate', function() {
    var tabindex = this._getElement().readAttribute("tabindex");
    this.tabindex = parseInt(tabindex && tabindex.length > 0 ? tabindex : -1);

    this._getElement().insert({after: this._getTemplateElement(this._getElement())});

    this._getElement().remove();
    this._element = null;

    this._getElement().update();
    this.tabindex && this.tabindex.length > 0 && this._getElement().writeAttribute('tabindex', this.tabindex);

    disableSelectionWithoutCursorStyle(this._getElement());
});

dynamicList.List.addMethod('_getTemplateElement', function(currentElement) {
    var id = this._listTemplateDomId;

    if (!dynamicList._templateHash[id]) {
        dynamicList._templateHash[id] = id;
    }
    
    var clone = $(dynamicList._templateHash[id]).cloneNode(true);
    clone.writeAttribute("id", this.getId());

    //clone.down().writeAttribute("tabIndex", -1);

    clone.templateId = id;
    clone.templateClassName = clone.className;

    cloneCustomAttributes(currentElement, clone);

    return clone;

});

dynamicList.List.addMethod('_addItemToSelected', function(item, remember) {
    if (item && !this.isItemSelected(item)) {
        this._selectedItems.push(item);
        if (remember) {
            this._lastSelectedItem = item;
        }

//        item.focus();
        item.refreshStyle();

        if (this._parentList) {
            this._parentList._addItemToSelected(item, remember);
        } else {
            this.fire(this.Event.ITEM_SELECTED, {item: item});
        }
    }
});

dynamicList.List.addMethod('_removeItemFromSelected', function(item) {
    if (item && this.isItemSelected(item)) {
        this._selectedItems = this._selectedItems.without(item);
        item.refreshStyle();

        if (this._parentList) {
            this._parentList._removeItemFromSelected(item);
        } else {
            this.fire(this.Event.ITEM_UNSELECTED, {item: item});
        }
    }
});

dynamicList.List.addMethod('_buildDnDOverlay', function(element) {
//    var wrapper = $(this.DND_WRAPPER_TEMPLATE).cloneNode(true), template = $(this.DND_ITEM_TEMPLATE).cloneNode(true);
    var items = [];

    element.setStyle({width: null, height: null});

    if (element.items.length > 1) {
        element.update(this._msgNItemsSelected.evaluate({count: element.items.length}));
    } else if (element.items.length == 1) {
        element.update(element.items[0].getLabel());
    }
});

dynamicList.List.addMethod('_registerCustomScroll', function() {
    if (!this.scroll && this._getElement()) {
		var scrollBar = this._getElement().up(layoutModule.SWIPE_SCROLL_PATTERN);
        if(scrollBar) {
            var scroll = layoutModule.scrolls.get(scrollBar.identify());
            scroll && (this.scroll = scroll);
//            scroll && (console.log(scroll.wrapper.inspect()));
        }
	}
});

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// List DnD
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
dynamicList.List.addMethod('createDraggableIfNeeded', function(event) {
    //make draggable - test in this order - for efficiency
    //test 1) does the tree have any drag patterns?
    //test 2) is a draggable already created for the clicked element?
    //test 3) does clicked element or its ancestors match any draggable patterns?
    //test 4) is a draggable already created for the clicked element or matching ancestor?
    //5) if it's complex markup then we go up to parent which matching pattern
    var thisElem = event.element();
    if (this.dragPattern && !this.draggables[thisElem.identify()]) {

        var matchingElem = matchAny(thisElem, [this.dragPattern], true);
        if (matchingElem) {
            //matchingElem = matchingElem.up(this.dragPattern);
            if (!matchingElem || this.draggables[matchingElem.identify()]) { return; }

            var item = this.getItemByEvent(event);
            this.draggables[matchingElem.identify()] = new Draggable(matchingElem, {
                superghosting: true,
                mouseOffset: true,
                onStart: this.setDragStartState.bind(this, item),
                onEnd: this.setDragEndState.bind(this, item)
            });
        }
    }
});

dynamicList.List.addMethod('setDragStartState', function(item, draggable, event) {
    var templateClassName = item._getElement().templateClassName;
    if (templateClassName) { draggable.element.addClassName(templateClassName); }
    draggable.element.addClassName(layoutModule.DRAGGING_CLASS).addClassName(this.getId());

    draggable.element.items = this.getSelectedItems().slice(0);
    this._buildDnDOverlay(draggable.element);

    draggable.options.scroll = this._getElement();
    draggable.options.scrollSensitivity = layoutModule.SCROLL_SENSITIVITY;
    Draggables.dragging = this.regionID || true;
});

dynamicList.List.addMethod('setDragEndState', function(draggable, event, item) {
    delete Draggables.dragging;
});

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// List event handling
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

dynamicList.List.addMethod('_mouseupHandler', function(event) {
    var item = matchMeOrUp(event.element(), layoutModule.BUTTON_PATTERN) && this.getItemByEvent(event);

    if(!item || item._isElementInExcluded(event)) { return; }

    event.listEvent = true;

    if (item._respondOnItemEvents && !event.isInvoked) {
        this.fire(this.Event.ITEM_MOUSEUP, { targetEvent: event, item: item });
        
        if(!item._isExcludedFromSelectionTriggers(event)){
            var isSelect = !this.selectOnMousedown && !TouchController.element_scrolled && (!isSupportsTouch() || event.changedTouches.length == 1);
            //console.info('up: ' + isSelect);
            isSelect && item.getList().selectItem(item, isMetaHeld(event), isShiftHeld(event), isRightClick(event));
            item.getList().deselectOthers(item, isMetaHeld(event), isShiftHeld(event), isRightClick(event));
        }
        this.createDraggableIfNeeded(event);
    }

    event.isInvoked = true;
});

dynamicList.List.addMethod('_mousedownHandler', function(event) {
    event = (event.type == 'dataavailable') ?  event.memo.targetEvent : event;

    var element = event.element();
    var item = matchMeOrUp(element, layoutModule.BUTTON_PATTERN) && this.getItemByEvent(event);

    if(!item || item._isElementInExcluded(event)) { return; }

    event.listEvent = true;
    
    if(event.touches && event.touches.length == 2) {
    	var li = jQuery(element).parents('li:first');     
    	li.hasClass('selected') && document.fire(layoutModule.ELEMENT_CONTEXTMENU, {targetEvent: event, node: element});
    	return;
    }

    if (item._respondOnItemEvents && !event.isInvoked) {
        this.fire(this.Event.ITEM_MOUSEDOWN, { targetEvent: event, item: item });

        if(!item._isExcludedFromSelectionTriggers(event)){
            var isSelect = this.selectOnMousedown && (!isSupportsTouch() || event.touches.length == 1);
            //console.info('down: '+isSelect);
            isSelect && item.getList().selectItem(item, isMetaHeld(event), isShiftHeld(event), isRightClick(event));
        }
        //item.focus();
    }

    event.isInvoked = true;
});

dynamicList.List.addMethod('_mouseoverHandler', function(event) {
    matchMeOrUp(event.element(), layoutModule.BUTTON_PATTERN) && this.createDraggableIfNeeded(event);
});

dynamicList.List.addMethod('_clickHandler', function(event) {
    var item = matchMeOrUp(event.element(), layoutModule.BUTTON_PATTERN) && this.getItemByEvent(event);

    if(!item || item._isElementInExcluded(event)) { return; }

    if (!event.isInvoked) {
        if (item._respondOnItemEvents) {
            this.fire(this.Event.ITEM_CLICK, { targetEvent: event, item: item });
        }

        if(!item.isComposite) return;

        var element = item._getElement(), source = event.element();
        if (item._isCloseHandler(source) && baseList.isItemOpen(element)) {

            baseList.closeItem(element);
            this.fire(this.Event.ITEM_CLOSED, { targetEvent: event, item: item });
        } else if (item._isOpenHandler(source) && !baseList.isItemOpen(element)) {

            baseList.openItem(element);
            this.fire(this.Event.ITEM_OPEN, { targetEvent: event, item: item });
        }
    }
    event.isInvoked = true;
});

dynamicList.List.addMethod('_dblclickHandler', function(event) {
    var item = matchMeOrUp(event.element(), layoutModule.BUTTON_PATTERN) && this.getItemByEvent(event);

    if(!item || item._isElementInExcluded(event)) { return; }

    if (item._respondOnItemEvents && !event.isInvoked) {
        this.fire(this.Event.ITEM_DBLCLICK, { targetEvent: event, item: item });
    }

    event.isInvoked = true;
});

dynamicList.List.addMethod('_initEvents', function() {
    var container = this._getElement();

    this.draggables = [];

    if(isSupportsTouch()) {
        container.stopObserving('touchstart').observe('touchstart', this._mousedownHandler.bindAsEventListener(this));
        //scriptaculous stopped mousedown event but we made it throw this instead
        container.stopObserving('drag:touchstart').observe('drag:touchstart', this._mousedownHandler.bindAsEventListener(this));
        container.stopObserving('touchend').observe('touchend', this._mouseupHandler.bindAsEventListener(this));
    } else {
        container.stopObserving('mouseup').observe('mouseup', this._mouseupHandler.bindAsEventListener(this));
        container.stopObserving('mousedown').observe('mousedown', this._mousedownHandler.bindAsEventListener(this));
        //scriptaculous stopped mousedown event but we made it throw this instead
        container.stopObserving('drag:mousedown').observe('drag:mousedown', this._mousedownHandler.bindAsEventListener(this));
    }
    if(!isIPad) container.stopObserving('mouseover').observe('mouseover', this._mouseoverHandler.bindAsEventListener(this));
    container.stopObserving('click').observe('click', this._clickHandler.bindAsEventListener(this));
    container.stopObserving('dblclick').observe('dblclick', this._dblclickHandler.bindAsEventListener(this));
    container.stopObserving('key:down').observe('key:down', this.selectNext.bindAsEventListener(this));
    container.stopObserving('key:up').observe('key:up', this.selectPrevious.bindAsEventListener(this));
    container.stopObserving('key:right').observe('key:right', this.selectInwards.bindAsEventListener(this));
    container.stopObserving('key:left').observe('key:left', this.selectOutwards.bindAsEventListener(this));
});



