/*!
 * The 2023 r/place Atlas
 * Copyright (c) 2017 Roland Rytz <roland@draemm.li>
 * Copyright (c) 2023 Place Atlas Initiative and contributors
 * Licensed under AGPL-3.0 (https://2023.place-atlas.stefanocoding.me/license.txt)
 */

const linesCanvas = document.getElementById("linesCanvas")
const linesContext = linesCanvas.getContext("2d")
let hovered = []

let previousScaleZoomOrigin
let previousZoom

const backgroundCanvas = document.createElement("canvas")
backgroundCanvas.width = canvasSize.x
backgroundCanvas.height = canvasSize.y
const backgroundContext = backgroundCanvas.getContext("2d")

const wrapper = document.getElementById("wrapper")
const bottomBar = document.getElementById("bottomBar")

const showListButton = document.getElementById("showListButton")
const offcanvasList = document.getElementById("offcanvasList")
const bsOffcanvasList = new bootstrap.Offcanvas(offcanvasList)

const offcanvasDraw = document.getElementById("offcanvasDraw")
const bsOffcanvasDraw = new bootstrap.Offcanvas(offcanvasDraw)

const objectsContainer = document.getElementById("objectsList")
const closeObjectsListButton = document.getElementById("closeObjectsListButton")
const objectsListOverflowNotice = document.getElementById("objectsListOverflowNotice")

const searchInput = document.getElementById("searchList")
const sortInput = document.getElementById("sort")

const entriesList = document.getElementById("entriesList")
let entriesListShown = false

const drawButton = document.getElementById("drawLink")
const objectEditNav = document.createElement("a")
objectEditNav.className = "btn btn-outline-primary"
objectEditNav.id = "objectEditNav"
objectEditNav.textContent = "Edit"

let atlas = null
window.atlas = atlas

let atlasOrder = []
window.atlasOrder = atlasOrder

const entriesLimit = 50
let entriesOffset = 0

const moreEntriesButton = document.createElement("button")
moreEntriesButton.innerHTML = "Show " + entriesLimit + " more"
moreEntriesButton.type = "button"
moreEntriesButton.className = "btn btn-primary d-block mb-2 mx-auto"
moreEntriesButton.id = "moreEntriesButton"
let showMoreEntries = () => {}

const moreEntriesObserver = new IntersectionObserver(entries => {
	for (const entry of entries) {
		if (!entry.isIntersecting) continue
		showMoreEntries()
		break
	}
})

moreEntriesObserver.observe(moreEntriesButton)

let defaultSort = sortInput.value

let lastPos = [0, 0]

let fixed = false; // Fix hovered items in place, so that clicking on links is possible

searchInput.addEventListener("input", function () {
	updateAtlas()
})

sortInput.addEventListener("input", function () {
	updateAtlas()
})

offcanvasDraw.addEventListener('show.bs.offcanvas', () => {
	wrapper.classList.remove('listHidden')
	wrapper.classList.add('listTransitioning')
	applyView()
})

offcanvasDraw.addEventListener('shown.bs.offcanvas', () => {
	wrapper.classList.remove('listTransitioning')
	applyView()
})

offcanvasDraw.addEventListener('hide.bs.offcanvas', () => {
	wrapper.classList.add('listHidden')
	wrapper.classList.add('listTransitioning')
	applyView()
})

offcanvasDraw.addEventListener('hidden.bs.offcanvas', () => {
	wrapper.classList.remove('listTransitioning')
	applyView()
})

offcanvasList.addEventListener('show.bs.offcanvas', () => {
	wrapper.classList.remove('listHidden')
	wrapper.classList.add('listTransitioning')
	applyView()
})

offcanvasList.addEventListener('shown.bs.offcanvas', e => {
	entriesListShown = true
	wrapper.classList.remove('listTransitioning')
	updateHovering(e)
	applyView()
	renderHighlight()
	renderLines()
})

offcanvasList.addEventListener('hide.bs.offcanvas', () => {
	wrapper.classList.add('listHidden')
	wrapper.classList.add('listTransitioning')
	applyView()
})

