/**
 * This class is used to generate the rendering parameters for an event
 * in a {@link Ext.calendar.view.Weeks}. The purpose of this class is
 * to provide the rendering logic insulated from the DOM.
 * 
 * @private
 */
Ext.define('Ext.calendar.view.WeeksRenderer', {
    /**
     * @cfg {Number} days
     * The number of days.
     */
    days: null,

    /**
     * @cfg {Number} index
     * The index of this week.
     */
    index: null,

    /**
     * @cfg {Number} maxEvents
     * The maximum number of events per day before overflow.
     * `null` to disable this.
     */
    maxEvents: null,

    /**
     * @cfg {Boolean} overflow
     * `true` to calculate sizes as if overflow could occur.
     */
    overflow: true,

    /**
     * @cfg {Date} start
     * The start of the week.
     */
    start: null,

    /**
     * {@link Ext.calendar.view.Base} view
     * The view.
     */
    view: null,

    constructor: function(config) {
        var me = this,
            D = Ext.Date,
            start;

        Ext.apply(me, config);

        start = me.start;
        me.end = D.add(start, D.DAY, me.days);
        me.utcStart = this.view.utcTimezoneOffset(start);
        me.utcEnd = this.view.utcTimezoneOffset(me.end);

        me.hasMaxEvents = me.maxEvents !== null;

        me.rows = [];
        me.events = [];
        me.seen = {};
        me.overflows = [];
    },

    /**
     * Add an event if it occurs within the range for this week.
     * @param {Ext.calendar.model.EventBase} event The event.
     */
    addIf: function(event) {
        var me = this,
            start, end;

        if (event.getAllDay()) {
            // Treat all day events as UTC
            start = me.utcStart;
            end = me.utcEnd;
        }
        else {
            start = me.start;
            end = me.end;
        }

        if (event.occursInRange(start, end)) {
            me.events.push(event);
        }
    },

    /**
     * Indicates that all events are added and the positions can be calculated.
     */
    calculate: function() {
        var me = this,
            D = Ext.Date,
            view = me.view,
            seen = me.seen,
            events = me.events,
            len = events.length,
            days = me.days,
            rangeEnd = me.end,
            utcRangeEnd = me.utcEnd,
            start = D.clone(me.start),
            utcStart = D.clone(me.utcStart),
            maxEvents = me.maxEvents,
            hasMaxEvents = me.hasMaxEvents,
            overflows = me.overflows,
            overflowOffset = me.overflow ? 1 : 0,
            dayEventList = [],
            i, j, dayEvents, event, eLen,
            utcEnd, end, id, eventEnd, span,
            offsetStart, offsetEnd, offsetRangeEnd,
            allDay, item, offset, isSpan, dayLen,
            hasAnyOverflows, overflow, map, prev, dayOverflows;

        for (i = 0; i < days; ++i) {
            end = D.add(start, D.DAY, 1);
            utcEnd = me.view.utcTimezoneOffset(end);
            dayEvents = [];

            for (j = 0; j < len; ++j) {
                event = events[j];
                id = event.id;

                allDay = event.getAllDay();

                if (allDay) {
                    offsetStart = utcStart;
                    offsetEnd = utcEnd;
                    offsetRangeEnd = utcRangeEnd;
                }
                else {
                    offsetStart = start;
                    offsetEnd = end;
                    offsetRangeEnd = rangeEnd;
                }

                if (event.occursInRange(offsetStart, offsetEnd)) {
                    isSpan = event.isSpan();

                    if (!seen[id]) {
                        span = 1;

                        // If the event only spans 1 day, don't bother calculating
                        if (isSpan) {
                            eventEnd = event.getEndDate();

                            if (eventEnd > offsetRangeEnd) {
                                // If the event finishes after our range, then just span it
                                // to the end
                                span = days - i;
                            }
                            else {
                                // Otherwise, calculate the number of days used by this event
                                span = view.getDaysSpanned(offsetStart, eventEnd, allDay);
                            }
                        }

                        seen[id] = span;
                        dayEvents.push({
                            event: event,
                            id: id
                        });
                    }
                    else if (isSpan) {
                        // Seen this span already, but it needs to go in the day events so
                        // overflows get generated correctly
                        dayEvents.push({
                            isPlaceholder: true,
                            event: event,
                            id: id
                        });
                    }
                }
            }

            eLen = dayEvents.length;
            // Now we have all of the events for this day, sort them based on "priority",
            // then add them to our internal structure
            dayEvents.sort(me.sortEvents);

            if (hasMaxEvents) {
                map = {};
                overflows[i] = overflow = [];
                overflow.$map = map;

                // Can't check previous days on the first day
                if (i > 0) {
                    prev = overflows[i - 1].$map;

                    // Loop through all the events for today. If the event
                    // is overflowed in the previous day, it must also be overflowed today
                    for (j = 0; j < eLen; ++j) {
                        item = dayEvents[j];
                        id = item.id;

                        if (prev[id]) {
                            overflow.push(item.event);
                            map[id] = true;
                            dayEvents.splice(j, 1);
                            hasAnyOverflows = true;
                            --j;
                            --eLen;
                        }
                    }
                }

                if (eLen > 0) {
                    if (eLen > maxEvents) {
                        // This is a naive attempt to calculate overflows, without taking
                        // into account other days in the row. We'll use this as the basis
                        // going forward if we need to do any extra calculations once we start
                        // considering other days.

                        // -1 here because maxEvents is the total we can show, without the
                        // "show more" item.
                        // Assuming that "show more" is roughly the same size as an event,
                        // which we'll also need to show, we have to lop off another event.
                        offset = Math.max(0, maxEvents - overflowOffset);
                        dayOverflows = Ext.Array.splice(dayEvents, offset);

                        for (j = 0, dayLen = dayOverflows.length; j < dayLen; ++j) {
                            item = dayOverflows[j];
                            overflow.push(item.event);
                            map[item.id] = true;
                        }

                        hasAnyOverflows = true;
                    }
                    else if (overflowOffset > 0 && overflow.length && eLen === maxEvents) {
                        item = dayEvents.pop();
                        overflow.push(item.event);
                        map[item.id] = true;
                    }
                }
            }

            dayEventList.push(dayEvents);

            start = end;
            utcStart = utcEnd;
        }

        if (hasAnyOverflows && maxEvents > 0) {
            me.calculateOverflows(dayEventList, overflows);
        }

        // Once we have overflows, place the events
        for (i = 0; i < days; ++i) {
            dayEvents = dayEventList[i];
            eLen = dayEvents.length;

            for (j = 0; j < eLen; ++j) {
                item = dayEvents[j];

                if (!item.isPlaceholder) {
                    event = item.event;
                    me.addToRow(event, i, seen[event.id]);
                }
            }
        }
    },

    /**
     * Compress existing rows into consumable pieces for the view.
     * @param {Number} rowIdx The row index to compress.
     * @return {Object[]} A compressed set of config objects for the row.
     */
    compress: function(rowIdx) {
        var row = this.rows[rowIdx],
            ret = [],
            days = this.days,
            count = 0,
            i = 0,
            item;

        while (i < days) {
            item = row[i];

            if (item.event) {
                if (count > 0) {
                    ret.push({
                        isEmpty: true,
                        len: count
                    });
                    count = 0;
                }

                ret.push(item);
                i += item.len;
            }
            else {
                ++count;
                ++i;
            }
        }

        if (count > 0) {
            ret.push({
                isEmpty: true,
                len: count
            });
        }

        return ret;
    },

    /**
     * Checks if this renderer has any events.
     * @return {Boolean} `true` if there are events.
     */
    hasEvents: function() {
        return this.events.length > 0;
    },

    privates: {
        /**
         * Add an event to an existing row. Creates a new row
         * if one cannout be found.
         * @param {Ext.calendar.model.EventBase} event The event.
         * @param {Number} dayIdx The start day for the event.
         * @param {Number} days The number of days to span
         *
         * @private
         */
        addToRow: function(event, dayIdx, days) {
            var me = this,
                rows = me.rows,
                len = rows.length,
                end = days + dayIdx,
                found, i, j, row, idx;

            for (i = 0; i < len; ++i) {
                row = rows[i];

                for (j = dayIdx; j < end; ++j) {
                    if (row[j]) {
                        // Something occupying the space
                        break;
                    }
                }

                // If we got to the end of the loop above, we're ok to use this row
                if (j === end) {
                    found = row;
                    idx = i;
                    break;
                }
            }

            if (!found) {
                found = me.makeRow();
                rows.push(found);
                idx = rows.length - 1;
            }

            me.occupy(event, found, idx, dayIdx, end - 1);
        },

        /**
         * Setup any overflows triggered by overflows in other days.
         * @param {Object[]} dayEventList The list of events for all days.
         * @param  {Object[]} overflows The list overflows for all days.
         *
         * @private
         */
        calculateOverflows: function(dayEventList, overflows) {
            var days = this.days,
                maxEvents = this.maxEvents,
                i, dayEvents, len, item, id;

            // We are checking if we have events overflowing to the right
            // which may be truncated by overflow. Ignore the last day since it
            // will have no influence because there's nothing it can overflow into
            for (i = days - 2; i >= 0; --i) {
                dayEvents = dayEventList[i];
                len = dayEvents.length;

                // If we don't have any overflow, but we are on the boundary of being
                // overflowed, we need to check if we move into another cell that would
                // cause us to be hidden
                if (len > 0 && overflows[i].length === 0 && len === maxEvents) {
                    item = dayEvents[len - 1];
                    id = item.id;

                    // Only need to check one day forward, since an event can't "jump".
                    // If it's not in the day forward, then it can't be in any further day.
                    if (overflows[i + 1].$map[id]) {
                        // If it's overflowed, then we must overflow it
                        overflows[i].unshift(item.event);
                        overflows[i].$map[id] = true;
                        dayEvents.length = len - 1;
                    }
                }
            }
        },

        /**
         * Construct a new row.
         * @return {Object[]} The new row.
         *
         * @private
         */
        makeRow: function() {
            var row = [],
                days = this.days,
                i;

            for (i = 0; i < days; ++i) {
                row[i] = 0;
            }

            return row;
        },

        /**
         * Have an event occupy space in a row.
         * @param {Ext.calendar.model.EventBase} event The event.
         * @param {Object[]} row  The row.
         * @param {Number} rowIdx The local index of the row.
         * @param {Number} fromIdx The start index to occupy.
         * @param {Number} toIdx The end index to occupy.
         *
         * @private
         */
        occupy: function(event, row, rowIdx, fromIdx, toIdx) {
            var len = toIdx - fromIdx + 1,
                i;

            for (i = fromIdx; i <= toIdx; ++i) {
                row[i] = i === fromIdx
                    ? {
                        event: event,
                        len: len,
                        start: fromIdx,
                        weekIdx: this.index,
                        localIdx: rowIdx
                    }
                    : true;
            }
        },

        /**
         * A sort comparator function for processing events.
         * @param {Object} event1 The first event.
         * @param {Object} event2 The second event,
         * @return {Number} A standard sort comparator.
         * @private
         */
        sortEvents: function(event1, event2) {
            event1 = event1.event;
            event2 = event2.event;

            return +event2.isSpan() - +event1.isSpan() ||
                   Ext.calendar.model.Event.sort(event1, event2);
        }
    }
});
