/* 
Schedule
Main Schedule class. Controls schedule groups, loading show data, and hour-to-hour transitions
*/
Schedule = Class.create({
    initialize: function(element) {
        this.element = $(element);
        this.groups = {};
        this.offset = 0;
        this.locked = false;
        this.currentDate = $$('p.currentDate')[0];
        this.scheduleDays = $$('select.scheduleDays')[0];
        this.addChannels = $$('.scheduleTools p.add')[0];
        this.inactiveChannels = $$('select.addToMySchedule')[0];
        //this.currentDate = document.getElementById('currentDate');
        //this.scheduleDays = document.getElementById('scheduleDays');
        //this.addChannels = $('AddChannelContainer');
        //this.inactiveChannels = $('AddToMySchedule');
        this.loader = new ScheduleLoader(this.onShowsLoaded.bind(this));
        this.fx = new Fx.Generic(this.setOffset.bind(this), {
            duration: 500, 
            onComplete: this.onScrollComplete.bind(this)
        });
        
        // Preload schedule images
        Starz.preloadImages(Schedule.IMAGES);
        
        // Setup add channel behavior
        this.inactiveChannels.observe('change', this.onInactiveChannelsChange.bind(this));
        if (this.inactiveChannels.options.length<2) {
            this.addChannels.hide();
        }
        
        // Build display layer
        Schedule.CHANNELS.each(this.createGroup, this);
        
        // Move to the current time today.
        this.setHourOffset((new Date()).getHours());
        this.loadingScreen = $('gridLoader');
        this.loadingScreen.removeClassName("scheduleViewLoading");
        this.loadingScreen.addClassName("scheduleViewLoadingHidden");
        
        $('grid_toggle').removeClassName("grid_toggle_hidden");
        $('grid_toggle').addClassName("grid_toggle_visible");
        
    },
    createGroup: function(group) {
        if ($(group[0]+'Group')) {
            this.groups[group[0]] = new ScheduleGroup(group[0]);
        }
    },
    showPreviousHour: function() {
        if (!this.locked) {
            this.locked = true;
            this.fx.custom(this.offset, this.offset-Schedule.HOUR_WIDTH);
        }
    },
    showNextHour: function() {
        if (!this.locked) {
            this.locked = true;
            this.fx.custom(this.offset, this.offset+Schedule.HOUR_WIDTH);
        }
    },
    setOffset: function(offset) {
        this.offset = offset;
        for (var group in this.groups) {
            this.groups[group].setOffset(offset);
        }
    },
    setHourOffset: function(hour) {
        this.setOffset(hour * Schedule.HOUR_WIDTH);
        this.onScrollComplete(true);
    },
    onScrollComplete: function(jump) {
        var start = new Date();
        
        jump = !!jump;
        this.locked = false;
        for (var group in this.groups) {
            this.groups[group].onScrollComplete(jump);
        }
        
        // Update the date in the header
        var currentHour = this.offset/Schedule.HOUR_WIDTH;
        var daysOffset = Math.floor(currentHour/24);
        var date = new Date(Schedule.BASE_DATE);
            date.setHours(daysOffset*24);
        
        this.currentDate.innerHTML = (Schedule.Util.humanDate(date));
        this.scheduleDays.value = Schedule.Util.mmddyyyyDate(date);
        
        this.loadShows();
    },
    onInactiveChannelsChange: function(event) {
        var channel = this.inactiveChannels.value;
        var feed = this.inactiveChannels.select('option[value='+channel+']')[0].title;
        this.inactiveChannels.value = "";
        this.addChannel(channel, feed);
    },
    addChannel: function(channelName, feed) {
        // remove channel from drop-down (and hide drop-down if necessary)
        var options = $A(this.inactiveChannels.options);
        var index = options.indexOf(options.find(function(option){return option.value==channelName}));
        this.inactiveChannels.remove(index);
        if (this.inactiveChannels.options.length==1) {
            this.addChannels.hide();
        }
        
        // locate group channel is in, and delegate the actual channel creation to that group
        var group = Schedule.CHANNELS.find(function(group){ return group[1].indexOf(channelName)>=0 });
        if (group && group[0] && this.groups[group[0]]) {
            this.groups[group[0]].addChannel(channelName, feed);
        }
        
        // load channel data for currently displayed day
        var currentHour = this.offset/Schedule.HOUR_WIDTH;
        var daysOffset = Math.floor(currentHour/24);
        var date = new Date(Schedule.BASE_DATE);
            date.setHours(daysOffset*24);
        var startDate = new Date(date);
            startDate.setHours(-3);
        var finishDate = new Date(date);
            finishDate.setHours(27);
                                    
        this.loader.load([channelName], startDate, finishDate);
        if(Starz.isLoggedIn()) {
            this.updateChannelPrefs(channelName, 'add');
        }
    },
    removeChannel: function(channelName, feed) {
        var inactiveChannels = $A(this.inactiveChannels.options).map(function(option){return (option.value.length==0) ? null : option.value}).compact();

        if (inactiveChannels.indexOf(channelName)==-1) {
            //this.inactiveChannels.options.add(new Element('option', {'title':feed, 'value':channelName}).update(Schedule.Util.channelTitle(channelName)));
            var channelOption = new Option(Schedule.Util.channelTitle(channelName), channelName);
            channelOption.title = feed;
            this.inactiveChannels.options.add(channelOption);
            this.addChannels.show();
            
            var group = Schedule.CHANNELS.find(function(group){ return group[1].indexOf(channelName)>=0 });
            if (group && group[0] && this.groups[group[0]]) {
                this.groups[group[0]].removeChannel(channelName);
            }
        }
        if(Starz.isLoggedIn()) {
            this.updateChannelPrefs(channelName, 'remove');
        }
    },
    updateChannelPrefs: function(channel, action) {
        new Ajax.Request(Schedule.PREFS_URL, {
            method: 'get',
            parameters: {
                action: action,
                channel: channel
            }
        });
    },
    loadShows: function() {
        var pastChannelsToLoad   = $A()
        var futureChannelsToLoad = $A();
        var channelsLoading      = this.loader.channelsCurrentlyLoading();
        
        // Find out if we need to load shows, and if so, load them
        for (var group in this.groups) {
            pastChannelsToLoad   = pastChannelsToLoad.concat(this.groups[group].getChannelsToLoad(true));
            futureChannelsToLoad = futureChannelsToLoad.concat(this.groups[group].getChannelsToLoad(false));
        }
        
        var shouldLoad       = function(item) { return channelsLoading.indexOf(item)>=0 }
        pastChannelsToLoad   = pastChannelsToLoad.reject(shouldLoad);
        futureChannelsToLoad = futureChannelsToLoad.reject(shouldLoad);
        
        // Okay, we need to load channels, gather start and finish times and execute
        // NOTE: Start and end dates have to be adjusted by an hour to ensure there aren't any short shows
        //       in between the last show and our download window. Better to download redundant data than miss some.
        if (pastChannelsToLoad.length>0) {
            var finishDate = new Date(Schedule.BASE_DATE);
            finishDate.setTime( finishDate.getTime() + (this.offset/Schedule.HOUR_WIDTH-Schedule.DOWNLOAD_WINDOW+1)*3600000);
            var startDate = new Date(finishDate);
            startDate.setTime( startDate.getTime() - (Schedule.DOWNLOAD_HOURS+1)*3600000);
            
            this.loader.load(pastChannelsToLoad, startDate, finishDate);
        }
        if (futureChannelsToLoad.length>0) {
            var startDate = new Date(Schedule.BASE_DATE);
            startDate.setTime( startDate.getTime() + ((this.offset+Schedule.VIEWPORT_WIDTH)/Schedule.HOUR_WIDTH+Schedule.DOWNLOAD_WINDOW-3)*3600000);
            var finishDate = new Date(startDate);
            finishDate.setTime( finishDate.getTime() + (Schedule.DOWNLOAD_HOURS+1)*3600000);

            this.loader.load(futureChannelsToLoad, startDate, finishDate);
        }
    },
    onShowsLoaded: function(showData) {
        for (var group in this.groups) {
            this.groups[group].addShowData(showData);
        }
    }
});
Object.extend(Schedule, {
    CHANNELS:         $H({'Starz':     ['starz', 'starz_edge', 'starz_inblack', 'starz_comedy', 'starz_cinema', 'starz_family'],
                          'Encore':    ['encore', 'encore_action', 'encore_love', 'encore_westerns', 'encore_mystery', 'encore_drama', 'encore_wam'],
                          'MoviePlex': ['movieplex', 'indieplex', 'retroplex']}),
    // CHANNEL_NAMES are special cases only. Anything not in that object will follow this rule: starz_comedy -> Starz Comedy
    CHANNEL_NAMES:    {'starz_inblack': 'Starz In Black',
                       'starz_family': 'Starz Kids & Family',
                       'movieplex': 'MoviePlex',
                       'indieplex': 'IndiePlex',
                       'retroplex': 'RetroPlex'},
    CHANNELS_IN_HD:   ['starz', 'starz_family', 'starz_edge', 'starz_comedy', 'encore'],
    FLAGSHIP_CHANNELS: ['starz', 'encore', 'movieplex'],
    MOVIE_DETAIL_URL: '/ajax/ScheduleDialog.aspx',
    SEARCH_URL:       '/ajax/scheduleData.aspx',
    PREFS_URL:        '/ajax/ScheduleChannel.aspx',
    VIEWPORT_WIDTH:   792,
    HOUR_WIDTH:       264,
    DOWNLOAD_WINDOW: 4,
    DOWNLOAD_HOURS:   20 ,
    IMAGES:           $A(['/siteImagesLib/schedule_close.gif', '/siteImagesLib/schedule_collapsed_bg.gif', '/siteImagesLib/schedule_minimize.gif', '/siteImagesLib/schedule_expand.gif',
                          '/siteImagesLib/icon_reminder_set.gif', '/siteImagesLib/icon_favorite_set.gif']),
    
    instance: null,
    options: {
        timelineOnly: false,
        showIcons: false
    },
    init: function(base_date, has_reminders) {
        Schedule.BASE_DATE = new Date(base_date);
        Schedule.HAS_REMINDERS = has_reminders;
        
        var element = $('Schedule');
        // Show loading screen
        element.className = 'ScheduleLoading';
        
        // Do the work
        Schedule.instance = new Schedule(element);            
        element.className = '';
    },
    showPreviousHour: function(event) {
        event.stop();
        // This is a pretty dirty hack
        Schedule.CHANNELS.each(function(channelGroup) {
            for(i in Schedule.instance.groups) {
                var scheduleGroup = Schedule.instance.groups[i];
                scheduleGroup.enableScroll(scheduleGroup.nextHour);
            }
        });
        Schedule.instance.showPreviousHour();
    },
    showNextHour: function(event) {
        event.stop();
        // This is a pretty dirty hack
        Schedule.CHANNELS.each(function(channelGroup) {
            for(i in Schedule.instance.groups) {
                var scheduleGroup = Schedule.instance.groups[i];
                scheduleGroup.enableScroll(scheduleGroup.previousHour);
            }
        });
        Schedule.instance.showNextHour();
    },
    addChannel: function(event, channelName) {
        event.stop();
        Schedule.instance.addChannel(channelName);
    },
    removeChannel: function(event, channelName, feed) {
        event.stop();
        Schedule.instance.removeChannel(channelName, feed);
    },
    Util: {
        DAYS: [
            'Sunday', 'Monday', 'Tuesday', 'Wednesday',
            'Thursday', 'Friday', 'Saturday'
        ],
        MONTHS: [
            'January', 'February', 'March', 'April', 'May', 'June', 'July',
            'August', 'September', 'October', 'November', 'December'
        ],
        channelTitle: function(channelName) {
            if (Schedule.CHANNEL_NAMES[channelName]) {
                return Schedule.CHANNEL_NAMES[channelName]
            } else {
                // 'starz_comedy' -> 'Starz Comedy'
                return channelName.replace(/_/g," ").replace(/\b[a-z]/g, function(str){return str.toUpperCase()});
            }
        },
        zeropad: function(num){ return (num<10) ? '0'+num : num },
        humanDate: function(date) {
            var day     = Schedule.Util.DAYS[date.getDay()];
            var month   = Schedule.Util.MONTHS[date.getMonth()];
            var ordinal;
            switch(date.getDate() % 10) {
                case 1: 
                    ordinal = date.getDate()==11 ? "th" : "st"; // Thanks a lot, Eleven...
                    break;
                case 2:
                    ordinal = date.getDate()==12 ? "th" : "nd"; // Thanks a lot, Twelve...
                    break;
                case 3:
                    ordinal = date.getDate()==13 ? "th" : "rd"; // Thanks a lot, Thirteen...
                    break;
                default: 
                    ordinal = "th";
            }
            return day+", "+month+" "+date.getDate()+ordinal;
        },
        serverDate: function(date) {
            var year    = date.getFullYear();
            var month   = Schedule.Util.zeropad(date.getMonth()+1);
            var day     = Schedule.Util.zeropad(date.getDate());
            var hour    = Schedule.Util.zeropad(date.getHours());
            var minutes = Schedule.Util.zeropad(date.getMinutes());
            
            return year+'-'+month+'-'+day+' '+hour+':'+minutes;
        },
        mmddyyyyDate: function(date) {
            var month   = Schedule.Util.zeropad(date.getMonth()+1);
            var day     = Schedule.Util.zeropad(date.getDate());
            var year    = date.getFullYear();
            
            return month+'/'+day+'/'+year;           
        }
    }
});

