/**********************************************************************
* 
*
*
**********************************************************************/

//var DEBUG = DEBUG != null ? DEBUG : true

// NOTE: this is a sparse list, i.e. all elements are in the same 
// 		position as they are in DATA.order, and the unmarked elements
// 		are undefined.
// 		This is done because:
// 			- it drasticly simplifies adding, removing and access as 
// 				there is no searching and checking involved, just insert
// 				to the same spot as in order and you are safe.
// 			- trivial sorting
// 			- less maintenance and sync
// 		The tradeoff being:
// 			- load/save conversion to keep the json	data "packed".
// NOTE: it would appear that JS is designed with sparse lists in mind:
// 		- all iterators (map, filter, forEach, ..) skip undefined values
// 		- really fast
var MARKED = []



/**********************************************************************
* helpers...
*/

function _addMark(cls, gid, image){
	gid = gid == null ? getImageGID() : gid
	image = image == null ? getImage() : $(image)

	// no image is loaded...
	if(image.length == 0){
		return
	}

	var mark = $('.mark.'+cls+'.'+gid)

	if(mark.length == 0){
		mark = $('<div class="mark"/>')
			.addClass(gid)
			.addClass(cls)
			//.insertAfter(image)
	} 

	// make sure the mark is explicitly after the image...
	// XXX think of an eficient way to test if we need to re-insert...
	mark.insertAfter(image)

	return mark
}
function _removeMark(cls, gid, image){
	gid = gid == null ? getImageGID() : gid
	image = image == null ? getImage() : $(image)

	// no image is loaded...
	if(image.length == 0){
		return
	}

	var mark = $('.mark.'+cls+'.'+gid)

	if(mark.length != 0){
		mark.detach()
	}
	return mark
}


function makeMarkedLister(get_marked){
	return function(mode){
		var marked = get_marked()
		mode = mode == null ? 'all' : mode
		return mode == 'all' ? getLoadedGIDs(marked) 
			: mode.constructor.name == 'Array' ? getLoadedGIDs(mode)
			: typeof(mode) == typeof(123) ? getRibbonGIDs(marked, mode)
			: getRibbonGIDs(marked)
	}
}


// Make lister of unmarked images...
//
// The resulting function can take one argument (mode) which can be:
// 	- null			- default, same as 'all'
// 	- 'all'			- process all loaded gids
// 	- 'ribbon'		- process curent ribbon
// 	- number		- ribbon index to process
// 	- Array			- list of gids to filter
function makeUnmarkedLister(get_marked){
	return function(mode){
		mode = mode == null ? 'all' : mode

		var marked = get_marked()

		var gids = mode == 'all' ? getLoadedGIDs() 
			: mode.constructor.name == 'Array' ? getLoadedGIDs(mode)
			: typeof(mode) == typeof(123) ? getRibbonGIDs(marked, mode)
			: getRibbonGIDs(marked)

		// calculate the set...
		var res = gids.filter(function(e){
			// keep only unmarked...
			return marked.indexOf(e) < 0
		})

		return res
	}
}


// The same as makeUnmarkedLister(..) but designed for sparse lists...
//
// NOTE: this is about an order of magnitude faster than the non-sparse
// 		version...
//
// XXX if this gets used often, add caching -- this may get quite slow
// 		for very large image sets...
function makeUnmarkedSparseLister(get_marked){
	return function(mode){
		mode = mode == null ? 'all' : mode

		var marked = get_marked()

		var res = mode == 'all' ? 
				DATA.order.slice()
			: mode == 'ribbon' ? 
				populateSparseGIDList(getRibbonGIDs())
			: typeof(mode) == typeof(123) ? 
				populateSparseGIDList(getRibbonGIDs(mode))
			: mode

		// for ribbon modes, remove non-ribbon marks...
		if(mode == 'ribbon'){
			marked = getRibbonGIDs(marked)
		} else if(typeof(mode) == typeof(123)){
			marked = getRibbonGIDs(marked, mode)
			mode = 'ribbon'
		}

		// negate the list...
		marked.forEach(function(e, i){
			delete res[i]
		})

		return getLoadedGIDs(compactSparceList(res))
	}
}


var getMarked = makeMarkedLister(function(){ return MARKED })
var getUnmarked = makeUnmarkedSparseLister(function(){ return MARKED })


