//=====================================================================
//
//
//
// TODO:
// 	- drag-n-drop
// 	- sort/move
// 	- crop selection
// 	- make the gallery into a web component
//
//
//=====================================================================

// This compansates for any resize rounding errors in patchFlexRows(..).
var PATCH_MARGIN = 2

// a timeout to let the resize settle before we handle dragover...
// XXX this is too long -- we can pickup and drag an image within this 
// 		timeout...
// 		need to make it more specific (handle only vertical drag???)
var RESIZE_SETTLE_TIMEOUT = 16


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

var patchFlexRows = 
function(elems, 
		prevent_row_expansion=false, 
		last_row_resize=1.5, 
		patch_margin=PATCH_MARGIN){
	if(elems.length == 0){
		return }
	// NOTE: -1 here is to compensate for rounding errors...
	var W = elems[0].parentElement.clientWidth - patch_margin
	var w = 0
	var h
	var row = []
	var top = elems[0].offsetTop
	// modes:
	// 		'as-is' | false
	// 		'expand'
	// 		'close'
	// closure: W, w, h, row, elem
	var max = 0
	var handleRow = function(mode='justify'){
		if(!mode || mode == 'as-is'){
			return false }

		if(typeof(mode) == 'number'){
			var r = Math.min(
				W / w, 
				max * mode)
		} else {
			var r = W / w }

		var expanded
		if(mode == 'expand'){
			var r2 = W / (w + elem.offsetWidth)
			// NOTE: we are checking which will require a lesser resize
			//		the current row or it with the next image...
			if(1/r < r2){
				expanded = true
				var r = r2
				row.push(elem) } }

		max = Math.max(max, r) 

		// patch the row...
		var nw = 0
		for(var e of row){
			if(r == 0 || h == 0){
				e.style.height = ''
			} else {
				e.style.height = (h * r) + 'px' }
			nw += e.offsetWidth }
		return !!expanded }
	// NOTE: this will by design skip the last row...
	// 		...because we handle the row only when we see an image at a 
	// 		different vertical offset...
	for(var elem of elems){
		elem.style.height = ''
		elem.style.width = ''
		h = h 
			?? elem.offsetHeight
		top = top 
			?? elem.offsetTop
		// collect row...
		if(elem.offsetTop == top){
			w += elem.offsetWidth
			row.push(elem)
		// row done + prep for next...
		} else {
			var expanded_row = 
				prevent_row_expansion ?
					handleRow()
					: handleRow('expand')
			// prep for next row...
			if(!expanded_row){
				w = elem.offsetWidth
				h = elem.offsetHeight 
				top = elem.offsetTop
				row = [elem] 
			} else {
				w = 0
				h = null
				top = null
				row = [] }}}
	// handle last row...
	last_row_resize
		&& handleRow(last_row_resize) }

var getScrollParent = 
function(elem){
	var parent = elem.parentElement
	while(parent !== document.body 
			&& parent.scrollHeight > parent.clientHeight){
		parent = elem.parentElement }
	return parent }

// XXX also need to check if scrolled under something...
var isVisible =
function(elem) {
    const rect = elem.getBoundingClientRect()
    return rect.top >= 0 
		&& rect.left >= 0 
		&& rect.bottom <= (window.innerHeight 
			|| document.documentElement.clientHeight) 
		&& rect.right <= (window.innerWidth 
			|| document.documentElement.clientWidth) }



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