/*
ScheduleLoader
Handles all of the loading of channel data.
*/
ScheduleLoader = Class.create({
    initialize: function(callback) {
        this.callback = callback;
    },
    channelsCurrentlyLoading: function() {
        // Returns a list of all the channels that have active requests associated with them
        var channels = this.requests.inject($A(), function(channels, request){
            channels.concat(request.channels);
            
            return channels;
        }, this);
        
        return channels.uniq();
    },
    load: function(channels, startDate, finishDate) {
        var request = {
            channels:   channels,
            startDate:  startDate,
            finishDate: finishDate,
            ajax:       new Ajax.Request(Schedule.SEARCH_URL, {
                method: 'get',
                parameters: {
                    channels: channels,
                    start:    Schedule.Util.serverDate(startDate),
                    finish:   Schedule.Util.serverDate(finishDate)
                },
                requestHeaders: { Accept: 'application/json' },
                evalJSON: 'force',
                onSuccess: function(transaction) {
                    if(Object.values(transaction.responseJSON).length != 0) {
                        setTimeout(function() {
                            this.onLoadComplete(transaction.responseJSON);
                        }.bind(this), 100)
                        Schedule.CHANNELS.each(function(channelGroup) {
                            for(i in Schedule.instance.groups) {
                                var scheduleGroup = Schedule.instance.groups[i];
                                scheduleGroup.enableScroll(scheduleGroup.previousHour);
                            }
                        });
                    } else {
                        Schedule.CHANNELS.each(function(channelGroup) {
                            for(i in Schedule.instance.groups) {
                                var scheduleGroup = Schedule.instance.groups[i];
                                scheduleGroup.disableScroll(scheduleGroup.previousHour);
                            }
                        });
                    }
                }.bind(this)
            })
        };
        this.requests.push(request);
        
        return request;
    },
    onLoadComplete: function(response) {
        var request = this.requests.find(function(request){
            return request.ajax == response.request
        });
        
        this.requests.splice(this.requests.indexOf(request),1);
        this.callback(response);
    },
    requests: $A()
});

