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



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

// XXX need to account for scrollbar -- add hysteresis???
var patchFlexRows = 
function(elems, prevent_row_expansion=false){
	// NOTE: -1 here is to compensate for rounding errors...
	var W = elems[0].parentElement.clientWidth - 1
	var w = 0
	var h
	var row = []
	var top = elems[0].offsetTop
	// NOTE: this will by design skip the last row.
	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 donw + prep for next...
		} else {
			// NOTE: we are checking which will require a lesser resize
			//		the current row or it with the next image...
			var r1 = W / w
			var r2 = W / (w + elem.offsetWidth)
			var expanded_row = 
				prevent_row_expansion ?
					false
					: 1/r1 < r2
			if(!expanded_row){
				var r = r1
			} else {
				var r = r2
				row.push(elem) }
			// patch the row...
			var nw = 0
			for(var e of row){
				e.style.height = (h * r) + 'px' 
				nw += e.offsetWidth }
			// 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 = [] }}}}

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...
var keyboard = {
	ArrowLeft: function(){
		gallery.lightbox.shown ?
			gallery.lightbox.prev()
			: gallery.prev() },
	ArrowRight: function(){
		gallery.lightbox.shown ?
			gallery.lightbox.next()
			: gallery.next() },
	ArrowUp: function(evt){
		evt.preventDefault()
		gallery.lightbox.shown
			|| gallery.up() },
	ArrowDown: function(evt){
		evt.preventDefault()
		gallery.lightbox.shown
			|| gallery.down() },
	Enter: function(){
		gallery.lightbox.toggle() },
	Escape: function(){
		gallery.lightbox.shown ?
			gallery.lightbox.hide() 
		// XXX should we remember which image was current and select 
		// 		it again when needed???
		: gallery.deselect_current ?
			(gallery.current = null) 
		: null },
	// selection...
	' ': function(evt){
		gallery.current
			&& evt.preventDefault()
		gallery.toggleSelect() },
	// XXX use key codes...
	'a': function(evt){
		evt.preventDefault()
		if(evt.ctrlKey){
			gallery.selectAll() } },
	'd': function(evt){
		evt.preventDefault()
		if(evt.ctrlKey){
			gallery.deselectAll() } },
	'i': function(evt){
		evt.preventDefault()
		if(evt.ctrlKey){
			gallery.selectInverse() } },
}



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

var Gallery = {

	// options...
	//
	deselect_current: true,
	loop_images: true,
	allow_row_expansion: true,
	click_to_select: true,


	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 },

	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',
			offset: 10,
		}) },

	// 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\//, '/') }) },
				//*/

	// XXX add .loop_images support...
	getRow: function(img, direction='current'){
		if(['above', 'current', 'below'].includes(img)){
			direction = img
			img = this.current }
		// 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.getRow(this.images.at(-1))
		} else if(direction == 'below'){
			// special case: nothing selected...
			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.getRow(this.images[0]) }
		// get current row...
		var cur = img 
			?? this.current
		if(cur == null){
			var scroll = getScrollParent(this.dom).scrollTop
			var images = this.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'){
		if(['left', 'above', 'current', 'below', 'right'].includes(img)){
			direction = img
			img = null }
		// current...
		if(direction == 'current'){
			return img 
				?? this.current 
				?? this.getRow(img)[0]
		// above/below...
		} else if(direction == 'above' || direction == 'below'){
			var row = this.getRow(direction)
			var cur = this.current 
				?? row[0]
			var c = cur.offsetLeft + cur.offsetWidth/2
			var target
			var min
			for(var img of row){
				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)
			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 },

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

	// navigate images visually...
	left: function(){
		var cur = this.current
		var row = this.getRow(cur)
		var i = row.indexOf(cur) - 1
		this.current = row[i < 0 ?
			row.length-1
			: i]
		return this },
	right: function(){
		var cur = this.current
		var row = this.getRow(cur)
		var i = row.indexOf(cur) + 1
		this.current = row[i >= row.length ?
			0
			: i]
		return this },
	up: function(){
		this.current = this.getImage('above')
		return this },
	down: function(){
		this.current = this.getImage('below')
		return this },

	// selection...
	get selected(){
		return this.dom.querySelectorAll('.images img.selected') },
	// 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.selected')){
			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('selected', 'mark')
				mark.addEventListener('click', function(evt){
					evt.stopPropagation()
					that.deselect(mark) })
				img.after(mark) } }
		// clear deselected...
		for(var mark of this.dom.querySelectorAll('.images img:not(.selected)+.mark')){
			mark.remove() }
		// update lightbox...
		this.lightbox.shown
			&& this.lightbox.update()
		return this },
	select: function(img){
		img = img ?? this.current
		img?.classList.add('selected')
		return this.updateMarkers() },
	deselect: function(img){
		img = img ?? this.current
		img?.classList.remove('selected')
		return this.updateMarkers() },
	toggleSelect: function(img){
		img = img ?? this.current
		img?.classList.toggle('selected')
		this.updateMarkers()
		return this },
	selectAll: function(){
		for(var img of this.images){
			img.classList.add('selected') }
		return this.updateMarkers() },
	deselectAll: function(){
		for(var img of this.images){
			img.classList.remove('selected') }
		return this.updateMarkers() },
	selectInverse: function(){
		for(var img of this.images){
			img.classList.toggle('selected') }
		return this.updateMarkers() },

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

	update: function(){
		patchFlexRows(this.images, !this.allow_row_expansion)
		return this },

	load: function(urls){
		this.clear()
		var images = this.dom.querySelector('.images')
		for(var url of urls){
			var img = document.createElement('img')
			img.src = url
			images.appendChild(img) }
		return this },
	clear: function(){
		this.dom.querySelector('.images').innerHTML = ''
		return this },

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

		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.toggleSelect(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.deselect_current){
					that.current = null } })
		this.dom
			.addEventListener('click', function(evt){
				that.deselect_current
					&& (that.current = null) })

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


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

// XXX ignore click from blur...
// XXX might be a good idea to close on click outside the image...
// XXX esc from context menu closes view...
var Lightbox = {
	dom: undefined,
	gallery: undefined,

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

	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_count != 0
			&& this.cache() },

	get shown(){
		return this.gallery.dom.classList.contains('lightboxed') },
	show: function(url){
		this.url = url 
			?? (this.gallery.current
				?? this.gallery.next().current
				?? {}).src
		this.update()
		this.gallery.dom.classList.add('lightboxed')
		return this },
	hide: function(){
		this.gallery.dom.classList.remove('lightboxed')
		return this },
	toggle: function(){
		return this.shown ?
			this.hide()
			: this.show() },

	update: function(){
		// set caption...
		this.dom.setAttribute('caption',
			(this.gallery.current
					?? this.gallery.next().current
					?? {})
				.getAttribute('caption') 
					?? '')
		// set selection...
		this.gallery.current.classList.contains('selected') ?
			this.dom.classList.add('selected')
			: this.dom.classList.remove('selected')
		return this },

	prev: function(){
		this.gallery.prev().show()
		return this },
	next: function(){
		this.gallery.next().show()
		return this },

	__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
		this.dom = dom
		this.gallery = gallery
		// controls...
		this.dom.querySelector('.close')
			.addEventListener('click', function(evt){
				evt.stopPropagation()
				that.hide() })
		// click...
		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.prev()
				evt.clientX > that.dom.offsetWidth / 2 + deadzone/2
					&& that.next() })
		// mousemove...
		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 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...
		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 :