// XXX add shift+arrow to select...
// XXX add home/end, pageup/pagedown...
// XXX need a real ui stack -- close things top to bottom (Enter/Escape/...)...
var keyboard = {
	ArrowLeft: function(){
		gallery.prev() },
	ArrowRight: function(){
		gallery.next() },
	// NOTE: up/down will not prevent the default scrolling behavior 
	// 		when at top/bottom of the gallery.
	ArrowUp: function(evt){
		gallery.__at_top_row 
			|| evt.preventDefault()
		;(gallery.dom.classList.contains('lightboxed')
				|| gallery.dom.classList.contains('detailed'))
			&& evt.preventDefault()
		gallery.lightbox.shown
			|| gallery.details.shown
			|| gallery.up() },
	ArrowDown: function(evt){
		gallery.__at_bottom_row 
			|| evt.preventDefault()
		;(gallery.dom.classList.contains('lightboxed')
				|| gallery.dom.classList.contains('detailed'))
			&& evt.preventDefault()
		gallery.lightbox.shown
			|| gallery.details.shown
			|| gallery.down() },
	Enter: function(){
		gallery.details.shown ?
			gallery.details.hide()
			: gallery.lightbox.toggle() },
	Escape: function(evt){
		gallery.details.shown ?
			gallery.details.hide() 
		: gallery.lightbox.shown ?
			gallery.lightbox.hide() 
		// XXX should we remember which image was current and select 
		// 		it again when needed???
		: gallery.unmark_current ?
			(gallery.current = null) 
		: null },
	// del (image) -> mark current for deletion
	// del (marked) -> toggle marked for deletion all marked
	// shift-del (none marked for deletion) -> delete current
	// shift-del (1+ marked for deletion) -> delete marked for deletion
	Delete: function(evt){
		// remove...
		if(evt.shiftKey){
			var to_remove = gallery.toBeRemoved
			// remove marked...
			if(to_remove.length > 0){
				gallery.removeQueued()
			// remove current...
			} else if(gallery.current){
				to_remove = gallery.current
				if(gallery.marked.includes(to_remove)){
					gallery.remove(...gallery.marked)
				} else {
					// move focus...
					gallery.next()
					gallery.current === to_remove
						&& gallery.prev()
					gallery.remove(to_remove) } } 
		// mark for removal...
		} else if(gallery.current){
			var cur = gallery.current
			// toggle marked...
			if(gallery.marked.includes(cur)){
				gallery.toggleQueueRemoval('marked') 
			// toggle current...
			} else {
				gallery.toggleQueueRemoval() } } },
	// selection...
	' ': function(evt){
		gallery.current
			&& evt.preventDefault()
		gallery.toggleMark() },
	// XXX use key codes...
	'a': function(evt){
		evt.preventDefault()
		if(evt.ctrlKey){
			gallery.markAll() } },
	'd': function(evt){
		evt.preventDefault()
		if(evt.ctrlKey){
			gallery.unmarkAll() } },
	'i': function(evt){
		evt.preventDefault()
		if(evt.ctrlKey){
			gallery.markInverse() } },
}



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

