/**
 * List is a custom styled DataView which allows Grouping, Indexing, Icons, and a Disclosure.
 *
 * # Example:
 *
 * Here is an example of the usage in a {@link Ext.List}:
 *
 *     @example miniphone preview
 *     Ext.regModel('Contact', {
 *        fields: ['firstName', 'lastName']
 *     });
 *
 *     var store = new Ext.data.JsonStore({
 *        model: 'Contact',
 *        sorters: 'lastName',
 *
 *        getGroupString: function(record) {
 *            return record.get('lastName')[0];
 *        },
 *
 *        data: [
 *            {firstName: 'Tommy',   lastName: 'Maintz'},
 *            {firstName: 'Rob',     lastName: 'Dougan'},
 *            {firstName: 'Ed',      lastName: 'Spencer'},
 *            {firstName: 'Jamie',   lastName: 'Avins'},
 *            {firstName: 'Aaron',   lastName: 'Conran'},
 *            {firstName: 'Dave',    lastName: 'Kaneda'},
 *            {firstName: 'Jacky',   lastName: 'Nguyen'},
 *            {firstName: 'Abraham', lastName: 'Elias'},
 *            {firstName: 'Jay',     lastName: 'Robinson'},
 *            {firstName: 'Nigel',   lastName: 'White'},
 *            {firstName: 'Don',     lastName: 'Griffin'},
 *            {firstName: 'Nico',    lastName: 'Ferrero'},
 *            {firstName: 'Nicolas', lastName: 'Belmonte'},
 *            {firstName: 'Jason',   lastName: 'Johnston'}
 *        ]
 *     });
 *
 *     var list = new Ext.List({
 *        fullscreen: true,
 *        itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
 *        store: store
 *     });
 *
*/
Ext.define('Ext.dataview.List', {
    alternateClassName: 'Ext.List',
    extend: 'Ext.dataview.DataView',
    xtype : 'list',

    requires: [
        'Ext.dataview.IndexBar',
        'Ext.dataview.ListItemHeader'
    ],

    config: {
        /**
         * @cfg {Boolean/Object} indexBar
         * True to render an alphabet IndexBar docked on the right.
         * This can also be a config object that will be passed to {@link Ext.IndexBar}
         * (defaults to false)
         * @accessor
         */
        indexBar: false,

        disclosure: null,

        icon: null,

        /**
         * @cfg {Boolean} clearSelectionOnDeactivate
         * True to clear any selections on the list when the list is deactivated (defaults to true).
         * @accessor
         */
        clearSelectionOnDeactivate: true,

        /**
         * @cfg {Boolean} preventSelectionOnDisclose True to prevent the item selection when the user
         * taps a disclose icon. Defaults to <tt>true</tt>
         * @accessor
         */
        preventSelectionOnDisclose: true,

        // @inherit
        baseCls: Ext.baseCSSPrefix + 'list',

        /**
         * @cfg {Boolean} pinHeaders
         * Whether or not to pin headers on top of item groups while scrolling for an iPhone native list experience.
         * Defaults to <tt>false</tt> on Android and Blackberry (for performance reasons)
         * Defaults to <tt>true</tt> on other devices.
         * @accessor
         */
        pinHeaders: true,

        grouped: false,

        /**
         * @cfg {Boolean/Function/Object} onItemDisclosure
         * True to display a disclosure icon on each list item.
         * This won't bind a listener to the tap event. The list
         * will still fire the disclose event though.
         * By setting this config to a function, it will automatically
         * add a tap event listeners to the disclosure buttons which
         * will fire your function.
         * Finally you can specify an object with a 'scope' and 'handler'
         * property defined. This will also be bound to the tap event listener
         * and is useful when you want to change the scope of the handler.
         * @accessor
         */
        onItemDisclosure: null
    },

    constructor: function() {
        this.translateHeader = (Ext.os.is.Android2) ? this.translateHeaderCssPosition : this.translateHeaderTransform;
        this.callParent(arguments);
    },

    initialize: function() {
        var me = this;
        me.callParent(arguments);
        me.elementContainer.element.on({
            delegate: '.' + this.getBaseCls() + '-disclosure',
            tap: 'handleItemDisclosure',
            scope: me
        });
    },

    applyIndexBar: function(indexBar) {
        if (this.getGrouped()) {
            return Ext.factory(indexBar, Ext.dataview.IndexBar, this.getIndexBar());
        }
    },

    updateIndexBar: function(indexBar) {
        if (indexBar && this.getScrollable()) {
            this.getScrollableBehavior().getScrollView().getElement().appendChild(indexBar.renderElement);

            indexBar.on({
                index: 'onIndex',
                scope: this
            });

            this.addCls(this.getBaseCls() + '-indexed');
        }
    },

    updatePinHeaders: function(pinnedHeaders) {
        var scrollable = this.getScrollable(),
            store = this.getStore(),
            scrollView = this.getScrollableBehavior().getScrollView(),
            scrollViewElement = scrollView.getElement(),
            header, scroller;

        if (scrollable && this.getGrouped()) {
            scroller = scrollable.getScroller();
            if (pinnedHeaders) {
                scroller.on({
                    refresh: 'doRefreshHeaders',
                    scroll: 'onScroll',
                    scope: this
                });

                store.on({
                    datachanged: 'doRefreshHeaders',
                    scope: this
                });

                this.header = header = Ext.create('Ext.dataview.ListItemHeader', {html: ' ', cls: 'x-list-header-swap'});
                scrollViewElement.dom.insertBefore(header.element.dom, scroller.getContainer().dom.nextSibling);
            } else {
                scroller.un({
                    refresh: 'onScrollerRefresh',
                    scroll: 'onScroll',
                    scope: this
                });

                store.un({
                    datachanged: 'doRefreshHeaders',
                    scope: this
                });

                if (this.header) {
                    this.header.destroy();
                }
            }
        }
    },

    onStoreClear: function() {
        this.callParent();
        if (this.header) {
            this.header.destroy();
        }
    },

    // @private
    getClosestGroups : function() {
        var groups = this.pinHeaderInfo.offsets,
            pos = this.getScrollable().getScroller().position,
            ln = groups.length,
            i = 0,
            group, current, next;

        for (; i < ln; i++) {
            group = groups[i];
            if (group.offset > pos.y) {
                next = group;
                break;
            }
            current = group;
        }

        return {
            current: current,
            next: next
        };
    },

    doRefreshHeaders: function() {
        var headerIndices = this.findGroupHeaderIndices(),
            ln = headerIndices.length,
            items = this.getViewItems(),
            headerInfo = this.pinHeaderInfo = {offsets: []},
            headerOffsets = headerInfo.offsets,
            i, headerItem, header;

        if (ln) {
            for (i = 0; i < ln; i++) {
                headerItem = items[headerIndices[i]];
                if (headerItem) {
                    header = this.getItemHeader(headerItem);

                    headerOffsets.push({
                        header: header,
                        offset: headerItem.offsetTop
                    });
                }
            }

            headerInfo.closest = this.getClosestGroups();
            this.setActiveGroup(headerInfo.closest.current);
            if (header) {
                headerInfo.headerHeight = Ext.get(header).getHeight();
            }
        }
    },

    getItemHeader: function(item) {
        return item.childNodes[0];
    },

    onScroll: function(scroller, x, y) {
        var me = this,
            headerInfo = me.pinHeaderInfo,
            closest = headerInfo.closest,
            activeGroup = me.activeGroup,
            headerHeight = headerInfo.headerHeight,
            next = closest.next,
            current = closest.current;

        if (y <= 0) {
            if (activeGroup) {
                me.setActiveGroup(false);
                closest.next = current;
            }
            return;
        }
        else if ((next && y > next.offset) || (current && y < current.offset)) {
            closest = headerInfo.closest = this.getClosestGroups();
            next = closest.next;
            current = closest.current;
            this.setActiveGroup(current);
        }

        if (next && y > 0 && next.offset - y <= headerHeight) {
            var headerOffset = headerHeight - (next.offset - y);
            this.translateHeader(headerOffset);
        }
        else {
            this.translateHeader(null);
        }
    },

    translateHeaderTransform: function(offset) {
        this.header.renderElement.dom.style.webkitTransform = (offset === null) ? null : 'translate3d(0px, -' + offset + 'px, 0px)';
    },

    translateHeaderCssPosition: function(offset) {
        this.header.renderElement.dom.style.top = (offset === null) ? null : '-' + Math.round(offset) + 'px';
    },

    /**
     * Set the current active group
     * @param {Object} group The group to set active
     * @private
     */
    setActiveGroup : function(group) {
        var me = this;
        if (group) {
            if (!me.activeGroup || me.activeGroup.header != group.header) {
                me.header.setHtml(group.header.innerHTML);
                me.header.show();
            }
        } else {
            me.header.hide();
        }

        this.activeGroup = group;
    },

    onIndex: function(index) {
        var key = index.toLowerCase(),
            store = this.getStore(),
            groups = store.getGroups(),
            ln = groups.length,
            scroller = this.getScrollable().getScroller(),
            group, i, closest, id, item;

        for (i = 0; i < ln; i++) {
            group = groups[i];
            id = group.name.toLowerCase();
            if (id == key || id > key) {
                closest = group;
                break;
            }
            else {
                closest = group;
            }
        }

        item = this.getViewItems()[store.indexOf(closest.children[0])];

        //stop the scroller from scrolling
        scroller.stopAnimation();

        //make sure the new offsetTop is not out of bounds for the scroller
        var containerSize = scroller.getContainerSize().y,
            size = scroller.getSize().y,
            maxOffset = size - containerSize,
            offset = (item.offsetTop > maxOffset) ? maxOffset : item.offsetTop;

        scroller.scrollTo(0, offset);
    },

    applyOnItemDisclosure: function(config) {
        if (Ext.isFunction(config)) {
            return {
                scope: this,
                handler: config
            };
        }
        if (Ext.isObject(config)) {
            return config;
        }
        return null;
    },

    getDisclosure: function() {
        var value = this._disclosure,
            onItemDisclosure = this.getOnItemDisclosure();

        if (onItemDisclosure && onItemDisclosure != value) {
            value = true;
            this.setDisclosure(value);
        }

        return value;
    },

    updateOnItemDisclosure: function(newOnItemDisclosure) {
        // If we have an onItemDisclosure configuration, force disclose config to true
        if (newOnItemDisclosure) {
            this.setDisclosure(true);
        }
    },

    handleItemDisclosure: function(e) {
        var me = this,
            item = e.getTarget().parentNode,
            index = item.getAttribute('itemIndex'),
            record = me.getStore().getAt(index),
            onItemDisclosure = me.getOnItemDisclosure();

        if (me.getPreventSelectionOnDisclose()) {
            e.stopEvent();
        }
        me.fireAction('disclose', [record, item, index, e], 'doDisclose');

        if (onItemDisclosure && onItemDisclosure.handler) {
            onItemDisclosure.handler.call(me, record, item, index);
        }
    },

    updateBaseCls: function(newBaseCls, oldBaseCls) {
        var me = this;
        me.callParent(arguments);
        me.itemClsShortCache = newBaseCls + '-item';

        me.headerClsShortCache = newBaseCls + '-header';
        me.headerClsCache = '.' + me.headerClsShortCache;

        me.labelClsShortCache = newBaseCls + '-item-label';
        me.labelClsCache = '.' + me.labelClsShortCache;

        me.disclosureClsShortCache = newBaseCls + '-disclosure';
        me.disclosureClsCache = '.' + me.disclosureClsShortCache;

        me.iconClsShortCache = newBaseCls + '-icon';
        me.iconClsCache = '.' + me.iconClsShortCache;
    },

    hiddenDisplayCache: Ext.baseCSSPrefix + 'hidden-display',

    doDisclose: Ext.emptyFn,

    updateListItem: function(record, item) {
        var extItem = Ext.get(item),
            innerItem = extItem.down(this.labelClsCache, true),
            index = this.getStore().indexOf(record),
            data = record.data,
            disclosure = data && data.hasOwnProperty('disclosure'),
            iconSrc = data && data.hasOwnProperty('iconSrc'),
            disclosureEl, iconEl;

        item.setAttribute('itemIndex', index);
        innerItem.innerHTML = this.getItemTpl().apply(data);

        if (this.getDisclosure() && disclosure) {
            disclosureEl = extItem.down(this.disclosureClsCache);
            disclosureEl[disclosure ? 'removeCls' : 'addCls'](this.hiddenDisplayCache);
        }

        if (this.getIcon()) {
            iconEl = extItem.down(this.iconClsCache, true);
            iconEl.style.backgroundImage = iconSrc ? 'url(' + iconSrc + ')' : '';
        }
    },

    getItemElementConfig: function(index, data) {
        var config = {
                cls: this.itemClsShortCache,
                itemIndex: index,
                children: [{
                    cls: this.labelClsShortCache,
                    html: this.getItemTpl().apply(data)
                }]
            },
            iconSrc;

        if (this.getIcon()) {
            iconSrc = data.iconSrc;
            config.children.push({
                cls: this.iconClsShortCache,
                style: 'background-image: ' + iconSrc ? 'url(' + iconSrc + ')' : ''
            });
        }

        if (this.getDisclosure()) {
            config.children.push({
                cls: this.disclosureClsShortCache + ((data.disclosure === false) ? this.hiddenDisplayCache : '')
            });
        }
        return config;
    },

    findGroupHeaderIndices: function() {
        if (!this.getGrouped()) {
            return;
        }
        var me = this,
            store = me.getStore(),
            groups = store.getGroups(),
            groupLn = groups.length,
            items = me.getViewItems(),
            existingHeaders = me.elementContainer.element.query(me.headerClsCache),
            existingHeadersLn = existingHeaders.length,
            newHeaderItems = [],
            i, firstGroupedRecord, index, item;

        // Remove headers
        for (i = 0; i < existingHeadersLn; i++) {
            Ext.removeNode(existingHeaders[i]);
        }

        // Add header
        for (i = 0; i < groupLn; i++) {
            firstGroupedRecord = groups[i].children[0];
            index = store.indexOf(firstGroupedRecord);
            item = items[index];
            me.doAddHeader(item, store.getGroupString(firstGroupedRecord));
            newHeaderItems.push(index);
        }

        return newHeaderItems;
    },

    doAddHeader: function(item, html) {
        Ext.get(item).insertFirst(Ext.Element.create({
            cls: this.headerClsShortCache,
            html: html
        }));
    }
}, function() {
    //TODO This is hacky, find a better way @Jacky
    var prototype = this.prototype;

    prototype.cachedConfigList = prototype.cachedConfigList.slice();
    Ext.Array.remove(prototype.cachedConfigList, 'baseCls');
});