offcanvasList.addEventListener('hidden.bs.offcanvas', e => {
	entriesListShown = false
	wrapper.classList.remove('listTransitioning')
	updateHovering(e)
	applyView()
	renderHighlight()
	renderLines()
})

closeObjectsListButton.addEventListener("click", clearObjectsList)

function clearObjectsList() {
	hovered = []
	fixed = false
	renderLines()
	renderHighlight()
	document.title = pageTitle
	closeObjectsListButton.classList.add("d-none")
	objectsListOverflowNotice.classList.add("d-none")
	entriesList.classList.remove("disableHover")
	objectsContainer.replaceChildren()
	objectEditNav.remove()
	updateHash(false)
}

function toggleFixed(e, tapped) {
	if (!fixed && hovered.length === 0) {
		entriesList.classList.remove("disableHover")
		return
	}
	fixed = !fixed
	if (!fixed) {
		updateHovering(e, tapped)
		renderHighlight()
	}
	entriesList.classList.add("disableHover")
	objectsListOverflowNotice.classList.add("d-none")
}

window.addEventListener("dblClick", renderLines)
window.addEventListener("wheel", renderLines)

objectsContainer.addEventListener("scroll", () => {
	renderLines()
})

window.addEventListener("resize", () => {
	applyView()
	renderHighlight()
	renderLines()

})

async function renderLines() {
	if (hovered.length === 0) {
		linesContext.clearRect(0, 0, linesCanvas.width, linesCanvas.height)
		return
	}
	// Line border
	linesCanvas.width = linesCanvas.clientWidth
	linesCanvas.height = linesCanvas.clientHeight
	linesContext.lineCap = "round"
	linesContext.lineWidth = Math.max(Math.min(zoom * 1.5, 16 * 1.5), 6)
	linesContext.strokeStyle = "#222"

	for (let i = 0; i < hovered.length; i++) {
		const element = hovered[i].element

		if (element.getBoundingClientRect().left !== 0) {

			linesContext.beginPath()
			// Align line based on which side the card is on
			if ((element.getBoundingClientRect().left + element.clientWidth / 2) < (document.documentElement.clientWidth / 2)) {
				linesContext.moveTo(
					element.getBoundingClientRect().left + document.documentElement.scrollLeft + element.clientWidth - 5,
					element.getBoundingClientRect().top + document.documentElement.scrollTop + 20
				)
			} else {
				linesContext.moveTo(
					element.getBoundingClientRect().left + document.documentElement.scrollLeft + 5,
					element.getBoundingClientRect().top + document.documentElement.scrollTop + 20
				)
			}
			linesContext.lineTo(
				~~((hovered[i].center[0] - canvasOffset.x) * zoom) + innerContainer.offsetLeft,
				~~((hovered[i].center[1] - canvasOffset.y) * zoom) + innerContainer.offsetTop
			)
			linesContext.stroke()
		}
	}

	// Line body
	linesContext.lineWidth = Math.max(Math.min(zoom, 16), 4)
	linesContext.strokeStyle = "#FFFFFF"

	for (let i = 0; i < hovered.length; i++) {
		const element = hovered[i].element

		if (element.getBoundingClientRect().left !== 0) {

			linesContext.beginPath()
			// Align line based on which side the card is on
			if ((element.getBoundingClientRect().left + element.clientWidth / 2) < (document.documentElement.clientWidth / 2)) {
				linesContext.moveTo(
					element.getBoundingClientRect().left + document.documentElement.scrollLeft + element.clientWidth - 5,
					element.getBoundingClientRect().top + document.documentElement.scrollTop + 20
				)
			} else {
				linesContext.moveTo(
					element.getBoundingClientRect().left + document.documentElement.scrollLeft + 5,
					element.getBoundingClientRect().top + document.documentElement.scrollTop + 20
				)
			}
			linesContext.lineTo(
				~~((hovered[i].center[0] - canvasOffset.x) * zoom) + innerContainer.offsetLeft,
				~~((hovered[i].center[1] - canvasOffset.y) * zoom) + innerContainer.offsetTop
			)
			linesContext.stroke()
		}
	}
}

