﻿/// <reference path="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5-vsdoc.js" />
/// <reference path="http://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.js" />
/// <reference path="/Scripts/BondiTouch.js" />
/// <reference path="/Scripts/BondiTouch.Core.js" />
/// <reference path="/Scripts/BondiTouch.Workflow.js" />
/// <reference path="/Scripts/BondiTouch.Tasks.js" />

(function () {

	var $ = jQuery,
		_b = BondiTouch, 
		touchSupported = "ontouchstart" in window,
		EV_DOWN = touchSupported ? "touchstart" : "mousedown",
		EV_MOVE = touchSupported ? "touchmove" : "mousemove",
		EV_UP = touchSupported ? "touchend" : "mouseup",

	Events = _b.Events = {

		// instance of iScroll powering the filmstrip zoom
		zoomer: null,

		drag: null, // becomes an object during a drag
		velocityTimeWindow: 0.25, // in seconds
		navVelocityThreshold: 100, // pixels/second velocity threshold for flick
		navDistanceFactor: 0.3, // 1.0 = the width of the screen

		init: function () {

			var $filmstrip = $(_b.Core.constants.imageContainer);

			$(window).hashchange(Events.onHashChange);
			
			$(window).bind("resize", $.debounce(250, Events.onResize));
			
			// note use of live() instead of bind() since many thumbnail images,
			// and they may not be loaded yet. better perf + no timing errors!
			$(_b.Core.constants.thumbnailContainer + " div img").live("click", Events.onThumbnailClick);
			
			$("#ftrPageBack").bind("click", Events.onThumbStripLeft);
			$("#ftrPageForward").bind("click", Events.onThumbStripRight);

			// Prevent the elastic bounce in iOS when the user flicks the page up or down
			$(document).bind("touchmove", Events.cancelEvent);

			// wrap "click" events to guard against pinch-zoom clicks
			// TODO optimize single-tap perf on edges (no double-tap-to-zoom support)
			Events.bindClick($filmstrip, Events.onFilmStripClick);

			// TODO our drag/swipe implementation needs improvement
			Events.bindDrag($filmstrip, Events.dragStart, Events.dragMove, Events.dragEnd);

			// bind iscroll to filmstrip container for zooming into current spread
			// TODO improve double-tap to fit width of page instead of just 2x
			// TODO improve multi-touch pinch zoom to make it 1:1 with fingers
			// TODO improve bounce behavior at edges to be smoother
			Events.zoomer = new iScroll($filmstrip[0].parentNode, {
				bounceLock: true,
				lockDirection: false,
				zoom: true,
				zoomMin: 1,
				zoomMax: Events.getMaxZoom(),
				zoomWheel: true,
				onZoomStart: Events.onZoomStart,
				onZoomEnd: Events.onZoomEnd
			});
			
		},

		cancelEvent: function (e) {
			e.preventDefault();
			// TODO fallback for other browsers?
		},
		
		// NAVIGATION EVENTS:
		
		onHashChange: function (e) {
			if (_b.Core.navState.hash === location.hash) {
				return;
			}
			
			var hashFragments = _b.Utility.reduceUrlFragments(location.hash),
				pg = _b.Core.pageListing.getPageByName(hashFragments[0]);
			
			_b.Events.zoomOut();
			_b.Core.navState.currentIndex = pg.index;
			_b.Workflow.executePagination();
		},
		
		onResize: function (e) {
			_b.IssueListing && _b.IssueListing.handleResize();
			_b.Events.zoomOut();
			_b.Workflow.executeResize();
			
			// update max zoom depending on mode
			Events.zoomer.options.zoomMax = Events.getMaxZoom();
		},
		
		onFilmStripClick: function (e) {
			var clickX = event.pageX,
				left = (_b.Core.navState.windowW / 4),
				right = (left * 3),
				newIdx = -1;
			
			_b.Tasks.hideOverlays();
			if (clickX > left && clickX < right) {
				//TODO: Replace with a ToggleChrome task?
				if (_b.Core.navState.headerVisible && _b.Core.navState.footerVisible) {
					_b.Tasks.hideChrome();
				}
				else {
					_b.Tasks.displayChrome();
				};
				return false;
			}
			else if (clickX < left) {
				newIdx = _b.Core.pageListing.getNextIndex(-1);
			}
			else if (clickX > right) {
				newIdx = _b.Core.pageListing.getNextIndex(1);
			}
			
			if (newIdx !== -1) {
				if (_b.Core.navState.itemsInTransition === 0) {
					_b.Events.zoomOut();
					_b.Core.navState.currentIndex = newIdx;
					_b.Workflow.executePagination();
					_b.Tasks.hideChrome();
				};
			};
		},
		
		onThumbnailClick: function (e) {
			///<summary>thumbnail click executes navigate</summary>
			var pageName = event.target.id.replace("pft", "")
			var pg = _b.Core.pageListing.getPageByName(pageName);
			var newIdx = pg.index;
			if (_b.Core.navState.itemsInTransition === 0) {
				_b.Events.zoomOut();
				_b.Core.navState.currentIndex = newIdx;
				_b.Workflow.executePagination();
			};
		},
		
		onThumbStripLeft: function (e) {
			var finalOffset = (_b.Core.navState.thumbOffset + (_b.Core.navState.windowW - 150));
			if (finalOffset > 50) {
				finalOffset = 50;
			};
			var transformFooter = _b.Core.navState.translateStart + finalOffset + "px, 0" + _b.Core.navState.translateEnd;
			$("#ftrItemWrapper").css(_b.Core.navState.browserTag + "transform", transformFooter);
			_b.Core.navState.thumbOffset = finalOffset;
		},
		
		onThumbStripRight: function (e) {
			///<summary>Take the current offset, move over the the width of the page - 150 to allow for the last page to become the first in the scroll area.</summary>
			var finalOffset = (_b.Core.navState.thumbOffset - (_b.Core.navState.windowW - 150));
			var maxScroll = (_b.Core.navState.navWidth - (_b.Core.navState.windowW - 10));
			maxScroll = maxScroll * -1;
			if (maxScroll > finalOffset) {
				finalOffset = maxScroll;
			}
			var transformFooter = _b.Core.navState.translateStart + finalOffset + "px, 0" + _b.Core.navState.translateEnd;
			$("#ftrItemWrapper").css(_b.Core.navState.browserTag + "transform", transformFooter);
			_b.Core.navState.thumbOffset = finalOffset;
		},
		
		// TOUCH HELPERS:

		// gets the number of touches still on the screen after this event,
		// or whether the mouse is still down after this event.
		// assumption: mousemove events are only passed to this function
		// during drags, i.e. after a mouseup and before a mousedown.
		getNumTouches: function (e) {
			e = e.originalEvent || e;   // get past jQuery's wrapper
			return e.touches ? e.touches.length :
						(e.type === EV_UP ? 0 : 1);
		},

		// binds the given dragstart, dragmove and dragend handlers,
		// abstracting away touch vs. mouse and being efficient.
		// also is smart/robust to multi-touch -- doesn't prematurely
		// unbind the dragmove/dragend handlers while there are still
		// fingers onscreen, and doesn't duplicate bindings either.
		bindDrag: function ($el, startFunc, moveFunc, endFunc) {

			var $window = $(window),
					capturing = false;

			function bindCaptures(event) {
				if (capturing) {
					return;
				}

				$window.bind(EV_MOVE, moveFunc);
				$window.bind(EV_UP, endFunc);
				$window.bind(EV_UP, unbindCaptures);

				capturing = true;
			}

			function unbindCaptures(event) {
				if (Events.getNumTouches(event) > 0) {
					// multi-touch still ongoing, don't unbind yet
					return;
				}

				$window.unbind(EV_MOVE, moveFunc);
				$window.unbind(EV_UP, endFunc);
				$window.unbind(EV_UP, unbindCaptures);

				capturing = false;
			}

			$el.bind(EV_DOWN, startFunc);
			$el.bind(EV_DOWN, bindCaptures);

		},

		// iScroll v4 dev supports double-tap-to-zoom, so it has custom handling of
		// touch events to emulate "click" events on single-taps only. unfortunately,
		// the handling seems to be a bit buggy -- it seems to fire sometimes when you
		// pinch zoom out also. so here's a wrapper that guards against that.
		// in addition, on desktop browsers, we don't want to fire a click if the mouse
		// moved between mousedown and mouseup. (for some reason it doesn't seem to be
		// an issue with touch, but in theory it should be, so guarding against it.)
		// TODO we should be smarter than a binary "moved" flag -- we should have a
		// configurable move threshold, e.g. 2px, to accommodate accidental moves.
		// but this will need some coordinating with our swipe detection, which also
		// needs to reset the pages if the drag was only a few pixels long...
		// UPDATE: also no longer counting clicks if the mouse/finger was held down.
		bindClick: function ($el, func) {

			var moved = false,
				multiple = false,
				startTime,
				deltaTime;

			function dragStart(event) {
				moved = false;
				// updating this flag only when a (series of) touch(es) starts!
				multiple = Events.getNumTouches(event) > 1;
				startTime = new Date().getTime();
			}

			function dragMoved(event) {
				moved = true;
			}

			function dragEnd(event) {
				deltaTime = new Date().getTime() - startTime;
			}

			Events.bindDrag($el, dragStart, dragMoved, dragEnd);

			$el.bind("click", function (event) {
				// if the last (series of) touch(es) had multiple fingers, ignore!
				// otherwise, if the one finger or the mouse moved, ignore also.
				// also ignore if too much time has passed between down and up.
				// TEMP HACK hardcoding a number here for now, but this should be
				// different between mouse and touch...
				if (!moved && !multiple && deltaTime < 200) {
					func(event);
				}
			});

		},

		// ZOOM HELPERS:

		// TEMP HACK these are totally not MVC-ish, but we're limited because the
		// zoom is abstracted inside iScroll, which itself is not MVC-ish at all.

		getZoom: function () {
			return Events.zoomer.scale;
		},

		isZoomedIn: function () {
			return Events.getZoom() > 1;
		},
		
		getMaxZoom: function () {
			// TODO ideally we'd adjust the max zoom depending on the mode.
			// unfortunately, navState.mode at startup hasn't been set yet, and
			// on resize, it hasn't been updated yet, so it's unreliable.
			
			// TEMP thus for now, always erring on the more performant side.
			return 2;
			
//			switch (_b.Core.navState.mode) {
//				case _b.Core.constants.spreadMode:
//					console.log("in spread mode");
//					return 3;	// since hi-res is 2x
//				case _b.Core.constants.pageMode:
//					console.log("in page mode");
//					return 2;	// since hi-res is 1.5x
//				default:
//					console.log("in unknown mode");
//					// err on the liberal side if uninitialized...
//					return 3;
//			}
		},

		// x and y are optional, they specify a point about which to zoom
		setZoom: function (scale, x, y) {
			Events.zoomer.zoom(x || 0, y || 0, scale);
		},

		zoomOut: function () {
			Events.setZoom(1);
			
			// TEMP explicitly unloading hi-res here. the zoom end listener will
			// eventually get called and do it, but we want this immediately!
			_b.Core.pageListing.getCurrentSpread().unloadHighRes();
		},
		
		// ZOOM EVENTS:
		
		onZoomStart: function () {
			// TODO various things; this fires on both pinch and double tap.
			// e.g. hide menu. anything else?
			_b.Tasks.hideChrome();
			
			// TEMP in the hopes of improving perf -- i think it does?
			_b.Core.pageListing.getCurrentSpread().hideHighRes();
		},
		
		onZoomEnd: function () {
			var spread = _b.Core.pageListing.getCurrentSpread();
			
			if (Events.isZoomedIn()) {
				spread.loadHighRes();
				spread.showHighRes();
			} else {
				spread.unloadHighRes();
			}
		},

		// DRAG AND SWIPE EVENTS:

		// note that we want our code here to execute only when:
		// (a) just one finger is down, and
		// (b) when we're zoomed out (since iScroll handles pan when we're zoomed in)

		// thus, we need to be robust to both of these things. we also want to work on
		// desktop with mouse (where (a) is not an issue). hence the checks inside the
		// various event listeners. reference example event executions below.

		// touch events for simple one-finger operations, e.g. drag-to-pan:
		// touchstart
		// touchmove
		// touchend

		// touch events for simple two-finger operations, e.g. pinch-to-zoom:
		// touchstart (1 or more, i.e. 1 can have 2 touches or 2 can have 1 touch each)
		// touchmove
		// touchend (1 or more like touchstart)

		// touch events for complex one- and two-finger operations, e.g.
		// start drag-to-pan then add a finger to pinch-zoom then reverse:
		// touchstart
		// touchmove
		// touchstart (now you're at 2)
		// touchmove (with 2 fingers)
		// touchend (now back to 1)
		// touchmove
		// touchend (now down to 0)

		// note also that finger A can be dragging first, then finger B goes down,
		// then finger A is lifted -- now finger B in theory could continue dragging,
		// but tracking fingers throughout the whole process is tricky, so for now
		// we'll just stop executing our code here the moment it becomes multi-touch.

		// ----------
		processDragMove: function(event) {
			var evtX = event.pageX;
			if (event.touches)
				evtX = (event.touches.length ? event.touches[0].pageX : Events.drag.lastX);
						
			var time = $.now();
			var timeSlice = (time - Events.drag.lastTime) / 1000; 
			var vx = (evtX - Events.drag.lastX) / timeSlice;
			// vx is now horizontal pixels per second for the most recent move
			
			if (Events.drag.velocityX === null) {
				Events.drag.velocityX = vx;
			} else {
				var factorForNew = Math.min(Events.velocityTimeWindow, timeSlice) /
						Events.velocityTimeWindow;
				Events.drag.velocityX = (vx * factorForNew) 
						+ (Events.drag.velocityX * (1 - factorForNew));
				// Events.drag.velocityX is now an average of the velocity for the time window
			}
			
			Events.drag.lastX = evtX; 
			Events.drag.lastTime = time; 
			Events.drag.distX = evtX - Events.drag.startX; 
			if (!Events.drag.dragging && Math.abs(Events.drag.distX) > 5) {
				Events.drag.dragging = true;
				_b.Tasks.hideChrome();
			}
		},
		
		// ----------
		dragStart: function () {
			var event = window.event;

			if (event.touches && event.touches.length > 1) {
				// ignore all multi-touch touchstart events, but also
				// abort the moment single-touch *becomes* multi-touch.
				// TODO what should we do if we started dragging and then
				// it became multi-touch (e.g. pinch zoom)? reset? end?
				Events.drag = null;
				return;
			}

			if (Events.isZoomedIn())
				return;

			var evtX = event.pageX;
			if (event.touches) 
				evtX = event.touches[0].pageX;

			Events.drag = {
				lastX: evtX,
				lastTime: $.now(),
				velocityX: null, 
				distX: 0,
				startX: evtX, 
				dragging: false
			};
		},

		// ----------
		dragMove: function () {
			if (!Events.drag) {
				// we won't drag during multi-touch, but we also won't drag if
				// multi-touch ever kicked in -- because we don't want to
				// duplicate/amplify the work iScroll is doing. this does mean
				// that if you zoom in, then back out, and start dragging with
				// one finger, nothing will happen. TODO fix that sometime, but
				// we then also need to be robust to the finger changing...
				return;
			}

			var event = window.event;
			Events.processDragMove(event);
			if (Events.drag.dragging)
				_b.Core.pageListing.drag(Events.drag.distX);
		},

		// ----------
		dragEnd: function () {
			if (!Events.drag)
				return;
			
			var event = window.event;
			Events.processDragMove(event);
			if (Events.drag.dragging) {	
				var navIndex = 0;
				var distanceThreshold = (_b.Core.navState.windowW * Events.navDistanceFactor);
				if (Events.drag.velocityX > Events.navVelocityThreshold)
					navIndex = -1;
				else if (Events.drag.velocityX < -Events.navVelocityThreshold)
					navIndex = 1;
				else if (Events.drag.distX > distanceThreshold)
					navIndex = -1;
				else if (Events.drag.distX < -distanceThreshold)
					navIndex = 1;
					
				var paginate = false;
				if (navIndex) {
					var newIndex = _b.Core.pageListing.getNextIndex(navIndex);
					if (newIndex != -1) {
						_b.Core.navState.currentIndex = newIndex;
						Events.zoomOut();
						paginate = true;
					}
				}
				
				if (paginate)
					_b.Workflow.executePagination();
				else if (Events.drag.distX) 
					_b.Core.pageListing.drag(0, 200, {resetBlinders: true}); 
			}
			
			Events.drag = null;
		}

	};

})();
