ITV.guide = function() {

	// canvas elements 
	var guide = document.getElementById("guide")
	var ctx = guide.getContext('2d')
	var ONE_MONTH = 1000 * 60 * 60 * 24 * 7 * 4
		
	// configuration
	var slotLength = 30 // in minutes
	var slotsPerHour = 60 / slotLength
	var slotsPerDay = 24 * slotsPerHour
	var offsetHours = ITV.offset / 60
	var offsetSlots = offsetHours * slotsPerHour
	var slotsAllowedBackward = slotsPerDay / 2
	var previousSlotsToLoad = slotsAllowedBackward
	var initialSlot = getSlotForTime(Date.now() - (previousSlotsToLoad * slotLength * 60 * 1000))
	var initialDate = getDateForSlot(initialSlot)
	var time = getSlotForTime(initialDate.getTime() - offsetHours * 60 * 60 * 1000).split(" ")
	var lineupId = ITV.lineupId
	var selected = {}
	var host = ITV.domain ? "http://" + ITV.domain : ""
	var dupChannels = {}
	
	// for keeping track of things
	var store = window.localStorage || {}
	var lastSlot
	var timeSlots = {}
	var channels = []
	var numChannels = 0
	var allChannels = []
	var channelKey = {}
	var moved = true
	var allSlots = []
	var urlsCalled = {}
	var slotKeys = {}
	
	// describe the guide
	var guideHeight = 0
	var guideWidth = 0
	
	// describes events
	var eventHeight = 55
	var eventWidth = 150
	
	// describes channels
	var channelWidth = 118
	var channelHeight = eventHeight
	var timeAxisHeight = 30
	
	// describes the grid
	var columns = 5
	var rows = 10
	var slotDuration = 30
	
	// scrollbar stuff
	var clicked = false
	var didScroll = false
	
	// animation
	var start_y = 0
	var start_x = offsetSlots * eventWidth
	var velocity_y = 0
	var velocity_x = 0
	var velocity_samples = 4 // the velocity will be the average of the previous samples
	var reDraw = true
	var topOfTheBottom = 0
	
	// touches
	var touch_start_y = 0
	var touch_start_x = 0
	
	// images
	var brokenImages = {}
	var goodImages = {}
	var isNew = new Image()
	var isHd = new Image()
	isNew.src= "/img/new.jpg"
	isHd.src = "/img/guide_hd_flag.png"
	
	
	// scrollbar
	var scrollbar = {
		x: 0,
		y: 0,
		width: 0,
		height: 0,
		position: function() {
			this.x = guideWidth - 40
			this.y = 50 + timeAxisHeight
			this.width = 30
			this.height = guideHeight - this.y + 20
		},
		draw: function(ctx) {
			var pct = start_y / topOfTheBottom
			var position =  Math.floor(pct * (this.height - this.y))
					
			// draw scrollbar
			ctx.beginPath()
			ctx.strokeStyle = "rgba(33,33,33,.8)"
			ctx.lineWidth = this.width
			ctx.lineCap = "round"	
			ctx.moveTo(this.x, this.y)
			ctx.lineTo(this.x, this.height)
			ctx.stroke()
			ctx.closePath()
			
			// draw indicator
			ctx.fillStyle = "#008CD0"
			ctx.fillRect(this.x - this.width / 2, this.y + position, this.width, 10)
		},
		touched: function(x, y) {
			var percents = null
			var xTouched = (x >= scrollbar.x - scrollbar.width / 2) && (x <= scrollbar.x + scrollbar.width / 2)
			var yTouched = (y >= scrollbar.y - scrollbar.width / 2 && y <= scrollbar.y + scrollbar.height)
			if (xTouched && yTouched) percents = {
				x: 0, // not yet implemented
				y: Math.min((y - scrollbar.y) / (scrollbar.height - scrollbar.y) , 1)
			}				
			return percents
		}
	}
	
	/************
	 * Init
	 ************/
	
	// make sure we're the right size
	resizeGrid()
	
	// get channels
	getChannels(function(channels) {

		// temp way to add space at the bottom
		channels.push({
			 sourceId: "FAKECHANNEL"
			 , tier: 3
			 , channel: ""
			 , callSign: ""
		})

		var newSourceId = ""
		var oldSourceId = ""
		var channelNum = 0
		allChannels = channels
		numChannels = channels.length
		for (var i = 0; i < numChannels; i++) {
			
			// save out the old source Id
			oldSourceId = channels[i].sourceId 
			channels[i].origSourceId = oldSourceId
			
			// check for dups
			if (typeof channelKey[oldSourceId] === 'number') {

				// create the dup channels array
				if (!dupChannels[oldSourceId]) dupChannels[oldSourceId] = [] 

				// set the channel num
				channels[i].channelNum = i 
				
				// save the dup channel
				dupChannels[oldSourceId].push(channels[i])
				
				channelNum = channels[i].channel // parseInt(channels[i].channel, 10)
				
				// create a new source id and save it
				channels[i].sourceId = oldSourceId + "|" + channelNum

				// console.log("found dup for " + oldSourceId + " at channel " + channelNum)

			}
			
	
			// save the sourceId for the channelKey
			channelKey[channels[i].sourceId] = i
		}
		
		// with channels we can now setup the slots array
		createEmptySlots(numChannels)
		
		// get first set of events
		get(ITV.urls.events.replace(":lineupId", lineupId).replace(":date", time[0]).replace(":time", time[1]), function(events) {
			fillSlots(events)
			setTopOfTheBottom()
			reDraw = true
			resizeGrid()
			draw()
		})
	})
	
	
	/**********
	 * Events
	 *********/
	
	$(window).resize(resizeGrid)
	
	$(".picker li").live("click", function() {
	
		var date = ""
		var slot = ""
		var detail = ""
		var time = ""
		var $picker = $(this).closest(".picker")
		var slotNum = 0
		
		// don't bother with the title text
		if ($(this).hasClass("title")) return
		
		// select this one, de-select the rest
		$(this).addClass("selected").siblings().removeClass("selected")
	
		// get the data
		date = $(this).data("date")
		detail = $(this).data("detail")
		time = $(this).text() + " " + new Date().getFullYear() + " " + $("time.time").text()
		slot = getSlotForTime(time)
		slotNum = getSlotKey(slot)
		
		// figure out what to do with the data
		start_x = (slotNum - 1) * eventWidth + channelWidth - (previousSlotsToLoad * eventWidth)
		
		// reDraw the grid after move
		reDraw = true
		
		// update the text
		$picker.next("time").html(date + " <span class='detail'>" + detail + "</span>")
		$picker.addClass("closed")
		setTimeout(function() {
			$picker.removeClass("closed")
		}, 50)	
	})
	
	$("#guide").bind("touchdown mousedown", function(e) {
	        touch_start_x = e.screenX
	        touch_start_y = e.screenY
			clicked = true
	        didScroll = false
			handleScrollbarTouch(e)
			e.preventDefault()
	})
	
	$(document).bind("touchmove mousemove", function(e) {
		if (clicked) {
			if (handleScrollbarTouch(e)) return
			handleGridTouch(e)
		}
		e.preventDefault()
	})
	
	$(document).bind("touchend mouseup", function(e) {
		clicked = false
	})
	
	$("#guide").bind("mousewheel MozMousePixelScroll", handleScroll)
	
	
	$("#guide").click(function(e) {
		if (handleScrollbarTouch(e) || didScroll) return
		handleShowcard(e)
	})
	
	$(document).keydown(function(e) {
		var modifier = e.shiftKey ? 10 : 1
		var velocityY = eventWidth
		var velocityX = eventHeight		
		switch (e.keyCode) {
			case 38: // up key-
				reDraw = true
				start_y -= velocityY * modifier
			break;
			case 40: // down key
			   reDraw = true
			   start_y += velocityY * modifier
			break;
			case 39: // right key
			   reDraw = true
			   start_x += velocityX * modifier / 2
			break;
			case 37: // left key
			   reDraw = true
			   start_x -= velocityX * modifier / 2
			break;                  
			case 32: // space bar
			   reDraw = true
			   start_y = e.shiftKey ? 0 : topOfTheBottom
			break;
		}
	})
	
	
	$("#showcard .favorite").live("click", function(e) {
		e.preventDefault()
		$.ajax({
			url: ITV.urls.favorite.replace(":showcardId", selected.seriesId),
			type: selected.isFavorite ? "DELETE" : "PUT",
			success: function(e) {
				selected.isFavorite = !selected.isFavorite
				$("#showcard").find(".favorite span").text(selected.isFavorite ? "Remove" : "Add")
			},
			error: function(e) {
				alert("Could not modify favorites!")
			}
		})
	})
	
	$("#showcard .close").live("click", function(e) {
		e.preventDefault()
		highlightEvent(selected, false)
		reDraw = true
		$("#showcard").fadeOut(300)
	})
	
	function highlightEvent(event, highlighted) {
		if (!event || !event.seriesId) return
		if (highlighted === undefined) highlighted = true
		event.selected = highlighted
		var slotNum = event.slotNum ? event.slotNum : getSlotKey(event.slots[0])
		var events = []
		var channelIndex = channelKey[event.sourceId]
		var numSlots = event.slots.length
		var numEvents = 0
		var i = 0
		var j = 0
		for (i = 0; i < numSlots; i++) {
			events = allSlots[slotNum + i][channelIndex]
			numEvents = events.length
			for (j = 0; j < numEvents; j++) {
				if (events[j].dt === event.dt) {
					events[j].selected = highlighted			
				}
			}
		}
	}
	
	$("#showcard .checkIn").live("click", function(e) {
		e.preventDefault()
		ITV.showShareUI({
			showcardId: selected.seriesId,
			title: selected.title,
			link: "http://" + location.href.split("/")[2] + "/showcards/" + selected.seriesId + "#" + selected.programId,
			description: "" // I wish we had this
		})
	})
	
	/*****************
	 * Event handling
	 *****************/
	
	function handleShowcard(e) {
		if (!e.offsetX) e.offsetX = e.layerX - $(e.target).position().left
		if (!e.offsetY) e.offsetY = e.layerY - $(e.target).position().top

		highlightEvent(selected, false)

		var slotNum = Math.floor((start_x + e.offsetX - channelWidth) / eventWidth) + previousSlotsToLoad
		var channelIndex = parseInt((start_y + e.offsetY - timeAxisHeight) / eventHeight, 10)
		if (slotNum < 0 || channelIndex < 0) return

		var events = allSlots[slotNum][channelIndex]
		if (!events.length) return
		
		var event = {}
		var x = (slotNum - previousSlotsToLoad) * eventWidth - start_x + channelWidth + events[0].offsetX
		var offset = e.offsetX - x
		for (var i = 0; i < events.length; i++) {			
			if (!events[i + 1]) break;
			if (offset > events[i].offsetX && offset < events[i + 1].offsetX) {
				event = events[i]
				break
			}
		}
		if (!event.seriesId) event = events[events.length - 1]

		highlightEvent(event)
		selected = event
		reDraw = true
		
		displayShowcard(e, event)
		
	}

	function resizeGrid() {
		var $nav = $("body > header")
		var start_x = 0
		var end_x = window.innerWidth
		var height = window.innerHeight - 55 - 55 - 8
		var width = end_x - start_x
		var providerHeight = $("#provider").outerHeight()
		var navHeight = $("#guideNav").outerHeight()
	
		// cache the new sizes
		guideWidth = width - 30
		guideHeight = height - providerHeight - navHeight - 50
		
		// set the guide sizes
		guide.width = guideWidth
		guide.height = guideHeight
		
		scrollbar.position()
		reDraw = true
	}
	
	function handleGridTouch(e) {
		moveVelocityY(touch_start_y - e.screenY)
		moveVelocityX(touch_start_x - e.screenX)
	
	    start_y += touch_start_y - e.screenY
	    start_x += touch_start_x - e.screenX
	
	    touch_start_x = e.screenX
	    touch_start_y = e.screenY
	
	    reDraw = true
	    didScroll = true
	}
	
	function handleScroll(e) {
	
		$("#showcard").fadeOut(300)
		
		var evt = e.originalEvent
		var dx = evt.wheelDeltaX
		var dy = evt.wheelDeltaY
	
		// moz stuff
		if (evt.axis && evt.detail) {
	        if (evt.axis === evt.HORIZONTAL_AXIS) {
				dx = -evt.detail
	        } else {
				dy = -evt.detail	
	        }
		} 
		
		// safari stuff
		if (dx % 120 === 0 && dy % 120 === 0) {
			dx /= 120
			dy /= 120
		}
	
		dx = dx || 0
		dy = dy || 0
		
		start_x -= dx
		start_y -= dy * 10
	
		reDraw = true
	}
	
	function handleScrollbarTouch(e) {
		if (!e.offsetX) e.offsetX = e.layerX - $(e.target).position().left
		if (!e.offsetY) e.offsetY = e.layerY - $(e.target).position().top
		var x = e.offsetX
		var y = e.offsetY
		var percents = scrollbar.touched(x, y)
		if (percents) {
			$("#showcard").fadeOut(300)
			start_y = percents.y * topOfTheBottom - timeAxisHeight
			reDraw = true
		}
		return percents
	}
	
	
	/********************
	 * Canvas Drawing
	 *******************/
	
	function draw() {
		var visible = []
		if (reDraw) {
			reDraw = false
			resetBounds()
			clearGrid()
			drawGrid()
			drawSlots()
			drawChannels()
			drawTimeAxis()
			drawScrollbar()
			moved = true
		}
	
		if(!clicked && velocity_y || velocity_x) {
			start_x += velocity_x
			start_y += velocity_y
			reDraw = true
		}
	
		moveVelocityY(0)
		moveVelocityX(0)
	
		requestAnimFrame(draw)
	
	}
	
	setInterval(updateTimeHeader, 250)
	setInterval(checkSlotNeeds, 250)
	
	function drawTimeAxis() {
	
		var width = eventWidth
		var height = timeAxisHeight
		var visible = visibleSlots()
		var x = 0
		var title = []
		for (var slotNum = visible.lower; slotNum <= visible.upper; slotNum++) {
	
			x = slotNum * eventWidth - start_x + channelWidth - previousSlotsToLoad * eventWidth
	
			// background
			ctx.fillStyle = "#232323"
			ctx.fillRect(x, 0, width, height)
	
			// text
			title = getTitleForSlot(getSlotForKey(slotNum - offsetSlots))
			ctx.fillStyle = "white"
			ctx.font = "bold 16px HelveticaNeue"
			ctx.fillText(title[0], x + 35, 20)	
			ctx.fillStyle = "#ccc"
			ctx.font = "16px HelveticaNeue"
			ctx.fillText(title[1], x + 35 + title[0].length * 7, 20)	
	
			// borders
			ctx.fillStyle = "#2E2E2E"
			ctx.fillRect(x, height, width, 1)
			ctx.fillRect(x, 0, 1, height)
		
		}
		
		// background
		ctx.fillStyle = "#232323"
		ctx.fillRect(0, 0, channelWidth, height)
		
		// edit btn text
		// ctx.fillStyle = "#009FD7"
		// ctx.font = "bold 14px HelveticaNeue" 
		// ctx.fillText("Change Prvdr", 20, 20)
		
		// separator
		ctx.fillStyle = "#2E2E2E"
		ctx.fillRect(channelWidth, 0, 1, height)
		ctx.fillRect(0, height, channelWidth, 1)
		
	}
	
	function drawSlots() {
		var slots = visibleSlots()
		var channels = visibleChannels()
		var lowerSlots = slots.lower
		var upperSlots = slots.upper
		var channelsStart = channels.start
		var channelsEnd = channels.end
		var len = 0
		for (var i = lowerSlots; i < upperSlots; i++) {
			for (var j = channelsStart; j < channelsEnd; j++) {			
				len = allSlots[i][j].length || 0
				for (var k = 0; k < len; k++) {
					drawEvent(allSlots[i][j][k], j)
				}	
			}	
		}
	}
	
	function clearGrid() {
		ctx.clearRect(0,0,guideWidth,guideHeight)
	}
	
	function drawGrid() {
		
		// background
		ctx.fillStyle = "#FFF"
		ctx.fillRect(0,0, guideWidth, guideHeight)
	
		ctx.fillStyle = "#CCC"
		
		var startRow = parseInt(start_y / eventHeight, 10)
		
		while(startRow * eventHeight < guideHeight + start_y) {
			ctx.fillRect(0, startRow * eventHeight - start_y + timeAxisHeight, guideWidth, 1)
			startRow++
		}
	
		// outer lines	
		ctx.fillRect(guideWidth - 1, 0, guideWidth, guideHeight)
		ctx.fillRect(0, guideHeight - 1, guideWidth, guideHeight)
	
	}
	
	function drawEvent(event, j) {
		
		// calculations
		var width = event.width ? event.width : event.duration / slotLength * eventWidth
		var height = eventHeight
		var slotNum = event.slotNum ? event.slotNum : getSlotKey(event.slots[0])
		var minute = parseInt(event.dt.split(" ")[1].split(":")[1], 10)
		var off = minute >= slotLength ? event.minute - slotLength : minute
		var offsetX = off / slotLength * eventWidth
		var x = (slotNum - previousSlotsToLoad) * eventWidth - start_x + channelWidth + offsetX
		var y = channelKey[event.sourceId] * eventHeight - start_y + timeAxisHeight
		// if (j.match) console.log("#" + j, event.sourceId, y, event)
		var textX = (slotNum < getCurrentKey()) ? channelWidth  + 4 : x + 10
		event.width = width
		event.slotNum = slotNum
		event.minute = minute
		event.off = off
		event.offsetX = offsetX
			
		if (!x || !y || !width || !height) return
		
		// background
		ctx.fillStyle = event.selected ? "#333" : "white"
		ctx.fillRect(x, y, width, height)
	
		// text
		ctx.fillStyle = "#027FDD"
		ctx.font = "12pt Arial" 
		ctx.fillText(event.title, textX, y + eventHeight / 2)	
	
		// borders
		ctx.fillStyle = "#CCC"
		ctx.fillRect(x, y, width, 1)
		ctx.fillRect(x, y + height, width, 1)
		ctx.fillRect(x, y, 1, height)
	
		// flags
		if (event.new) ctx.drawImage(isNew, x + width - isNew.width - 10, y + height - 5 - isNew.height)
		if (event.hd)  ctx.drawImage(isHd,  x + width - isHd.width,  y + height / 2 - isHd.height)
	}
	
	function drawChannels(startChannel, endChannel) {
		var channels = visibleChannels()
		var start = channels.start
		var end = channels.end
	
		// draw background
		var gradient = ctx.createLinearGradient(0, 0, channelWidth, 0)
		gradient.addColorStop(0, "#008CD0")
		gradient.addColorStop(0.33, "#008CD0")
		gradient.addColorStop(1, "#0068A0")
		ctx.fillStyle = gradient
		ctx.fillRect(0, 0, channelWidth, guideHeight)
	
		// draw channels
		for (var i = start; i < end; i++) {
			drawChannel(allChannels[i])
		}
	}
	
	function drawScrollbar() {
		scrollbar.draw(ctx)
	}
	
	function drawChannel(channel) {
		var position = channelKey[channel.sourceId]
		var width = channelWidth
		var height = channelHeight
		var x = 0
		var y = position * channelHeight - start_y + timeAxisHeight
		var src = "http://media.i.tv.s3.amazonaws.com/channels/46x35/" + channel.origSourceId + ".png"
		var channelImageWidth = 46
		var channelImageHeight = 35
		var img
		
		// borders
		ctx.fillStyle = "#309CCE"
		ctx.fillRect(x, y, width, 1)
		ctx.fillStyle = "#006B9A"
		ctx.fillRect(x, y + height - 1, width, 1)
		
		// draw image, but not if they're broken
		if (goodImages[src]) {
			ctx.drawImage(goodImages[src], x, y + 10, channelImageWidth, channelImageHeight)
		} else if (!brokenImages[src]) {
			img = new Image()
			img.onerror = function() {
				brokenImages[this.src] = 1
			}
			img.onload = function() {
				goodImages[src] = this
				reDraw = true
			}
			img.src = src
		}
	
		// draw text
		ctx.fillStyle = "white"
		ctx.font = "10pt Arial"
		ctx.fillText(channel.channel, x + 55, y + 25)
		ctx.fillText(channel.callSign, x + 55, y + 40)
	}
	
	/***************
	 * Ajax Helpers
	 ***************/
	 
	// try to use local storage for the channels
	function getChannels(cb) {
		var lastUpdate = store.lastUpdate || 0
		var channelsAreRecent = (lastUpdate + ONE_MONTH) > Date.now()
		var channels = store.lineupId === lineupId ? store.channels : null
		if (channels && channelsAreRecent) return cb(JSON.parse(channels))
		get(ITV.urls.channel.replace(":lineupId", lineupId), function(channels) {
			store.lineupId = lineupId
			store.channels = JSON.stringify(channels)
			store.lastUpdate = Date.now()
			cb(channels)
		})
	}
	
	function get(url, cb) {
		if (urlsCalled[url]) return cb(null)
		urlsCalled[url] = true
		$.getJSON(url, function(json) {
			cb(json)
		})	
	}
	
	function fillSlots(events) {
		if (!events) return
		var len = events.length
		var event = {}
		var oldEvents = []
		var slot = ""
		var slotKey = 0
		var channelNum = 0
		var channel = {}
		var sourceId
		var newEvent = {}

		// console.log(events)
		// var TEST_SOURCE_ID = allChannels[68].origSourceId
		// console.log(allChannels[68], allChannels[68].sourceId, allChannels[68].origSourceId)
		
		for (var i = 0; i < len; i++) {
			event = events[i]
			for (var j = 0; j < event.slots.length; j++) {
				sourceId = event.sourceId || false
				slot = event.slots[j]
				slotKey = getSlotKey(slot)
				channelNum = channelKey[sourceId]

				// return if we have a bad slot key
				if (slotKey <= 0) return
				
				// add the event to the right place
				allSlots[slotKey][channelNum].push(event)				
				
				// handle dups by creating a copy of the event
				if (dupChannels[sourceId]) {
					for (var k = 0; k < dupChannels[sourceId].length; k++) {
						newEvent = $.extend({}, event)
						channel = dupChannels[sourceId][k]
						channelNum = channel.channelNum
						newEvent.sourceId = channel.sourceId
						allSlots[slotKey][channelNum].push(newEvent)				
						// if (sourceId === TEST_SOURCE_ID) {
						//	console.log("Applying dup for " + sourceId)
						//	console.log("Adding to slot " + slotKey + " at channel number " + allChannels[channelNum].channel + " at index " + channelNum, newEvent) 
						// }
					}
				}
			}
		}
		
		// console.log(allSlots[slotKey][68])
		
		reDraw = true
	}		
	
	/**************
	 * Utilities
	 **************/
	 
	function resetBounds() {
		start_y = Math.min(Math.max(0, start_y), topOfTheBottom)
		start_x = Math.max(-slotsAllowedBackward * eventWidth, start_x)
	}
	
	function visibleSlots() {
	    var lowerSlot =  Math.max(0, Math.floor(start_x / eventWidth) + previousSlotsToLoad - 1) 
	    var numSlots = Math.ceil(guideWidth / eventWidth) + 1
	    var upperSlot = lowerSlot + numSlots
		var visible = {
			lower: lowerSlot,
			upper: upperSlot,
			length: numSlots
		}
		return visible
	}
	 
	function visibleChannels() {
		var startRow = Math.floor(start_y / eventHeight)
		var height = Math.ceil(guideHeight / eventHeight)
		var endRow = startRow + height
		return {
			start: startRow,
			end: endRow,
			height: height
		}
	}
	
	function checkSlotNeeds() {
		if (!moved) return
		var visible = visibleSlots()
		var time = []
		for(var i = visible.lower; i < visible.upper; i += 4) {
			time = getSlotForKey(i).split(" ")		
			get(ITV.urls.events.replace(":lineupId", lineupId).replace(":date", time[0]).replace(":time", time[1]), fillSlots)
		}
		moved = false
	}
	
	function getSlotForKey(key) {
		var start = initialDate.getTime()
		var diff = key * 30 * 1000 * 60
		var d = new Date(start + diff)
		return getSlotForTime(d)
	}
	
	function getSlotForTime(time) {
		var d = new Date(time)
		var date = d.getFullYear() + "-" + zeroPad(d.getMonth() + 1) + "-" + zeroPad(d.getDate())
		time = zeroPad(d.getHours()) + ":" + zeroPad(d.getMinutes() > 29 ? 30 : 0)	
		return date + " " + time
	}
	
	function getStringForSlot(slot) {
		return slot.split(" ")[1]	
		var date = new Date(slot)
		var timeString = date.getHours() % 12 + ":"+ (date.getMinutes() || "00") +" "+ (date.getHours() > 12 ? "PM" : "AM")
		return timeString
	}
	
	function getDateForSlot(slot) {
		return new Date(slot.replace(/-/g, "/"))
	}
	
	function getSlotKey(slot) {
		if (slotKeys[slot]) return slotKeys[slot]
		var start = initialDate.getTime()
		var end = getDateForSlot(slot).getTime()
		var diff = end - start
		var key = diff / 1000 / 60 / 30
		slotKeys[slot] = key
		return key
	}
	
	function zeroPad(time) {
		var timeStr = time.toString()
		return timeStr.length > 1 ? timeStr :  "0" + timeStr
	}
	
	function setTopOfTheBottom() {
		topOfTheBottom = (numChannels * eventHeight - parseInt(guideHeight / eventHeight, 10) * eventHeight) - timeAxisHeight
	}
	
	function getTitleForSlot(slot) {
		if (timeSlots[slot]) return timeSlots[slot]
		var title = []
		var both = slot.split(" ")
		var date = both[0]
		var time = both[1].split(":")
		var hours = time[0]
		var minutes = time[1]
		var amPm = "AM"
		if (hours >= 12) {
			hours -= 12
			amPm = "PM"
		}
		if (hours < 1) {
			hours = "12"
		}
		title = [parseInt(hours, 10) + ":" + minutes + " ", amPm]
		timeSlots[slot] = title
		return title
	}
	
	function moveVelocityX(newVelocity) {
		if (newVelocity === 0 && Math.abs(velocity_x) < 0.01) {
			velocity_x = 0
			return
		}
		velocity_x = (newVelocity+velocity_x*(velocity_samples-1))/velocity_samples
	}
	
	function moveVelocityY(newVelocity) {
		if (newVelocity === 0 && Math.abs(velocity_y) < 0.01) {
			velocity_y = 0
			return
		}
		velocity_y = (newVelocity+velocity_y*(velocity_samples-1))/velocity_samples
	}
	
	// create a grid of all possible channels / slots
	function createEmptySlots(numChannels) {
		var numSlots = 2 * 7 * 24 * 2 // 2 weeks of half an hour
		var slot
		for (var i = 0; i < numSlots + previousSlotsToLoad; i++) {
			slot = []
			for (var j = 0; j < numChannels; j++) {
				slot.push([])
			}
			allSlots.push(slot)
		}
	}
	
	function getCurrentKey() {
		var key = Math.floor((start_x + channelWidth + previousSlotsToLoad * eventWidth) / eventWidth)
		return key
	}
	
	function updateTimeHeader() {
		var key = getCurrentKey()
		var slot = getSlotForKey(key - offsetSlots)
		var date
		var t
		if (slot === lastSlot) return
		lastSlot = slot
		date = getDateForSlot(slot)
		t = getTitleForSlot(slot)
		$("#guideNav li[data-date='" + getDay(date.getDay()) + ", ']").addClass("selected").siblings().removeClass("selected")
		$("#guideNav time.date").html(getDay(date.getDay()) + ', <span class="detail">' + getMonth(date.getMonth()) + ' ' + date.getDate() + ' </span>')
		$("#guideNav time.time").html(t[0] + '<span class="detail">' + t[1] + ' </span>')
	}
	
	function getMonth(month) {
		var months = [
			"Jan",
			"Feb",
			"Mar",
			"Apr",
			"May",
			"Jun",
			"Jul",
			"Aug",
			"Sep",
			"Oct",
			"Nov",
			"Dec"
		]
		return months[parseInt(month, 10)]
	}
	
	function getDay(day) {
		var days = [
			"Sunday",
			"Monday",
			"Tuesday",
			"Wednesday",
			"Thursday",
			"Friday",
			"Saturday"
		]
		return days[parseInt(day, 10)]
	}
	
	
	function prettyDateWithDuration(date, duration) {
		var d = getDateForSlot(date)
		d.setTime(d.getTime() - offsetHours * 60 * 60 * 1000)
		var slot = getSlotForTime(d.getTime())
		var title = getTitleForSlot(slot)
		var pretty = ""
		pretty += getDay(d.getDay()).slice(0,3) + ". "
		pretty += getMonth(d.getMonth()) + " "
		pretty += d.getDate() + ", "
		pretty += title[0] + " "
		pretty += title[1] + " - "
		title = getTitleForSlot(getSlotForTime(d.getTime() + duration * 60 * 1000))
		pretty += title[0] + " "	
		pretty += title[1]
		return pretty
	}

	
	function displayShowcard(e, event) {
	
		var img = ITV.urls.img.replace(":genre", event.genre).replace(":imageId", event.seriesId).replace(":showcardId", event.seriesId)
			, showcardUrl = ITV.urls.details.replace(":showcardId", event.seriesId).replace(":programId", event.programId)
			, channelIndex = channelKey[event.sourceId]
			, channel = allChannels[channelIndex]
			, channelText = channel.callSign + " " + channel.channel 
			, date = prettyDateWithDuration(event.dt, event.duration)
			, time = ""
			, desc = ""

		$.get(showcardUrl, function(data) {
			event.isFavorite = data.isFavorite
			var episode  = "Season " + data.season + ", Episode " + data.episode
			var x = (e.screenX + $("#showcard").outerWidth()) > $(document).width() ? $(document).width() - $("#showcard").outerWidth() - 50 : e.offsetX
			var y = (e.screenY + $("#showcard").outerHeight()) > $(document).height() ? $(document).height() - $("#showcard").outerHeight() - 200 : e.offsetY
			$("#showcard").fadeIn(300).css({left: x, top: y})
				.find(".img").css({backgroundImage: "url(" + img + ")"}).end()
				.find(".pulse").attr("href", ITV.urls.pulse.replace(":programId", event.programId)).end()
				.find(".title").text(event.episodeTitle || event.title).end()
				.find(".episode").text(episode).show().end()
				.find(".time").text(date).end()
				.find(".more").attr("href", "/showcards/" + event.seriesId + "#" + event.programId).end()
				.find(".new").css("display", event["new"] ? "block" : "none").end()
				.find(".channel").text(channelText).end()
				.find(".favorite span").text(data.isFavorite ? "Remove" : "Add").end()
				.find(".description").text(data.description || "")
			if (!data.season || !data.episode) $("#showcard").find(".episode").hide()			
		})
			
	}
	
}