var Gallery = {

	// Options...
	//
	unmark_current: true,

	// If true navigation will loop over top/bottom rows and first/last 
	// images...
	// This is mainly usefull for small galleries that do not need paging.
	// XXX might be a good idea to auto-disable this when paging is required.
	loop_images: false,

	// If true for each row two versions will be compared, as-is and with 
	// one image from the next row and the closest resiae will be chosen.
	//
	// This produses a more uniform image grid but will have greater 
	// effect on gallery height / slower.
	allow_row_expansion: true,

	// define how to handle last row
	//
	// can be:
	// 	'as-is'		- do not resize
	// 	'justify'	- fit width
	// 	<number>	- maximum ration relative to largest row (must be >=1)
	//
	// Recomended value close to 1
	last_row_resize: 1,

	// If true first click on image will select it the second click will
	// open it...
	click_to_select: true,

	exit_fullscreen_on_lightbox_close: true,

	// Mode to select the above/below image...
	//
	// 					 	   	   
	// 		     - -----+-------------+
	// 					|	   .	  |
	// 					|   current	  |
	// 					|	   .	  |
	// 		- --+-------+---.---+--.--+
	// 			|		.	    |  .  |			
	// 			|	    B	    |  A  |			
	// 			|		.	    |  .  |			
	// 		- --+---------------+-----+
	// 			 		^   ^      ^
	// 			 		c   i      c   			
	//
	// Here, A has the closest center (c) to current but B has the closest 
	// center of intersection (i), thus the two approaches will yield 
	// different results, moving down from current:
	// 		current ----(center)----> A
	// 		current -(intersection)-> B
	//
	// can be:
	// 	'intersection'	- closest center of intersecting part to center 
	// 						of current image.
	// 	'center'		- closest center of image to current image center
	// XXX remove this when/if the selected options feels natural...
	vertical_navigate_mode: 'intersection',


	code: `
		<div class="gallery">
			<!-- gallery: content -->
			<div class="images">
			</div>
			<!-- lightbox -->
			<div class="lightbox">
				<img>
				<div class="button close"></div>
			</div>
		</div>`,

	dom: undefined,

	__lightbox: undefined,
	get lightbox(){
		if(this.dom){
			return this.__lightbox 
				?? (this.__lightbox = { __proto__: Lightbox }
					.setup(
						this.dom.querySelector('.lightbox'),
						this)) }
		delete this.__lightbox 
		return undefined },
	__details: undefined,
	get details(){
		if(this.dom){
			return this.__details 
				?? (this.__details = { __proto__: Details }
					.setup(
						this.dom.querySelector('.details'),
						this)) }
		delete this.__details 
		return undefined },

	__at_top_row: undefined,
	__at_bottom_row: undefined,
	get current(){
		return this.dom.querySelector('.images img.current') },
	set current(img){
		// unset...
		if(img == null){
			this.current?.classList.remove('current')
			return }
		// set...
		for(var i of this.dom.querySelectorAll('.images img.current')){
			i.classList.remove('current') }
		img.classList.add('current') 
		// XXX add offsets from borders...
		img.scrollIntoView({
			behavior: 'smooth',
			block: 'nearest',
		}) 
		// helpers...
		this.__at_top_row = !this.getRow('above')
		this.__at_bottom_row = !this.getRow('below') },

	// XXX should this be writable???
	get images(){
		return [...this.dom.querySelectorAll('.images img')] },

	get urls(){
		return this.images
			.map(function(img){ 
				// XXX not sure if we should remove the preview dir...
				return img.src }) },
				/*/
				return img.src 
					// remove preview dir...
					.replace(/\/[0-9]+px\//, '/') }) },
				//*/

	get marked(){
		return [...this.dom.querySelectorAll('.images img.marked')] },

	get length(){
		return this.images.length },
	get index(){
		return this.images.indexOf(this.current) },

	getRow: function(img, direction='current', images){
		if(['above', 'current', 'below'].includes(img)){
			images = direction
			direction = img
			img = this.current }
		if(direction instanceof Array){
			images = direction
			direction = null }
		direction ??= 'current'
		images ??= this.images
		// get above/below row...
		// XXX these are wastefull...
		if(direction == 'above'){
			var row = this.getRow(img)
			var e = row[0].previousElementSibling
			while(e && e.tagName != 'IMG'){
				e = e.previousElementSibling }
			return e ?
					this.getRow(e)
				: this.loop_images ?
					this.getRow(images.at(-1))
				: undefined
		} else if(direction == 'below'){
			// special case: nothing marked...
			if(img == null){
				return this.getRow() }
			var row = this.getRow(img)
			var e = row.at(-1).nextElementSibling
			while(e && e.tagName != 'IMG'){
				e = e.nextElementSibling }
			return e ?
					this.getRow(e) 
				: this.loop_images ?
					this.getRow(images[0])
				: undefined }
		// get current row...
		var cur = img 
			?? this.current
		if(cur == null){
			var scroll = getScrollParent(this.dom).scrollTop
			var images = images 
			for(cur of images){
				if(cur.offsetTop >= scroll){
					break } } }
		var top = cur.offsetTop
		var row = []
		var e = cur
		while(e && e.offsetTop == top){
			row.push(e)
			e = e.nextElementSibling
			while(e && e.tagName != 'IMG'){
				e = e.nextElementSibling } }
		e = cur
		while(e && e.offsetTop == top){
			e === cur
				|| row.unshift(e)
			e = e.previousElementSibling
			while(e && e.tagName != 'IMG'){
				e = e.previousElementSibling } }
		return row },
	// XXX add .loop_images support???
	getImage: function(img, direction='current', images){
		// .getImage(direction[, images])
		if(['left', 'above', 'current', 'below', 'right'].includes(img)){
			images = direction
			direction = img
			img = null }
		// .getImage(img, images)
		if(direction instanceof Array){
			images = direction
			direction = null }
		direction ??= 'current'
		images ??= this.images
		// current...
		if(direction == 'current'){
			return img 
				?? this.current 
				?? this.getRow(img, images)[0]
		// above/below...
		// get image with closest center to target image center...
		} else if(direction == 'above' || direction == 'below'){
			var row = this.getRow(direction, images)
			if(row == null){
				return undefined }
			var cur = this.current 
				?? row[0]
			// image center point...
			var c = cur.offsetLeft + cur.offsetWidth/2
			var target
			var min
			for(var img of row){
				// length of intersection...
				if(this.vertical_navigate_mode == 'intersection'){
					var l = Math.max(
						img.offsetLeft, 
						cur.offsetLeft)
					var r = Math.min(
						img.offsetLeft + img.offsetWidth,
						cur.offsetLeft + cur.offsetWidth)
					var w = r - l
					var n = l + w/2
				// closest center...
				} else {
					var n = img.offsetLeft + img.offsetWidth/2 }
				var d = Math.abs(n - c)
				min = min ?? d
				if(d <= min){
					min = d
					target = img } } 
		// left/right...
		} else {
			var row = this.getRow(img, images)
			var i = row.indexOf(
				img 
					?? this.current 
					?? row[0])
			i += direction == 'left' ?
				-1
				: +1
			i = i < 0 ?
					row.length-1
				: i >= row.length-1 ?
					0
				: i
			var target = row[i] }
		return target },
	//
	// 	.getImages()
	// 		-> []
	//
	// 	.getImages('*')
	// 	.getImages('all')
	// 		-> <all-images>
	//
	// 	.getImages('marked')
	// 		-> <marked-images>
	//
	// 	.getImages(<img>, ..)
	// 	.getImages([<img>, ..])
	// 		-> <images>
	//
	// 	.getImages('sorted', ..)
	// 	.getImages(.., 'sorted')
	// 		-> <sorted-images>
	//
	// <img> ::=
	// 		ImageElement
	// 		| <index>
	// 		| 'current'
	//
	getImages: function(...images){
		var that = this
		var sorted = 
			images.at(-1) == 'sorted' ?
				!!images.pop()
			: images[0] == 'sorted' ?
				!!images.shift()
			: false
		images = images.flat()
		if(images.includes('all') 
				|| images.includes('*')){
			return this.images }
		images = [...new Set(images
			.map(function(img){
				return ((typeof(img) == 'string' 
								&& img.trim() == '') ?
							[]
						: !isNaN(img*1) ?
							that.images.at(img*1)
						: img == 'marked' ?
							that.marked
						: img == 'current' ?
							that.current
						: typeof(img) == 'string' ?
							[]
						: img)
					?? [] })
   			.flat())] 
		if(sorted){
			var order = new Map(
				Object.entries(this.images)
					.map(function(e){ 
						return e.reverse() }))
			images
				.sort(function(a, b){
					return order.get(a) - order.get(b) }) }
		return images },

	// XXX cache image list???
	prev: function(images){
		images ??= this.images
		var i = this.current == null ? 
			images.length-1
			: images.indexOf(this.current)-1
		i = i >= 0 ?
				i
			: this.loop_images ?
				images.length-1
			: 0
		this.current = images[i]
		this.update()
		return this },
	next: function(images){
		images ??= this.images
		var i = this.current == null ? 
			0
			: images.indexOf(this.current)+1
		i = i < images.length ?
				i
			: this.loop_images ?
				0
			: images.length-1
		this.current = images[i]
		this.update()
		return this },

	// navigate images visually...
	// XXX BUG: these seem not to work with passed list of images...
	left: function(images){
		var cur = this.current
		var row = this.getRow(cur, images)
		var i = row.indexOf(cur) - 1
		this.current = row[i < 0 ?
			row.length-1
			: i]
		return this },
	right: function(images){
		var cur = this.current
		var row = this.getRow(cur, images)
		var i = row.indexOf(cur) + 1
		this.current = row[i >= row.length ?
			0
			: i]
		return this },
	up: function(images){
		var img = this.getImage('above', images)
		img 
			&& (this.current = img)
		return this },
	down: function(images){
		var img = this.getImage('below', images)
		img 
			&& (this.current = img)
		return this },

	// selection...
	//
	// NOTE: this is here because we can't use :before / :after directly 
	// 		on the img tag...
	// XXX make this generic and use a .marks list...
	updateMarkers: function(){
		var that = this
		// select...
		for(var img of this.dom.querySelectorAll('.images img.marked')){
			var mark = img.nextElementSibling
			while(mark && mark.tagName != 'IMG' && !mark.classList.contains('mark')){
				mark = img.nextElementSibling }
			if(!mark || !mark.classList.contains('mark')){
				mark = document.createElement('div')
				mark.classList.add('marked', 'mark')
				mark.addEventListener('click', function(evt){
					evt.stopPropagation()
					that.unmark(mark) })
				img.after(mark) } }
		// clear unmarked...
		for(var mark of this.dom.querySelectorAll('.images img:not(.marked)+.mark')){
			mark.remove() }
		// update lightbox...
		this.lightbox.shown
			&& this.lightbox.update()
		return this },
	mark: function(img){
		img = img ?? this.current
		img?.classList.add('marked')
		return this.updateMarkers() },
	unmark: function(img){
		img = img ?? this.current
		img?.classList.remove('marked')
		return this.updateMarkers() },
	toggleMark: function(img){
		img = img ?? this.current
		img?.classList.toggle('marked')
		this.updateMarkers()
		return this },
	markAll: function(){
		for(var img of this.images){
			img.classList.add('marked') }
		return this.updateMarkers() },
	unmarkAll: function(){
		for(var img of this.images){
			img.classList.remove('marked') }
		return this.updateMarkers() },
	markInverse: function(){
		for(var img of this.images){
			img.classList.toggle('marked') }
		return this.updateMarkers() },

	show: function(){
		return this.showLightbox() },
	showLightbox: function(){
		this.lightbox.show()
		return this },
	showDetails: function(){
		this.details.show()
		return this },

	__update_grid_size_timeout: undefined,
	__update_grid_size_running: false,
	__update_grid_size: function(){
		// if still running then delay untill done...
		// NOTE: this will keep only one call no matter how many times 
		// 		the method was actually called...
		if(this.__update_grid_size_running){
			var that = this
			this.__update_grid_size_timeout
				&& clearTimeout(this.__update_grid_size_timeout)
			this.__update_grid_size_timeout = setTimeout(function(){
				that.__update_grid_size() 
				delete that.__update_grid_size_timeout }, 10)
			return this }
		// do the update...
		this.__update_grid_size_running = true
		try{
			patchFlexRows(this.images, 
				!this.allow_row_expansion, 
				this.last_row_resize ?? 1.2)
		}catch(err){ }
		delete this.__update_grid_size_running
		return this },
	update: function(){
		this.__update_grid_size()
		// XXX should this update markers???
		//this.updateMarkers()
		this.lightbox.shown
			&& this.lightbox.update()
		this.details.shown
			&& this.details.update()
		return this },

	//	.load(<image>)
	//	.load(<images>)
	//	.load(<image>, <index>)
	//	.load(<images>, <index>)
	//
	//	<images> ::=
	//		<image>
	//		| [ <image>, .. ]
	//	<image> ::=
	//		<url>
	//		| [ <url>, <caption>, .. ]
	//		| { url: <url>, caption: <caption>, .. }
	//
	// XXX BUG: for some reason this breaks the gallery:
	// 			gallery.load(gallery.json('marked'))
	// XXX do we handle previews here???
	load: function(images, index=undefined){
		var that = this
		images = images instanceof Array ?
			images
			: [images]
		// create images...
		var elems = []
		for(var data of images){
			if(typeof(data) == 'string'){
				var [url, data] = [data, {}]
			} else if(data instanceof Array){
				var [url, caption] = data
				data = {}
				caption 
					?? (data.caption = caption)
			} else {
				var {url, ...data} = data }
			var elem = document.createElement('img')
			elem.onload = function(){
				that.update() }
			elem.src = url
			elem.setAttribute('draggable', 'true')
			for(var [key, value] of Object.entries(data)){
				value
					// XXX is this a good way to destinguish classes and attrs???
					&& (typeof(value) == 'boolean' ? 
						elem.classList.add(key)
						: elem.setAttribute(key, value)) }
			elems.push(elem) }
		// add to gallery...
		if(index == null){
			this.clear() }
		if(index == null 
				&& this.length == 0){
			this.dom.querySelector('.images')
				.append(...elems) 
		} else {
			var sibling = this.images.at(index)
			index < 0 ?
				sibling.after(...elems)
				: sibling.before(...elems) }
		return this
			.updateMarkers()
			.update() },
	__image_attributes__: [
		'caption',
		'filename',
	],
	__image_classes__: [
		// XXX should this be here or set as a root attribute???
		'current',
		'marked',
	],
	// XXX add option to include images as data urls...
	// XXX do we handle previews here???
	json: function(images=undefined){
		var that = this
		return this.getImages(images ?? this.images)
			.map(function(img, i){
				var res = { url: img.src }
				for(var key of that.__image_attributes__ ?? []){
					var value = img.getAttribute(key)
					value 
						&& (res[key] = value) }
				for(var key of that.__image_classes__ ?? []){
					img.classList.contains(key)
						&& (res[key] = true) }
				return res }) },

	// XXX
	zip: function(){
		var json = this.json()
		// XXX
	},

	// removal queue...
	get toBeRemoved(){
		return [...gallery.dom.querySelectorAll('.images img.to-remove')] },
	set toBeRemoved(lst){
		this.queueRemoval(lst) },
	queueRemoval: function(...images){
		images = images.length == 0 ?
			['current']
			: images
		for(var img of this.getImages(...images)){
			img.classList.add('to-remove') } 
		return this },
	unqueueRemoval: function(...images){
		images = images.length == 0 ?
			['current']
			: images
		for(var img of this.getImages(...images)){
			img.classList.remove('to-remove') } 
		return this },
	toggleQueueRemoval: function(...images){
		images = images.length == 0 ?
			['current']
			: images
		for(var img of this.getImages(...images)){
			img.classList.toggle('to-remove') } 
		return this },
	removeQueued: function(){
		return this.remove(...this.toBeRemoved) },

	remove: function(...images){
		if(images.includes('all') 
				|| images.includes('*')){
			return this.clear() }
		// NOTE: we need to remove images from the end so as not to be 
		// 		affected by shifed indexes...
		images = this.getImages('sorted', ...images)
			.reverse()
		for(var img of images){
			typeof(img) == 'number' ?
				this.images.at(img)?.remove() 
			: img instanceof Element ?
				(this.images.includes(img) 
					&& img.remove())
			: null }
		return this
			.updateMarkers()
			.update() },
	clear: function(){
		this.dom.querySelector('.images').innerHTML = ''
		return this },

	setup: function(dom){
		var that = this
		this.dom = dom

		// prevent clicks in toolbar from affecting the gallery...
		this.dom.querySelector('.toolbar')
			.addEventListener('click', function(evt){
				evt.stopPropagation() })
		// image clicks...
		this.dom.querySelector('.images')
			.addEventListener('click', function(evt){
				evt.stopPropagation()
				var target = evt.target
				if(target.tagName == 'IMG'){
					// shift+click: toggle selections...
					if(evt.shiftKey){
						that.toggleMark(target)
					// first click selects, second shows...
					} else if(that.click_to_select){
						target.classList.contains('current') ?
							that.show()
							: (that.current = target)
					// first click selects and shows...
					} else {
						that.current = target
						that.show() } 
				} else if(that.unmark_current){
					that.current = null } })
		this.dom
			.addEventListener('click', function(evt){
				that.unmark_current
					&& (that.current = null) })
		// drag/drop: sort...
		var dragged
		this.dom
			.addEventListener('dragstart', function(evt){
				var i = that.images.indexOf(evt.target)
				if(i >= 0){
					dragged = evt.target 
					dragged.classList.add('dragged') } })
		var skip_dragover = false
		this.dom
			.addEventListener('dragenter', function(evt){
				// NOTE: this prevents jumping back and forth if moving 
				// 		image causes a resize that causes the image to 
				// 		move again...
				if(skip_dragover){
					return }
				var target = evt.target
				var i = that.images.indexOf(target)
				if(dragged 
						&& i >= 0){
					evt.offsetX < target.offsetWidth / 2 ?
						target.after(dragged)
						: target.before(dragged) 
					// skip a dragover event if triggered by (right 
					// after) resize...
					skip_dragover = true
					setTimeout(function(){
						skip_dragover = false }, RESIZE_SETTLE_TIMEOUT)
					that
						.__update_grid_size()
						.updateMarkers() 
					dragged.scrollIntoView({
						behavior: 'smooth',
						block: 'nearest',
					}) } })
		// check if we just went out of the edge image...
		// NOTE: this handles a special case:
		// 			|[A][  B  ][C]   |
		// 		when a narrow image (A, C) is at the edge and the 
		// 		adjacent image is wide (b). 
		// 		dragging the narrow image over the wide places it at 
		// 		the other side of the wide image but the cursor is now 
		// 		over the wide image so to drag back we either need to 
		// 		drag out of it and drag over again (not intuitive) or 
		// 		simply drag over the oppoiste edge of the wide image 
		// 		(dragleave handler)
		// NOTE: we are not implementing the whole drag process here 
		// 		because dragging up/down here is far more complicated 
		// 		than when doing it in dragover...
		this.dom
			.addEventListener('dragleave', function(evt){
				if(skip_dragover){
					return }
				var target = evt.target
				var images = that.images
				var i = images.indexOf(target)
				if(dragged !== target 
						&& i >= 0){
					// left edge...
					if(evt.offsetX <= 0){
						var prev = images[i-1]
						// adjacent image is not on the same offsetTop (edge)
						if(prev == null 
								|| prev.offsetTop != target.offsetTop){
							target.before(dragged) 
							that.updateMarkers() }
					// right edge...
					} else {
						var next = images[i+1]
						// adjacent image is not on the same offsetTop (edge)
						if(next == null 
								|| next.offsetTop != target.offsetTop){
							target.after(dragged)
							that.updateMarkers() } } } })
		var dragged_over
		this.dom
			.addEventListener('dragover', function(evt){
				evt.preventDefault()
				evt.stopPropagation()
				evt.dataTransfer.dropEffect = 'copy' 
				var target = evt.target
				var i = that.images.indexOf(target)
				if(!dragged 
						&& i >= 0){
					dragged_over
						&& dragged_over.classList.remove('dragged-over') 
					dragged_over = target
					dragged_over.classList.add('dragged-over') } }, false)
		// drag/drop: files...
		// NOTE: if shift is pressed then this will add files to the 
		// 		loaded list, otherwise it will replace the list...
		// XXX handle serilized data...
		this.dom
			.addEventListener('drop', function(evt){
				evt.preventDefault()
				evt.stopPropagation()

				// non-local drag...
				if(!dragged){
					var expand = (evt.shiftKey && dragged_over) ?
						that.images.indexOf(dragged_over)
						: undefined

					var files = 
						evt.dataTransfer.items ?
							[...evt.dataTransfer.items]
								.map(function(elem){
									return elem.kind == 'file' ?
										[elem.getAsFile()]
										: [] })
								.flat()
							: [...evt.dataTransfer.files]

					Promise.all(files
							.map(function(file){
								// XXX handle serilized data...
								// XXX

								// images...
								if(file.type.match('image.*')){
									// XXX TEST...
									if(file.path){
										return {
											url: file.path,
											filename: file.name,
										}
									} else {
										return new Promise(function(resolve, reject){
											var reader = new FileReader()
											reader.onload = function(f){
												resolve({
													url: f.target.result,
													filename: file.name,
													// XXX any other metadata to include???
												}) }
											reader.readAsDataURL(file) }) } } 
								// other files...
								return [] })
							.flat())
						.then(
							function(images){
								// no images...
								if(images.length == 0){
									return }
								return that.load(images, expand) }, 
							function(err){
								// XXX handle errors...
							}) }
				dragged
					&& dragged.classList.remove('dragged')
				dragged_over
					&& dragged_over.classList.remove('dragged-over')
				// XXX if this is used in the promise, move to the point 
				// 		after we nned this...
				dragged = undefined 
				dragged_over = undefined }, false)

		// XXX
		for(var img of this.images){
			img.setAttribute('draggable', 'true') }

		// handle resizing...
		new ResizeObserver(
			function(elems){
				that.__update_grid_size() })
			.observe(this.dom)
		
		return this
			.updateMarkers()
   			.update() },
}


