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

if(typeof(process) != 'undefined'){
	var fse = requirejs('fs-extra')
	var pathlib = requirejs('path')
	var file = require('imagegrid/file')
}

var util = require('lib/util')

var data = require('imagegrid/data')

var toggler = require('lib/toggler')
var actions = require('lib/actions')
var features = require('lib/features')

var browse = require('lib/widget/browse')

var core = require('features/core')
var widgets = require('features/ui-widgets')



/*********************************************************************/
// 
// XXX need a way to load a collection directly...
// 		- reload from .location.collection
// 		- path syntax 
// 			<path>:<title-selector>	-- needs to be normalized
// 			<path>:<gid>
// XXX might be a good idea to make collection loading part of the 
// 		.load(..) protocol...
// 		...this could be done via a url suffix, as a shorthand.
// 		something like:
// 			/path/to/index:collection
// 				-> /path/to/index/sub/path/.ImageGrid/collections/collection
// XXX loading collections by direct path would require us to look 
// 		in the containing index for missing parts (*images.json, ...)
// XXX saving a local collection would require us to save to two 
// 		locations:
// 			- collection specific stuff (data) to collection path
// 			- global stuff (images, tags, ...) to base index...
// XXX local tags:
// 		- save		- done
// 		- load		- done
// 		- save/merge use...
// XXX merge data -- collection/crop/all
// 		- vertical only
// 		- horizontal only
// 		- both
// XXX external / linked collections (.location)...
// XXX merge collections from multiple indexes -- can we avoid extending the format???
//

// XXX should these be in .config???
var MAIN_COLLECTION_TITLE =
module.MAIN_COLLECTION_TITLE = '$ALL'

var MAIN_COLLECTION_GID =
module.MAIN_COLLECTION_GID = '0'

var COLLECTION_TRANSFER_CHANGES =
module.COLLECTION_TRANSFER_CHANGES = [
	'metadata',
	'data',
]



//---------------------------------------------------------------------