var getMarkedGIDBefore = makeGIDBeforeGetterFromList(
		function(){ 
			return compactSparceList(MARKED)
		})


// NOTE: this is not too fast as it will filter the marked images...
// NOTE: this is restricted to current ribbon...
var getUnmarkedGIDBefore = makeGIDBeforeGetterFromList(
		function(ribbon){ 
			return getUnmarked(ribbon)
		}, true)


// Make a mark toggler
//
// The toggler will:
// 	- toggle img_class on the target image
// 	- add/remove a mark element after the image
// 	- toggle mark_class on the mark element
// 	- call the callback, if defined, passing it:
// 		- gid
// 		- action ('on' or 'off')
// 	- trigger the evt_name on the viewer passing it:
// 		- gid
// 		- action ('on' or 'off')
//
// The actual toggler is built with createCSSClassToggler(..), see its
// docs for protocol descrittion.
//
// The resulting toggler, by default, marks the current image 
// (.current.image), but can be passed a different image as first 
// argument.
//
// NOTE: when passing an alternative image as an argument, the second 
// 		argument MUST also be passed. it can be one of:
// 			- 'on'		: force create mark
// 			- 'off'		: force remove mark
// 			- 'next'	: toggle next state (default)
// NOTE: when passing this a gid, the 'next' action is not supported
function makeMarkToggler(img_class, mark_class, evt_name, callback){
	return createCSSClassToggler(
		'.current.image', 
		img_class,
		function(action, elem){
			toggleMarksView('on')

			// we got a gid...
			if(elem.length == 0 && elem.selector in IMAGES){
				var gid = elem.selector
				elem = getImage(gid)
				elem = elem.length == 0 ? null : elem

			// we are given an image...
			} else {
				var gid = getImageGID(elem)
			}

			// do this only of the image is loaded...
			if(elem != null){
				if(action == 'on'){
					_addMark(mark_class, gid, elem)
				} else {
					_removeMark(mark_class, gid, elem)
				}
			}

			if(callback != null){
				callback(gid, action)
			}

			$('.viewer').trigger(evt_name, [gid, action])
		})
}


// Generate an image updater function...
//
// the resulting function will update image mark state by adding or 
// removing the mark the specific mark object.
function makeMarkUpdater(img_class, mark_class, test){
	var _updater = function(gid, image){
		// marks...
		if(test(gid)){
			image.addClass(img_class)
			_addMark(mark_class, gid, image)
		} else {
			image.removeClass(img_class)
			_removeMark(mark_class, gid, image)
		}
		return image
	}
	IMAGE_UPDATERS.push(_updater)
	return _updater
}


// NOTE: this supports only shifts by one position...
// XXX this is similar to insertGIDToPosition(..) do we need both?
// 		...this one is a special case and insertGIDToPosition(..) is 
// 		general, the later uses search to find the position, here we 
// 		know the aproximate location, the question is if this speedup
// 		is worth the effort of maintaining a special case function...
function shiftGIDToOrderInList(gid, direction, list){
	var gid_o = DATA.order.indexOf(gid)
	var gid_m = list.indexOf(gid)

	var a_m = gid_m + (direction == 'next' ? 1 : -1)
	if(a_m < 0 || a_m >= list.length){
		return false
	}
	var a_gid = list[a_m]
	var a_o = DATA.order.indexOf(a_gid)

	// if relative positions of cur and adjacent gids in list 
	// are different to that in DATA.order, then replace the gids
	// in list...
	if(sign(a_m - gid_m) != sign(a_o - gid_o)){
		list[a_m] = gid
		list[gid_m] = a_gid
		return true
	}
	return false
}


// a sparse version of shiftGIDToOrderInList(..)...
//
// returns true if list is updated....
function shiftGIDInSparseList(gid, from, to, list){
	// XXX do we need this???
	if(list[from] == null 
			&& list[to] == null 
			// NOTE: if there is something between 'from' and 'to' it must 
			// 		be shifted...
			&& compactSparceList(list.slice(from, to)).length == 0){
		return false
	}

	// if gid was never in list, we must it and remove leave things as 
	// we got them, and remove it again...
	var cleanup = list.indexOf(gid) < 0

	// move the marked gid...
	list.splice(from, 1)
	list.splice(to, 0, gid)

	// NOTE: essentially, we are using gid as a marker, as we can't 
	// 		.splice(..) an undefined into a list...
	if(cleanup){
		delete list[to]
	}

	return true
}