/* 
ScheduleGroup
Represents a grouping of channels. Has own navigation buttons and timeline. 
*/
ScheduleGroup = Class.create({
    initialize: function(groupName) {
        /* This is part of a pretty complicated setup procedure. 
           Lots of HTML generation and shuffling of existing DOM nodes. */
                
        this.groupName = groupName;
        this.group     = $(groupName+'Group');
        this.channels  = this.group.select('div');
        this.shows     = {};
        this.offset    = 0;
        this.locked    = false;
        
        // Add collapse link to Group
        this.collapse = new Element('a', {'href': '#', 'class': 'collapse'});
        this.group.insert({top:this.collapse});
        
        // Add disabled state to Group
        this.collapsedState = new Element('div', {'class':'collapsed'});
        this.collapsedState.update('<div class="channel"></div><div class="message">Click (+) to expand the '+groupName+' Channels</div>');
        this.expand = new Element('a', {'href': '#', 'class': 'expand'});
        this.collapsedState.appendChild(this.expand);
        this.group.appendChild(this.collapsedState);
        
        // Add containers to Group
        this.channelContainer  = new Element('div', {'class': 'channelContainer'});
        this.scroller          = new Element('div', {'class': 'listingsScroller'});
        this.timeline          = new Element('div', {'class': 'timeline'});
        this.listingsContainer = new Element('div', {'class': 'listingsContainer'});

        this.group.appendChild(this.channelContainer);
        this.group.appendChild(this.scroller);
        this.scroller.appendChild(this.timeline);
        this.scroller.appendChild(this.listingsContainer);
        
        // Add navigation buttons to Group
        this.previousHour = new Element('a', {'class': 'previousHour', href: "#"}).update("<span></span>");
        this.nextHour = new Element('a', {'class': 'nextHour', href: "#"}).update("<span></span>");
        this.group.appendChild(this.previousHour);
        this.group.appendChild(this.nextHour);
        
        // Add timeline items 
        this.timelineItems = $A([]);
        for (var i=0; i<5; i++) {
            var item = new Element('div');
            item.style.left = ((i-1)*Schedule.HOUR_WIDTH)+'px';
            this.timeline.appendChild(item);
            this.timelineItems.push(item);
        }
        
        // Process channels and move to appropriate containers
        this.channels = this.channels.map(function(channel) {
            return new ScheduleChannel(this, channel);
        }, this);
        
        // Adjust dimensions
        this.setHeight();
                
        // Add event listeners
        this.enableScroll(this.previousHour);
        this.enableScroll(this.nextHour);
        this.collapse.observe('click', this.collapseGroup.bind(this));
        this.expand.observe('click', this.expandGroup.bind(this));
    },
    setHeight: function() {
        var height = this.channels.length*65 + (this.channels.length-1)*5;
        
        this.previousHour.style.height = height+'px';
        this.nextHour.style.height = height+'px';
        this.scroller.style.height = (height+23)+'px';
        this.timeline.style.height = (height+23)+'px';
    },
    collapseGroup: function(event) {
        event.stop();
        this.group.addClassName('disabled');
    },
    expandGroup: function(event) {
        event.stop();
        this.group.removeClassName('disabled');
        
        this.setOffset(this.offset);
        this.onScrollComplete(true);
    },
    setOffset: function(offset) {
        this.offset = offset;
        if (this.group.className == 'group disabled') return;

        this.timeline.style.left = (-offset)+'px';
        if (!Schedule.options.timelineOnly) {
            this.listingsContainer.style.left = (-offset)+'px';
        }

        this.channels.invoke('setOffset', offset);
    },
    onScrollComplete: function(jump) {
        if (this.group.className == 'group disabled') return;

        if (Schedule.options.timelineOnly) {
            this.listingsContainer.style.left = (-this.offset)+'px';
        }
        this.updateTimeline(jump);
        this.updateShowVisibility();
    },
    updateTimeline: function(jump) {
        var currentHour = this.offset/Schedule.HOUR_WIDTH;
        
        if (!jump) {
            // If we didn't just jump to an arbitrary time, we can shift the timeline markers
            // in a better way which won't result in Firefox flickering while updating
            if (this.timeline.offsetLeft+this.timelineItems.first().offsetLeft < -Schedule.HOUR_WIDTH) {
                var newLeft = -this.timeline.offsetLeft+Schedule.VIEWPORT_WIDTH;
                this.timelineItems.first().setStyle({left: newLeft+"px"});
                this.timelineItems.push(this.timelineItems.shift());
            } else if (this.timeline.offsetLeft+this.timelineItems.last().offsetLeft >= Schedule.VIEWPORT_WIDTH+Schedule.HOUR_WIDTH) {
                var newLeft = -this.timeline.offsetLeft-Schedule.HOUR_WIDTH;
                this.timelineItems.last().setStyle({left: newLeft+"px"});
                this.timelineItems.unshift(this.timelineItems.pop());
            }
        }
        
        for (var i=0, item; i<this.timelineItems.length; i++) {
            item = this.timelineItems[i]
            var hour = (currentHour+(i-1)) % 24;
            if (hour < 0) hour += 24;
            
            var klass = '';
            var inner = '';
            if (hour>0) {
                var text = (hour == 0) ? 12 : ((hour>12) ? (hour-12) : hour);
                klass = '';
                inner = "<span>"+text+":00 "+(hour<12 ? "AM" : "PM")+"</span>";      
            } else {
                // Show e.g. "Tuesday, December 29th" instead of 12:00AM
                klass = 'midnight';
                var daysOffset = Math.round(currentHour/24);
                var date = new Date(Schedule.BASE_DATE);
                date.setHours(daysOffset*24);
                inner = "<span>"+Schedule.Util.humanDate(date)+"</span>";
            }
            item.innerHTML = inner;
            if (jump)
                item.style.left = this.offset+(i-1)*Schedule.HOUR_WIDTH+'px';
            item.className = klass;
        }
    },
    updateShowVisibility: function() {
        var windowStart = this.offset - Schedule.HOUR_WIDTH;
        var windowEnd   = this.offset + Schedule.VIEWPORT_WIDTH + Schedule.HOUR_WIDTH;
        this.channels.invoke('updateShowVisibility', windowStart, windowEnd);
    },
    addShowData: function(showData) {
        for (var channelName in showData) {
            var channel = this.channels.find(function(channel){
                return channel.channelName == channelName;
            });
            if (typeof channel=="undefined") continue;
            
            channel.addShowData(showData[channelName]);
            this.updateShowVisibility();
        }
    },
    getChannelsToLoad: function(past) {
        // Returns an array of channels that need to load data
        if (!!past) {
            loadCutoff = this.offset - Schedule.DOWNLOAD_WINDOW*Schedule.HOUR_WIDTH;
            channelsToLoad = this.channels.inject([], function(channels, channel){
                if (channel.shows.length==0) return channels;
                
                var first = channel.shows.first();
                var cutoff = Math.ceil((first.startTime)/60*Schedule.HOUR_WIDTH -
                             (Schedule.BASE_DATE-first.startDate)/3600000*Schedule.HOUR_WIDTH);
                
                if (cutoff >= loadCutoff) {
                    channels.push(channel.channelName);
                }
                
                return channels;
            });
        } else {
            loadCutoff = this.offset + Schedule.VIEWPORT_WIDTH + Schedule.DOWNLOAD_WINDOW*Schedule.HOUR_WIDTH;
            channelsToLoad = this.channels.inject([], function(channels, channel){
                if (channel.shows.length==0) return channels;

                var last = channel.shows.last();
                var cutoff = Math.ceil((last.startTime+last.duration)/60*Schedule.HOUR_WIDTH -
                             (Schedule.BASE_DATE-last.startDate)/3600000*Schedule.HOUR_WIDTH);
                
                if (cutoff <= loadCutoff) {
                    channels.push(channel.channelName);
                }
                
                return channels;
            });
        }
        
        return channelsToLoad;
    },
    addChannel: function(channelName, feed) {
        this.channels.push( ScheduleChannel.create(this, channelName, feed) );
        this.setHeight();
        if (this.channels.length>0) {
            this.group.show();
        }
    },
    removeChannel: function(channelName) {
        var channel = this.channels.find(function(channel){return channel.channelName==channelName});
        var index = this.channels.indexOf(channel);
        channel.remove();
        if (this.channels.length==1) {
            this.group.hide();
        }
        this.channels.splice(index,1);
        
        this.setHeight();
    },
    disableScroll: function(scroller) {
        scroller.stopObserving('click');
        scroller.observe('click', function(event) { event.stop(); });
    },
    enableScroll: function(scroller) {
        switch(scroller) {
            case this.previousHour:
                this.previousHour.stopObserving('click');
                this.previousHour.observe('click', Schedule.showPreviousHour);
            break;
            case this.nextHour:
                this.nextHour.stopObserving('click');
                this.nextHour.observe('click', Schedule.showNextHour);
            break;
        }
    }
});

