// XXX need a uniform way to address images (filename?)
/******************************************* Setup Data and Globals **/
// the list of style modes...
// these are swithched through in order by toggleBackgroundModes()
var BACKGROUND_MODES = [
	'dark',
	'black'
]
// this sets the zooming factor used in manual zooming...
var ZOOM_FACTOR = 2
// sets the amoun of move when a key is pressed...
var MOVE_DELTA = 50
/********************************************************** Helpers **/
// this will create a function that will add/remove a css_class to elem 
// calling the callbacks before and/or after.
// NOTE: of only one callback is given then it will be called after the 
// 		 class change...
// 		 a way around this is to pass an empty function as callback_b
// the resulting function understands the folowing arguments:
// 	- 'on'			: switch mode on
// 	- 'off'			: switch mode off
// 	- '?'			: return current state ('on'|'off')
// 	- no arguments	: toggle the state
function createCSSClassToggler(elem, css_class, callback_a, callback_b){
	// prepare the pre/post callbacks...
	if(callback_b == null){
		var callback_pre = null
		var callback_post = callback_a
	} else {
		var callback_pre = callback_a
		var callback_post = callback_b
	}
	// build the acual toggler function...
	return function(action){
		if(action == null || action == '?'){
			var getter = action == '?' ? true : false
			action = 'on'
			// get current state...
			if( $(elem).hasClass(css_class) ){
				action = 'off'
			}
			if(getter){
				// as the above actions indicate intent and not state, 
				// we'll need to swap the values...
				return action == 'on' ? 'off' : 'on'
			}
		}
		if(callback_pre != null){
			callback_pre(action)
		}
		// play with the class...
		if(action == 'on'){
			$(elem).addClass(css_class)
		} else {
			$(elem).removeClass(css_class)
		}
		if(callback_post != null){
			callback_post(action)
		}
	}
}
// disable transitions on obj, call func then enable transitions back...
function doWithoutTransitions(obj, func){
	obj
		.addClass('unanimated')
		.one("webkitTransitionEnd oTransitionEnd msTransitionEnd transitionend", function(){
			func()
			$('.viewer')
				.one("webkitTransitionEnd oTransitionEnd msTransitionEnd transitionend", function(){
					obj.removeClass('unanimated')
				})
		})
}
// find an image object after which to position image ID...
// used for two main tasks:
// 	- positioning promoted/demoted images
// 	- centering ribbons
// returns:
// 	- null		- empty ribbon or no element greater id should be first
// 	- element
// XXX do we need to make ids numbers for this to work?
function getImageBefore_lin(id, ribbon){
	// walk the ribbon till we find two images one with an ID less and 
	// another greater that id...
	id = parseInt(id)
	var images = ribbon.children('.image')
	var prev = null
	for(var i=0; i < images.length; i++){
		if(parseInt($(images[i]).attr('id')) > id){
			return prev
		}
		prev = $(images[i])
	}
	return prev
}
// generic binery search for element just before the id...
// NOTE: if id is in lst, this will return the element just before it.
// NOTE: lst must be sorted.
function binarySearch(id, lst, get){
	if(get == null){
		get = function(o){return o}
	}
	
	// empty list...
	if(lst.length == 0){
		return null
	}
	
	// current section length
	var l = Math.round((lst.length-1)/2)
	// current position...
	var i = l
	while(true){
		var i_id = get(lst[i])
		// beginning of the array...
		if(i == 0){
			if(id > i_id){
				return i
			}
			return null
		}
		// we got a hit...
		if(i_id == id){
			return i-1
		}
		// we are at the end...
		if(i == lst.length-1 && id > i_id){
			return i
		}
		var ii_id = get(lst[i+1])
		// test if id is between i and i+1...
		if( i_id < id && id < ii_id ){
			return i
		}
		// prepare for next iteration...
		// NOTE: we saturate the values so we will never get out of bounds.
		l = Math.round(l/2)
		if(id < i_id){
			// lower half...
			i = Math.max(0, i-l)
		} else {
			// upper half...
			i = Math.min(i+l, lst.length-1)
		}
	}
}
// wrapper around binarySearch.
// this is here to make binarySearch simpler to test and debug...
function getImageBefore_bin(id, ribbon){
	var images = ribbon.children('.image') 
	var i = binarySearch(
					parseInt(id), 
					images, 
					function(o){return parseInt($(o).attr('id'))})
	if(i == null){
		return null
	}
	return $(images[i])
}
// set the default search...
var getImageBefore = getImageBefore_bin
/************************************************** Setup Functions **/
// XXX is this a correct place for these?
function setDefaultInitialState(){
	if($('.current.ribbon').length == 0){
		$('.ribbon').first().addClass('current')
	}
	if($('.current.image').length == 0){
		$('.current.ribbon').children('.image').first().addClass('current')
	}
}
function setupEvents(){
	// resize...
	$(window).resize(function() {
		// XXX HACK
		$('.current.image').click()
	})
	// keyboard...
	$(document)
		.keydown(makeKeyboardHandler(keybindings, function(k){alert(k)}))
		//.keydown(handleKeys)
	// swipe...
	$('.viewer')
		.swipe({
			swipeLeft: nextImage,
			swipeRight: prevImage,
			swipeUp: shiftImageUp,
			swipeDown: shiftImageDown
		})
}
function setupControlElements(){
	// images...
	$(".image").click(setCurrentImage)
	// buttons...
	$('.screen-button.next-image').click(nextImage)
	$('.screen-button.prev-image').click(prevImage)
	// XXX rename classes to "shift-image-up" and "shift-image-down"...
	$('.screen-button.demote').click(shiftImageUp)
	$('.screen-button.promote').click(shiftImageDown)
	$('.screen-button.zoom-in').click(function(){scaleContainerBy(ZOOM_FACTOR)})
	$('.screen-button.zoom-out').click(function(){scaleContainerBy(1/ZOOM_FACTOR)})
	$('.screen-button.toggle-wide').click(toggleWideView)
	$('.screen-button.toggle-single').click(toggleSingleImageMode)
	$('.screen-button.fit-three').click(fitThreeImages)
	$('.screen-button.show-controls').click(showControls)
	$('.screen-button.settings').click(function(){alert('not implemented yet...')})
}
/**************************************************** Serialization **/
function loadImages(json){
	var images = json.images
	var ribbon = $('.ribbon').last()
	$('.image').remove()
	for(var i = 0; i < images.length; i++){
		$('
')
			.css({ 'background-image': 'url('+images[i]+')' })
			// set a unique id for each image...
			.attr({'id': i})
			.click(setCurrentImage)
			.appendTo(ribbon)
	}
	ribbon.children().first().click()
}
/*
 * format:
 * 	{
 * 		ribbons: [
 * 			0: {
 * 				url: ,
 * 				id: 
 * 			},				
 * 			...
 * 		]
 * 	}
 */
function buildJSON(){
	var ribbons = $('.ribbon')
	res = {
		ribbons: []
	}
	for(var i=0; i < ribbons.length; i++){
		var images = $(ribbons[i]).children('.image')
		var ribbon = []
		res.ribbons[res.ribbons.length] = ribbon
		for(var j=0; j < images.length; j++){
			var image = $(images[j])
			ribbon[ribbon.length] = {
				// unwrap the url...
				// XXX would be nice to make this a relative path...
				url: /url\((.*)\)/.exec(image.css('background-image'))[1],
				id: image.attr('id'),
			}
		}
	}
	return res
}
// XXX add incremental or partial updates...
// XXX might be good to add images as packs here, not one by one...
function loadJSON(data){
	var ribbons = data.ribbons
	var field = $('.field')
	// drop all old content...
	field.children().remove()
	for(var i=0; i < ribbons.length; i++){
		var images = ribbons[i]
		// create ribbon...
		var ribbon = $('')
			.appendTo(field)
		for(var j=0; j < images.length; j++){
			var image = $(images[j])
			// create image...
			$('')
				.css({ 'background-image': 'url('+image.attr('url')+')' })
				// set a unique id for each image...
				.attr({'id': image.attr('id')})
				.click(setCurrentImage)
				.appendTo(ribbon)
		}
	}
	$('.image').first().click()
}
/*************************************************** Event Handlers **/
function setCurrentImage(){
	// set classes...
	$('.current').removeClass('current')
	$(this)
		.addClass('current')
		.parents('.ribbon')
			.addClass('current')
	// position the field and ribbons...
	centerSquare()
	alignRibbons()
}
function clickAfterTransitionsDone(img){
	if(img == null){
		img = $('.current.image')
	}
	$('.viewer')
		.one("webkitTransitionEnd oTransitionEnd msTransitionEnd transitionend", function(){
			img.click()
			return true
		})
}
// center other ribbons relative to current image...
// NOTE: only two ribbons are positioned at this point...
function alignRibbons(){
	// XXX might be good to move this to a more generic location...
	var id = $('.current.image').attr('id')
	var directions = ['prev', 'next']
	for(var i in directions){
		var ribbon = $('.current.ribbon')[directions[i]]('.ribbon')
		if(ribbon.length == 1){
			var img = getImageBefore(id, ribbon)
			if(img != null){
				alignRibbon(img, 'before')
			} else {
				// there are no images before...
				alignRibbon(ribbon.children('.image').first(), 'after')
			}
		}
	}
}
// XXX we essentially need three things:
// 		- keycodes, including modifier keys
// 		- function
// 		- meta information...
/*
 * Basic key format:
 * 		 : ,
 * 		 : {
 * 			default: [, ],
 *			// a modifier can be any single modifier, like shift or a 
 *			// combination of modifers like 'ctrl+shift', given in order 
 *			// of priority.
 *			// supported modifiers are (in order of priority):
 *			//	- ctrl
 *			//	- alt
 *			//	- shift
 * 			: [...]
 * 		},
 *		// alias...
 * 		 : ,
 */
function makeKeyboardHandler(keybindings, unhandled){
	if(unhandled == null){
		unhandled = function(){return false}
	}
	return function(evt){
		var key = evt.keyCode
		// XXX ugly...
		var modifers = evt.ctrlKey ? 'ctrl' : ''
		modifers += evt.altKey ? (modifers != '' ? '+alt' : 'alt') : ''
		modifers += evt.shiftKey ? (modifers != '' ? '+shift' : 'shift') : ''
		var handler = keybindings[key]
		// alias...
		while (typeof(handler) == typeof(123)) {
			handler = keybindings[handler]
		}
		// no handler...
		if(handler == null){
			return unhandled(key)
		}
		// complex handler...
		if(typeof(handler) == typeof({})){
			var callback = handler[modifers]
			if(callback == null){
				callback = handler['default']
			}
			if(callback != null){
				callback()
				return false
			}
		} else {
			// callback...
			handler() 
			return false
		}
		return unhandled(key)
	}
}
/************************************************************ Modes **/
// ribbon/single view modes...
// global: stores the scale before we went into single image mode...
// XXX HACK
var ORIGINAL_FIELD_SCALE = 1
// remember the default backgrounds for modes...
// ...this effectively makes the modes independant...
// NOTE: null represent the default value...
// XXX HACK
var NORMAL_MODE_BG = null 
var SINGLE_IMAGE_MODE_BG = BACKGROUND_MODES[BACKGROUND_MODES.length-1]
var toggleSingleImageMode = createCSSClassToggler('.viewer', 'single-image-mode', 
		// pre...
		function(action){
			if(action == 'on'){
				NORMAL_MODE_BG = getBackgroundMode()
				ORIGINAL_FIELD_SCALE = getElementScale($('.field'))
			} else {
				SINGLE_IMAGE_MODE_BG = getBackgroundMode()
			}
		},
		// post...
		function(action){
			if(action == 'on'){
				fitImage()
				setBackgroundMode(SINGLE_IMAGE_MODE_BG)
			} else {
				setContainerScale(ORIGINAL_FIELD_SCALE)
				setBackgroundMode(NORMAL_MODE_BG)
			}
			clickAfterTransitionsDone()
})
// wide view mode toggle...
var toggleWideView = createCSSClassToggler('.viewer', 'wide-view-mode', function(action){
	if(action == 'on'){
		ORIGINAL_FIELD_SCALE = getElementScale($('.field'))
		setContainerScale(0.1)
	} else {
		setContainerScale(ORIGINAL_FIELD_SCALE)
	}
}, function(){})
function getBackgroundMode(){
	var mode = null
	// find a mode to set...
	for(var i = 0; i < BACKGROUND_MODES.length; i++){
		// we found our mode...
		if( $('.' + BACKGROUND_MODES[i]).length > 0 ){
			return BACKGROUND_MODES[i]
		}
	}
	return mode
}
// set the background mode
// NOTE: passing null will set the default.
function setBackgroundMode(mode){
	var cur = BACKGROUND_MODES.indexOf(mode)
	// invalid mode...
	if( cur == -1 && mode != null ){
		return null
	}
	// set the mode...
	if(mode != null){
		$('.viewer').addClass(mode)
	}
	// remove all others...
	for(var i = 0; i < BACKGROUND_MODES.length; i++){
		if( i == cur ){
			continue
		}
		mode = BACKGROUND_MODES[i]
		$('.' + mode).removeClass(mode)
	}
}
// this will toggle through background theems: none -> dark -> black
function toggleBackgroundModes(){
	var mode = getBackgroundMode()
	// default -> first
	if(mode == null){
		setBackgroundMode(BACKGROUND_MODES[0])
	// last -> default...
	} else if(mode == BACKGROUND_MODES[BACKGROUND_MODES.length-1]){
		setBackgroundMode()
	// next...
	} else {
		setBackgroundMode(BACKGROUND_MODES[BACKGROUND_MODES.indexOf(mode)+1])
	}
}
var toggleSingleImageModeTransitions = createCSSClassToggler('.viewer', 'no-single-image-transitions')
var toggleControls = createCSSClassToggler('.viewer', 'hidden-controls')
var showControls = function(){toggleControls('on')}
var hideControls = function(){toggleControls('off')}
var toggleTransitions = createCSSClassToggler('.viewer', 'transitions-enabled')
var enableTransitions = function(){toggleTransitions('on')}
var disableTransitions = function(){toggleTransitions('off')}
/********************************************************* Movement **/
// XXX for some odd reason these are not liner... something to do with origin?
// XXX virtually identical, see of can be merged...
function moveViewUp(){
	var t = parseInt($('.field').css('top'))
	$('.field').css({'top': t-(MOVE_DELTA)})
}
function moveViewDown(){
	var t = parseInt($('.field').css('top'))
	$('.field').css({'top': t+(MOVE_DELTA)})
}
function moveViewLeft(){
	var l = parseInt($('.field').css('left'))
	$('.field').css({'left': l-(MOVE_DELTA)})
}
function moveViewRight(){
	var l = parseInt($('.field').css('left'))
	$('.field').css({'left': l+(MOVE_DELTA)})
}
/******************************************************* Navigation **/
// basic navigation...
function firstImage(){
	$('.current.ribbon').children('.image').first().click()
}
function prevImage(){
	$('.current.image').prev('.image').click()
}
function nextImage(){
	$('.current.image').next('.image').click()
}
function lastImage(){
	$('.current.ribbon').children('.image').last().click()
}
// XXX add skip N images back and forth handlers...
// XXX
function focusRibbon(direction){
	var id = $('.current.image').attr('id')
	var prev = getImageBefore(id, $('.current.ribbon')[direction]('.ribbon'))
	if(prev){
		var next = prev.next()
		// NOTE: direction is accounted for to make the up/down shifts 
		// 		 symmetrical in the general case...
		if(next.length == 0 || direction == 'next'){
			prev.click()
		} else {
			next.click()
		}
	} else {
		$('.current.ribbon')[direction]('.ribbon').children('.image').first().click()
	}
}
function focusAboveRibbon(){
	focusRibbon('prev')
}
function focusBelowRibbon(){
	focusRibbon('next')
}
/********************************************************** Actions **/
// basic actions...
// NOTE: below 'direction' argument is meant in the html sence, 
//       i.e. next/prev...
// create ribbon above/below helpers...
// XXX adding a ribbon above the current is still jumpy, need to devise 
// 		a cleaner way to do this...
function createRibbon(direction){
	if(direction == 'next'){
		var insert = 'insertAfter'
	} else if(direction == 'prev') {
		var insert = 'insertBefore'
	} else {
		return false
	}
	// adding a new ribbon above the current effectively pushes the 
	// whole view down, so we need to compensate for this.
	// NOTE: the problem is partly caused by clicks fiering BEFORE the 
	// 		 animation is done...
	$('.field').addClass('unanimated')	
	
	if(direction == 'prev'){
		$('.field').css({
			'margin-top': parseInt($('.field').css('margin-top')) - $('.ribbon').outerHeight()
		})
	}
	// the actual insert...
	var res = $('')[insert]('.current.ribbon')
	
	// restore the animated state...
	$('.field').removeClass('unanimated')	
	return res
}
// merge current and direction ribbon...
// NOTE: this will take all the elements from direction ribbon and add
//       them to current
// XXX this uses jquery animation...
// XXX one way to optimise this is to add the lesser ribbon to the 
//     greater disregarding their actual order...
function mergeRibbons(direction){
	var current_ribbon = $('.current.ribbon')
	var images = $('.current.ribbon')[direction]('.ribbon').children()
	for(var i=0; i < images.length; i++){
		var image = $(images[i])
		// get previous element after which we need to put the current...
		var prev_elem = getImageBefore(image.attr('id'), current_ribbon)
		// check if we need to be before the first element...
		if(prev_elem == null){
			image
				.detach()
				.insertBefore(current_ribbon.children('.image').first())
		} else {
			image
				.detach()
				.insertAfter(prev_elem)
		}
	}
	// animate...
	$('.current.ribbon')[direction]('.ribbon')
			.slideUp(function(){
				$(this).remove()
				$('.current.image').click()
			})
}
/*************************************************** Editor Actions **/
// now the actual modifiers...
function shiftImage(direction){
	if($('.current.ribbon')[direction]('.ribbon').length == 0){
		createRibbon(direction)
	}
	// get previous element after which we need to put the current...
	var prev_elem = getImageBefore(
					$('.current.image').attr('id'), 
					$('.current.ribbon')[direction]('.ribbon'))
	// last image in ribbon, merge...
	if($('.current.ribbon').children('.image').length == 1){
		mergeRibbons(direction)
	} else {
		img = $('.current.image')
		if(img.next('.image').length == 0){
			prevImage()
		} else {
			nextImage()
		}
		// do the actual move...
		if(prev_elem){
			// insert element after current...
			img
				.detach()
				.insertAfter(prev_elem)
		} else {
			// empty ribbon or fisrt element...
			img
				.detach()
				.prependTo($('.current.ribbon')[direction]('.ribbon'))
		}
	}
	$('.current.image').click()
}
function shiftImageDown(){
	return shiftImage('next')
}
function shiftImageUp(){
	return shiftImage('prev')
}
function flipRibbons(){
	var ribbons = $('.ribbon')
	// index of current ribbon, needed to adjust placement of everything...
	var cur = ribbon.index($('.current.ribbon'))
	// XXX would have been nice if we could do detach reverse attach or just reverse...
	
}
/*********************************************************************/
// vim:set ts=4 sw=4 nowrap :