function renderBackground(atlas) {

	backgroundContext.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height)

	//backgroundCanvas.width = 1000 * zoom
	//backgroundCanvas.height = 1000 * zoom

	//backgroundContext.lineWidth = zoom

	backgroundContext.fillStyle = "rgba(0, 0, 0, 0.6)"
	backgroundContext.fillRect(0, 0, backgroundCanvas.width, backgroundCanvas.height)

	for (const entry of Object.values(atlas)) {

		const path = entry.path

		backgroundContext.beginPath()

		if (path[0]) {
			//backgroundContext.moveTo(path[0][0]*zoom, path[0][1]*zoom)
			backgroundContext.moveTo(path[0][0] - canvasOffset.x, path[0][1] - canvasOffset.y)
		}

		for (let p = 1; p < path.length; p++) {
			//backgroundContext.lineTo(path[p][0]*zoom, path[p][1]*zoom)
			backgroundContext.lineTo(path[p][0] - canvasOffset.x, path[p][1] - canvasOffset.y)
		}

		backgroundContext.closePath()

		let bgStrokeStyle
		switch (entry.diff) {
			case "add":
				bgStrokeStyle = "rgba(0, 255, 0, 1)"
				backgroundContext.lineWidth = 2
				break
			case "edit":
				bgStrokeStyle = "rgba(255, 255, 0, 1)"
				backgroundContext.lineWidth = 2
				break
			case "delete":
				bgStrokeStyle = "rgba(255, 0, 0, 1)"
				backgroundContext.lineWidth = 2
				break
			default:
				bgStrokeStyle = "rgba(255, 255, 255, 0.8)"
				break
		}
		backgroundContext.strokeStyle = bgStrokeStyle
		backgroundContext.stroke()
		backgroundContext.lineWidth = 1


	}

}

function filterAtlas(prevAtlas) {

	const sort = sortInput.value || defaultSort
	const search = searchInput?.value.toLowerCase()
	let newAtlas = Object.assign({}, prevAtlas)
	let newAtlasOrder = []

	document.getElementById("atlasSize").innerHTML = ""

	if (search) {
		for (const [id, entry] of Object.entries(prevAtlas)) {
			if (!(
				entry.name.toLowerCase().includes(search.toLowerCase()) ||
				entry.description?.toLowerCase().includes(search.toLowerCase()) ||
				Object.values(entry.links).flat().some(str => str.toLowerCase().includes(search)) ||
				id.toString() === search
			)) delete newAtlas[id]
		}
	}

	// document.getElementById("sort").value = sort

	let sortFunction

	switch (sort) {
		case "shuffle":
			sortFunction = () => Math.random() - 0.5
			break
		case "alphaAsc":
			sortFunction = (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())
			break
		case "alphaDesc":
			sortFunction = (a, b) => b.name.toLowerCase().localeCompare(a.name.toLowerCase())
			break
		case "newest":
			sortFunction = (a, b) => b._index - a._index
			break
		case "oldest":
			sortFunction = (a, b) => a._index - b._index
			break
		case "area":
			sortFunction = (a, b) => calcPolygonArea(b.path) - calcPolygonArea(a.path)
			break
		case "relevant":
			sortFunction = (a, b) => {
				if (a.name.toLowerCase().includes(filter) && b.name.toLowerCase().includes(filter)) {
					return a.name.toLowerCase().indexOf(filter) - b.name.toLowerCase().indexOf(filter) || a.name.toLowerCase().localeCompare(b.name.toLowerCase())
				} else if (a.name.toLowerCase().includes(filter)) {
					return -1
				} else if (b.name.toLowerCase().includes(filter)) {
					return 1
				} else {
					return a.description.toLowerCase().indexOf(filter) - b.description.toLowerCase().indexOf(filter) || a.name.toLowerCase().localeCompare(b.name.toLowerCase())
				}
			}
			break
	}

	newAtlasOrder = Object.keys(newAtlas)
	if (sortFunction) {
		newAtlasOrder = newAtlasOrder.sort((a, b) => sortFunction(prevAtlas[a], prevAtlas[b]))
	}

	// console.log(newAtlas, newAtlasOrder)

	return [newAtlas, newAtlasOrder]

}

