/**********************************************************************
* 
*
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/

var sha1 = require('ext-lib/sha1')

var object = require('lib/object')
var util = require('lib/util')



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

// decide to use a hashing function...
if(typeof(sha1) != 'undefined'){
	var hash = sha1.hash.bind(sha1)
} else {
	var hash = function(g){ return g }
}





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

// A stub image, also here for documentation...
var IMAGE_DATA =
module.IMAGE_DATA = {
	// Entity GID...
	id: 'GID',

	// Entity type
	type: 'image',

	// Entity state
	//
	// can be:
	// 	- 'single'
	// 	- 'grouped'
	// 	- 'hidden'
	// 	- ...
	state: 'single',

	// Creation time...
	ctime: 0,

	// Original path...
	path: './images/900px/SIZE.jpg',

	// Previews...
	// NOTE: the actual values depend on specific image and can be
	// 		any size...
	preview: {
		'150px': './images/150px/SIZE.jpg',
		'350px': './images/350px/SIZE.jpg',
		'900px': './images/900px/SIZE.jpg',
	},

	// Classes
	// XXX currently unused...
	//classes: '',

	// Image orientation (optional)
	//
	// can be:
	// 	- null/undefined	- same as 0
	// 	- 0 (default)		- load as-is
	// 	- 90				- rotate 90deg CW
	// 	- 180				- rotate 180deg CW
	// 	- 270				- rotate 270deg CW (90deg CCW)
	//
	// NOTE: use orientationExif2ImageGrid(..) to convert from EXIF 
	// 		orientation format to ImageGrid format...
	//orientation: 0,

	// Image flip state (optional)
	//
	// can be:
	// 	- null/undefined
	// 	- array
	//
	// can contain:
	// 	- 'vertical'
	// 	- 'horizontal'
	//
	// NOTE: use orientationExif2ImageGrid(..) to convert from EXIF 
	// 		orientation format to ImageGrid format...
	//flipped: null,

	// Image comment (optional)
	//
	// can be:
	// 	- null/undefined
	// 	- string
	//comment: null,

	// List of image tags (optional)
	//
	// can be:
	// 	- null/undefined
	// 	- array
	//tags: null,
}


var GROUP_DATA =
module.GROUP_DATA = {
	// Entity GID...
	id: 'GID',

	// Entity type
	type: 'group',

	// Entity state
	//
	// can be:
	// 	- 'single'
	// 	- 'grouped'
	// 	- 'hidden'
	// 	- ...
	state: 'single',

	// image used to represent/display group...
	cover: 'GID',

	// list of group contents, including .cover
	items: [
		'GID',
	],

	// Classes
	// XXX currently unused...
	//classes: '',

	// Image comment (optional)
	//
	// can be:
	// 	- null/undefined
	// 	- string
	//comment: null,

	// List of image tags (optional)
	//
	// can be:
	// 	- null/undefined
	// 	- array
	//tags: null,
}

// Calculate relative rotation angle...
//
// Calculate rotation angle relative to from:
// 	calcRelativeRotation(from, 'cw')
// 	calcRelativeRotation(from, 'ccw')
// 		-> 0 | 90 | 180 | 270
//
// Validate an angle:
// 	calcRelativeRotation(angle)
// 	calcRelativeRotation(from, angle)
// 		-> 0 | 90 | 180 | 270
// 		-> null
//
//
module.calcRelativeRotation = function(from, to){
	if(to == null){
		to = from
		from = 0
	}
	to = to == 'cw' ? 1 
		: to == 'ccw' ? -1
		: [0, 90, 180, 270].indexOf(to*1) >= 0 ? to*1
		: [-90, -180, -270].indexOf(to*1) >= 0 ? 360+(to*1)
		: null

	// relative rotation...
	if(to == 1 || to == -1){
		var res = from
		res = res == null ? 0 : res*1
		res += 90*to
		res = res < 0 ? 270 
			: res > 270 ? 0
			: res

	// explicit direction...
	} else {
		var res = to
	}

	return res
}



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

// cmp functions...
// XXX is this the right way to seporate these???

module.makeImageDateCmp = function(data, get){
	return function(a, b){
		if(get != null){
			a = get(a)
			b = get(b)
		}
		b = data[b].ctime
		a = data[a].ctime

		if(a == b){
			return 0
		} else if(a < b){
			return -1
		} else {
			return +1
		}
	}
}

// NOTE: this expects gids...
module.makeImageNameCmp = function(data, get){
	return function(a, b){
		if(get != null){
			a = get(a)
			b = get(b)
		}
		a = data.getImageFileName(a)
		b = data.getImageFileName(b)
		if(a == b){
			return 0
		} else if(a < b){
			return -1
		} else {
			return +1
		}
	}
}

module.makeImageSeqOrNameCmp = function(data, get, seq){
	seq = seq == null ? data.getImageNameSeq : seq

	return function(a, b){
		// XXX this is ugly and non-generic...
		if(get != null){
			a = get(a)
			b = get(b)
		}
		// XXX this is ugly and non-generic...
		var aa = seq.call(data, a)
		var bb = seq.call(data, b)

		// special case: seq, name
		if(typeof(aa) == typeof(123) && typeof(bb) == typeof('str')){ return -1 }
		// special case: name, seq
		if(typeof(aa) == typeof('str') && typeof(bb) == typeof(123)){ return +1 }

		// get the names if there are no sequence numbers...
		// NOTE: at this point both a and b are either numbers or NaN's...
		a = isNaN(aa) ? data.getImageFileName(a) : aa
		b = isNaN(bb) ? data.getImageFileName(b) : bb

		// do the actual comparison
		if(a == b){
			return 0
		} else if(a < b){
			return -1
		} else {
			return +1
		}
	}
}



/*********************************************************************/
// XXX Image base class...
// 		...not sure if we need this... (???)