// XXX add undo...
var CollectionActions = actions.Actions({
	config: {
		// can be:
		// 	'all'		- save crop state for all collections (default)
		// 	'main'		- save crop state for main state only
		// 	'none'		- do not save crop state
		'collection-save-crop-state': 'all',


		// XXX should this be in config???
		// 		...technically no, but we need this to resolve correctly 
		// 		to a relevant feature...
		'collection-transfer-changes': COLLECTION_TRANSFER_CHANGES.slice(),

		// Global default collections...
		//
		// NOTE: delete or set to null for none...
		//'default-collections': null,
	},

	// Format:
	// 	{
	// 		<title>: {
	// 			title: <title>,
	// 			gid: <gid>,
	// 			count: <number>,
	//
	// 			crop_stack: [ .. ],
	//
	// 			// base collection format -- raw data...
	// 			data: <data>,
	//
	// 			...
	// 		},
	// 		...
	// 	}
	collections: null,

	get collectionGIDs(){
		var res = {}
		var c = this.collections || {}
		Object.keys(c)
			.forEach(function(title){
				res[c[title].gid || title] = title
			})
		return res
	},

	get collection(){
		return this.location.collection },
	set collection(value){
		this.loadCollection(value) },
	get collectionGID(){
		return ((this.collections || {})[this.collection] || {}).gid 
			|| MAIN_COLLECTION_GID },
	set collectionGID(value){
		this.collection = value },

	// XXX should this check consistency???
	get collection_order(){
		var collections = this.collections
		var defaults = this.config['default-collections'] || []

		// no collections -> return defaults | []
		if(this.collections == null){
			return defaults.slice()
		}

		var keys = Object.keys(collections)
		var order = this.__collection_order = this.__collection_order || []

		// add unsorted things to the head of the list...
		var res = keys
			.concat(order)
			.reverse()
			.unique()
			.reverse()

		// XXX defaults...
		res = res.concat(defaults).unique()

		// keep MAIN_COLLECTION_TITLE out of the collection order...
		var m = res.indexOf(MAIN_COLLECTION_TITLE)
		m >= 0
			&& res.splice(m, 1)

		// remove stuff not present...
		if(res.length > keys.length){
			res = res.filter(function(e){ 
				return e in collections 
					|| defaults.indexOf(e) >= 0 })
		}

		this.__collection_order.splice(0, this.__collection_order.length, ...res)

		return this.__collection_order.slice()
	},
	set collection_order(value){
		value && this.sortCollections(value) },

	// NOTE: this accounts only for actual collections and does not counts
	// 		MAIN_COLLECTION_TITLE that can be contained in .collections,
	// 		thus this is NOT the same as:
	// 			Object.keys(this.collections).length
	// XXX do we need this???
	get collections_length(){
		var c = (this.collections || {})
		return MAIN_COLLECTION_TITLE in c ? 
			Object.keys(c).length - 1
			: Object.keys(c).length
	},

	// Format:
	// 	{
	// 		// NOTE: this is always the first handler...
	// 		'data': <action-name>,
	// 		'gid': <gid>,
	//
	// 		<format>: <action-name>,
	// 		...
	// 	}
	//
	// XXX revise doc...
	// XXX this is almost the same as .store_handlers...
	get collection_handlers(){
		return this.cache('collection_handlers', function(handlers){
			// cached value...
			if(handlers){
				return Object.assign({}, handlers)
			}

			var that = this
			handlers = {}

			handlers['data'] = null
			this.actions.forEach(function(action){
				var fmt = that.getActionAttr(action, 'collectionFormat')
				handlers[fmt]
					&& console.warn('Multiple handlers for collection format:', store)
				if(fmt){
					handlers[fmt] = action
				}
			})

			// cleanup...
			if(handlers['data'] == null){
				delete handlers['data']
			}

			return handlers
		}) },


	// Collection events...
	//
	collectionLoading: ['- Collections/',
		core.doc`This is called by .loadCollection(..) or one of the 
		overloading actions around the collection load...

		The .pre phase is called just before the load and the .post phase 
		just after.
		
		`,
		core.Event(function(collection){
			// This is the window resize event...
			//
			// Not for direct use.
		})],
	collectionUnloaded: ['- Collections/',
		core.doc`This is called when unloading a collection.
		`,
		core.Event(function(collection){
			// This is the window resize event...
			//
			// Not for direct use.
		})],
	collectionCreated: ['- Collections/',
		core.doc`This is called when a collection is created.

		NOTE: this is not triggered for the "${MAIN_COLLECTION_TITLE}" collection...
		`,
		core.Event(function(collection){
			// This is the window resize event...
			//
			// Not for direct use.
		})],
	collectionRemoved: ['- Collections/',
		core.doc`This is called when a collection is removed.

		NOTE: this is not triggered for the "${MAIN_COLLECTION_TITLE}" collection...
		`,
		core.Event(function(collection){
			// This is the window resize event...
			//
			// Not for direct use.
		})],


	// XXX should there be a force arg when we can't actually stop the 
	// 		running promise and recover???
	// XXX need to figure out error handling for this scheme...
	// XXX do we need timeouts here????
	ensureCollection: ['- Collections/',
		core.doc`Ensure a collection exists and is consistent...

			Ensure collection exists and is initialized...
			.ensureCollection(title)
				-> promise(collection)
				NOTE: this will not start a new check until the previous
					is done (i.e. the previous promise is resolved/rejected)

		
		This will:
			- create a collection if it does not exist
			- initialize if needed

		While the promise for a specific action is not resolved this 
		will return it and not start a new promise thus queuing all 
		subsequent calls.
		`,
		function(collection){
			var that = this

			var running = this.__running_collection_ensure = 
				this.__running_collection_ensure || {}

			// create collection if needed...
			;(!this.collections 
					|| !(collection in this.collections))
				&& this.newCollection(collection)

			var collection_data = this.collections[collection]
			var handlers = this.collection_handlers

			// if a promise has not yet resolved/rejected, return it 
			// and do not start a new one...
			if(running[collection]){
				return running[collection]
			}

			// handle collection...
			p = running[collection] = new Promise(function(resolve, reject){
				// NOTE: we do not need to return this as we'll resolve/reject
				// 		manually in .then(..) / .catch(..)
				Promise
					.all(Object.keys(handlers)
						// filter relevant handlers...
						.filter(function(format){ 
							return format == '*' || collection_data[format] })
						// run handlers...
						.map(function(format){
							return that[handlers[format]](collection, collection_data) }))
					.then(function(){
						delete running[collection]
						resolve(collection_data) })
					.catch(function(err){
						delete running[collection]
						reject(err) })
			})

			return p
		}],


	// Collection life-cycle...
	//
	// NOTE: if collection does not exist this will do nothing...
	// NOTE: this is not sync, if it is needed to trigger post collection
	// 		loading then bind to collectionLoading.post...
	loadCollection: ['- Collections/',
		core.doc`Load collection...

			Load collection...
			.loadCollection(collection)
			.loadCollection(gid)
				-> this
			
			Force reload current collection...
			.loadCollection('!')
				-> this
				NOTE: this will not call .saveCollection(..) before 
					reloading, thus potentially losing some state that 
					was not explicitly saved.


		When loading a collection, previous state is saved.

		If .data for a collection is not available this will do nothing, 
		this enables extending actions to handle the collection in 
		different ways.

		Protocol:
		- collection format handlers: .collection_handlers
			- built from actions that define .collectionFormat attr to 
			  contain the target format string.
		- format is determined by matching it to a key in .collections[collection]
			e.g. 'data' is applicable if .collections[collection].data is not null
		- the format key's value is passed to the handler action
		- the handler is expected to return a promise
		- only the first matching handler is called
		- the data handler is always first to get checked

		Example loader action:
			collectionXLoader: [
				// handle .x
				{collectionFormat: 'x'}
				function(title, state){
					return new Promise(function(resolve){ 
						var x = state.x
		
						// do stuff with .x
		
						resolve() 
					}) }],


		The .data handler is always first to enable caching, i.e. once some
		non-data handler is done, it can set the .data which will be loaded
		directly the next time.
		To invalidate such a cache .data should simply be deleted.


		NOTE: cached collection state is persistent.
		NOTE: when current collection is removed from .collections this 
			will not save state when loading another collection...
		`,
		function(collection){
			var that = this
			var force = collection == '!'
			collection = collection == '!' ? 
				this.collection 
				: collection
			// if collection is a gid, get the title...
			collection = this.collectionGIDs[collection] || collection 
			if(collection == null 
					|| this.collections == null 
					|| !(collection in this.collections)){
				return
			}
			var crop_mode = this.config['collection-save-crop-state'] || 'all'

			var current = this.current
			var ribbon = this.current_ribbon

			var prev = this.collection
			var collection_data = this.collections[collection]
			//var handlers = this.collection_handlers

			// save current collection state...
			//
			// main view...
			// NOTE: we save here unconditionally because MAIN_COLLECTION_TITLE
			// 		is stored ONLY when we load some other collection...
			if(this.collection == null){
				this.saveCollection(
					MAIN_COLLECTION_TITLE, 
					crop_mode == 'none' ?  'base' : 'crop', 
					true)

			// collection...
			// NOTE: we only save if the current collection exists, it 
			// 		may not exist if it was just removed...
			} else if(this.collection in this.collections
					// prevent saving over changed current state...
					&& !force){
				this.saveCollection(
					this.collection, 
					crop_mode == 'all' ? 'crop': null)
			}

			// load collection...
			// XXX should this be sync???
			//return this.ensureCollection(collection)
			this.ensureCollection(collection)
				.then(function(){
					var data = collection_data.data

					if(!data){
						return
					}

					// current...
					data.current = data.getImage(current) 
						// current is not in collection -> try and keep 
						// the ribbon context...
						|| that.data.getImage(
							current, 
							data.getImages(that.data.getImages(ribbon)))
						// get closest image from collection...
						|| that.data.getImage(current, data.order)
						|| data.current

					that
						.collectionLoading.chainCall(that, 
							function(){
								// do the actual load...
								that.load.chainCall(that, 
									function(){
										that.collectionUnloaded(
											prev || MAIN_COLLECTION_TITLE)
									}, {
										location: that.location,

										data: data,

										crop_stack: collection_data.crop_stack
											&& collection_data.crop_stack.slice(),

										// NOTE: we do not need to pass collections 
										// 		and order here as they stay in from 
										// 		the last .load(..) in merge mode...
										//collections: that.collections,
										//collection_order: that.collection_order,
									}, true)

								// maintain the .collection state...
								if(collection == MAIN_COLLECTION_TITLE){
									// no need to maintain the main data in two 
									// locations...
									delete that.collections[MAIN_COLLECTION_TITLE]
									delete this.location.collection

								} else {
									that.data.collection = 
										that.location.collection = 
										collection
									// cleanup...
									if(collection == null){
										delete this.location.collection
									}
								}
							}, 
							collection)
				})
		}],
	// XXX should this call .loadCollection('!') when saving to current
	// 		collection???
	// 		This would reaload the view to a consistent (just saved) 
	// 		state...
	// 		...see comments inside...
	// XXX it feels like we need two levels of actions, low-level that 
	// 		just do their job and user actions that take care of 
	// 		consistent state and the like...
	saveCollection: ['- Collections/',
		core.doc`Save current state to collection

			Save current state to current collection
			.saveCollection()
			.saveCollection('current')
				-> this
				NOTE: this will do nothing if no collection is loaded.

			Save state as collection...
			.saveCollection(collection)
				-> this
				NOTE: if saving to self the default mode is 'crop' else
					it is 'current' (see below for info on respective 
					modes)...

			Save current state as collection ignoring crop stack
			.saveCollection(collection, 0)
			.saveCollection(collection, 'current')
				-> this

			Save new empty collection
			.saveCollection(collection, 'empty')
				-> this

			Save current crop state to collection
			.saveCollection(collection, 'crop')
				-> this

			Save top depth crops from current crop stack to collection
			.saveCollection(collection, depth)
				-> this

			Save base crop state to collection
			.saveCollection(collection, 'base')
				-> this


		NOTE: this will overwrite collection .data and .crop_stack only, 
			the rest of the data is untouched...
		NOTE: when saving to current collection and maintain consistent 
			state it may be necessary to .loadCollection('!')
		`,
		function(collection, mode, force){
			var that = this
			collection = collection || this.collection
			collection = collection == 'current' ? this.collection : collection

			if(!force 
					&& (collection == null || collection == MAIN_COLLECTION_TITLE)){
				return
			}

			var collections = this.collections = this.collections || {}
			var depth = typeof(mode) == typeof(123) ? mode : null
			mode = depth == 0 ? 'current' 
				: depth ? 'crop' 
				: mode
			// default mode -- if saving to self then 'crop' else 'current'
			if(!mode){
				mode = ((collection in collections 
							&& collection == this.collection) 
						|| collection == MAIN_COLLECTION_TITLE) ? 
					'crop' 
					: 'current'
			}
			var new_collection = 
				!collections[collection] 
				&& collection != MAIN_COLLECTION_TITLE

			// save the data...
			var state = collections[collection] = collections[collection] || {}
			state.title = state.title || collection
			state.gid = state.gid 
				// maintain the GID of MAIN_COLLECTION_TITLE as MAIN_COLLECTION_GID...
				|| (collection == MAIN_COLLECTION_TITLE ? 
					MAIN_COLLECTION_GID 
					// generate unique gid...
					: (function(){
						var gids = that.collectionGIDs
						do{
							var gid = that.data.newGID()
						} while(gids[gid] != null)
						return gid
					})())
			// NOTE: we do not need to care about tags here as they 
			// 		will get overwritten on load...
			state.data = (mode == 'empty' ? 
					(new this.data.constructor())
				: mode == 'base' && this.crop_stack ? 
					(this.crop_stack[0] || this.data.clone())
				: mode == 'crop' ? 
					this.data.clone()
				// current...
				: this.data.clone()
					.run(function(){
						var d = this
						this.collection = collection
					})
					.clear('unloaded'))

			// crop mode -> handle crop stack...
			if(mode == 'crop' && this.crop_stack && depth != 0){
				depth = depth || this.crop_stack.length
				depth = this.crop_stack.length - Math.min(depth, this.crop_stack.length)

				state.crop_stack = this.crop_stack.slice(depth)

			// other modes...
			} else {
				delete state.crop_stack
			}


			// XXX this leads to recursion????
			// 		.loadCollection('X')
			// 			-> .saveCollection('current')
			// 				-> .loadCollection('!')
			// XXX should we be doing this here or on case by case basis externally...
			//collection == this.collection
			//	&& this.loadCollection('!')

			new_collection
				&& this.collectionCreated(collection)
		}],
	newCollection: ['- Collections/',
		core.doc` Shorthand to .saveCollection(collection, 'empty')`,
		function(collection){ return this.saveCollection(collection, 'empty') }],
	removeCollection: ['- Collections/',
		core.doc`

			.removeCollection(collection)
			.removeCollection(gid)
				-> this
		
		NOTE: when removing the currently loaded collection this will 
			just remove it from .collections and do nothing...`,
		function(collection){
			if(!this.collections || collection == MAIN_COLLECTION_TITLE){
				return
			}
			collection = this.collectionGIDs[collection] || collection
			if(collection in this.collections){
				delete this.collections[collection]
				this.collectionRemoved(collection)
			}
		}],
	renameCollection: ['- Collections/',
		function(from, to){
			if(to == from 
					|| from == MAIN_COLLECTION_TITLE 
					|| to == MAIN_COLLECTION_TITLE 
					|| (this.collections || {})[from] == null){
				return
			}

			var order = this.collection_order
			order.splice(order.indexOf(from), 1, to)

			var data = this.collections[to] = this.collections[from]
			delete this.collections[from]

			data.title = to

			if(this.collection == from){
				this.location.collection = to
			}
		}],


	// Collection list manipulation...
	//
	sortCollections: ['- Collections/',
		core.doc`Sort collection list...

			Sort collections...
			.sortCollections()	
				NOTE: this is equivalent to [].sort()
				-> this

			Sort collections via cmp function...
			.sortCollections(cmp)	
				NOTE: this is equivalent to [].sort(cmp)
				-> this

			Sort collections via list...
			.sortCollections([item, ...])	
				-> this

		`,
		function(cmp){
			// XXX handle the case when there's no .__collection_order
			if(!this.__collection_order){
				return
			}

			// sort via list...
			if(cmp instanceof Array){
				this.__collection_order = cmp.slice()

			// cmp...
			} else if(cmp instanceof Function){
				this.__collection_order.sort(cmp)

			// basic sort...
			} else {
				this.__collection_order.sort()
			}
			this.collection_order
		}],
	collectionToTop: ['Collections/Bring collection to $top',
		core.doc`Bring collection to top...

			Bring current collection to top of collection list
			.collectionToTop()
				-> this

			Bring collection title to top of collection list
			.collectionToTop(title)
				-> this

			Bring collection gid to top of collection list
			.collectionToTop(gid)
				-> this
		`,
		{browseMode: 'uncollect'},
		function(collection){
			collection = collection || this.collection
			collection = this.collectionGIDs[collection] || collection
			var o = this.collection_order 

			if(!collection || o.indexOf(collection) < 0){
				return
			}

			this.collection_order = [collection].concat(o).unique()
		}],
	

	// Introspection...
	//
	// XXX make this check offline collections -- use .ensureCollection(..)???
	inCollections: ['- Image/',
		core.doc`Get list of collections containing item
		
		NOTE: this currently does not load or check offline collections.
		`,
		function(gid, collections){
			var that = this
			gid = this.data.getImage(gid)
			collections = collections || this.collection_order
			collections = collections instanceof Array ? collections : [collections]
			return collections
				.filter(function(c){
					return c != MAIN_COLLECTION_TITLE
						&& that.collections[c]
						&& that.collections[c].data
						&& (!gid 
							|| that.collections[c].data.getImage(gid)) })
		}],


	// Collection editing....
	//
	// NOTE: Currently these are sync, and sequencing collections 
	// 		operations happens automatically as everything uses 
	// 		.ensureCollection(..) internally...
	// 		to explecitly sequence code do:
	//			.collect(..)
	//			.ensureCollection(..)
	//				.then(function(){
	//					// this is run strictly after .collect(..) 
	//					...
	//				})
	// NOTE: see .ensureCollection(..) for more details...
	//
	// XXX undo: need to be able to place collected stuff...
	collect: ['Collections|Image/Add $image to collection...',
		core.doc`Add items to collection

			Add current image to collection...
			.collect('current', collection)
				-> this

			Add current ribbon to collection...
			.collect('ribbon', collection)
				-> this

			Add loaded images to collection...
			.collect('loaded', collection)
				-> this

			
			Add gid(s) to collection...
			.collect(gid, collection)
			.collect([gid, ,. ], collection)
				-> this


		NOTE: this will not account for item topology. To merge accounting
			for topology use .joinCollect(..)
		NOTE: if an image gid is not found locally it will be searched in
			base data...
		`,
		function(gids, collection){
			var that = this
			collection = collection || this.collection
			collection = this.collectionGIDs[collection] || collection 
			if(collection == null || collection == MAIN_COLLECTION_TITLE){
				return
			}

			gids = gids == 'loaded' ? 
					this.data.getImages('loaded')
				: gids == 'ribbon' ?
					[this.current_ribbon]
				: gids instanceof Array ? 
					gids 
				: [gids]
			gids = gids
				.map(function(gid){ 
					return gid in that.data.ribbons ? 
						// when adding a ribbon gid expand to images...
						that.data.ribbons[gid].compact()
						: [ that.data.getImage(gid) 
							// check base data for image gid...
							|| that.collections[MAIN_COLLECTION_TITLE].data.getImage(gid) ] })
				.flat()

			this.ensureCollection(collection)
				.then((function(c){
					var remove = c.data.getImages(gids, 'all')
					// only add gids that do not exist in collection...
					gids = gids
						.filter(function(g){ return remove.indexOf(g) < 0 })

					if(gids.length == 0){
						return
					}

					// add to collection...
					var data = this.data.constructor.fromArray(gids)

					// XXX should we use collection.data.placeImage(..)???
					return this.joinCollect(null, collection, data)
				}).bind(this))
		}],
	joinCollect: ['Collections/$Merge view to collection...',
		core.doc`Merge/Join current state to collection

			Join current state into collection
			.joinCollect(collection)
				-> this

			Join current state with specific alignment into collection
			.joinCollect(align, collection)
				-> this

			Join data to collection with specific alignment
			.joinCollect(align, collection, data)
				-> this

		This is like .collect(..) but will preserve topology.
		
		NOTE: for align docs see Data.join(..)
		NOTE: if align is set to null or not given then it will be set 
			to default value.
		NOTE: this will join to the left (prepend) of the collections, this is 
			different from how basic .join(..) works (appends)
		`,
		function(align, collection, data){
			collection = arguments.length == 1 ? align : collection
			collection = this.collectionGIDs[collection] || collection 
			if(collection == null || collection == MAIN_COLLECTION_TITLE){
				return
			}

			// if only collection is given, reset align to null...
			align = align === collection ? null : align

			// create collection if it does not exist...
			this.ensureCollection(collection)
				.then((function(c){
					var target = c.crop_stack ? 
						c.crop_stack[0] 
						: c.data
					//this.collections[collection].data.join(align, data || this.data.clone())
					var res = (data || this.data)
						.clone()
						.clear('unloaded')
						.join(align, target)

					var rorder = res.order.slice().reverse()

					// write to base data...
					if(c.crop_stack){
						c.crop_stack[0] = res

						c.crop_stack
							.concat([c.data])
							.forEach(function(data){
								data.order = data.order
									.reverse()
									.concat(rorder)
									.unique()
									.reverse() 
								data.updateImagePositions()
							})

					} else {
						c.data = res
					}

					// joining into the current collection...
					if(collection == this.collection){
						if(this.crop_stack){
							this.crop_stack[0] = res

							this.crop_stack
								.concat([this.data])
								.forEach(function(data){
									data.order = data.order
										.reverse()
										.concat(rorder)
										.unique()
										.reverse() 
									data.updateImagePositions()
								})

						} else {
							var cur = this.current
							this.data = res 
							this.data.current = cur
						}
					}
				}).bind(this))
		}],
	// XXX undo: see .removeFromCrop(..) for a reference implementation...
	// 		this will need:
	// 			- .collect(..) to be able to place images...
	//			- also store image order as .data.order is cleared of 
	//				removed images...
	uncollect: ['Collections|Image/Remove from collection',
		core.doc`Remove gid(s) from collection...

			Remove current image from current collection...
			.uncollect()
			.uncollect('current')
				-> this

			Remove gid(s) from current collection...
			.uncollect(gid)
			.uncollect([gid, ..])
				-> this

			Remove gid(s) from collection...
			.uncollect(gid, collection)
			.uncollect([gid, ..], collection)
				-> this


		NOTE: this will remove any gid, be it image or ribbon.
		`,
		{
			browseMode: function(){ return !this.collection && 'disabled' },
			// XXX two ways to go:
			//		- .collect(..) + .data.placeImage(..)
			//		- rewrite .collect(..) to use .data.placeImage(..) (like: .addToCrop(..))
			getUndoState: function(d){
				d.placements = this.data.getImagePositions(d.args[0])
				d.collection = d.args[1] || this.collection
			},
			// XXX this does not work yet...
			// 		...need to trigger .reload(..), and considering that
			// 		this is a UI action, doing so explicitly is logical...
			// XXX can we unify .collect(..) and .addToCrop(..)???
			// 		...they essentially do the same thing with one exception
			// 		a crop retains the full order while a collection has a 
			// 		cleared order...
			// 		..might also be a good idea to unify much of the 
			// 		collection and crop mechanics...
			undo: function(d){
				var that = this
				var gids = d.args[0] || [d.current]
				gids = gids instanceof Array ? gids : [gids]
				var collection = d.collection
				this
					// XXX is this the right approach???
					.collect(gids, collection)
					.ensureCollection(collection)
						.then(function(){
							d.placements.forEach(function(e){
								// XXX does not place correctly...
								that.data.placeImage(e[0], e[1], that.data.order[e[2]]) 
							that.reload(true)
						}) })
			},
			//*/
		},
		function(gids, collection){
			collection = collection || this.collection
			collection = this.collectionGIDs[collection] || collection 
			if(collection == null 
					|| collection == MAIN_COLLECTION_TITLE
					|| !this.collections 
					|| !(collection in this.collections)){
				return
			}

			var that = this

			gids = gids == 'loaded' ? this.data.getImages('loaded')
				: gids instanceof Array ? gids 
				: [gids]
			gids = gids
				.map(function(gid){ 
					return gid in that.data.ribbons ? 
						// when adding a ribbon gid expand to images...
						that.data.ribbons[gid].compact()
						: [that.data.getImage(gid)] })
				.flat()

			// remove from the loaded state...
			this.collection == collection
				&& this.data.clear(gids)

			// NOTE: we do both this and the above iff data is cloned...
			// NOTE: if tags are saved to the collection it means that 
			// 		those tags are local to the collection and we do not 
			// 		need to protect them...
			if(this.collections[collection].data 
					&& this.data !== this.collections[collection].data){
				this.collections[collection].data
					.clear(gids)
			}
		}],
	uncollectRibbon: ['Collections|Ribbon/Remove ribbon from collection',
		core.doc`Remove ribbons from collection...

			Remove current ribbon from current collection...
			.uncollectRibbon()
			.uncollectRibbon('current')
				-> this

			Remove gid(s) from current collection...
			.uncollectRibbon(gid)
			.uncollectRibbon([gid, .. ])
				-> this

			Remove gid(s) from collection...
			.uncollectRibbon(gid, collection)
			.uncollectRibbon([gid, .. ], collection)
				-> this


		NOTE: this is the same as .uncollect(..) but removes whole ribbons, 
			i.e. each gid given will be resolved to a ribbon which will be
			removed.
		`,
		{browseMode: 'uncollect'},
		function(gids, collection){
			var that = this
			gids = gids || 'current'
			gids = gids instanceof Array ? gids : [gids]
			gids = gids.map(function(gid){ return that.data.getRibbon(gid) })
			return this.uncollect(gids, collection) 
		}],


	// Serialization...
	//
	// NOTE: this will handle collection title and data only, the rest 
	// 		is copied in as-is.
	// 		It is the responsibility of the extending features to transform
	// 		their data on load as needed.
	load: [function(json){
		var that = this

		var collections = {}
		var c = json.collections || {}
		var order = json.collection_order || Object.keys(c)

		if((json.location || {}).collection){
			this.location.collection = json.location.collection
		}
			
		Object.keys(c).forEach(function(title){
			if(c[title] === false){
				return
			}

			var state = collections[title] = { title: title }

			// load data...
			var d = c[title].data == null ?
					null
				: c[title].data instanceof data.Data ?
					c[title].data
				: data.Data.fromJSON(c[title].data)
			if(d){
				state.data = d
			}

			// image count...
			if(c[title].count){
				state.count = c[title].count
			}

			// NOTE: this can be done lazily when loading each collection
			// 		but doing so will make the system more complex and 
			// 		confuse (or complicate) some code that expects 
			// 		.collections[*].crop_stack[*] to be instances of Data.
			if(c[title].crop_stack){
				state.crop_stack = c[title].crop_stack
					.map(function(c){ 
						return c instanceof data.Data ? 
							c 
							: data.Data(c) })
			}

			// copy the rest of collection data as-is...
			Object.keys(c[title])
				.forEach(function(key){
					if(key in state){
						return
					}

					state[key] = c[title][key]
				})
		})

		return function(){
			if(Object.keys(collections).length > 0){
				this.collections = collections
				this.collection_order = order
			}
		}
	}],
	//
	// Supported modes:
	// 	current (default) 	- ignore collections
	// 	base				- save only base data in each collection and
	// 							the main collection is saved as current
	// 	full				- full current state.
	// 	
	// NOTE: we do not store .collection_order here, because we order 
	// 		the collections in the object.
	// 		...when saving a partial collection set, for example in
	// 		.prepareIndexForWrite(..) it would be necessary to add it 
	// 		in to maintain the correct order when merging... (XXX)
	// NOTE: currently this only stores title and data, it is the 
	// 		responsibility of extending features to store their specific 
	// 		stuff in collections...
	// 		XXX is this the right way to go???
	// NOTE: .chnages are handled separately in feature .handlers...
	json: [function(mode){ return function(res){
		mode = mode || 'current'

		var collections = this.collections || {}
		var order = this.collection_order

		// NOTE: if mode is 'current' ignore collections...
		if(mode != 'current' && order.length > 0){
			// NOTE: .collection_order does not return MAIN_COLLECTION_TITLE 
			// 		so we have to add it in manually...
			order = MAIN_COLLECTION_TITLE in collections ?
				order.concat([MAIN_COLLECTION_TITLE])
				: order

			// in base mode save the main view as current...
			if(mode == 'base' && this.collection){
				var main = collections[MAIN_COLLECTION_TITLE]
				res.data =  (main.crop_stack ? 
						(main.crop_stack[0] || main.data)
						: main.data)
					.dumpJSON()

				delete res.location.collection
			}

			res.collections = {}
			order.forEach(function(title){
				// in base mode skip the main collection...
				if(mode == 'base' && title == MAIN_COLLECTION_TITLE){
					return
				}

				var state = collections[title]

				// collection does not exist (default collection)...
				// XXX
				if(state == null){
					res.collections[title] = false
					return
				}

				// build the JSON...
				var s = res.collections[title] = { title: title }
				if(state.gid){
					s.gid = state.gid
				}
				var data = ((mode == 'base' && state.crop_stack) ? 
						(state.crop_stack[0] || state.data)
						: state.data)
				if(data){
					s.data = data.dumpJSON()
					s.count = data.length

				} else if(state.count) {
					s.count = state.count
				}

				// handle .crop_stack of collection...
				// NOTE: in base mode, crop_stack is ignored...
				if(mode != 'base' && state.crop_stack){
					s.crop_stack = state.crop_stack
						.map(function(d){ return d.dumpJSON() })
				}
			})
		}
	} }],
	clone: [function(full){
		return function(res){
			if(this.collections){
				var cur = this.collections

				if(this.collection){
					res.location.collection = this.collection
				}

				collections = res.collections = {}
				this.collection_order
					.forEach(function(title){
						var c = collections[title] = {
							title: title,
						}

						if(cur[title].data){
							c.data = cur[title].data.clone()
						}

						if(cur[title].crop_stack){
							c.crop_stack = cur[title].crop_stack
								.map(function(d){ return d.clone() })
						}
					})
			}

		} }],

	clear: [function(){
		this.collection
			&& this.collectionUnloaded('*')
		delete this.collections
		delete this.__collection_order
		delete this.location.collection
	}],


	// Config and interface stuff...
	//
	toggleCollectionCropRetention: ['Interface/Collection crop save mode',
		core.makeConfigToggler(
			'collection-save-crop-state', 
			[
				'all',
				'main', 
				'none',
			])],
	// XXX can we combine a toggler with list editor???
	toggleCollections: ['- Collections/Collections',
		toggler.Toggler(null,
			function(_, state){
				return state == null ?
					// cur state...
					(this.collection 
						|| MAIN_COLLECTION_TITLE)
					// new state...
					: (this.loadCollection(state) 
						&& state) },
			function(){ 
				return [MAIN_COLLECTION_TITLE].concat(this.collection_order || []) })],
})