// NOTE: this is sparse-only...
function setAllMarks(action, mode, list, toggler){
	action = action == null ? toggler('?') : action
	mode = mode == null ? 'ribbon' : mode

	var updated = []

	if(action == 'on'){
		var _update = function(e){
			if(list.indexOf(e) < 0){
				list[DATA.order.indexOf(e)] = e
				updated.push(e)
			}
		}
	} else {
		var _update = function(e){
			var i = list.indexOf(e)
			if(i >= 0){
				delete list[i]
				updated.push(e)
			}
		}
	}

	// marks from current ribbon (default)...
	if(mode == 'ribbon'){
		var res = getRibbonGIDs()

	// all marks...
	} else if(mode == 'all'){
		var res = getLoadedGIDs()
	} 

	res.forEach(_update)

	updateImages(updated)

	//return res
	return updated
}



/**********************************************************************
* 
*/

var updateSelectedImageMark = makeMarkUpdater(
		'marked',
		'selected', 
		function(gid){ 
			return MARKED.indexOf(gid) > -1 
		})


// NOTE: to disable MARKED cleanout set no_cleanout_marks to true.
// NOTE: MARKED may contain both gids that are not loaded and that do 
// 		not exist, as there is no way to distinguish between the two 
// 		situations the cleanup is optional...
function cropMarkedImages(keep_ribbons, keep_unloaded_gids){
	cropDataTo(MARKED, keep_ribbons, keep_unloaded_gids)
	return DATA
}



/**********************************************************************
* Modes
*/

var toggleMarkedOnlyView = makeCropModeToggler(cropMarkedImages)


var toggleMarkedOnlyWithRibbonsView = makeCropModeToggler(
		function(){
			cropMarkedImages(true)
		})


var toggleMarksView = createCSSClassToggler(
	'.viewer', 
	'marks-visible',
	function(){
		var cur = getImage()
		// current is marked...
		if(cur.hasClass('marked')){
			centerView(null, 'css')
			return
		} 
		// there is a marked image in this ribbon...
		var target = getImageBefore(cur, null)
		if(target.length > 0){
			centerView(focusImage(target), 'css')
			return
		}
		// get marked image from other ribbons...
		prevRibbon()
		if(getImage().hasClass('marked')){
			return
		}
		nextRibbon()
	})



/**********************************************************************
* Actions
*/

var toggleMark = makeMarkToggler(
		'marked', 
		'selected', 
		'togglingMarks',
		function(gid, action){
			// add marked image to list...
			if(action == 'on'){
				if(MARKED.indexOf(gid) == -1){
					MARKED[DATA.order.indexOf(gid)] = gid
				} 

			// remove marked image from list...
			} else {
				delete MARKED[MARKED.indexOf(gid)]
			}

			marksUpdated()
		})


function markAllImagesTo(action, mode){
	mode = mode == null ? 'ribbon' : mode
	var res = setAllMarks(action, mode, MARKED, toggleMark)
	$('.viewer')
		.trigger('togglingMarks', [res, action])
		.trigger('removingMarks', [res, mode])
	marksUpdated()
	return res
}


// Mark/Unmark images...
//
// mode can be:
//	- 'ribbon' (default)
//	- 'all'
//
function markAll(mode){ markAllImagesTo('on', mode) }
function unmarkAll(mode){ markAllImagesTo('off', mode) }


// Invert marks on images...
//
// mode can be:
//	- 'ribbon' (default)
//	- 'all'
//
function invertImageMarks(mode, gids){
	mode = mode == null ? 'ribbon' : mode
	gids = gids != null ? gids 
			: mode == 'ribbon' ? getRibbonGIDs()
			: getLoadedGIDs()

	var on = []
	var off = []
	var order = DATA.order

	$.each(gids, function(_, e){
		var i = order.indexOf(e)
		if(MARKED[i] === undefined){
			on.push(e)
			MARKED[i] = e
		} else {
			off.push(e)
			delete MARKED[i]
		}
	})
	updateImages(gids)

	$('.viewer')
		.trigger('invertingMarks', [gids])
		.trigger('togglingMarks', [on, 'on'])
		.trigger('togglingMarks', [off, 'off'])

	marksUpdated()

	return gids
}