var ImageClassPrototype =
module.ImageClassPrototype = {
}

var ImagePrototype =
module.ImagePrototype = {
}

var Image = 
module.Image = 
object.makeConstructor('Image', 
		ImageClassPrototype, 
		ImagePrototype)



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

// XXX depends on jli.quoteRegExp(..)
var ImagesClassPrototype =
module.ImagesClassPrototype = {
	// XXX populate the image doc better...
	// NOTE: if base is given then it will be set as .base_path and 
	// 		removed from each url if present...
	fromArray: function(data, base){
		var images = new this()
		// XXX stub...
		var i = 0
		//var base_pattern = base ? RegExp('^' + base) : null 
		var base_pattern = base ? RegExp('^' + RegExp.quoteRegExp(base)) : null 
		data.forEach(function(path){
			// XXX need to normalize path...
			var p = path.startsWith('data') ? 
				path 
				: (base_pattern ? path.replace(base_pattern, './') : path)
					.replace(/([\/\\])\1+/g, '/')
			// XXXX
			var gid = hash('I'+i+':'+p)

			var name = (p
					// basename...
					.split(/[\\\/]/g).pop() || '')
					// ext...
					.split(/(\.[^\.]*$)/)

			// XXX populate the image doc better...
			images[gid] = {
				id: gid,
				path: p,
				// basename...
				name: name[0],
				// ext with leading '.'
				ext: name[1],
			}

			// remove only if base path is given and in path...
			if(base && base_pattern.test(path)){
				images[gid].base_path = base
			}
			i += 1
		})
		return images
	},
	fromJSON: function(data){
		return new this().loadJSON(data)
	},
}