var Collection = 
module.Collection = core.ImageGridFeatures.Feature({
	title: '',
	doc: '',

	tag: 'collections',
	depends: [
		'cache',
		'base',
		'location',
		'crop',
	],
	suggested: [
		'collections-local-config',
		'collection-tags',
		'collection-marks',
		'auto-collections',

		'ui-collections',
		'ui-collection-marks',
		'fs-collections',
	],

	actions: CollectionActions, 

	handlers: [
		// save before we serialize...
		['json.pre',
			function(){ this.saveCollection() }],


		// Handle changes...
		//
		// 	Global tags:
		// 		'collections'	- mark collection list as changed
		// 		'collection: <gid>'	
		// 						- collection-specific changes
		//
		// 	Collection-specific tags:
		// 		'metadata'		- marks metadata as changed
		// 							NOTE: this is ignored for the base 
		// 								collection...
		//
		// 	Collection-local tags (see: .config['collection-transfer-changes']):
		// 		'metadata'
		// 		'data'
		//
		//
		//	Mark collection list as changed...
		// 	.markChanged('collections')
		// 		NOTE: this will not affect collections...
		// 		NOTE: this is useful alone when removing collections...
		//
		// 	Mark tag as changed when collection is loaded...
		// 	.markChanged(<tag>)
		// 		NOTE: this only applies to collection-local or specific tags...
		//
		// 	Mark tag as changed when collection is not loaded...
		// 	.markChanged('collection: '+JSON.stringify(<gid>), [<tag>, ..])
		//
		//
		// collection add/remove...
		[[
			'collectionCreated',
			'collectionRemoved',
		],
			function(_, collection){
				// collection list changed...
				this.markChanged('collections')
				// collection changed...
				collection in this.collections
					&& this.markChanged(
						'collection: '
							+JSON.stringify(this.collections[collection].gid || collection))
			}],
		// collection list sort...
		['sortCollections.pre',
			function(){
				var o = (this.collection_order || []).slice()

				return function(){
					;(this.collection_order || [])
							.filter(function(e, i){ return e != o[i] }).length > 0
						&& this.markChanged('collections')
				}
			}],
		// collection title/list...
		['renameCollection',
			function(_, from, to){
				this
					.markChanged('collections')
					.markChanged('collection: '
						+ JSON.stringify(this.collections[to].gid), ['metadata']) }],
		// basic collection edits...
		[[
			// NOTE: no need to handle .collect(..) here as it calls .joinCollect(..)
			'joinCollect.pre',
			'uncollect.pre',
		], 
			function(){
				var that = this
				var args = [...arguments]
				var title = (args.length == 1 ? args[0] : args[1]) || this.collection
				var collection = (this.collections || {})[title] || {}

				var count = collection.data ? 
					collection.data.length 
					: collection.count

				return function(){
					// NOTE: if a collection does not exist by this point 
					// 		it will be handled by collection .collectionCreated(..)
					// 		...this means we are either creating a new collection
					// 		or removing from a non-existing collection.
					title in this.collections
						&& this.ensureCollection(title)
							.then(function(){
								var new_count = collection.data ? 
									collection.data.length 
									: collection.count

								new_count != count
									&& that.markChanged('collections')
									&& that.markChanged(
										'collection: '
											+JSON.stringify(collection.gid || title),
										['data'])
							}) }
			}],
		['joinCollect',
			function(_, align, collection, data){
				var args = [...arguments]
				var title = (args.length == 1 ? args[0] : args[1]) || this.collection
				var collection = (this.collections || {})[title] || {}

				data = data || this.data

				;(!data || data.ribbon_order.length > 1)
					&& this.markChanged(
						'collection: '
							+JSON.stringify(collection.gid || title),
						['data'])
			}],
		// transfer changes on load/unload collection...
		['collectionLoading.pre',
			function(to){
				var that = this
				var from = this.collection || MAIN_COLLECTION_TITLE
				if(from == to || this.changes === undefined || this.changes === true){
					return
				}

				var change_tags = this.config['collection-transfer-changes'] 
					|| COLLECTION_TRANSFER_CHANGES

				var from_changes = change_tags
					.filter(function(item){
						return that.changes === true || (that.changes || {})[item] })

				return function(){
					if(to == from){
						return
					}
					var gid = (this.collections[to] || {}).gid || to
					var changes = this.changes !== false ? 
						this.changes['collection: '+JSON.stringify(gid)] 
						: []
					var from_id = 'collection: '
						+JSON.stringify(from == MAIN_COLLECTION_TITLE ?
							MAIN_COLLECTION_GID
							: this.collections[from].gid || from)

					// everything has changed, no need to bother with details...
					if(changes === true){
						return
					}

					// save changes to 'from'...
					from_changes.length > 0
						&& this.markChanged(from_id, from_changes)

					// load changes from 'to'..
					change_tags.forEach(function(item){
						if(changes && changes.indexOf(item) >= 0){
							that.markChanged(item)

						} else if(that.changes && that.changes[item]){
							delete that.changes[item]
						}
					})
				}
			}],
		// update current collection changes...
		//
		// This will:
		// 	1) update .changes['collection: <gid>'] with the current
		// 		loaded .changes state...
		// 	2) in 'base' mode, update the res.changes with base data 
		// 		changes...
		//
		// NOTE: we do not need to do anything on the .load(..) side...
		['json.pre', 
			function(mode){
				var cur = this.collection || MAIN_COLLECTION_TITLE
				if(cur == null || cur == MAIN_COLLECTION_TITLE){
					return
				}

				var changes = this.changes

				// everything/nothing changed -- nothing to do...
				if(!changes || changes === true || changes[cur] === true){
					return
				}

				var gid = this.collectionGID
				var id = 'collection: '+ JSON.stringify(gid)
				var change_tags = this.config['collection-transfer-changes']
					|| COLLECTION_TRANSFER_CHANGES

				var changed = change_tags 
					.filter(function(tag){
						return changes[tag] === true })

				if(changed.length > 0 && this.changes[id] !== true){
					this.changes[id] = (this.changes[id] || [])
						.concat(changed)
						.unique() 
				}

				// reset the base change tags to the base data (from collection data)...
				if(mode == 'base'){
					return function(res){
						var base_id = 'collection: '+ JSON.stringify(MAIN_COLLECTION_GID)
						var base = this.changes[base_id] || []

						// no need to save the base collection changes...
						delete res.changes[base_id]

						// clear...
						change_tags.forEach(function(tag){
							delete res.changes[tag] })
						// set...
						base.forEach(function(tag){
							res.changes[tag] = true })
					}
				}
			}],


		// Handle collection serialization format...
		//
		// Return format:
		// 	{
		// 		...
		//
		// 		// Collection gid-title index...
		//		//
		// 		// NOTE: this is sorted via .collection_order in .json(..)...
		// 		// 
		// 		// NOTE: if .collections is undefined this is not returned...
		// 		// XXX this may cause issues if after removing the 
		// 		//		last collection and .collections is deleted,
		// 		//		then the last saved collection state will 
		// 		//		get loaded instead of an empty collection list
		// 		//		...currently this is not a problem as .collections
		// 		//		is never explicitly set to undefined, but is a 
		// 		//		potential pitfall...
		// 		//		Q: should this return {} when .collections is undefined?
		// 		collections: {
		// 			// normal collection...
		// 			<gid>: {
		// 				title: <title>,
		// 				count: <count>,
		// 				...
		// 			},
		//
		// 			// un-initialise default collection...
		// 			//
		// 			// i.e. a collection that is included in 
		// 			// .config['default-collections'] and thus present in
		// 			// .collection_order but not present in .collections
		// 			<gid>: false,
		//
		// 			...
		// 		}
		//
		// 		// Collection metadata...
		// 		'collections/<gid>/metadata': {
		// 			gid: <gid>,
		// 			title: <title>,
		// 			...
		// 		},
		//
		// 		// Collection index data...
		//		//
		// 		// NOTE: this can contain the same tags as the root index
		// 		//		this the collection format is the same as the 
		// 		//		containing index format...
		// 		// 		This is built by: 
		// 		//			.prepareIndexForWrite(
		// 		//				<collection-data>, 
		// 		//				<collection-changes>)
		// 		//		Where:
		// 		//			<collection-data>
		// 		//				taken as-is from .collections[gid] as 
		// 		//				returned by .json(..)
		// 		//			<collection-changes>
		// 		//				built from .changes['collection: <gid>']
		// 		//		And placed under path:
		// 		//			collections/<gid>
		// 		// NOTE: as the collection index is recursive, care must
		// 		//		be taken when/if nested collections are needed
		// 		//		to avoid self referencing...
		// 		'collections/<gid>/<tag>': <tag-data>,
		//
		// 		...
		// 	}
		//
		//
		// NOTE: the base collection (MAIN_COLLECTION_TITLE) is not saved 
		// 		in collections, it is stored in the root index...
		//
		// XXX we do not need .count in collection metadata as it is 
		// 		stored in collections...
		['prepareIndexForWrite', 
			function(res){
				if(!res.changes){
					return
				}
				var that = this
				var changes = res.changes
				var collections = this.collections

				// collections fully/partially changed...
				var full = changes === true ? 
					Object.keys(collections || {})
					: Object.keys(collections || {})
						.filter(function(t){ 
							return res.changes['collection: '
								+ JSON.stringify(collections[t].gid)] === true })
				var partial = changes === true ? []
					: Object.keys(collections || {})
						.filter(function(t){ 
							return full.indexOf(t) < 0
								&& res.changes['collection: '
									+ JSON.stringify(collections[t].gid)] })

				// collection index...
				// NOTE: we are placing this in the index root to 
				// 		simplify lazy-loading of the collection 
				// 		index...
				// NOTE: if there are no collections defined this section
				// 		is skipped...
				if(collections 
						&& changes 
						&& (changes === true 
							|| changes.collections)){
					//var index = res.index['collection-index'] = {}
					var index = res.index['collections'] = {}
					// NOTE: we do not need to use .collection_order here
					// 		as .json(..) returns the collections in the 
					// 		correct order...
					Object.keys(res.raw.collections || {})
						.forEach(function(title){ 
							if(title in collections){
								var gid = (collections[title] || {}).gid || title
								var m = index[gid] = { title: title }

								if(res.raw.collections[title].count){
									m['count'] = res.raw.collections[title].count
								}

							// empty / default collections (placeholders)...
							} else {
								index[title] = false
							}
					  	})
				}

				// collections...
				if((full.length > 0 || partial.length > 0)
						&& res.raw.collections){
					// select the actual changed collection list...
					changed = changes === true ? 
						Object.keys(res.raw.collections)
						: (full).concat(partial)

					var change_tags = this.config['collection-transfer-changes']
						|| COLLECTION_TRANSFER_CHANGES

					changed
						// skip the raw field...
						.filter(function(k){ 
							return res.raw.collections[k] 
								&& changed.indexOf(k) >= 0 })
						.forEach(function(k){
							var gid = res.raw.collections[k].gid || k
							var id = 'collection: '+ JSON.stringify(gid)
							var path = 'collections/'+ gid
							var raw = res.raw.collections[k]

							// local collection changes...
							var local_changes = partial.indexOf(k) < 0 || {}
							if(local_changes !== true && res.changes[id] !== true){
								(res.changes[id] || [])
									.forEach(function(c){ local_changes[c] = true })
							}

							// collections/<gid>/metadata
							var metadata = {}
							if(full.indexOf(k) >= 0 
									|| res.changes[id].indexOf('metadata') >= 0){
								res.index[path +'/metadata'] = metadata
							}
							Object.keys(raw)
								.forEach(function(key){ metadata[key] = raw[key] })

							raw.date = res.date
							var prepared = that.prepareIndexForWrite(raw, local_changes).index

							// move the collection data to collection path...
							Object.keys(prepared)
								.filter(function(key){ return key != 'collections' })
								.forEach(function(key){
									res.index[path +'/'+ key] = prepared[key]
									delete metadata[key]
								})
							// cleanup metadata...
							// XXX do we need this???
							change_tags.forEach(function(key){
								delete metadata[key]
							})
						})
				}
			}],
		// XXX merge multiple collections...
		// 		...this can be called multiple times pre single load, once
		// 		per merged index...
		['prepareIndexForLoad',
			function(res, json, base_path){
				// collection index...
				var collections = {}
				var collections_index = {}

				var index = json['collections']

				if(index){
					// get collection order...
					var order = Object.keys(index)
						.map(function(k){ 
							return index[k] ? index[k].gid || index[k].title || k : k })
					if(order.length > 0){
						res.collection_order = order
					}

					// collection data...
					Object.keys(index).forEach(function(gid){
						if(index[gid] === false){
							return
						}

						//var title = index[gid]
						var title = index[gid].title || index[gid]
						var path = 'collections/'+ gid

						var m = collections_index[gid] = collections[title] = {
							gid: gid,
							title: title,

							// XXX
							path: path,
						}

						if(index[gid].count){
							m.count = index[gid].count
						}
					})
				}

				if(Object.keys(collections).length > 0){
					res.collections = collections
				}

				// group collection data...
				//
				// NOTE: this will load collections/* stuff if present...
				//
				// XXX would be nice to have a mechanism to pass info to 
				// 		the loader on what paths to load without actually 
				// 		loading them manually...
				// 		...without this mechanism the data used here would
				// 		not exist...
				var collection_data = {}
				Object.keys(json)
					.filter(function(k){ return k.startsWith('collections/') })
					.forEach(function(k){
						var s = k.split(/[\\\/]+/g).slice(1)
						var gid = s.shift()
						var key = s.shift()
						var title = collections_index[gid].title

						// load only collections in index...
						if(title){
							var data = collection_data[gid] = collection_data[gid] || {}

							// overwrite metadata...
							if(key == 'metadata'){
								collections_index[gid] = collections[title] = json[k]

							// other stuff -> collection data...
							} else {
								data[key] = json[k]
							}
						}
					})

				// XXX prepare collection data for loading...
				Object.keys(collection_data)
					.forEach(function(gid){
						// XXX would be nice to be able to use .prepareIndexForLoad(..) 
						// 		to handle collection internals produced by
						// 		.prepareIndexForLoad(..)...
						// 		...would need to pass it the local data...
						// XXX
					})
			}],
	],
})



