/* 
Schedule
Main Schedule class. Controls schedule groups, loading show data, and hour-to-hour transitions
*/

//alert('schedule.js');

Schedule = Class.create({
    initialize: function (element) {

        //debugger;

        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 newChannelName = "";
            if (channelName == "encore_wam") {
                newChannelName = "Encore Family";
            }
            else if (channelName == "encore_mystery") {
                newChannelName = "Encore Suspense";
            }
            else
                newChannelName = channelName;
            var channelOption = new Option(Schedule.Util.channelTitle(newChannelName), 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_espanol', '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_comedy', 'starz_cinema', 'starz_inblack', 'starz_edge', 'starz_comedy', 'encore', 'encore_action', 'encore_drama', 'indieplex', 'retroplex'],
    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.toUpperCase() + ' 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.url = data.url;
        this.tId = data.tId;
        this.eNbr = data.eNbr;

        this.idQueryParams = '?version_id=' + this.id.split('_')[0];
        this.idQueryParams += '&eNbr=' + data.eNbr;
        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' });
        if (data.canPromoteNow == "1") {
            this.titleWrapper = new Element('span', { 'class': 'title', 'title': Schedule.MOVIE_DETAIL_URL + this.idQueryParams });
            this.titleNode = new Element('a', { 'class': 'highlight', 'href': this.url }).update(this.title);
        } else {
            this.titleWrapper = new Element('span', { 'class': 'title' });
            this.titleNode = new Element('a').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;

        if ($(this.titleWrapper).readAttribute('title')) {
            Tooltip.attach(this.titleWrapper, ScheduleTooltip);
        }

        if ($(this.favoriteNode).hasClassName('login') || $(this.reminderNode).hasClassName('login')) {
			// this is now handled in JQuery
		}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);
    }
});