//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// This is a prototype for all modal views...

var Overlay = {
	dom: undefined,
	gallery: undefined,
	cls: 'overlay',

	get url(){
		return this.dom.querySelector('img').src },
	set url(url){
		this.dom.querySelector('img').src = url
			// remove preview dir...
			.replace(/\/[0-9]+px\//, '/') 
		// cache...
		this.cache
			&& this.cache_count != 0
			&& this.cache() },

	get shown(){
		return this.gallery.dom.classList.contains(this.cls) },
	show: function(){
		this.update()
		this.gallery.dom.classList.add(this.cls)
		return this },
	hide: function(){
		this.gallery.exit_fullscreen_on_lightbox_close
			&& document.fullscreenElement
			&& document.exitFullscreen()
		this.gallery.dom.classList.remove(this.cls)
		return this },
	toggle: function(){
		return this.shown ?
			this.hide()
			: this.show() },

	cache: null,

	update: function(){
		this.url = 
			(this.gallery.current
				?? this.gallery.next().current
				?? {}).src
		var caption = 
			(this.gallery.current
					?? this.gallery.next().current
					?? {})
				.getAttribute('caption') 
					?? ''
		var index = (this.gallery.index+1) +'/'+ this.gallery.length
		// set caption...
		// XXX should these be separate elements???
		this.dom.setAttribute('caption',
			(this.caption_format ?? '${CAPTION}')
				.replace(/\${CAPTION}/, caption)
				.replace(/\${INDEX}/, index))
		// set selection...
		this.gallery.current.classList.contains('marked') ?
			this.dom.classList.add('marked')
			: this.dom.classList.remove('marked')
		return this },

	setup: function(dom, gallery){
		var that = this
		this.dom = dom
		this.gallery = gallery

		// controls...
		this.dom.querySelector('.close')
			?.addEventListener('click', function(evt){
				evt.stopPropagation()
				that.hide() })
		this.dom.querySelector('.fullscreen')
			?.addEventListener('click', function(evt){
				evt.stopPropagation()
				document.fullscreenElement ?
					document.exitFullscreen()
					: that.dom.requestFullscreen() })
		this.dom.querySelector('.info')
			?.addEventListener('click', function(evt){
				evt.stopPropagation()
				that.gallery.showDetails() })
		this.dom.querySelector('.prev')
			?.addEventListener('click', function(evt){
				evt.stopPropagation()
				that.gallery.prev() })
		this.dom.querySelector('.next')
			?.addEventListener('click', function(evt){
				evt.stopPropagation()
				that.gallery.next() })
		// stop body from scrolling...
		var stop = function(evt){
			evt.stopPropagation()
			evt.preventDefault()
			return false }
		this.dom.addEventListener('touchmove', stop)
		this.dom.addEventListener('mousewheel', stop)
		this.dom.addEventListener('wheel', stop)
		this.dom.addEventListener('scroll', stop)
		// drag...
		this.dom
			.addEventListener('dragover', function(evt){
				that.gallery.dom.scrollIntoView({
					behavior: 'smooth',
					block: 'nearest',
				})
				that.hide() })
		return this },
}


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

var Lightbox = {
	__proto__: Overlay,

	cls: 'lightboxed',

	caption_format: '${INDEX} ${CAPTION}',

	navigation_deadzone: 100,
	caption_hysteresis: 10,
	cache_count: 1,

	__cache: undefined,
	cache: function(){
		var cache = []
		var _cache = this.__cache = []
		var cur = this.gallery.current
		var images = [...this.gallery.dom.querySelectorAll('img')].slice(1)
		var i = images.indexOf(cur)
		var c = this.cache_count ?? 2
		for(var j=i+1; j<=i+c; j++){
			cache.push(j >= images.length ? 
				j % images.length 
				: j) }
		for(var j=i-1; j>=i-c; j--){
			cache.unshift(j < 0 ?
				images.length+j
				: j) }
		for(i of cache){
			var img = document.createElement('img')
			img.src = images[i].src
				.replace(/\/[0-9]+px\//, '/')
			_cache.push(img) } 
		return this },
	
	setup: function(dom, gallery){
		var that = this

		Overlay.setup.call(this, ...arguments)

		// click zones...
		var deadzone = this.navigation_deadzone ?? 100
		this.dom
			.addEventListener('click', function(evt){
				evt.stopPropagation()
				// click left/right side of view...
				// NOTE: this is vewport-relative...
				evt.clientX < that.dom.offsetWidth / 2 - deadzone/2
					&& that.gallery.prev()
				evt.clientX > that.dom.offsetWidth / 2 + deadzone/2
					&& that.gallery.next() })
		// hover zones...
		var hysteresis = this.caption_hysteresis ?? 10
		this.dom
			.addEventListener('mousemove', function(evt){
				// indicate action...
				if(evt.clientX < that.dom.offsetWidth / 2 - deadzone/2){
					that.dom.classList.contains('clickable')
						|| that.dom.classList.add('clickable')
				} else if( evt.clientX > that.dom.offsetWidth / 2 + deadzone/2){
					that.dom.classList.contains('clickable')
						|| that.dom.classList.add('clickable')
				} else {
					that.dom.classList.contains('clickable')
						&& that.dom.classList.remove('clickable') }
				// show/hide caption...
				// hysteresis:
				//		     +---+-- off
				//		     |	 |
				//		     v	 ^
				//		     |	 |
				// 		on  -+---+
				evt.clientY > that.dom.offsetHeight / 2 + hysteresis
					&& that.dom.classList.add('show-caption')
				evt.clientY < that.dom.offsetHeight / 2 - hysteresis
					&& that.dom.classList.remove('show-caption') })
		return this },
}


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

var Details = {
	__proto__: Overlay,

	cls: 'detailed',

	// XXX
}


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

var setupGallery = function(gallery){
	return {__proto__: Gallery}
		.setup(gallery) }

var setup = function(){
	var galleries = document.body.querySelectorAll('.gallery')
	for(var gallery of galleries){
		// XXX this is wrong -- handle multiple galleries...
		window.gallery = setupGallery(gallery) } 
	// keyboard...
	document.addEventListener('keydown', function(evt){
		var key = evt.key
		if(key in keyboard){
			keyboard[key](evt) } }) }
	


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