var ImagesPrototype =
module.ImagesPrototype = {
	//version: '3.1',

	get length(){
		return Object.keys(this).length 
			// XXX is this the correct way to deal with service props???
			+ (this.hasOwnProperty('version') ? -1 : 0) },

	// Generic iterators...
	//
	// function format:
	// 		function(key, value, index, object)
	//
	// reduce function format:
	// 		function(value1, value2, key, index, object)
	//
	//
	// this will be set to the value...
	//
	// XXX revise...
	// XXX are these slower than doing it manualy via Object.keys(..)
	forEach: function(func){
		var i = 0
		for(var key in this){
			// reject non images...
			// XXX make this cleaner...
			if(key == 'length' || typeof(this[key]) == typeof(function(){})){
				continue
			}
			func.call(this[key], key, this[key], i++, this)
		}
		return this
	},
	filter: function(func){
		var res = new this.constructor()
		var i = 0
		for(var key in this){
			// reject non images...
			// XXX make this cleaner...
			if(key == 'length' || typeof(this[key]) == typeof(function(){})){
				continue
			}
			if(func.call(this[key], key, this[key], i++, this)){
				res[key] = this[key]
			}
		}
		return res
	},
	// NOTE: .map(..) and .reduce(..) will not return Images objects...
	map: function(func){
		//var res = this.constructor()
		var res = []
		var i = 0
		for(var key in this){
			// reject non images...
			// XXX make this cleaner...
			if(key == 'length' || typeof(this[key]) == typeof(function(){})){
				continue
			}
			//res[key] = func.call(this[key], key, this[key], i++, this)
			res.push(func.call(this[key], key, this[key], i++, this))
		}
		return res
	},
	reduce: function(func, initial){
		var res = initial
		for(var key in this){
			// reject non images...
			// XXX make this cleaner...
			if(key == 'length' || typeof(this[key]) == typeof(function(){})){
				continue
			}
			res = func.call(this[key], res, this[key], key, i++, this)
		}
		return res
	},

	keys: function(){
		return Object.keys(this) },

	// Build an image index relative to an attribute...
	//
	// Format:
	// 	{
	// 		<attr-value> : [
	// 			<gid>,
	// 			...
	// 		],
	// 		...
	// 	}
	//
	// XXX test out the attr list functionality...
	makeIndex: function(attr){
		var res = {}
		attr = attr.constructor !== Array ? [attr] : attr

		// buld the index...
		var that = this
		this.forEach(function(key){
			var n = attr.map(function(n){ return that[n] })
			n = JSON.stringify(n.length == 1 ? n[0] : n)
				// XXX is this the right way to go?
				.replace(/^"(.*)"$/g, '$1')
			res[n] = n in res ? res[n].concat(key) : [key]
		})

		return res
	},


	// Image data helpers...
	
	getImagePath: function(gid, path){
		var img = this[gid] || IMAGE_DATA

		return (img.base_path || path) ? 
			[img.base_path || path, img.path].join('/')
			: util.path2url(img.path || IMAGE_DATA.path)
	},
	// NOTE: actual URL decoding and encoding is not done here to keep
	// 		things consistent, rather it is done the the latest possible 
	// 		stage, in images._loadImagePreviewURL(..)
	// XXX see: ribbons.js for details...
	// XXX this is the same (in part) as .getImagePath(..) 
	getBestPreview: function(gid, size, img_data, full_path){
		if(img_data === true){
			full_path = true
			img_data = null
		}
		//gid = gid == null ? getImageGID(): gid
		//size = size == null ? getVisibleImageSize('max') : size
		img_data = img_data == null ? this[gid] : img_data

		// if no usable images are available use STUB data...
		if(!img_data 
				|| (img_data.preview == null 
					|| Object.keys(img_data.preview).length == 0)
				&& img_data.path == null){
			img_data = IMAGE_DATA
		}

		var s
		var url = img_data.path
		var preview_size = 'Original'
		var p = Infinity
		var previews = img_data.preview || {}

		for(var k in previews){
			s = parseInt(k)
			if(s < p && s > size){
				preview_size = k
				p = s
				url = previews[k]
			}
		}

		// XXX LEGACY...
		//url = url.indexOf('%20') >= 0 ? decodeURI(url) : url

		return {
			url: (full_path && img_data.base_path ?
				  	img_data.base_path + '/' 
					: '') 
				+ url,
			size: preview_size
		}
	},

	// Get image filename...
	//
	// NOTE: this will default to gid if not filename (.path) is set... (???)
	getImageFileName: function(gid, do_unescape){
		do_unescape = do_unescape == null ? true : do_unescape
		if(!this[gid] || !this[gid].path){
			return gid
		}
		if(do_unescape){
			return unescape(this[gid].path.split('/').pop())
		} else {
			return this[gid].path.split('/').pop()
		}
	},
	// Get the first sequence of numbers in the file name...
	//
	// NOTE: if no filenmae (.path) is set, this will return gid... (???)
	getImageNameSeq: function(gid){
		if(!this[gid] || !this[gid].path){
			return gid
		}
		var n = this.getImageFileName(gid)
		var r = /([0-9]+)/m.exec(n)
		return r == null ? n : parseInt(r[1])
	},
	// Get the sequence of numbers in the file name but only if it is 
	// at the filename start...
	getImageNameLeadingSeq: function(gid){
		if(!this[gid] || !this[gid].path){
			return gid
		}
		var n = this.getImageFileName(gid)
		var r = /^([0-9]+)/g.exec(n)
		return r == null ? n : parseInt(r[1])
	},

	// Replace image gid...
	//
	replaceGid: function(from, to){
		var img = this[from]

		// XXX is the test needed here???
		if(img != null){
			delete this[from]
			this[to] = img
		}

		return this
	},

	// Gid sorters...
	// XXX might be a good idea to add caching...
	// XXX chainCmp(..) is loaded from lib/jli.js
	sortImages: function(gids, cmp, reverse){
		gids = gids == null ? Object.keys(this) : gids

		cmp = cmp == null ? module.makeImageDateCmp(this) : cmp
		cmp = cmp.constructor === Array ? chainCmp(cmp) : cmp

		gids = gids.sort(cmp)
		gids = reverse ? gids.reverse() : gids

		return gids
	},
	// Shorthands...
	// XXX 
	sortedImagesByFileNameSeqWithOverflow: function(gids, reverse){
		gids = gids == null ? Object.keys(this) : gids

		// XXX see ../ui/sort.js
	},

	// Actions...

	// Rotate image...
	//
	// Rotate image clockwise:
	//	.rotateImage(target, 'cw')
	//		-> images
	//
	// Rotate image counterclockwise:
	//	.rotateImage(target, 'ccw')
	//		-> images
	//
	// Set explicit image rotation angle:
	//	.rotateImage(target, 0|90|180|270)
	//	.rotateImage(target, -90|-180|-270)
	//		-> images
	//
	// NOTE: target can be a gid or a list of gids...
	rotateImage: function(gids, direction){
		gids = gids.constructor !== Array ? [gids] : gids
		// validate direction...
		if(module.calcRelativeRotation(direction) == null){
			return this
		}

		var that = this
		gids.forEach(function(key){
			var img = that[key]
			if(img == null){
				img = that[key] = {}
			}
			var o = direction == 'cw' || direction == 'ccw' 
				? module.calcRelativeRotation(img.orientation, direction) 
				: direction*1

			/* XXX seting orientation to undefined does not save correctly (BUG?) 
			if(o == 0){
				delete img.orientation
			} else {
				img.orientation = o
			}
			//*/
			img.orientation = o

			// account for proportions...
			//that.correctImageProportionsForRotation(img)
			// XXX this is a bit of an overkill but it will update the 
			// 		preview if needed...
			//that.updateImage(img)
		})
		return this
	},

	// Flip image...
	//
	//	.flipImage(target, 'horizontal')
	//	.flipImage(target, 'vertical')
	//		-> images
	//
	flipImage: function(gids, direction, reference){
		gids = gids.constructor !== Array ? [gids] : gids
		reference = reference || 'view'
		var that = this
		gids.forEach(function(key){
			var img = that[key]
			if(img == null){
				img = that[key] = {}
			}
			var o = img.orientation
			var d = direction

			// flip relative to 
			if(reference == 'view' && (o == 90 || o == 270)){
				d = d == 'horizontal' ? 'vertical' : 'horizontal'
			}

			if(img == null){
				img = that[key] = {}
			}
			var state = img.flipped
			state = state == null ? [] : state
			// toggle the specific state...
			var i = state.indexOf(d)
			if(i >= 0){
				state.splice(i, 1)
			} else {
				state.push(d)
			}
			if(state.length == 0){
				delete img.flipped
			} else {
				img.flipped = state
			}
		})
		return this
	},


	clone: function(){
		return (new Images()).loadJSON(this.dumpJSON()) },
	// NOTE: this will join the other data into the current object in-place,
	// 		use .clone() to preserve current data...
	join: function(other){
		var that = this

		other.forEach(function(gid, img){
			that[gid] = img
		})

		return this
	},

	// serialization...
	loadJSON: function(data){
		data = typeof(data) == typeof('str') 
			? JSON.parse(data) 
			: JSON.parse(JSON.stringify(data))
		var version = data.versio
		for(var k in data){
			var img = this[k] = data[k]

			// keep the preview paths decoded...
			//
			// NOTE: updating from legacy format...
			// XXX move this to version conversion... (???)
			if(version == null){
				Object.keys(img && img.preview || {})
					.forEach(function(res){
						var p = img.preview[res]
						img.preview[res] = p.indexOf(k+'%20-%20') >= 0 ? decodeURI(p) : p
					})
			}
		}
		return this
	},
	// XXX this is really odd: renaming this to 'toJSON' breaks JavaScript
	// 		making chrome/node just say: "<error>" and a filename...
	dumpJSON: function(data){
		var res = JSON.parse(JSON.stringify(this))
		// XXX
		res.version = '3.0'
		return res
	},

	_reset: function(){
	},


	// XXX is this a good name for this??? (see: object.js)
	__init__: function(json){
		// load initial state...
		if(json != null){
			this.loadJSON(json)
		} else {
			this._reset()
		}
		return this
	},
}



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

// Main Images object...
var Images = 
module.Images = 
object.makeConstructor('Images', 
		ImagesClassPrototype, 
		ImagesPrototype)



/**********************************************************************
* vim:set ts=4 sw=4 :                               */ return module })