function updateAtlas() {
	;[atlas, atlasOrder] = filterAtlas(atlasAll)
	;[atlasDisplay, atlasOrder] = generateAtlasDisplay(atlas, atlasOrder, currentPeriod, currentVariation)
	const atlasSizeEl = document.getElementById("atlasSize")
	if (Object.keys(atlas).length === Object.keys(atlasAll).length) {
		atlasSizeEl.innerHTML = Object.keys(atlasAll).length + " entries in total."
	} else {
		atlasSizeEl.innerHTML = "Found " + Object.keys(atlas).length + " entries."
	}
	atlasSizeEl.innerHTML += " Displaying " + Object.keys(atlasDisplay).length + " entries."
	resetEntriesList()
	renderBackground(atlasDisplay)
	renderHighlight(atlasDisplay)
}

async function resetEntriesList() {
	entriesOffset = 0
	entriesList.replaceChildren()
	entriesList.appendChild(moreEntriesButton)

	moreEntriesButton.removeEventListener('click', showMoreEntries)
	showMoreEntries = () => {

		if (entriesList.contains(moreEntriesButton)) {
			entriesList.removeChild(moreEntriesButton)
		}

		let entriesLeft = entriesLimit
		let element

		while (entriesLeft > 0 && atlasOrder.length > entriesOffset) {

			if (atlasDisplay[atlasOrder[entriesOffset]]) {
				// console.log(i, entriesLeft)

				let entry = atlasDisplay[atlasOrder[entriesOffset]]
				element = createInfoBlock(entry)

				element.addEventListener("mouseenter", function () {
					if (fixed || dragging) return
					objectsContainer.replaceChildren()

					previousScaleZoomOrigin ??= [...scaleZoomOrigin]
					previousZoom ??= zoom
					setView(entry.center[0], entry.center[1], calculateZoomFromPath(entry.path))

					hovered = [entry]
					renderHighlight()
					hovered[0].element = this
					renderLines()

				})

				element.addEventListener("click", e => {
					fixed = true
					previousScaleZoomOrigin ??= [...scaleZoomOrigin]
					previousZoom ??= zoom
					applyView()
				})

				element.addEventListener("mouseleave", () => {
					if (fixed || dragging) return

					scaleZoomOrigin = [...previousScaleZoomOrigin]
					zoom = previousZoom
					previousScaleZoomOrigin = undefined
					previousZoom = undefined
					applyView()

					hovered = []
					renderLines()
					renderHighlight()
				})
			} else {
				let entry = atlas[atlasOrder[entriesOffset]]
				element = createInfoBlock(entry, 1)

				element.addEventListener("click", async e => {
					e.preventDefault()
					const [nearestPeriod, nearestVariation] = getNearestPeriod(entry, currentPeriod, currentVariation)

					await updateTime(nearestPeriod, nearestVariation, true)

					entry = atlasDisplay[entry.id]
					element = createInfoBlock(entry)
					hovered = [{ ...entry, element }]
					fixed = true
					previousScaleZoomOrigin = undefined
					previousZoom = undefined

					const hash = formatHash(entry.id, nearestPeriod, nearestVariation, entry.center[0], entry.center[1], calculateZoomFromPath(entry.path))
					location.hash = hash
				})
			}

			entriesOffset += 1
			entriesLeft -= 1

			entriesList.appendChild(element)

		}

		if (atlasOrder.length > entriesOffset) {
			moreEntriesButton.innerHTML = "Show " + Math.min(entriesLimit, atlasOrder.length - entriesOffset) + " more"
			entriesList.appendChild(moreEntriesButton)
		}

	}
	moreEntriesButton.addEventListener('click', showMoreEntries)
	showMoreEntries()
}