/* 
ScheduleChannel
Acts as the storage medium for shows. Lays shows out according to their start time and duration
*/
ScheduleChannel = Class.create({
    initialize: function(group, channel, init) {
        this.group       = group;
        this.channel     = channel;
        this.listings    = this.channel.getElementsByTagName('ol')[0];
        this.channelName = channel.className;
        this.feed        = this.channel.select('p.zone')[0].className.match(/east|west/)[0];

        // Decide where to insert this channel within the containers.
        var channels       = this.group.channelContainer.childElements();
        var channelNames   = Schedule.CHANNELS.get(this.group.groupName);
        var targetIndex    = channelNames.indexOf(this.channelName);
        var insertionIndex = null;
        
        for (var i=0; i<channels.length; i++) {
            if (channelNames.indexOf(channels[i].className) > targetIndex) {
                insertionIndex = i;
                break;
            }
        }
        
        if (insertionIndex!=null) {
            this.group.listingsContainer.childElements()[insertionIndex].insert({before: this.listings});
            this.group.channelContainer.childElements()[insertionIndex].insert({before: this.channel});            
        } else {
            this.group.listingsContainer.appendChild(this.listings);
            this.group.channelContainer.appendChild(this.channel);            
        }
        
        this.isFlagship = false;
        Schedule.FLAGSHIP_CHANNELS.each(function(flagship) {
            if(flagship == this.channelName && this.isFlagship != true) {
                this.isFlagship = true;
            }
        }.bind(this));

        // Create close button
        if(!this.isFlagship) {
            this.close = new Element('a', {'href': '#', 'class':'close', 'title': 'Remove this channel'});
            this.close.observe('click', Schedule.removeChannel.bindAsEventListener(null, this.channelName, this.feed));
            this.channel.appendChild(this.close);
        }
        
        // Create shows
        this.shows = $A([]);
        var listings = this.listings.getElementsByTagName('li');
        var len = listings.length;
        for (var i=0; i<len; i++) {
            this.shows[i] = ScheduleItem.create(listings[i], this.feed);
        }

        this.sortShows();
        this.layoutShows();
    },
    sortShows: function() {
        this.shows.sort(function(a,b){
            var aStart = a.startTime - (Schedule.BASE_DATE-a.startDate)/60000;
            var bStart = b.startTime - (Schedule.BASE_DATE-b.startDate)/60000;
            return aStart - bStart;
        });
    },
    layoutShows: function(shows) {
        // This function can accept an array of shows to lay out
        // so that we don't need to lay out all of the shows over again

        var showsToLayout = []
        if (typeof shows != "undefined") {
            showsToLayout = shows;
        } else {
            showsToLayout = this.shows;
        }
        
        showsToLayout.each(function(show){
            var start  = Math.ceil(show.startTime/60*Schedule.HOUR_WIDTH);
            var dateOffset = (Schedule.BASE_DATE-show.startDate)/3600000*Schedule.HOUR_WIDTH;
            var length = show.duration/60*Schedule.HOUR_WIDTH;
            
            show.element.style.left = (start-dateOffset)+'px';
            show.element.style.width = Math.floor(length)+'px';
        }.bind(this));
    },
    updateShowVisibility: function(start, end) {
        // Any shows outside of the window (viewport + 1-hour's padding on each side)
        // should be removed from the DOM tree. Any shows inside should be added
        this.shows.each(function(show) {
            var showLeft = show.startTime/60*Schedule.HOUR_WIDTH - (Schedule.BASE_DATE-show.startDate)/3600000*Schedule.HOUR_WIDTH;
            var showRight = showLeft + show.duration/60*Schedule.HOUR_WIDTH;

            if (showRight < start || showLeft >= end) {
                if (show.element.parentNode==this.listings) {
                    this.listings.removeChild(show.element);
                }
            } else {
                if (show.element.parentNode!=this.listings) {
                    this.listings.appendChild(show.element);
                }
            }
        }.bind(this));
    },
    addShowData: function(showData) {
        // Create ScheduleItems from the showData, but only for shows without existing schedule IDs
        var IDs = this.shows.map(function(show){return show.id});
        var shows = showData.reject(function(show){
            return IDs.indexOf(show.id)>=0;
        }).map(function(show){
            return new ScheduleItem(show, this.feed);
        }.bind(this));
        // layout the new shows, then add them to our full list and sort
        this.shows = this.shows.concat(shows);
        this.sortShows();
        this.layoutShows(shows);
    },
    setOffset: function(offset) {
        if(offset) {
            var scheduleOffset = offset;
            var len = this.shows.length;
            for(var i = 0; i < len; i++) {
                if (!this.shows[i].element.parentNode) continue;
                
                var showOffset = this.shows[i].element.offsetLeft;
                if(showOffset <= scheduleOffset+Schedule.VIEWPORT_WIDTH && showOffset > scheduleOffset) {
                    var showWidth = this.shows[i].element.offsetWidth;
                    var rightDifference = showWidth - (Schedule.VIEWPORT_WIDTH - (showOffset - scheduleOffset));
                    if(rightDifference > 0) {
                        this.shows[i].actions.style.paddingRight = rightDifference+'px';
                    }
                } else {
                    this.shows[i].actions.style.paddingRight = '0px';
                }
            }
        }
    },
    remove: function() {
        this.channel.parentNode.removeChild(this.channel);
        this.channel = null;
        this.listings.parentNode.removeChild(this.listings);
        this.listings = null;
        this.shows = null;
    }
});
ScheduleChannel.create = function(group, channelName, feed) {
    var channel = new Element('div', {'class':channelName});
    channel.appendChild(new Element('p', {'class':'channel'}).update(Schedule.Util.channelTitle(channelName)));
    if (Schedule.CHANNELS_IN_HD.indexOf(channelName)>=0) {
       channel.appendChild(new Element('p').update('also in HD'));
    }
    channel.appendChild(new Element('p', {'class':'zone '+feed}).update(feed.capitalize()));
    channel.appendChild(new Element('ol'));
    
    return new ScheduleChannel(group, channel);
}

