class ParseTja{
	constructor(...args){
		this.init(...args)
	}
	init(file, difficulty, stars, offset, metaOnly){
		this.data = []
		for(let line of file){
			var indexComment = line.indexOf("//")
			if(indexComment !== -1 && !line.trim().toLowerCase().startsWith("maker:")){
				line = line.slice(0, indexComment).trim()
			}else{
				line = line.trim()
			}
			if(line !== ""){
				this.data.push(line)
			}
		}
		this.difficulty = difficulty
		this.stars = stars
		this.offset = (offset || 0) * -1000
		this.soundOffset = 0
		this.noteTypes = {
			"0": {name: false, txt: false},
			"1": {name: "don", txt: strings.note.don},
			"2": {name: "ka", txt: strings.note.ka},
			"3": {name: "daiDon", txt: strings.note.daiDon},
			"4": {name: "daiKa", txt: strings.note.daiKa},
			"5": {name: "drumroll", txt: strings.note.drumroll},
			"6": {name: "daiDrumroll", txt: strings.note.daiDrumroll},
			"7": {name: "balloon", txt: strings.note.balloon},
			"8": {name: false, txt: false},
			"9": {name: "balloon", txt: strings.note.balloon},
			"A": {name: "daiDon", txt: strings.note.daiDon},
			"B": {name: "daiKa", txt: strings.note.daiKa}
		}
		this.noteTypes_ex = strings.ex_note;
		this.courseTypes = {
			"0": "easy",
			"1": "normal",
			"2": "hard",
			"3": "oni",
			"4": "ura",
			"edit": "ura"
		}
		
		this.metadata = this.parseMetadata()
		this.measures = []
		this.beatInfo = {}
		this.events = []
		if(!metaOnly){
			this.circles = this.parseCircles(difficulty)
		}
	}
	parseMetadata(){
		var metaNumbers = ["bpm", "offset", "demostart", "level", "scoremode", "scorediff"]
		var inSong = false
		var hasSong = false
		var courses = {}
		var currentCourse = {}
		var courseName = "oni"
		for(var lineNum = 0; lineNum < this.data.length; lineNum++){
			var line = this.data[lineNum]
			
			if(line.slice(0, 1) === "#"){
				
				var name = line.slice(1).toLowerCase()
				if((name === "start" || name === "start p1") && !inSong){
					
					inSong = true
					if(!hasSong || name === "start" && courses[courseName] && courses[courseName].startName !== "start"){
						hasSong = false
						if(!(courseName in courses)){
							courses[courseName] = {}
						}
						courses[courseName].startName = name
						for(var opt in currentCourse){
							if(opt !== "branch"){
								courses[courseName][opt] = currentCourse[opt]
							}
						}
						courses[courseName].start = lineNum + 1
						courses[courseName].end = this.data.length
					}
				}else if(name === "end" && inSong){
					inSong = false
					if(!hasSong){
						hasSong = true
						courses[courseName].end = lineNum
					}
				}else if(name.startsWith("branchstart") && inSong){
					courses[courseName].branch = true
				}else if(name.startsWith("lyric") && inSong){
					courses[courseName].inlineLyrics = true
				}
				
			}else if(!inSong){
				
				if(line.indexOf(":") > 0){
					
					var [name, value] = this.split(line, ":")
					name = name.toLowerCase().trim()
					value = value.trim()
					
					if(name === "course"){
						value = value.toLowerCase()
						if(value in this.courseTypes){
							courseName = this.courseTypes[value]
						}else{
							courseName = value
						}
						hasSong = false
					}else if(name === "balloon"){
						value = value ? value.split(",").map(digit => parseInt(digit)) : []
					}else if(this.inArray(name, metaNumbers)){
						value = parseFloat(value)
					}
					else if (name === "scoreinit") {
						value = value ? parseFloat(value.split(",")[0]) : 0; 
					}

					currentCourse[name] = value
				}
				
			}
		}
		return courses
	}
	inArray(string, array){
		return array.indexOf(string) >= 0
	}
	split(string, delimiter){
		var index = string.indexOf(delimiter)
		if(index < 0){
			return [string, ""]
		}
		return [string.slice(0, index), string.slice(index + delimiter.length)]
	}
	parseCircles(difficulty, lyricsOnly){
		var meta = this.metadata[difficulty] || {}
		var ms = (meta.offset || 0) * -1000 + this.offset
		var bpm = Math.abs(meta.bpm) || 120
		var scroll = 1
		var measure = 4
		if(!lyricsOnly){
			this.beatInfo.beatInterval = 60000 / bpm
		}
		var gogo = false
		var barLine = true
		
		var balloonID = 0
		var balloons = meta.balloon || []
		
		var lastDrumroll = false
		
		var branches
		var branch = false
		var branchObj = {}
		var currentBranch = false
		var branchSettings = {}
		var branchFirstMeasure = false
		var sectionBegin = true
		var lastBpm = bpm
		var lastGogo = gogo
		var lyrics
		var lyricsIndex = null
		var lyricsLine = null
		var lyricsCopy = false
		
		var measures = []
		var currentMeasure = []
		var firstNote = true
		var circles = []
		var circleID = 0
		var events = []
		var regexAZ = /[A-Z]/
		var regexSpace = /\s/
		var regexLinebreak = /\\n/g
		var isAllDon = (note_chain, start_pos) => { 
			for (var i = start_pos; i < note_chain.length; ++i) { 
				var note = note_chain[i];
				if (note && note.type !== "don" && note.type !== "daiDon") { 
					return false;
				}
			}
			return true;
		}
		var checkChain = (note_chain, measure_length, is_last) => {
			//console.log(note_chain, measure_length, is_last);
			/*if (measure_length >= 24) {
				for (var note of note_chain) {
					note.text = this.noteTypes_ex[note.type][0];
				}
			} else { */
				var alldon_pos = null;
				for (var i = 0; i < note_chain.length - (is_last ? 1 : 0); ++i) {
					var note = note_chain[i];
					if (alldon_pos === null && is_last && isAllDon(note_chain, i)) { 
						alldon_pos = i;
					}
					note.text = this.noteTypes_ex[note.type][alldon_pos != null ? (i - alldon_pos) % 2 : 0];
				}
			//}
		}
		var pushMeasure = () => {
			var note = currentMeasure[0]
			if(note){
				var speed = note.bpm * note.scroll / 60
			}else{
				var speed = bpm * scroll / 60
			}
			if(!lyricsOnly){
				measures.push({
					ms: ms,
					originalMS: ms,
					speed: speed,
					visible: barLine,
					branch: currentBranch,
					branchFirst: branchFirstMeasure
				})
			}
			branchFirstMeasure = false
			if(currentMeasure.length){
				for(var i = 0; i < currentMeasure.length; i++){
					var note = currentMeasure[i]
					if(firstNote && note.type && note.type !== "event"){
						firstNote = false
						if(ms < 0){
							this.soundOffset = ms
							ms = 0
						}
					}
					note.start = ms
					if(note.endDrumroll){
						note.endDrumroll.endTime = ms
						note.endDrumroll.originalEndTime = ms
					}
					var msPerMeasure = 60000 * measure / note.bpm
					ms += msPerMeasure / currentMeasure.length
				}
				var note_chain = [];
				for (var i = 0; i < currentMeasure.length; i++){
					var note = currentMeasure[i]
					if(!lyricsOnly){
						circleID++
						var circleObj = new Circle({
							id: circleID,
							start: note.start,
							type: note.type,
							txt: note.txt,
							speed: note.bpm * note.scroll / 60,
							gogoTime: note.gogo,
							endTime: note.endTime,
							requiredHits: note.requiredHits,
							beatMS: 60000 / note.bpm,
							branch: currentBranch,
							section: note.section
						})
						if(note.type){
							if(note.type === "don" || note.type === "ka" || note.type === "daiDon" || note.type === "daiKa"){
								note_chain.push(circleObj)
							}else{
								if(note_chain.length > 1 && currentMeasure.length >= 8){
									checkChain(note_chain, currentMeasure.length, false)
								}
								note_chain = []
							}
							if (lastDrumroll === note) {
								lastDrumroll = circleObj
							}
							
							if(note.type !== "event"){
								circles.push(circleObj)
							}
						}else if(
							(currentMeasure.length < 24 ||
								currentMeasure[i + 1]
								&& !currentMeasure[i + 1].type
							) && (currentMeasure.length < 48 ||
								currentMeasure[i + 2]
								&& !currentMeasure[i + 2].type
								&& currentMeasure[i + 3]
								&& !currentMeasure[i + 3].type
							)
						){
							if(note_chain.length > 1 && currentMeasure.length >= 8){
								checkChain(note_chain, currentMeasure.length, true)
							}
							note_chain = []
						}
						if(note.event){
							events.push(circleObj)
						}
					}
					var lyricsObj = null
					if("lyricsLine" in note){
						lyricsObj = {
							start: note.start,
							text: note.lyricsLine
						}
					}else if(note.lyricsCopy){
						lyricsObj = {
							start: note.start,
							copy: true
						}
					}
					if(lyricsObj){
						if(currentBranch){
							lyricsObj.branch = currentBranch.name
						}
						insertLyrics(lyricsObj)
					}
				}
				if(!lyricsOnly && note_chain.length > 1 && currentMeasure.length >= 8){
					checkChain(note_chain, currentMeasure.length, false)
				}
			}else{
				var msPerMeasure = 60000 * measure / bpm
				ms += msPerMeasure
			}
		}
		var insertNote = circleObj => {
			if(circleObj){
				if(bpm !== lastBpm || gogo !== lastGogo){
					circleObj.event = true
					lastBpm = bpm
					lastGogo = gogo
				}
				if(lyricsLine !== null){
					circleObj.lyricsLine = lyricsLine
					lyricsLine = null
				}else if(lyricsCopy){
					circleObj.lyricsCopy = true
				}
				lyricsCopy = false
				currentMeasure.push(circleObj)
			}
		}
		var insertBlankNote = circleObj => {
			if(bpm !== lastBpm || gogo !== lastGogo){
				insertNote({
					type: "event",
					bpm: bpm,
					scroll: scroll,
					gogo: gogo
				})
			}else if(!circleObj){
				var circleObj2 = {
					bpm: bpm,
					scroll: scroll
				}
				if(lyricsLine !== null){
					circleObj2.lyricsLine = lyricsLine
					lyricsLine = null
				}else if(lyricsCopy){
					circleObj2.lyricsCopy = true
				}
				lyricsCopy = false
				currentMeasure.push(circleObj2)
			}
			if(circleObj){
				if(lyricsLine !== null){
					circleObj.lyricsLine = lyricsLine
					lyricsLine = null
				}else if(lyricsCopy){
					circleObj.lyricsCopy = true
				}
				lyricsCopy = false
				currentMeasure.push(circleObj)
			}
		}
		var insertLyrics = obj => {
			if(!lyrics){
				lyrics = []
			}else if(lyricsIndex !== null){
				lyrics[lyricsIndex].end = obj.start
			}
			lyricsIndex = lyrics.length
			lyrics.push(obj)
		}
		
		for(var lineNum = meta.start; lineNum < meta.end; lineNum++){
			var line = this.data[lineNum]
			if(line.slice(0, 1) === "#"){
				
				var line = line.slice(1)
				var [name, value] = this.split(line, " ")
				name = name.toLowerCase()
				
				switch(name){
					case "gogostart":
						gogo = true
						break
					case "gogoend":
						gogo = false
						break
					case "bpmchange":
						bpm = parseFloat(value) || bpm
						break
					case "scroll":
						scroll = Math.abs(parseFloat(value)) || scroll
						break
					case "measure":
						var [numerator, denominator] = value.split("/")
						measure = numerator / denominator * 4 || measure
						break
					case "delay":
						ms += (parseFloat(value) || 0) * 1000
						break
					case "barlineon":
						barLine = true
						break
					case "barlineoff":
						barLine = false
						break
					case "branchstart":
						branch = true
						currentBranch = false
						branchFirstMeasure = true
						branchSettings = {
							ms: ms,
							gogo: gogo,
							bpm: bpm,
							scroll: scroll,
							sectionBegin: sectionBegin,
							lyricsCopy: !!lyrics
						}
						if(lyrics && lyricsIndex !== null){
							var line = lyrics[lyricsIndex]
							line.end = ms
						}
						lyricsIndex = null
						
						value = value.split(",")
						if(!branches){
							branches = []
						}
						var req = {
							advanced: parseFloat(value[1]) || 0,
							master: parseFloat(value[2]) || 0
						}
						if(req.advanced > 0){
							var active = req.master > 0 ? "normal" : "master"
						}else{
							var active = req.master > 0 ? "advanced" : "master"
						}
						branchObj = {
							ms: ms,
							originalMS: ms,
							active: active,
							type: value[0].trim().toLowerCase() === "r" ? "drumroll" : "accuracy",
							requirement: req
						}
						branches.push(branchObj)
						if(measures.length === 1 && branchObj.type === "drumroll"){
							for(var i = circles.length; i--;){
								var circle = circles[i]
								if(circle.endTime && (circle.type === "drumroll" || circle.type === "daiDrumroll" || circle.type === "balloon")){
									measures.push({
										ms: circle.endTime,
										originalMS: circle.endTime,
										speed: circle.bpm * circle.scroll / 60,
										visible: false,
										branch: circle.branch
									})
									break
								}
							}
						}
						if(measures.length !== 0){
							measures[measures.length - 1].nextBranch = branchObj
						}
						break
					case "branchend":
						branch = false
						currentBranch = false
						lyricsCopy = lyricsCopy || !!lyrics
						break
					case "section":
						sectionBegin = true
						if(branch && !currentBranch){
							branchSettings.sectionBegin = true
						}
						break
					case "n": case "e": case "m":
						if(!branch){
							break
						}
						if(lyrics){
							if(lyricsIndex !== null){
								var line = lyrics[lyricsIndex]
								line.end = ms
							}
							lyricsIndex = null
						}
						ms = branchSettings.ms
						gogo = branchSettings.gogo
						bpm = branchSettings.bpm
						scroll = branchSettings.scroll
						sectionBegin = branchSettings.sectionBegin
						lyricsCopy = branchSettings.lyricsCopy
						branchFirstMeasure = true
						var branchName = name === "m" ? "master" : (name === "e" ? "advanced" : "normal")
						currentBranch = {
							name: branchName,
							active: branchName === branchObj.active
						}
						branchObj[branchName] = currentBranch
						break
					case "lyric":
						lyricsLine = value.replace(regexLinebreak, "\n").trim()
						break
				}
				
			}else{
				
				var string = line.toUpperCase().split("")

				const abekobe = localStorage.getItem("abekobe") ?? "false";
				const detarame = parseFloat(localStorage.getItem("detarame") ?? "0", 10);

				for(let symbol of string){

					if (abekobe === "true") {
						if (symbol === "1") {
							symbol = "2";
						} else if (symbol === "2") {
							symbol = "1";
						} else if (symbol === "3") {
							symbol = "4";
						} else if (symbol === "4") {
							symbol = "3";
						} else if (symbol === "A") {
							symbol = "B";
						} else if (symbol === "B") {
							symbol - "A";
						}
					}

					if (detarame > 0) {
						const randomValue = Math.random() * 100;
						if (randomValue < detarame) {

							const first = ["1", "2"];
							const second = ["3", "4", "A", "B"];
							if (first.includes(symbol)) {
								const firstIndex = Math.floor(Math.random() * first.length);
								symbol = first[firstIndex];
							} else if (second.includes(symbol)) {
								const secondIndex = Math.floor(Math.random() * second.length);
								symbol = second[secondIndex];
							}
						}
					}

					var error = false
					switch(symbol){
						
						case "0":
							insertBlankNote()
							break
						case "1": case "2": case "3": case "4": case "A": case "B":
							var type = this.noteTypes[symbol]
							var circleObj = {
								type: type.name,
								txt: type.txt,
								gogo: gogo,
								bpm: bpm,
								scroll: scroll,
								section: sectionBegin
							}
							sectionBegin = false
							if(lastDrumroll){
								circleObj.endDrumroll = lastDrumroll
								lastDrumroll = false
							}
							insertNote(circleObj)
							break
						case "5": case "6": case "7": case "9":
							var type = this.noteTypes[symbol]
							var circleObj = {
								type: type.name,
								txt: type.txt,
								gogo: gogo,
								bpm: bpm,
								scroll: scroll,
								section: sectionBegin
							}
							sectionBegin = false
							if(lastDrumroll){
								if(symbol === "9"){
									insertNote({
										endDrumroll: lastDrumroll,
										gogo: gogo,
										bpm: bpm,
										scroll: scroll,
										section: sectionBegin
									})
									sectionBegin = false
									lastDrumroll = false
								}else{
									insertBlankNote()
								}
								break
							}
							if(symbol === "7" || symbol === "9"){
								var hits = balloons[balloonID]
								if(!hits || hits < 1){
									hits = 1
								}
								circleObj.requiredHits = hits
								balloonID++
							}
							lastDrumroll = circleObj
							insertNote(circleObj)
							break
						case "8":
							if(lastDrumroll){
								insertNote({
									endDrumroll: lastDrumroll,
									gogo: gogo,
									bpm: bpm,
									scroll: scroll,
									section: sectionBegin
								})
								sectionBegin = false
								lastDrumroll = false
							}else{
								insertBlankNote()
							}
							break
						case ",":
							if(currentMeasure.length === 0 && (bpm !== lastBpm || gogo !== lastGogo || lyricsLine !== null)){
								insertBlankNote()
							}
							pushMeasure()
							currentMeasure = []
							break
						default:
							if(regexAZ.test(symbol)){
								insertBlankNote()
							}else if(!regexSpace.test(symbol)){
								error = true
							}
							break
						
					}
					if(error){
						break
					}
				}
				
			}
		}
		if(lastDrumroll){
			lastDrumroll.endTime = ms
			lastDrumroll.originalEndTime = ms
		}
		if(lyricsLine !== null){
			insertLyrics({
				start: ms,
				text: lyricsLine
			})
		}
		pushMeasure()
		
		if(!lyricsOnly){
			if(branches){
				circles.sort((a, b) => a.ms > b.ms ? 1 : -1)
				measures.sort((a, b) => a.ms > b.ms ? 1 : -1)
				circles.forEach((circle, i) => circle.id = i + 1)
			}
			this.measures = measures
			this.events = events
			this.branches = branches
			this.scoreinit = meta.scoreinit
			this.scorediff = meta.scorediff
			if(this.scoreinit && this.scorediff){
				this.scoremode = meta.scoremode || 1
			}else{
				this.scoremode = meta.scoremode || 2
				var autoscore = new AutoScore(difficulty, this.stars, this.scoremode, circles)
				this.scoreinit = autoscore.ScoreInit
				this.scorediff = autoscore.ScoreDiff
			}
		}
		if(lyrics && lyricsIndex !== null){
			var line = lyrics[lyricsIndex]
			line.end = Math.max(ms, line.start) + 5000
		}
		if(lyrics){
			this.lyrics = lyrics
		}else if(!lyricsOnly){
			for(var courseName in this.metadata){
				if(this.metadata[courseName].inlineLyrics){
					this.parseCircles(courseName, true)
					break
				}
			}
		}
		return circles
	}
}