async function renderHighlight() {

	highlightContext.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height)

	//canvas.width = 1000*zoom
	//canvas.height = 1000*zoom

	highlightContext.globalCompositeOperation = "source-over"
	highlightContext.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height)

	if (hovered.length > 0) {
		container.style.cursor = "pointer"
	} else {
		container.style.cursor = "default"
	}

	for (let i = 0; i < hovered.length; i++) {

		const path = hovered[i].path

		highlightContext.beginPath()

		if (path[0]) {
			//context.moveTo(path[0][0]*zoom, path[0][1]*zoom)
			highlightContext.moveTo(path[0][0] - canvasOffset.x, path[0][1] - canvasOffset.y)
		}

		for (let p = 1; p < path.length; p++) {
			//context.lineTo(path[p][0]*zoom, path[p][1]*zoom)
			highlightContext.lineTo(path[p][0] - canvasOffset.x, path[p][1] - canvasOffset.y)
		}

		highlightContext.closePath()

		highlightContext.globalCompositeOperation = "source-over"

		highlightContext.fillStyle = "rgba(0, 0, 0, 1)"
		highlightContext.fill()
	}

	highlightContext.globalCompositeOperation = "source-out"
	highlightContext.drawImage(backgroundCanvas, 0, 0)

	for (let i = 0; i < hovered.length; i++) {

		const path = hovered[i].path

		highlightContext.beginPath()

		if (path[0]) {
			//context.moveTo(path[0][0]*zoom, path[0][1]*zoom)
			highlightContext.moveTo(path[0][0] - canvasOffset.x, path[0][1] - canvasOffset.y)
		}

		for (let p = 1; p < path.length; p++) {
			//context.lineTo(path[p][0]*zoom, path[p][1]*zoom)
			highlightContext.lineTo(path[p][0] - canvasOffset.x, path[p][1] - canvasOffset.y)
		}

		highlightContext.closePath()

		highlightContext.globalCompositeOperation = "source-over"

		let hoverStrokeStyle
		switch (hovered[i].diff) {
			case "add":
				hoverStrokeStyle = "rgba(0, 155, 0, 1)"
				break
			case "edit":
				hoverStrokeStyle = "rgba(155, 155, 0, 1)"
				break
			default:
				hoverStrokeStyle = "rgba(0, 0, 0, 1)"
				break
		}
		highlightContext.strokeStyle = hoverStrokeStyle
		//context.lineWidth = zoom
		highlightContext.stroke()
	}

}

function updateCoordsDisplay(e) {
	const pos = [
		(e.clientX - (container.clientWidth / 2 - innerContainer.clientWidth / 2 + zoomOrigin[0] + container.offsetLeft)) / zoom + canvasOffset.x,
		(e.clientY - (container.clientHeight / 2 - innerContainer.clientHeight / 2 + zoomOrigin[1] + container.offsetTop)) / zoom + canvasOffset.y
	]
	const coordsEl = document.getElementById("coords_p")

	// Displays coordinates as zero instead of NaN
	if (isNaN(pos[0])) {
		coordsEl.textContent = "0, 0"
	} else {
		coordsEl.textContent = Math.floor(pos[0]) + ", " + Math.floor(pos[1])
	}

	return pos
}

function updateHovering(e, tapped) {

	if (dragging || (fixed && !tapped)) return
	const pos = updateCoordsDisplay(e)

	if (!(pos[0] <= canvasSize.x + canvasOffset.x + 200 && pos[0] >= canvasOffset.x - 200 && pos[1] <= canvasSize.y + canvasOffset.y + 200 && pos[1] >= canvasOffset.x - 200)) return

	let newHovered = []
	for (const entry of Object.values(atlasDisplay)) {
		if (pointIsInPolygon(pos, entry.path)) newHovered.push(entry)
	}

	newHovered = newHovered.sort(function (a, b) {
		return calcPolygonArea(a.path) - calcPolygonArea(b.path)
	})

	let changed = false

	if (hovered.length === newHovered.length) {
		for (let i = 0; i < hovered.length; i++) {
			if (hovered[i].id !== newHovered[i].id) {
				changed = true
				break
			}
		}
	} else {
		changed = true
	}

	if (!changed) return

	hovered = newHovered

	objectsContainer.replaceChildren()

	for (const entry of hovered) {
		const element = createInfoBlock(entry)

		objectsContainer.appendChild(element)

		entry.element = element
	}

	if (hovered.length) {
		document.getElementById("timeControlsSlider").blur()
		closeObjectsListButton.classList.remove("d-none")
		if ((objectsContainer.scrollHeight > objectsContainer.clientHeight) && !tapped) {
			objectsListOverflowNotice.classList.remove("d-none")
		} else {
			objectsListOverflowNotice.classList.add("d-none")
		}
	} else {
		closeObjectsListButton.classList.add("d-none")
		objectsListOverflowNotice.classList.add("d-none")
		entriesList.classList.remove("disableHover")
	}
	renderLines()
	renderHighlight()
}