//---------------------------------------------------------------------

var CollectionLocalConfig = actions.Actions({
	config: {
		// XXX should this be user editable???
		// XXX should/can this be local to collection???
		'collection-local-config': [
		],
	},

	// handle collection .config
	// XXX problems:
	// 		- config leaks -- when moving crom collection to collection 
	// 			with individual option sets some options may not get 
	// 			restored if handled incorrectly...
	// 			...one way to deal with this is to restore the base config
	// 			on every load before loading the new config...
	collectionConfigLoader: ['- Collections/',
		{collectionFormat: 'config'},
		function(title, state, logger){ 
			// XXX save old config -- in their respective collection...

			// XXX load MAIN_COLLECTION_TITLE config...

			// XXX load new config -- from target collection... 
		}],
})

var CollectionLocalConfig = 
module.CollectionLocalConfig = core.ImageGridFeatures.Feature({
	title: '',
	doc: '',

	tag: 'collections-local-config',
	depends: [
		'collections',
	],

	handlers: [
		/* XXX
		['collectionLoading.pre',
			function(){
				var that = this
				var state = {}
				var opts = this.config['collection-local-config'] || []

				// save outgoing collection state...
				var cfg = {}
				opts.forEach(function(n){
					cfg[n] = JSON.parse(JSON.stringify(that.config[n])) 
				})
			}],
		//*/
	],
})