// Toggle marks in the current continuous section of marked or unmarked
// images...
function toggleMarkBlock(image){
	image = image == null ? getImage() : image
	var gid = typeof(image) == typeof('str') ? image : getImageGID(image)
	image = typeof(image) == typeof('str') ? getImage(gid) : image

	var state = toggleMark(image, 'next') == 'off' ? false : true

	var ribbon = DATA.ribbons[getRibbonIndex(image)]
	var i = ribbon.indexOf(gid)

	var updated = [gid]
	var order = DATA.order

	var _convert = function(_, e){
		// break if state differs from current...
		if((MARKED.indexOf(e) >= 0) == state){
			return false
		}
		// do the toggle...
		if(state){
			MARKED[order.indexOf(e)] = e
		} else {
			delete MARKED[MARKED.indexOf(e)]
		}
		updated.push(e)
	}

	// go left...
	var left = ribbon.slice(0, i)
	left.reverse()
	$.each(left, _convert)

	// go right...
	var right = ribbon.slice(i+1)
	$.each(right, _convert)

	updateImages(updated)

	$('.viewer')
		.trigger('togglingImageBlockMarks', [image, updated, state])
		.trigger('togglingMarks', [updated, state ? 'on' : 'off'])

	marksUpdated()

	return state
}


// XXX need to account for empty ribbons...
function shiftMarkedImages(direction, mode, new_ribbon){
	mode = mode == null ? 'ribbon' : mode
	var cur = getRibbonIndex()
	var orig_ribbon = cur

	// ribbon only...
	if(mode == 'ribbon'){
		var ribbon = DATA.ribbons[cur]
		// remove all the marked images form the current ribbon...
		// NOTE: this builds a list of marked images ONLY in current 
		// 		ribbon...
		var marked = $.map(MARKED, function(e){
			var i = ribbon.indexOf(e)
			if(i >= 0){
				ribbon.splice(i, 1)
				return e
			}
			return null
		})

	// shift all marked images...
	} else {
		var marked = MARKED.slice()
		// remove all the marked images form all other ribbons...
		$.each(DATA.ribbons, function(ribbon){
			$.each(marked, function(e){
				var i = ribbon.indexOf(e)
				i >= 0 ? ribbon.splice(i, 1) : null
			})
		})
	}

	// if we are at the top or bottom ribbons we need to create a new 
	// ribbon regardless...
	if((cur == 0 && direction == 'prev') 
			|| (cur == DATA.ribbons.length-1 && direction == 'next')){
		new_ribbon = true
	}

	// add marked to new ribbon...
	if(new_ribbon){
		cur += direction == 'next' ? 1 : 0
		DATA.ribbons.splice(cur, 0, marked)
	
	// add marked to existing ribbon...
	} else {
		cur += direction == 'next' ? 1 : -1
		DATA.ribbons[cur] = fastSortGIDsByOrder(DATA.ribbons[cur].concat(marked))
	}
	
	dataUpdated()

	// remove empty ribbons and reload...
	dropEmptyRibbons()
	reloadViewer()

	$('.viewer').trigger('shiftedImages', [marked, orig_ribbon, cur])
}
function shiftMarkedImagesUp(mode, new_ribbon){
	return shiftMarkedImages('prev', mode, new_ribbon)
}
function shiftMarkedImagesDown(mode, new_ribbon){
	return shiftMarkedImages('next', mode, new_ribbon)
}


/*
// XXX these are ribbon wise only (???)
// XXX this on first step this must pack all marked images
function horizontalShiftMarkedImages(direction){
	// XXX
}
function shiftMarkedImagesLeft(){
	return horizontalShiftMarkedImages('prev')
}
function shiftMarkedImagesRight(){
	return horizontalShiftMarkedImages('next')
}
*/


// Focus next/prev marked image...
//
// NOTE: these will not jump to marks on other ribbons... to prevent this
// 		add true as the final argument (see restrict_to_ribbon argument 
// 		of makeNextFromListAction(..) for more info)
var nextMark = makeNextFromListAction(
		getMarkedGIDBefore, 
		function(){ return compactSparceList(MARKED) })
var prevMark = makePrevFromListAction(
		getMarkedGIDBefore, 
		function(){ return compactSparceList(MARKED) })