window.addEventListener("hashchange", updateViewFromHash)

async function updateViewFromHash() {

	const hash = window.location.hash.substring(1); //Remove hash prefix
	let [hashEntryId, hashPeriod, hashX, hashY, hashZoom] = hash.split('/')

	// Handle zzz and 0.. prefix
	let newId = hashEntryId.replace(/^zzz([a-z0-9]{8,})$/g, "$1").replace(/^0+/, '')
	if (hashEntryId !== newId) {
		hashEntryId = newId
		const newLocation = new URL(window.location)
		newLocation.hash = '#' + [newId, hashPeriod, hashX, hashY, hashZoom].join('/')
		history.replaceState({}, "", newLocation)
	}

	let targetPeriod, targetVariation

	if (hashPeriod) {
		[targetPeriod, , targetVariation] = parsePeriod(hashPeriod)
	} else {
		targetPeriod = defaultPeriod
		targetVariation = defaultVariation
	}
	await updateTime(targetPeriod, targetVariation)

	setView(
		(isNaN(hashX) || hashX === '') ? undefined : Number(hashX),
		(isNaN(hashY) || hashY === '') ? undefined : Number(hashY),
		(isNaN(hashZoom) || hashZoom === '') ? undefined : Number(hashZoom)
	)

	if (!hashEntryId) return

	// Highlight entry from hash

	const entry = atlasDisplay[hashEntryId]
	if (!entry) return

	document.title = entry.name + " on " + pageTitle

	if ((!entry.diff || entry.diff !== "delete")) {
		objectEditNav.href = "./?mode=draw&id=" + hashEntryId
		objectEditNav.title = "Edit " + entry.name
		if (!objectEditNav.isConnected) {
			showListButton.parentElement.appendChild(objectEditNav)
		}
	} else if (entry.diff === "delete" && document.getElementById("objectEditNav")) {
		objectEditNav.remove()
	}

	const infoElement = createInfoBlock(entry)
	objectsContainer.replaceChildren()
	objectsContainer.appendChild(infoElement)

	setView(
		(isNaN(hashX) || hashX === '') ? entry.center[0] : Number(hashX),
		(isNaN(hashY) || hashY === '') ? entry.center[1] : Number(hashY),
		(isNaN(hashZoom) || hashZoom === '') ? calculateZoomFromPath(entry.path) : Number(hashZoom)
	)

	closeObjectsListButton.classList.remove("d-none")
	entriesList.classList.add("disableHover")

	hovered = [{...entry, element: infoElement}]
	renderBackground(atlasDisplay)
	renderHighlight(atlasDisplay)
	renderLines()
}

function calculateZoomFromPath(path) {

	let zoom
	let boundingBox = [canvasSize.x + canvasOffset.x, canvasOffset.x, canvasSize.y + canvasOffset.y, canvasOffset.y]
	path?.forEach(([x, y]) => {
		boundingBox[0] = Math.min(boundingBox[0], x)
		boundingBox[1] = Math.max(boundingBox[1], x)
		boundingBox[2] = Math.min(boundingBox[2], y)
		boundingBox[3] = Math.max(boundingBox[3], y)
	})
	const boundingBoxSize = [boundingBox[1] - boundingBox[0], boundingBox[3] - boundingBox[2]]
	const clientSize = [
		Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0),
		Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
	]
	zoom = Math.min(clientSize[0] / boundingBoxSize[0], clientSize[1] / boundingBoxSize[1])
	zoom = Math.min(4, zoom/2)

	return zoom

}