//---------------------------------------------------------------------

var CollectionTagsActions = actions.Actions({
	config: {
		// List of tags to be stored in a collection, unique to it...
		//
		// NOTE: the rest of the tags are shared between all collections
		// NOTE: to disable local tags either delete this, set it to null
		// 		or to an empty list.
		'collection-local-tags': [],
		
		/*
		'collection-transfer-changes': 
			// XXX need a way to exrtend config values in order of merge
			// 		and not manually...
			CollectionActions.config['collection-transfer-changes']
				.concat([
				]),
		//*/
	},

	collectTagged: ['- Collections|Tag/',
		function(tags, collection){
			return this.collect(this.data.getTaggedByAll(tags), collection) }],
	uncollectTagged: ['- Collections|Tag/',
		function(tags, collection){
			return this.uncollect(this.data.getTaggedByAll(tags), collection) }],
})

var CollectionTags = 
module.CollectionTags = core.ImageGridFeatures.Feature({
	title: 'Collection tag handling',
	doc: core.doc`
	What this does:
	- Makes tags global through all collections
	- Handles local tags per collection


	Global tags:
	------------

	Global tags are shared through all the collections, this helps keep
	image-specific tags, keywords and meta-information stored in tags 
	global, i.e. connected to specific image and not collection. 

	Global tags are stored in .data.tags and cleared out of from the 
	collection's: 
		.collections[<title>].data


	Collection local tags:
	----------------------

	Local tags are listed in .config['collection-local-tags'], this makes
	selection, bookmarking and other process related tags local to each 
	collection.

	Collection-local tags are stored in .collections[<title>].local_tags
	and overwrite the corresponding tags in .data.tags on collection load.

	`,

	tag: 'collection-tags',

	depends: [
		'collections',
		'tags',
	],

	actions: CollectionTagsActions,

	handlers: [
		// move tags between collections...
		['collectionLoading.pre',
			function(title){
				var that = this
				var local_tag_names = this.config['collection-local-tags'] || []
				var tags = this.data.tags

				// NOTE: this is done at the .pre stage as we need to grab 
				// 		the tags BEFORE the data gets cleared (in the case 
				// 		of MAIN_COLLECTION_TITLE)...
				var local_tags = (this.collections[title] || {}).local_tags || {}

				return function(){
					// load local_tags...
					local_tag_names
						.forEach(function(tag){ 
							tags[tag] = local_tags[tag] || [] 
						})

					;(this.crop_stack || [])
						.forEach(function(d){ d.tags = tags })
					this.data.tags = tags

					this.data.sortTags()
				}
			}],
		// remove tags from unloaded collections...
		['collectionUnloaded',
			function(_, title){
				if(title in this.collections 
						&& 'data' in this.collections[title]){
					delete this.collections[title].data.tags
				}
			}],
		// remove tags when saving...
		['saveCollection.pre',
			function(title, mode, force){
				var that = this
				title = title || this.collection || MAIN_COLLECTION_TITLE
				var local_tag_names = this.config['collection-local-tags'] || []

				// do not do anything for main collection unless force is true...
				if(title == MAIN_COLLECTION_TITLE && !force){
					return
				}

				// we need this to prevent copy of tags on first save...
				var new_set = !(title in (this.collections || {}))

				return function(){
					// save local tags...
					var local_tags = this.collections[title].local_tags = {}
					local_tag_names
						.forEach(function(tag){ 
							local_tags[tag] = (!new_set || title == MAIN_COLLECTION_TITLE) ? 
								((that.data.tags || {})[tag] || []) 
								: []
						})

					delete (this.collections[title].data || {}).tags
				}
			}],
		// prevent .uncollect(..) from removing global tags...
		// XXX this seems a bit hacky (???)
		['uncollect.pre',
			function(_, gids, title){
				var that = this
				var local_tag_names = this.config['collection-local-tags'] || []

				// prevent global tag removal...
				var tags = this.data.tags
				delete this.data.tags

				return function(){
					// update local tags...
					local_tag_names.forEach(function(tag){
						tags[tag] = that.data.makeSparseImages(tags[tag], true) })

					this.data.tags = tags
					this.data.sortTags()
				}
			}],
		// save .local_tags to json...
		// NOTE: we do not need to explicitly load anything as .load() 
		// 		will load everything we need and .collectionLoading(..)
		// 		will .sortTags() for us...
		['json',
			function(res, mode){
				var c = this.collections
				var rc = res.collections

				// NOTE: at this point .crop_stack is handled, so we 
				// 		do not need to care about it...

				// in 'base' mode set .data.tags and .local_tags to 
				// the base collection data...
				if(mode == 'base' 
						&& this.collection != null
						&& this.collection != MAIN_COLLECTION_TITLE){
					var tags = this.data.tags || {}
					var ltags = c[MAIN_COLLECTION_TITLE].local_tags || {}
					var rtags = res.data.tags = {}

					// move all the tags...
					Object.keys(tags)
						.filter(function(tag){ return ltags[tag] == null })
						.forEach(function(tag){ rtags[tag] = tags[tag].compact() })
					// overwrite the local tags for the base...
					Object.keys(ltags)
						.forEach(function(tag){ rtags[tag] = ltags[tag].compact() })
				}

				// clear and compact tags for all collections...
				rc
					&& Object.keys(rc || {})
						// XXX skip unloaded collections...
						.filter(function(title){ return !!rc[title].data })
						.forEach(function(title){
							var tags = c[title].local_tags || {}
							var rtags = {}

							// compact the local tags...
							Object.keys(tags)
								.forEach(function(tag){
									rtags[tag] = tags[tag].compact() })

							// move .local_tags to .data.tags
							rc[title].data.tags = rtags
						})
			}],
		// load collection local tags from .data.tags to .local_tags...
		['load',
			function(_, json){
				var that = this
				this.collections
					&& Object.keys(json.collections || {})
						// skip loaded collections that are already Data objects...
						// XXX not sure about this...
						.filter(function(title){
							return !(json.collections[title].data instanceof data.Data) })
						// do the loading...
						.forEach(function(title){
							var c = that.collections[title]
							if(!c){
								return
							}
							var t = (c.data || {}).tags || {}

							c.local_tags = c.local_tags || {}
							;(that.config['collection-local-tags'] || [])
								.forEach(function(tag){
									c.local_tags[tag] = c.local_tags[tag] || t[tag] || [] })
						})
			}],
	],
})