// Focus next/prev unmarked image..
//
var nextUnmarked = makeNextFromListAction(
		getUnmarkedGIDBefore, 
		function(ribbon){ 
			return getUnmarked(ribbon == null ? 'ribbon' : ribbon) 
		})
var prevUnmarked = makePrevFromListAction(
		getUnmarkedGIDBefore, 
		function(ribbon){ 
			return getUnmarked(ribbon == null ? 'ribbon' : ribbon) 
		})



/**********************************************************************
* Dialogs... 
*/

function markImagesDialog(){

	updateStatus('Mark...').show()

	var alg = 'Mark images:'

	var cur = toggleMark('?') == 'on' ? 'Unmark' : 'Mark'

	cfg = {}
	cfg[alg] = [
		cur + ' current image',
		cur + ' current block | '+
			'A block is a set of similarly marked images\n'+
			'to the left and right of the current image,\n'+
			'up until the closest images marked differently',
		'Invert marks in current ribbon',
		'Invert all marks',
		'Mark all in current ribbon',
		'Unmark all in current ribbon',
		'Mark all images',
		'Unmark all images'
	]

	formDialog(null, '', 
			cfg,
			'OK', 
			'markImagesDialog')
		.done(function(res){
			res = res[alg]

			// NOTE: these must be in order of least-specific last...
			if(/current image/.test(res)){
				toggleMark()
				var msg = (cur + ' image').toLowerCase()

			} else if(/current block/.test(res)){
				toggleMarkBlock()
				var msg = 'toggled block marks'

			} else if(/Invert .* ribbon/.test(res)){
				invertImageMarks()
				var msg = 'inverted ribbon marks'

			} else if(/Invert all/.test(res)){
				invertImageMarks('all')
				var msg = 'inverted all marks'

			} else if(/Mark all.*current ribbon/.test(res)){
				markAll('ribbon')
				var msg = 'marked ribbon'

			} else if(/Mark all/.test(res)){
				markAll('all')
				var msg = 'marked all'

			} else if(/Unmark all in/.test(res)){
				unmarkAll('ribbon')
				var msg = 'unmarked ribbon'

			} else if(/Unmark all images/.test(res)){
				unmarkAll('all')
				var msg = 'unmarked all'
			}

			showStatusQ('Mark: '+msg+'...')
		})
		.fail(function(){
			showStatusQ('Marking: canceled.')
		})
}



/**********************************************************************
* Files...
*/

// Load image marks form file
//
// NOTE: if no marks are found then set them to []
var loadFileMarks = makeFileLoader(
		'Marks', 
		CONFIG.marked_file, 
		[],
		function(data){ 
			MARKED = populateSparseGIDList(data)
		},
		null,
		'marksLoaded')


// Save image marks to file
var saveFileMarks = makeFileSaver(
		'Marks',
		CONFIG.marked_file, 
		function(){ 
			return compactSparceList(MARKED)
		})


function marksUpdated(){
	fileUpdated('Marks')
	$('.viewer').trigger('marksUpdated')
}



/**********************************************************************
* Setup...
*/


function setupMarks(viewer){
	console.log('Marks: setup...')

	// XXX make this viewer specific...
	makeContextIndicatorUpdater('marked')

	// XXX make these viewer specific...
	showGlobalIndicator(
			'marks-visible', 
			'Marks visible (F2)')
		.click(function(){ toggleMarksView() })
	showGlobalIndicator(
			'marked-only-visible', 
			'Marked only images visible (shift-F2)')
		.click(function(){ toggleMarkedOnlyView() })
	showContextIndicator(
			'current-image-marked', 
			'Marked (Ins)')
		.click(function(){ toggleMark() })

	return viewer
		.on('sortedImages', function(){
			MARKED = populateSparseGIDList(MARKED)
			marksUpdated()
		})
		.on('horizontalShiftedImage', function(evt, gid, direction, from, to){
			if(shiftGIDInSparseList(gid, from, to, MARKED)){
				marksUpdated()
			}
		})
		// when the gid is swapped update the cache...
		.on('updatedImageGID', function(evt, was, is){
			var i = MARKED.indexOf(was)
			if(i >= 0){
				MARKED[i] = is
				marksUpdated()
			}
		})
}
SETUP_BINDINGS.push(setupMarks)



/**********************************************************************
* vim:set ts=4 sw=4 :                                                */