function initView() {

	updateAtlas()

	document.addEventListener('timeupdate', () => {
		updateAtlas()
	})

	// parse linked atlas entry id from link hash
	/*if (window.location.hash.substring(3)){
		zoom = 4
		applyView()
		updateViewFromHash()
	}*/

	applyView()
	renderLines()

}

function initExplore() {
	window.updateHovering = updateHovering
	window.renderHighlight = () => { }

	function updateHovering(e, tapped) {
		if (dragging || (fixed && !tapped)) return
		updateCoordsDisplay(e)
	}

	renderBackground({})

	applyView()

}

function initGlobal() {
	container.addEventListener("mousemove", e => {
		if (e.sourceCapabilities) {
			if (!e.sourceCapabilities.firesTouchEvents) {
				updateHovering(e)
			}
		} else {
			updateHovering(e)
		}
	})

	document.addEventListener('timeupdate', () => {
		updateHash()
	})
}

function initViewGlobal() {
	container.addEventListener("mousedown", e => {
		lastPos = [
			e.clientX,
			e.clientY
		]
	})

	container.addEventListener("touchstart", e => {
		if (e.touches.length === 1) {
			lastPos = [
				e.touches[0].clientX,
				e.touches[0].clientY
			]
		}
	}, { passive: true })

	container.addEventListener("mouseup", e => {
		if (Math.abs(lastPos[0] - e.clientX) + Math.abs(lastPos[1] - e.clientY) <= 4) {
			toggleFixed(e)
		}
	})

	container.addEventListener("touchend", e => {
		e.preventDefault()

		if (e.changedTouches.length !== 1) return

		e = e.changedTouches[0]

		if (Math.sqrt(Math.pow(lastPos[0] - e.clientX, 2) + Math.pow(lastPos[1] - e.clientY, 2)) < 10)
			setTimeout(() => updateHovering(e, true), 0)

		dragging = false
		fixed = false
	})

	if (window.location.hash) { // both "/" and just "/#" will be an empty hash string
		updateViewFromHash()
	}

	document.addEventListener('timeupdate', event => {
		drawButton.href = "./?mode=draw" + formatHash(null, event.detail.period, event.detail.variation)
	})

	document.addEventListener("mouseleave", () => {
		if (!fixed) clearObjectsList()
	})
}


async function loadTemplateData(initUrl, datas, blacklistUrls, level = 0) {
	datas ??= {}
	blacklistUrls ??= new Set()

	if (datas[initUrl] || blacklistUrls.has(initUrl)) return [ datas, blacklistUrls ]

	datas[initUrl] = {}

	try {
		const data = await (await fetch(initUrl)).json()
		datas[initUrl] = data
		for (const blacklisted of data?.blacklist) {
			blacklistUrls.add(blacklisted.url)
		}
		await Promise.all(data?.whitelist.map(async wl => {
			const [ wlDatas, wlBlacklistUrls ] = await loadTemplateData(wl.url, datas, blacklistUrls, level + 1)
			Object.assign(datas, wlDatas)
			blacklistUrls.add(...wlBlacklistUrls)
		}))
	} catch (e) {}

	return [ datas, [...blacklistUrls] ]
}

async function loadTemplateImages(datas) {

	const templates = []

	for (const data of Object.values(datas)) {
		if (!data?.templates) continue
		for (const template of data?.templates) {
			templates.push(template)
		}
	}

	await Promise.all(templates.map(async (template, i) => {

		if (!template.sources) return

		for (const source of template.sources) {
			try {
				const sourceResponse = await (await fetch(source)).blob()
				template.blob = URL.createObjectURL(sourceResponse)
				break
			} catch (e) {}
		}
		delete template.sources

		if (!template.blob) return

		const imageLayer = new Image()
		await new Promise(resolve => {
			imageLayer.onload = () => {
				template.imageLayer = imageLayer
				delete template.blob
				resolve()
			}
			imageLayer.onerror = () => {
				delete template
				resolve()
			}
			imageLayer.src = template.blob
		})
	}))

	for (const layer of templates) {
		if (!layer.imageLayer) delete layer
	}

	return templates

}