//---------------------------------------------------------------------

// XXX add UI...
// XXX removing items from auto-collection has no effect as it will be 
// 		reconstructed on next load -- is this the right way to go???
var AutoCollectionsActions = actions.Actions({
	collectionAutoLevelLoader: ['- Collections/',
		core.doc`

		`,
		{collectionFormat: 'level_query'},
		function(title, state){ 
			return new Promise((function(resolve){
				var source = state.source || MAIN_COLLECTION_TITLE
				source = source == MAIN_COLLECTION_TITLE ? 
					((this.crop_stack || [])[0] 
						|| this.data)
					// XXX need a way to preload collection data...
					: ((this.collection[source].crop_stack || [])[0] 
						|| this.collections[source].data)

				var query = state.level_query
				query = query == 'top' ? 
						[0, 1]
					: query == 'bottom' ?
						[-1]
					: query instanceof Array ? 
						query
					: typeof(query) == typeof('str') ? 
						query.split('+').map(function(e){ return e.trim() })
					: query > 0 ? 
						[0, query]
					: [query]
				query = query[0] == 'top' ?
						[0, parseInt(query[1])+1]
					: query[0] == 'bottom' ?
						[-parseInt(query[1])-1]
					: query

				var levels = source.ribbon_order.slice.apply(source.ribbon_order, query)

				var gids = []
				levels.forEach(function(gid){
					source.makeSparseImages(source.ribbons[gid], gids) })
				gids = gids.compact()

				// get items that topped matching the query...
				var remove = state.data ?
					state.data.order
						.filter(function(gid){ return gids.indexOf(gid) < 0 })
					: []

				// build data...
				state.data = data.Data.fromArray(gids)
					// join with saved state...
					.join(state.data || data.Data())
					// remove unmatching...
					.clear(remove)

				resolve()
			}).bind(this)) }],
	makeAutoLevelCollection: ['- Collections/',
		core.doc`Make level auto-collection...

		`,
		function(title, source, a, b){
			// XXX query 
			var query = b != null ? [a, b] : a

			this.saveCollection(title, 'empty')

			this.collections[title].level_query = query
			this.collections[title].source = source
		}],

	// XXX do we need real tag queries???
	collectionAutoTagsLoader: ['- Collections/',
		core.doc`

		NOTE: this will ignore local tags.
		NOTE: this will prepend new matching items to the saved state.
		`,
		{collectionFormat: 'tag_query'},
		function(title, state){ 
			return new Promise((function(resolve){
				var local_tag_names = this.config['collection-local-tags'] || []

				var tags = (state.tag_query || [])
					.filter(function(tag){ 
						return local_tag_names.indexOf(tag) < 0 })

				// XXX should this be a real tag query???
				//var gids = this.data.getTaggedByAll(tags, true)
				var gids = this.data.getTaggedByAll(tags)

				// get items that topped matching the query...
				var remove = state.data ?
					state.data.order
						.filter(function(gid){ return gids.indexOf(gid) < 0 })
					: []

				// build data...
				state.data = data.Data.fromArray(gids)
					// join with saved state...
					.join(state.data || data.Data())
					// remove unmatching...
					.clear(remove)

				resolve()
			}).bind(this)) }],
	makeAutoTagCollection: ['- Collections/',
		core.doc`Make tag auto-collection...

			Make a tag auto-collection...
			.makeAutoTagCollection(title, tag)
			.makeAutoTagCollection(title, tag, tag, ..)
			.makeAutoTagCollection(title, [tag, tag, ..])
				-> this

		NOTE: at least one tag must be supplied...
		`,
		function(title, tags){
			tags = arguments.length > 2 ? [...arguments].slice(1) : tags
			tags = tags instanceof Array ? tags : [tags]

			if(tags.length == 0){
				return
			}

			this.saveCollection(title, 'empty')

			this.collections[title].tag_query = tags
		}],
})

var AutoCollections =
module.AutoCollections = core.ImageGridFeatures.Feature({
	title: 'Auto collections',
	doc: core.doc`
	A collection is different from a crop in that it:
		- preserves ribbon state
		- preserves order
		- preserves local tags

	Tag changes are handled by removing images that were untagged (no 
	longer matching) from the collection and adding newly tagged/matching 
	images to collection.
	`,

	tag: 'auto-collections',
	depends: [
		'collections',
	],

	actions: AutoCollectionsActions,

	handlers: [
		['json',
			function(res){
				var c = this.collections || {}
				var rc = res.collections || {}

				Object.keys(rc)
					.forEach(function(title){
						var cur = c[title]
						var r = rc[title]

						if(!cur){
							return
						}

						// XXX is this the right way to go???
						if('tag_query' in cur){
							r.tag_query = cur.tag_query

						} else if('level_query' in cur){
							r.level_query = cur.level_query
							if(cur.source){
								r.source = cur.source
							}
						}
					})
			}],
	],
})



//---------------------------------------------------------------------

