class Loader{
	constructor(...args){
		this.init(...args)
	}
	init(callback){
		this.callback = callback
		this.loadedAssets = 0
		this.assetsDiv = document.getElementById("assets")
		this.screen = document.getElementById("screen")
		this.startTime = Date.now()
		this.errorMessages = []
		this.songSearchGradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), "
		
		var promises = []
		
		promises.push(this.ajax("src/views/loader.html").then(page => {
			this.screen.innerHTML = page
		}))
		
		promises.push(this.ajax("api/config").then(conf => {
			gameConfig = JSON.parse(conf)
		}))
		
		Promise.all(promises).then(this.run.bind(this))
	}
	run(){
		this.promises = []
		this.loaderDiv = document.querySelector("#loader")
		this.loaderPercentage = document.querySelector("#loader .percentage")
		this.loaderProgress = document.querySelector("#loader .progress")
		
		this.queryString = gameConfig._version.commit_short ? "?" + gameConfig._version.commit_short : ""
		
		if(gameConfig.custom_js){
			this.addPromise(this.loadScript(gameConfig.custom_js), gameConfig.custom_js)
		}
		var oggSupport = new Audio().canPlayType("audio/ogg;codecs=vorbis")
		if(!oggSupport){
			assets.js.push("lib/oggmented-wasm.js")
		}
		assets.js.forEach(name => {
			this.addPromise(this.loadScript("src/js/" + name), "src/js/" + name)
		})
		
		var pageVersion = versionLink.href
		var index = pageVersion.lastIndexOf("/")
		if(index !== -1){
			pageVersion = pageVersion.slice(index + 1)
		}
		this.addPromise(new Promise((resolve, reject) => {
			if(
				versionLink.href !== gameConfig._version.url &&
				gameConfig._version.commit &&
				versionLink.href.indexOf(gameConfig._version.commit) === -1
			){
				reject("Version on the page and config does not match\n(page:  " + pageVersion + ",\nconfig: "+ gameConfig._version.commit + ")")
			}
			var cssCount = document.styleSheets.length + assets.css.length
			assets.css.forEach(name => {
				var stylesheet = document.createElement("link")
				stylesheet.rel = "stylesheet"
				stylesheet.href = "src/css/" + name + this.queryString
				document.head.appendChild(stylesheet)
			})
			var checkStyles = () => {
				if(document.styleSheets.length >= cssCount){
					resolve()
					clearInterval(interval)
				}
			}
			var interval = setInterval(checkStyles, 100)
			checkStyles()
		}))
		
		for(var name in assets.fonts){
			var url = gameConfig.assets_baseurl + "fonts/" + assets.fonts[name]
			this.addPromise(new FontFace(name, "url('" + url + "')").load().then(font => {
				document.fonts.add(font)
			}), url)
		}
		
		assets.img.forEach(name => {
			var id = this.getFilename(name)
			var image = document.createElement("img")
			image.crossOrigin = "anonymous"
			var url = gameConfig.assets_baseurl + "img/" + name
			this.addPromise(pageEvents.load(image), url)
			image.id = name
			image.src = url
			this.assetsDiv.appendChild(image)
			assets.image[id] = image
		})
		
		var css = []
		for(let selector in assets.cssBackground){
			let name = assets.cssBackground[selector]
			var url = gameConfig.assets_baseurl + "img/" + name
			this.addPromise(loader.ajax(url, request => {
				request.responseType = "blob"
			}).then(blob => {
				var id = this.getFilename(name)
				var image = document.createElement("img")
				let blobUrl = URL.createObjectURL(blob)
				var promise = pageEvents.load(image).then(() => {
					var gradient = ""
					if(selector === ".pattern-bg"){
						loader.screen.style.backgroundImage = "url(\"" + blobUrl + "\")"
					}else if(selector === "#song-search"){
						gradient = this.songSearchGradient
					}
					css.push(this.cssRuleset({
						[selector]: {
							"background-image": gradient + "url(\"" + blobUrl + "\")"
						}
					}))
				})
				image.id = name
				image.src = blobUrl
				this.assetsDiv.appendChild(image)
				assets.image[id] = image
				return promise
			}), url)
		}
		
		assets.views.forEach(name => {
			var id = this.getFilename(name)
			var url = "src/views/" + name + this.queryString
			this.addPromise(this.ajax(url).then(page => {
				assets.pages[id] = page
			}), url)
		})
		
		this.addPromise(this.ajax("api/categories").then(cats => {
			assets.categories = JSON.parse(cats)
			assets.categories.forEach(cat => {
				if(cat.song_skin){
					cat.songSkin = cat.song_skin //rename the song_skin property and add category title to categories array
					delete cat.song_skin
					cat.songSkin.infoFill = cat.songSkin.info_fill
					delete cat.songSkin.info_fill
				}
			})
			
			assets.categories.push({
				title: "default",
				songSkin: {
					background: "#ececec",
					border: ["#fbfbfb", "#8b8b8b"],
					outline: "#656565",
					infoFill: "#656565"
				}
			})
		}), "api/categories")
		
		var url = gameConfig.assets_baseurl + "img/vectors.json" + this.queryString
		this.addPromise(this.ajax(url).then(response => {
			vectors = JSON.parse(response)
		}), url)
		
		this.afterJSCount =
			[
				"api/songs",
				"blurPerformance",
				"categories"
			].length +
			assets.audioSfx.length +
			assets.audioMusic.length +
			assets.audioSfxLR.length +
			assets.audioSfxLoud.length +
			(gameConfig.accounts ? 1 : 0)
		
		Promise.all(this.promises).then(() => {
			if(this.error){
				return
			}
			
			var style = document.createElement("style")
			style.appendChild(document.createTextNode(css.join("\n")))
			document.head.appendChild(style)
			
			this.addPromise(this.ajax("api/songs").then(songs => {
				songs = JSON.parse(songs)
				songs.forEach(song => {
					var directory = gameConfig.songs_baseurl + song.id + "/"
					var songExt = song.music_type ? song.music_type : "mp3"
					song.music = new RemoteFile(directory + "main." + songExt)
					if(song.type === "tja"){
						song.chart = new RemoteFile(directory + "main.tja")
					}else{
						song.chart = {separateDiff: true}
						for(var diff in song.courses){
							if(song.courses[diff]){
								song.chart[diff] = new RemoteFile(directory + diff + ".osu")
							}
						}
					}
					if(song.lyrics){
						song.lyricsFile = new RemoteFile(directory + "main.vtt")
					}
					if(song.preview > 0){
						song.previewMusic = new RemoteFile(directory + "preview." + gameConfig.preview_type)
					}
				})
				assets.songsDefault = songs
				assets.songs = assets.songsDefault
			}), "api/songs")
			
			var categoryPromises = []
			assets.categories //load category backgrounds to DOM
				.filter(cat => cat.songSkin && cat.songSkin.bg_img)
				.forEach(cat => {
					let name = cat.songSkin.bg_img
					var url = gameConfig.assets_baseurl + "img/" + name
					categoryPromises.push(loader.ajax(url, request => {
						request.responseType = "blob"
					}).then(blob => {
						var id = this.getFilename(name)
						var image = document.createElement("img")
						let blobUrl = URL.createObjectURL(blob)
						var promise = pageEvents.load(image)
						image.id = name
						image.src = blobUrl
						this.assetsDiv.appendChild(image)
						assets.image[id] = image
						return promise
					}).catch(response => {
						return this.errorMsg(response, url)
					}))
				})
			this.addPromise(Promise.all(categoryPromises))
			
			snd.buffer = new SoundBuffer()
			if(!oggSupport){
				snd.buffer.oggDecoder = snd.buffer.fallbackDecoder
			}
			snd.musicGain = snd.buffer.createGain()
			snd.sfxGain = snd.buffer.createGain()
			snd.previewGain = snd.buffer.createGain()
			snd.sfxGainL = snd.buffer.createGain("left")
			snd.sfxGainR = snd.buffer.createGain("right")
			snd.sfxLoudGain = snd.buffer.createGain()
			snd.buffer.setCrossfade(
				[snd.musicGain, snd.previewGain],
				[snd.sfxGain, snd.sfxGainL, snd.sfxGainR],
				0.5
			)
			snd.sfxLoudGain.setVolume(1.2)
			snd.buffer.saveSettings()
			
			this.afterJSCount = 0
			
			assets.audioSfx.forEach(name => {
				this.addPromise(this.loadSound(name, snd.sfxGain), this.soundUrl(name))
			})
			assets.audioMusic.forEach(name => {
				this.addPromise(this.loadSound(name, snd.musicGain), this.soundUrl(name))
			})
			assets.audioSfxLR.forEach(name => {
				this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => {
					var id = this.getFilename(name)
					assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL)
					assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR)
				}), this.soundUrl(name))
			})
			assets.audioSfxLoud.forEach(name => {
				this.addPromise(this.loadSound(name, snd.sfxLoudGain), this.soundUrl(name))
			})
			
			this.canvasTest = new CanvasTest()
			this.addPromise(this.canvasTest.blurPerformance().then(result => {
				perf.blur = result
				if(result > 1000 / 50){
					// Less than 50 fps with blur enabled
					disableBlur = true
				}
			}), "blurPerformance")
			
			if(gameConfig.accounts){
				this.addPromise(this.ajax("api/scores/get").then(response => {
					response = JSON.parse(response)
					if(response.status === "ok"){
						account.loggedIn = true
						account.username = response.username
						account.displayName = response.display_name
						account.don = response.don
						scoreStorage.load(response.scores)
						pageEvents.send("login", account.username)
					}
				}), "api/scores/get")
			}
			
			settings = new Settings()
			pageEvents.setKbd()
			scoreStorage = new ScoreStorage()
			db = new IDB("taiko", "store")
			plugins = new Plugins()
			
			if(localStorage.getItem("lastSearchQuery")){
				localStorage.removeItem("lastSearchQuery")
			}
			
			Promise.all(this.promises).then(() => {
				if(this.error){
					return
				}
				if(!account.loggedIn){
					scoreStorage.load()
				}
				for(var i in assets.songsDefault){
					var song = assets.songsDefault[i]
					if(!song.hash){
						song.hash = song.title
					}
					scoreStorage.songTitles[song.title] = song.hash
					var score = scoreStorage.get(song.hash, false, true)
					if(score){
						score.title = song.title
					}
				}
				var promises = []
				
				var readyEvent = "normal"
				var songId
				var hashLower = location.hash.toLowerCase()
				p2 = new P2Connection()
				if(hashLower.startsWith("#song=")){
					var number = parseInt(location.hash.slice(6))
					if(number > 0){
						songId = number
						readyEvent = "song-id"
					}
				}else if(location.hash.length === 6){
					p2.hashLock = true
					promises.push(new Promise(resolve => {
						p2.open()
						pageEvents.add(p2, "message", response => {
							if(response.type === "session"){
								pageEvents.send("session-start", "invited")
								readyEvent = "session-start"
								resolve()
							}else if(response.type === "gameend"){
								p2.hash("")
								p2.hashLock = false
								readyEvent = "session-expired"
								resolve()
							}
						})
						p2.send("invite", {
							id: location.hash.slice(1).toLowerCase(),
							name: account.loggedIn ? account.displayName : null,
							don: account.loggedIn ? account.don : null
						})
						setTimeout(() => {
							if(p2.socket.readyState !== 1){
								p2.hash("")
								p2.hashLock = false
								resolve()
							}
						}, 10000)
					}).then(() => {
						pageEvents.remove(p2, "message")
					}))
				}else{
					p2.hash("")
				}
				
				promises.push(this.canvasTest.drawAllImages().then(result => {
					perf.allImg = result
				}))
				
				if(gameConfig.plugins){
					gameConfig.plugins.forEach(obj => {
						if(obj.url){
							var plugin = plugins.add(obj.url, {
								hide: obj.hide
							})
							if(plugin){
								plugin.loadErrors = true
								promises.push(plugin.load(true).then(() => {
									if(obj.start){
										return plugin.start(false, true)
									}
								}).catch(response => {
									return this.errorMsg(response, obj.url)
								}))
							}
						}
					})
				}
				
				Promise.all(promises).then(() => {
					perf.load = Date.now() - this.startTime
					this.canvasTest.clean()
					this.clean()
					this.callback(songId)
					this.ready = true
					pageEvents.send("ready", readyEvent)
				}, e => this.errorMsg(e))
			}, e => this.errorMsg(e))
		})
	}
	addPromise(promise, url){
		this.promises.push(promise)
		promise.then(this.assetLoaded.bind(this), response => {
			return this.errorMsg(response, url)
		})
	}
	soundUrl(name){
		return gameConfig.assets_baseurl + "audio/" + name
	}
	loadSound(name, gain){
		var id = this.getFilename(name)
		return gain.load(new RemoteFile(this.soundUrl(name))).then(sound => {
			assets.sounds[id] = sound
		})
	}
	getFilename(name){
		return name.slice(0, name.lastIndexOf("."))
	}
	errorMsg(error, url){
		var rethrow
		if(url || error){
			if(typeof error === "object" && error.constructor === Error){
				rethrow = error
				error = error.stack || ""
				var index = error.indexOf("\n    ")
				if(index !== -1){
					error = error.slice(0, index)
				}
			}else if(Array.isArray(error)){
				error = error[0]
			}
			if(url){
				error = (error ? error + ": " : "") + url
			}
			this.errorMessages.push(error)
			pageEvents.send("loader-error", url || error)
		}
		if(!this.error){
			this.error = true
			cancelTouch = false
			this.loaderDiv.classList.add("loaderError")
			if(typeof allStrings === "object"){
				var lang = localStorage.lang
				if(!lang){
					var userLang = navigator.languages.slice()
					userLang.unshift(navigator.language)
					for(var i in userLang){
						for(var j in allStrings){
							if(allStrings[j].regex.test(userLang[i])){
								lang = j
							}
						}
					}
				}
				if(!lang){
					lang = "en"
				}
				loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang] && allStrings[lang].errorOccured || allStrings.en.errorOccured
			}
			var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0]
			loaderError.style.display = "flex"
			var diagTxt = loader.screen.getElementsByClassName("diag-txt")[0]
			var debugLink = loader.screen.getElementsByClassName("debug-link")[0]
			if(navigator.userAgent.indexOf("Android") >= 0){
				var iframe = document.createElement("iframe")
				diagTxt.appendChild(iframe)
				var body = iframe.contentWindow.document.body
				body.setAttribute("style", `
					font-family: monospace;
					margin: 2px 0 0 2px;
					white-space: pre-wrap;
					word-break: break-all;
					cursor: text;
				`)
				body.setAttribute("onblur", `
					getSelection().removeAllRanges()
				`)
				this.errorTxt = {
					element: body,
					method: "innerText"
				}
			}else{
				var textarea = document.createElement("textarea")
				textarea.readOnly = true
				diagTxt.appendChild(textarea)
				if(!this.touchEnabled){
					textarea.addEventListener("focus", () => {
						textarea.select()
					})
					textarea.addEventListener("blur", () => {
						getSelection().removeAllRanges()
					})
				}
				this.errorTxt = {
					element: textarea,
					method: "value"
				}
			}
			var show = () => {
				diagTxt.style.display = "block"
				debugLink.style.display = "none"
			}
			debugLink.addEventListener("click", show)
			debugLink.addEventListener("touchstart", show)
			this.clean(true)
		}
		var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount))
		this.errorTxt.element[this.errorTxt.method] = "```\n" + this.errorMessages.join("\n") + "\nPercentage: " + percentage + "%\n```"
		if(rethrow || error){
			console.error(rethrow || error)
		}
		return Promise.reject()
	}
	assetLoaded(){
		if(!this.error){
			this.loadedAssets++
			var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount))
			this.loaderProgress.style.width = percentage + "%"
			this.loaderPercentage.firstChild.data = percentage + "%"
		}
	}
	changePage(name, patternBg){
		this.screen.innerHTML = assets.pages[name]
		this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg")
	}
	cssRuleset(rulesets){
		var css = []
		for(var selector in rulesets){
			var declarationsObj = rulesets[selector]
			var declarations = []
			for(var property in declarationsObj){
				var value = declarationsObj[property]
				declarations.push("\t" + property + ": " + value + ";")
			}
			css.push(selector + "{\n" + declarations.join("\n") + "\n}")
		}
		return css.join("\n")
	}
	ajax(url, customRequest, customResponse, cancellationToken){
		var request = new XMLHttpRequest()
		request.open("GET", url)
		if(cancellationToken){
			cancellationToken.cancel = () => request.abort()
		}
		var promise = pageEvents.load(request)
		if(!customResponse){
			promise = promise.then(() => {
				if(request.status === 200){
					return request.response
				}else{
					return Promise.reject(`${url} (${request.status})`)
				}
			})
		}
		if(customRequest){
			customRequest(request)
		}
		request.send()
		return promise
	}
	loadScript(url){
		var script = document.createElement("script")
		var url = url + this.queryString
		var promise = pageEvents.load(script)
		script.src = url
		document.head.appendChild(script)
		return promise
	}
	getCsrfToken(){
		return this.ajax("api/csrftoken").then(response => {
			var json = JSON.parse(response)
			if(json.status === "ok"){
				return Promise.resolve(json.token)
			}else{
				return Promise.reject()
			}
		})
	}
	clean(error){
		delete this.loaderDiv
		delete this.loaderPercentage
		delete this.loaderProgress
		if(!error){
			delete this.promises
			delete this.errorText
		}
		pageEvents.remove(root, "touchstart")
	}
}