/*
ScheduleItem
Represents a show in its given timeslot.
*/
ScheduleItem = Class.create({
    initialize: function(data, feed) {
        if (typeof data == "undefined") {
            return;
        }
        
        // save data
        this.id          = data.id; // 12345_STZ1_01-01-2008X01-01-01
        this.showID      = data.showID;
        this.title       = data.title;
        this.feed        = feed;
        this.startDate   = new Date(data.startDate);
        this.startTime   = data.startTime; // in minutes
        this.duration    = data.duration; // in minutes
        this.favorite    = (data.favorite) ? '&action=remove' : '&action=add';
        this.reminder    = (data.reminder) ? '&action=remove' : '&action=add';
        this.tId         = data.tId;
        this.eNbr        = data.eNbr;
        
        this.idQueryParams = '?version_id='+this.id.split('_')[0];
        this.idQueryParams += '&service_code='+this.id.split('_')[1];
        //this.idQueryParams += '&exhib_dtm='+this.id.split('_')[2].replace('X', '%20').gsub('-', ':');
        this.idQueryParams += '&exhib_dtm='+this.id.split('_')[2].split('X')[0].split('-')[1] + '/';
        this.idQueryParams += this.id.split('_')[2].split('X')[0].split('-')[2] + '/';
        this.idQueryParams += this.id.split('_')[2].split('X')[0].split('-')[0];
        this.idQueryParams += '%20';
        //this.idQueryParams += '&exhib_dtm='+this.id.split('_')[2].split('X')[1].gsub('-',':');
        this.idQueryParams += this.id.split('_')[2].split('X')[1].gsub('-',':');
        

        // create HTML
        this.element = new Element('li', {'id': this.id});
        this.para    = new Element('p');
        this.element.appendChild(this.para);
        
        this.info         = new Element('span', {'class': 'info'});
        this.titleWrapper = new Element('span', {'class': 'title', 'title': Schedule.MOVIE_DETAIL_URL+this.idQueryParams });
        this.titleNode    = new Element('a', {'href': '/titleLookup.aspx?tId='+this.tId+'&eNbr='+this.eNbr }).update(this.title);
        this.timeNode     = new Element('span', {'class': 'time'});
        this.info.appendChild(this.titleWrapper);
        this.titleWrapper.appendChild(this.titleNode);
        this.info.appendChild(this.timeNode);
                        
        this.actions      = new Element('span', {'class': 'actions'});
        this.favoriteNode = new Element('a', {'href': '/ajax/ScheduleFavorites.aspx'+this.idQueryParams+this.favorite, 'title': 'Add to Favorites', 'class': 'favorite'});
        this.reminderNode = new Element('a', {'href': '/ajax/ScheduleReminder.aspx'+this.idQueryParams+this.reminder, 'title': 'Set a Reminder', 'class': 'reminder'});
        if(!Starz.LOGGED_IN) { this.reminderNode.addClassName('login'); this.favoriteNode.addClassName('login'); }
        //if(!Schedule.HAS_REMINDERS) { this.reminderNode.addClassName('noreminder'); }
        if(!Schedule.HAS_REMINDERS) { this.reminderNode.addClassName(' '); }
        this.actions.appendChild(this.reminderNode);
        this.actions.appendChild(this.favoriteNode);
        
        this.para.appendChild(this.info);
        this.para.appendChild(this.actions);
        
        if (data.favorite)     this.element.addClassName('favorite');
        if (data.favoriteCast) this.element.addClassName('favorite_cast');
        if (data.reminder)     this.element.addClassName('reminder');
        
        this.setupRollovers();
        this.updateTimeNode();
    },
    importElement: function(element, feed) {
        this.element      = element;
        this.para         = element.getElementsByTagName('p')[0];
        var spans         = this.para.getElementsByTagName('span');
        infoSpan          = spans[0];
        titleSpan         = spans[1];
        timeSpan          = spans[2];
        actionsSpan       = spans[3];

        this.info         = infoSpan;
        this.titleWrapper = titleSpan;
        this.titleNode    = titleSpan.getElementsByTagName('a')[0];
        this.timeNode     = timeSpan;
        
        this.actions      = actionsSpan;
        var actionNodes   = this.actions.getElementsByTagName('a');
        this.reminderNode = actionNodes[0];
        this.favoriteNode = actionNodes[1];
        
        this.id     = parseInt(this.element.id.replace("schedule_item_",""));
        this.title  = this.titleNode.innerHTML;
        this.feed   = feed;

        // calculate start time (in minutes) and duration (in minutes)
        var datetime = this.timeNode.innerHTML.split(' ');
        var times    = datetime[1].split('-');
        var start    = times[0].match(/(\d{1,2}):(\d{2})(AM|PM)/);
        var end      = times[1].match(/(\d{1,2}):(\d{2})(AM|PM)/);
        
        this.startDate  = new Date(datetime[0]);
        this.startTime  = start[1]=="12" && start[3]=="AM" ? 0 : Number(start[1])*60;
        this.startTime += Number(start[2]);
        this.startTime += start[3]=="PM" && Number(start[1])<12 ? 720 : 0;
        
        var endTime  = end[1]=="12" && end[3]=="AM" ? 0 : Number(end[1])*60;
            endTime += Number(end[2]);
            endTime += end[3]=="PM" && Number(end[1])<12 ? 720 : 0;
        
        this.duration = endTime - this.startTime;
        if (this.duration <= 0) {
            // If the show crosses midnight, we need to adjust
            this.duration += 1440;
        }
        
        this.setupRollovers();
        this.updateTimeNode();
    },
    setupRollovers: function() {
        this.faderTimer = null;
        this.fader = new Fx.Style(this.actions, 'opacity', {
            duration: 100,
            transition: Fx.Transitions.linear,
            wait: false
        });
        
        // Short circuit Prototype's Event.observe method, to avoid extending elements for no reason
        // Be sure to add our functions to Event.cache so Prototype will clean up after us on unload in IE.
        var show = this.showActions.bind(this);
        var hide = this.hideActions.bind(this);
        Event.cache[this.id] = {'mouseover':show, 'mouseout':hide};
        
        if (this.para.addEventListener) {
            this.para.addEventListener('mouseover', show, false);
            this.para.addEventListener('mouseout', hide, false);
        } else {
            this.para.attachEvent('onmouseover', show);
            this.para.attachEvent('onmouseout', hide);
        }
        
        if (!this.isFavorite() && !this.hasReminder()) {
            this.actions.setStyle({opacity: 0});
        }
    },
    showActions: function() {
        this.attachTooltips();
        
        if (this.faderTimer) {
            window.clearTimeout(this.faderTimer);
            this.faderTimer = null;
        }
        if (!this.isFavorite() && !this.hasReminder()) {
            var opacity = this.actions.getStyle('opacity');
            if (opacity<1) {
                this.fader.custom(opacity, 1);
            }
        }
    },
    hideActions: function() {
        if (this.faderTimer) {
           window.clearTimeout(this.faderTimer);
           this.faderTimer = null;
        }
        if (!this.isFavorite() && !this.hasReminder()) {
            this.faderTimer = window.setTimeout(function(){
                var opacity = this.actions.getStyle('opacity');
                if (opacity > 0) {
                    this.fader.custom(opacity, 0);
                }
            }.bind(this), 50);
        }
    },
    attachTooltips: function() {
        // Attach tooltips
        if (this.tooltipsAttached) return;
        
        this.tooltipsAttached = true;
        Tooltip.attach(this.titleWrapper, ScheduleTooltip);
        
        if ($(this.favoriteNode).hasClassName('login') || $(this.reminderNode).hasClassName('login')) {
            Tooltip.attach(this.favoriteNode, LoginTooltip);
            Tooltip.attach(this.reminderNode, LoginTooltip);
        } else {
            Tooltip.attach(this.favoriteNode, FavoriteTooltip, new Array(this.feed, this.title));
            Tooltip.attach(this.reminderNode, ReminderTooltip, new Array(this.feed, this.title));
            this.reminderNode.observe('click', function(event) {
                if(event.target.rel == 'add') {
                    Tracking.Reminder(this.title);
                }
            });
            this.favoriteNode.observe('click', function(event) {
                if(event.target.rel == 'add') {
                    Tracking.Favorite(this.title);
                }
            });
        }
    },
    updateTimeNode: function() {
        var startHour   = Math.floor(this.startTime/60);
            startHour   = (startHour==0) ? 12 : startHour;
            startHour   = (startHour > 12) ? startHour-12 : startHour;
        var startMinute = this.startTime % 60;
            startMinute = (startMinute < 10) ? '0'+startMinute : startMinute;
            
        var endTime   = (this.startTime + this.duration) % 720;
        var endHour   = Math.floor(endTime/60);
            endHour   = (endHour==0) ? 12 : endHour;
            endHour   = (endHour > 12) ? endHour-12 : endHour;
        var endMinute = endTime % 60;
            endMinute = (endMinute < 10) ? '0'+endMinute : endMinute;
        
        this.timeNode.innerHTML = [startHour,":",startMinute,"-",endHour,":",endMinute].join("");
    },
    isFavorite: function() {
        return /(^|\b)favorite(\b|$)/.test(this.element.className);
    },
    hasFaveCast: function() {
        return /(^|\b)favorite_cast(\b|$)/.test(this.element.className);
    },
    hasReminder: function() {
        return /(^|\b)reminder(\b|$)/.test(this.element.className);
    }
});
ScheduleItem.create = function(element, feed) {
    var item = new ScheduleItem();
    item.importElement(element, feed);

    return item;
}
ScheduleItem.showIDRegExp = new RegExp("^.*?"+Schedule.MOVIE_DETAIL_URL, "i");

/* Create a generic tweening class */
Fx.Generic = Class.create(Fx.Base, {
	initialize: function(callback, options){
		this.callback = callback;
		this.setOptions(options);
	},
	increase: function(){
		this.callback(this.now);
	}
});