// Make an action that when called without enough arguments show a 
// collection selector dialog and just call the given function when 
// enough arguments are given.
//
// NOTE: if n > 1 and <n args are given then the given args will get 
// 		passed to func with an appended title...
var mixedModeCollectionAction = function(func, n, last_used_collection){
	return widgets.uiDialog(function(){
		var args = [...arguments]
		// check if minimum number of arguments is reached...
		return args.length < (n || 1) ? 
			// show the dialog...
			//this.browseCollections(func) 
			this.browseCollections(function(title){ 
					return func.call(this, ...args.concat([title])) },
				null,
				last_used_collection) 
			: func.apply(this, args) }) }

// Like mixedModeCollectionAction(..) but will do nothing if enough args 
// are given...
var collectionGetterWrapper = function(func, n, last_used_collection){
	return widgets.uiDialog(function(){
		var args = [...arguments]
		// check if minimum number of arguments is reached...
		return args.length < (n || 1)
			// show the dialog...
			&& this.browseCollections(function(title){ 
					return func.call(this, ...args.concat([title])) },
				null,
				last_used_collection) }) }


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

// XXX show collections in image metadata... (???)
// XXX might be nice to indicate if a collection is loaded -- has .data???
// XXX might be nice to add collection previews to the collection list...
// 		...show the base ribbon from collection as background
var UICollectionActions = actions.Actions({
	config: {
		// Last used collection (for adding merging)...
		//
		// This will be auto-selected in .browseCollections(..) on next 
		// add/edit operation...
		//'collection-last-used': null,
	},

	// UI...
	//
	// XXX would be nice to make this nested (i.e. path list) -- collection grouping... (???)
	// XXX should we use options object instead of arguments???
	// XXX might need to check (in a standard way) that nothing is loaded...
	browseCollections: ['Collections/$Collections...',
		core.doc`Collection list...

			.browseCollections(action, new_message, last_used_collection)
				-> dialog

		All arguments are optional.

		If action is given last_used_collection defaults to true.

		If last_used_collection is true then .config['collection-last-used']
		is used to select the last used collection and set when selecting
		an item.
		It last_used_collection is a string, then .config[last_used_collection]
		will be used to store the last used collection title.

		NOTE: collections are added live and not on dialog close...
		`,
		widgets.makeUIDialog(function(action, new_message, last_used_collection){
			var that = this
			last_used_collection = last_used_collection == null ? 
					(action && 'collection-last-used')
				: last_used_collection === true ? 
					'collection-last-used' 
				: last_used_collection
			var to_remove = []

			var collections = that.collection_order = 
				//(that.collection_order || []).slice()
				that.collection_order.slice()

			//var defaults = that.config['default-collections'] || []
			//collections = collections.concat(defaults).unique()

			return browse.makeLister(null, 
				function(path, make){
					var dialog = this
						.on('update', function(){
							dialog.filter(JSON.stringify((that.collection || MAIN_COLLECTION_TITLE)
									.replace(/\$/g, '')))
								.addClass('highlighted')
						})

					// nothing loaded...
					// NOTE: we have to check both .data and .collection as
					// 		we can have an empty collection loaded -- empty
					// 		.data but a set .collection...
					if(that.data.length == 0 && that.collection == null){
						make.Empty('No collections...')
						return
					}

					var openHandler = function(_, title){
						var title = $(this).find('.text').attr('text') || title
						// create collection if it does not exist...
						if(!that.collections 
								|| !(title in that.collections)){
							that.newCollection(title)
						}

						var gid = that.current
						action ?
							action.call(that, title)
							: that.loadCollection(title)
						that.focusImage(gid)
						dialog.close()
					}
					var setItemState = function(title){
						var gid = ((that.collections || {})[title] || {}).gid || title
						// handle main collection changes...
						gid = title == MAIN_COLLECTION_TITLE ? 
							MAIN_COLLECTION_GID 
							: gid

						var text = this.find('.text').last()

						// saved state...
						var unsaved = that.changes === true 
							|| (that.changes || {})['collection: '+ JSON.stringify(gid)]
							|| (that.collectionGID == gid 
								&& (that.config['collection-transfer-changes']
										|| COLLECTION_TRANSFER_CHANGES)
									.filter(function(a){ 
										return !!(that.changes || {})[a] }).length > 0)
						unsaved
							&& text.attr('unsaved', true)

						// collection crop...
						var cs = 
							title == (that.collection || MAIN_COLLECTION_TITLE) ? 
								that.crop_stack
							: (that.collections || {})[title] ?
								that.collections[title].crop_stack
							: null
						cs
							&& text.attr('cropped', cs.length)

						// collection size...
						var c = (that.collections || {})[title] || {}
						var i = (c.data && c.data.length)
								|| c.count
								|| false 
						// main collection loaded...
						i = (!i && title == MAIN_COLLECTION_TITLE && !that.collection) ?
							that.data.length 
							: i
						i && $(this).attr('count', i)
					}

					// update collection list if changed externally...
					collections.splice.apply(collections, 
						// NOTE: if the length calculation here looks a "bit"
						// 		convoluted, that's because it is, this fixes
						// 		a really odd bug in old Chrome versions where
						// 			L.splice(0, L.length, ...) 
						// 		in some odd conditions leaves an element 
						// 		in the original array...
						// 		(is a jit error???)
						[0, (that.collection_order || []).length + collections.length]
							.concat(collections
								.concat(that.collection_order || [])
								.unique()))

					// main collection...
					!action 
						&& collections.indexOf(MAIN_COLLECTION_TITLE) < 0
						&& make([
								MAIN_COLLECTION_TITLE,
							], 
							{ 
								events: {
									update: function(_, title){
										// make this look almost like a list element...
										// XXX hack???
										$(this).find('.text:first-child')
											.before($('<span>')
												.css('color', 'transparent')
												.addClass('sort-handle')
												.html('&#x2630;'))
										setItemState
											//.call($(this), title)
											.call($(this), $(this).find('.text').attr('text'))
									},
									open: openHandler,
								},
								// NOTE: we are adding a blank button here 
								// 		to align item counts...
								// XXX HACK: can we automate this -- html5 grid layout???
								buttons: [['&times;']],
							})

					// collection list...
					make.EditableList(collections, 
						{
							new_item: new_message ? 
									new_message 
								: action ? 
									'$New...' 
								: '$New from current state...',

							unique: true,
							sortable: 'y',
							to_remove: to_remove,

							itemopen: openHandler,

							normalize: function(title){ 
								return title.trim() },
							check: function(title){ 
								return title.length > 0 },

							each: setItemState, 

							itemadded: function(title){
								action ?
									that.newCollection(title)
									: that.saveCollection(title) },

							disabled: action ? [MAIN_COLLECTION_TITLE] : false,

							update_merge: 'merge',

							// XXX REVISE...
							itemedit: function(_, from, to){
								that.renameCollection(from, to)

								// rename was successful...
								if(to in that.collections){
									collections[collections.indexOf(from)] = to
								}
							},
						})
				}, {
					cls: 'collection-list',
					// focus current collection...
					selected: (last_used_collection 
							&& that.config[last_used_collection]) ?
						that.config[last_used_collection]
						: JSON.stringify(
							(that.collection || MAIN_COLLECTION_TITLE)
								// XXX not sure it is good that we have to do this...
								.replace(/\$/g, '')),
				})
				.open(function(_, title){
					last_used_collection
						&& (that.config[last_used_collection] = title) })
				.close(function(){
					that.collection_order = collections

					to_remove
						.forEach(function(title){ that.removeCollection(title) }) 
				})
		})],
	// XXX should this be able to add new collections???
	browseImageCollections: ['Collections|Image/Image $collections...',
		widgets.makeUIDialog(function(gid){
			var that = this
			gid = this.data.getImage(gid)

			var defaults = this.config['default-collections']

			var all
			var collections
			var to_remove
			var t

			return browse.makeLister(null, 
				function(path, make){
					var dialog = this
						.on('update', function(){
							dialog.filter(JSON.stringify((that.collection || MAIN_COLLECTION_TITLE)
									.replace(/\$/g, '')))
								.addClass('highlighted')
						})

					all = all || that.collection_order || []
					if(defaults){
						all.splice.apply(all, 
							[0, all.length]
								.concat(all.concat(defaults).unique()))
					}

					// load collections...
					var loading = all
						.filter(function(c){
							return (that.collections || {})[c] 
								&& !that.collections[c].data })
						.map(function(c){ 
							that
								.ensureCollection(c) 
								.then(function(collection){
									collection.data.getImage(gid || that.current) ?
										collections.push(c)
										: to_remove.push(c.replace(/\$/g, ''))

									// NOTE: we'll avoid calling update 
									// 		too often...
									clearTimeout(t)
									t = setTimeout(function(){
										dialog.update() 
									}, 100)
								}) 
							return c
						})
					
					// containing collections...
					collections = collections
						|| that.inCollections(gid || null)
							.filter(function(title){ 
								return loading.indexOf(title) < 0 })

					// build the disabled list...
					if(!to_remove){
						to_remove = []
						all.forEach(function(title){
							collections.indexOf(title) < 0
								&& loading.indexOf(title) < 0
								&& to_remove.push(title.replace(/\$/g, ''))
						})
					}

					all.length > 0 ?
						make.EditableList(all, 
							{
								new_item: false,
								sortable: 'y',
								disabled: loading,
								to_remove: to_remove,
								itemopen: function(_, title){
									var i = to_remove.indexOf(title)

									i >= 0 ? 
										to_remove.splice(i, 1) 
										: to_remove.push(title)

									dialog.update()
								},
								itemedit: function(_, from, to){
									that.renameCollection(from, to)

									all[all.indexOf(from)] = to

									that.collection_order = all
								},
							})
						: make.Empty('No collections...')
				})
				.close(function(){
					that.collection_order = all

					all.forEach(function(title){
						collections.indexOf(title) < 0
							&& to_remove.indexOf(title.replace(/\$/g, '')) < 0
							&& that.collect(gid, title) })
					to_remove.forEach(function(title){ 
						that.uncollect(gid, title) }) 
				})
		})],


	// Collection actions with collection selection...
	//
	// XXX need to add "ALL" -- might need to rework .browseCollections(..) for this...
	// XXX also do:
	// 		.saveCollection(..)
	// XXX EXPERIMENTAL...
	// 		...we might not actually need this as this essentially will
	// 		do the same thing as .browseCollections(..)
	// 		...combining this with .browseCollections(..) might complicate
	// 		things as we still need to reuse the later for other things...
	loadCollection: [
		collectionGetterWrapper(function(title){ this.loadCollection(title) })],
	loadMainCollection: ['Collections/Exit collection view',
		{
			browseMode: 'uncollect', 
			// prevent this from showing up in .uiDialogs list...
			__dialog__: false,
		},
		`loadCollection: "${MAIN_COLLECTION_TITLE}"`],

	// XXX extend .saveCollection(..) and remove this...
	// 		...see .loadCollection(..) notes above...
	// XXX should we warn the user when overwriting???
	saveAsCollection: ['Collections/$Save as collection...',
		mixedModeCollectionAction(function(title){
			this.saveCollection(title, 'current') 
			// XXX should we be doing this manually here or in .saveCollection(..)
			title == this.collection
				&& this.loadCollection('!')
		})],
	collect: [
		collectionGetterWrapper(function(gids, title){ 
			if(title == null){
				title = gids
				gids = null
			}
			this.collect(gids || 'current', title) }, 2)],
	collectRibbon: ['Collections|Ribbon/Add $ribbon to collection...',
		'collect: "ribbon"'],
	collectLoaded: ['Collections/$Add loaded images to collection...',
		'collect: "loaded"'],
	joinCollect: [
		collectionGetterWrapper(function(title){ this.joinCollect(title) })],

	// XXX do we need this???
	cropImagesInCollection: ['Collections|Crop/Crop images in collection...',
		{browseMode: function(){ 
			return (!this.collections 
					|| Object.keys(this.collections).length == 0) 
				&& 'disabled' }},
		mixedModeCollectionAction(function(title){
			var that = this
			this.ensureCollection(title)
				.then(function(collection){
					var images = collection.data.getImages('all')

					that.crop(images, false)
				})
		}, null, false)],
	cropOutImagesInCollection: ['Collections|Crop/Remove collection images from crop...',
		{browseMode: 'cropImagesInCollection'},
		mixedModeCollectionAction(function(title){
			var that = this
			this.ensureCollection(title)
				.then(function(collection){
					var to_remove = collection.data.getImages('all')
					var images = that.data.getImages('loaded')
						.filter(function(gid){ return to_remove.indexOf(gid) < 0 })
					that.crop(images, false)
				})
		}, null, false)],

	// XXX should this be in Collections/ ???
	editDefaultCollections: ['Interface|Collections/Edit default collections...',
		widgets.makeConfigListEditorDialog(
			'default-collections', 
			{
				cls: 'collection-list',

				unique: true,
				sortable: 'y',

				normalize: function(title){ 
					return title.trim() },
				check: function(title){ 
					return title.length > 0 
						&& title != MAIN_COLLECTION_TITLE },
			})],

	/*/ XXX this is not used by metadata yet...
	metadataSection: ['- Image/',
		function(gid, make){
			// XXX
		}],
	//*/
	

	/*/ XXX experementing...
	//		would be nice to:
	//			- have an action accessible within the action menu and standalone
	//			- topology:
	//				<collection>/
	//					<collection-option>: <value>
	//					...
	//				...
	//			- creating a collection should open its options...
	//
	collectionsList: ['Collections/Collections list/*',
		function(path, make){
			make.EditableList(this.collection_order)
		}],
	//*/
})

var UICollection = 
module.UICollection = core.ImageGridFeatures.Feature({
	title: '',
	doc: '',

	tag: 'ui-collections',
	depends: [
		'ui',
		'collections',

		// XXX needed only for .addMarkedToCollection(..)
		'collection-tags',
	],

	actions: UICollectionActions, 

	handlers: [
		// we need to do this as we transfer tags after everything is 
		// loaded...
		['collectionLoading',
			function(){
				this.reload() 
			}],

		// update view when editing current collection...
		[[
			'uncollect', 
			'joinCollect',
		],
			function(_, gids, collection){
				(collection == null || this.collection == collection)
					&& this.reload(true)
			}],

		// maintain crop viewer state when loading/unloading collections...
		['load clear reload collectionLoading collectionUnloaded',
			function(){
				if(!this.dom){
					return
				}
				this.dom[this.collection ? 
					'addClass' 
					: 'removeClass']('collection-mode')

				this.dom[this.cropped ? 
					'addClass' 
					: 'removeClass']('crop-mode')
			}],
	],
})



//---------------------------------------------------------------------

var CollectionMarksActions = actions.Actions({
	config: {
		'collection-local-tags': 
			// XXX need a way to exrtend config values in order of merge
			// 		and not manually...
			CollectionTagsActions.config['collection-local-tags']
				.concat([
					'bookmark',
					'marked',
				]),
		
		'collection-transfer-changes': 
			// XXX need a way to exrtend config values in order of merge
			// 		and not manually...
			//CollectionTagsActions.config['collection-transfer-changes']
			CollectionActions.config['collection-transfer-changes']
				.concat([
					'bookmarked', 
					'marked',
				]),
	},

	// marked...
	collectMarked: ['- Collections|Mark/',
		function(collection){
			return this.collect(this.marked, collection) }],
			//return this.collectTagged('marked', collection) }],
	uncollectMarked: ['Collections|Mark/Remove marked from collection',
		{browseMode: function(){ 
			return (!this.collection || this.marked.length == 0) && 'disabled' }},
		function(collection){
			return this.uncollect(this.marked, collection) }],
			//return this.uncollectTagged('marked', collection) }],

	// bookmarked...
	collectBookmarked: ['- Collections|Bookmark/',
		function(collection){
			return this.collectTagged('bookmark', collection) }],
	uncollectBookmarked: ['Collections|Bookmark/Remove bookmarked from collection',
		{browseMode: function(){ 
			return (!this.collection || this.bookmarked.length == 0) && 'disabled' }},
		function(collection){
			return this.uncollectTagged('bookmark', collection) }],
})

var CollectionMarks = 
module.CollectionMarks = core.ImageGridFeatures.Feature({
	title: '',
	doc: '',

	tag: 'collection-marks',
	depends: [
		'marks',
		'collection-tags',
		'ui-collections',
	],

	actions: CollectionMarksActions,
})



//---------------------------------------------------------------------

var UICollectionMarksActions = actions.Actions({
	// UI...
	// XXX should these be a separate feature???
	markImagesInCollection: ['Collections|Mark/$Mark images in collection...',
		{browseMode: 'cropImagesInCollection'},
		mixedModeCollectionAction(function(title){
			var that = this
			this.ensureCollection(title)
				.then(function(collection){
					var images = collection.data.getImages('all')

					that.toggleMark(images, 'on')
				})
		})],
	addMarkedToCollection: ['Collections|Mark/Add marked to $collection...',
		{browseMode: function(){ 
			return this.marked.length == 0 && 'disabled' }},
		mixedModeCollectionAction(function(title){ this.collectMarked(title) })],
})

var UICollectionMarks = 
module.UICollectionMarks = core.ImageGridFeatures.Feature({
	title: '',
	doc: '',

	tag: 'ui-collection-marks',
	depends: [
		'collection-marks',
		'ui-collections',
	],

	actions: UICollectionMarksActions,
})



//---------------------------------------------------------------------
// XXX Things to try/do:
// 		- load directories as collections (auto?)...
// 		- export collections to directories...
// 		- auto-export collections (on save)...
// 			- add new images
// 			- remove old images...

var FileSystemCollectionActions = actions.Actions({

	// Format:
	// 	{
	// 		path: <string>,
	// 		...
	// 	}
	collections: null,

	// XXX this does not work for merged indexes as each index has 
	// 		different gids and paths for same collection title...
	// 		...need to merge these correctly...
	// 			- merge collections by title
	// 			- multiple gids
	// 			- multiple paths
	collectionPathLoader: ['- Collections/',
		{collectionFormat: 'path'},
		function(title, state, logger){ 
			var that = this

			// if data is present, do not reload...
			if(state.data){
				return
			}

			// XXX get a logger...
			logger = logger || this.logger
			logger = logger && logger.push('Load')

			return Promise.all((this.location.loaded || [this.location.path])
				.map(function(path){
					path = util.normalizePath([
						path,
						that.config['index-dir'], 
						// XXX use index-specific path...
						state.path,
					].join('/'))

					return file.loadIndex(path, false, logger)
						.then(function(res){
							// load the collection data...
							that.collections[title].data = 
								that.prepareIndexForLoad(res[path]).data
						})
				}))
		}],

	// XXX revise...
	// XXX this should be generic... (???)
	// 		...I think the action itself should be generic, but what this
	// 		specific action does is very specific to file collections...
	// 		...think of a protocol))))
	unloadUnchangedCollections: ['Collections|File/Unload saved collections',
		function(logger){
			var that = this

			if(this.changes === true || this.changes === undefined){
				return
			}

			// XXX get a logger...
			logger = logger || this.logger
			logger = logger && logger.push('Unload')

			Object.keys(this.collections)
				.forEach(function(title){
					var c = that.collections[title] 

					var key = 'collection: '+JSON.stringify(c.gid || title)

					if(title != MAIN_COLLECTION_TITLE
							&& title != that.collection
							&& c.path 
							&& c.data
							&& (that.changes === false 
								|| !(key in that.changes))){

						logger && logger.emit('title', title)

						c.count = c.data.length
						delete c.data
					}
				})
		}],

	importCollectionsFromPath: ['- Collections|File/Import collections from path',
		function(path){
			// XXX
		}],
})

var FileSystemCollection = 
module.FileSystemCollection = core.ImageGridFeatures.Feature({
	title: '',
	doc: '',

	tag: 'fs-collections',
	depends: [
		'index-format',
		'fs',
		'collections',
	],

	actions: FileSystemCollectionActions,

	handlers: [
	],
})



//---------------------------------------------------------------------
// XXX localstorage-collections (???)




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