var DateTime = luxon.DateTime;
var Interval = luxon.Interval;
luxon.Settings.defaultZone = "utc";

var globalBusStartTime = DateTime.fromISO("1970-01-01T00:00:00");
var globalStreamName = "";
var globalStartTimeString = "";
var globalEndTimeString = "";

var globalPlayer = null;
var globalSetUpControls = false;
var globalSeekTimer = null;

var globalChatData = [];
var globalLoadChatWorker = null;

Hls.DefaultConfig.maxBufferHole = 600;

const VIDEO_FRAMES_PER_SECOND = 30;

const PLAYBACK_RATES = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4, 8];

function commonPageSetup() {
	if (!Hls.isSupported()) {
		addError(
			"Your browser doesn't support MediaSource extensions. Video playback and editing won't work.",
		);
	}

	globalLoadChatWorker = new Worker("scripts/chat-load.js");
}

function addError(errorText) {
	const errorElement = document.createElement("div");
	errorElement.innerText = errorText;

	const dismissElement = document.createElement("a");
	dismissElement.classList.add("error-dismiss");
	dismissElement.innerText = "[X]";
	errorElement.appendChild(dismissElement);
	dismissElement.addEventListener("click", (event) => {
		const errorHost = document.getElementById("errors");
		errorHost.removeChild(errorElement);
	});

	const errorHost = document.getElementById("errors");
	errorHost.appendChild(errorElement);
}

async function loadVideoPlayer(playlistURL) {
	let rangedPlaylistURL = assembleVideoPlaylistURL(playlistURL);
	const videoElement = document.getElementById("video");

	videoElement.addEventListener("loadedmetadata", (_event) => {
		setUpVideoControls();
		sendChatLogLoadData();
	});

	videoElement.addEventListener("loadeddata", (_event) => {
		const qualitySelector = document.getElementById("video-controls-quality");
		globalPlayer.currentLevel = +qualitySelector.value;
	});

	globalPlayer = new Hls();
	globalPlayer.attachMedia(video);
	return new Promise((resolve, _reject) => {
		globalPlayer.on(Hls.Events.MEDIA_ATTACHED, () => {
			const startTime = getStartTime();
			const endTime = getEndTime();
			if (endTime && endTime.diff(startTime).milliseconds < 0) {
				addError(
					"End time is before the start time. This will prevent video loading and cause other problems.",
				);
			}
			globalPlayer.loadSource(rangedPlaylistURL);

			globalPlayer.on(Hls.Events.ERROR, (_event, data) => {
				if (data.fatal) {
					switch (data.type) {
						case Hls.ErrorTypes.NETWORK_ERROR:
							if (data.reason === "no level found in manifest") {
								addError(
									"There is no video data between the specified start and end times. Change the times so that there is video content to play.",
								);
							} else {
								console.log("A fatal network error occurred; retrying", data);
								globalPlayer.startLoad();
							}
							break;
						case Hls.ErrorTypes.MEDIA_ERROR:
							console.log("A fatal media error occurred; retrying", data);
							globalPlayer.recoverMediaError();
							break;
						default:
							console.log("A fatal error occurred; resetting video player", data);
							addError(
								"Some sort of video player error occurred. Thrimbletrimmer is resetting the video player.",
							);
							resetVideoPlayer();
					}
				} else {
					console.log("A non-fatal video player error occurred; HLS.js will retry", data);
				}
			});

			resolve();
		});
	});
}

async function loadVideoPlayerFromDefaultPlaylist() {
	const playlistURL = `/playlist/${globalStreamName}.m3u8`;
	await loadVideoPlayer(playlistURL);
}

function resetVideoPlayer() {
	updateSegmentPlaylist();
}

function updateSegmentPlaylist() {
	const videoElement = document.getElementById("video");
	const currentPlaybackRate = videoElement.playbackRate;
	globalPlayer.destroy();
	loadVideoPlayerFromDefaultPlaylist();
	// The playback rate isn't maintained when destroying and reattaching hls.js
	videoElement.playbackRate = currentPlaybackRate;
}

function setUpVideoControls() {
	// Setting this up so it's removed from the event doesn't work; loadedmetadata fires twice anyway.
	// We still need to prevent double-setup, so here we are.
	if (globalSetUpControls) {
		return;
	}
	globalSetUpControls = true;

	const videoElement = document.getElementById("video");

	const playPauseButton = document.getElementById("video-controls-play-pause");
	if (videoElement.paused) {
		playPauseButton.src = "images/video-controls/play.png";
	} else {
		playPauseButton.src = "images/video-controls/pause.png";
	}

	const togglePlayState = (_event) => {
		if (videoElement.paused) {
			videoElement.play();
		} else {
			videoElement.pause();
		}
	};
	playPauseButton.addEventListener("click", togglePlayState);
	videoElement.addEventListener("click", (event) => {
		if (!videoElement.controls) {
			togglePlayState(event);
		}
	});

	videoElement.addEventListener("play", (_event) => {
		playPauseButton.src = "images/video-controls/pause.png";
	});
	videoElement.addEventListener("pause", (_event) => {
		playPauseButton.src = "images/video-controls/play.png";
	});

	const currentTime = document.getElementById("video-controls-current-time");
	currentTime.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime);
	videoElement.addEventListener("timeupdate", (_event) => {
		currentTime.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime);
	});

	const duration = document.getElementById("video-controls-duration");
	duration.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.duration);
	videoElement.addEventListener("durationchange", (_event) => {
		duration.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.duration);
	});

	const volumeMuted = document.getElementById("video-controls-volume-mute");
	if (videoElement.muted) {
		volumeMuted.src = "images/video-controls/volume-mute.png";
	} else {
		volumeMuted.src = "images/video-controls/volume.png";
	}
	const volumeLevel = document.getElementById("video-controls-volume-level");
	const defaultVolume = +(localStorage.getItem("volume") ?? 0.5);
	if (isNaN(defaultVolume)) {
		defaultVolume = 0.5;
	} else if (defaultVolume < 0) {
		defaultVolume = 0;
	} else if (defaultVolume > 1) {
		defaultVolume = 1;
	}
	videoElement.volume = defaultVolume;
	volumeLevel.value = videoElement.volume;

	volumeMuted.addEventListener("click", (_event) => {
		videoElement.muted = !videoElement.muted;
	});
	volumeLevel.addEventListener("click", (event) => {
		videoElement.volume = event.offsetX / event.target.offsetWidth;
		videoElement.muted = false;
	});
	videoElement.addEventListener("volumechange", (_event) => {
		if (videoElement.muted) {
			volumeMuted.src = "images/video-controls/volume-mute.png";
		} else {
			volumeMuted.src = "images/video-controls/volume.png";
		}
		volumeLevel.value = videoElement.volume;
		localStorage.setItem("volume", videoElement.volume);
	});

	const playbackSpeed = document.getElementById("video-controls-playback-speed");
	for (const speed of PLAYBACK_RATES) {
		const speedOption = document.createElement("option");
		speedOption.value = speed;
		speedOption.innerText = `${speed}x`;
		if (speed === 1) {
			speedOption.selected = true;
		}
		playbackSpeed.appendChild(speedOption);
	}
	playbackSpeed.addEventListener("change", (_event) => {
		const speed = +playbackSpeed.value;
		videoElement.playbackRate = speed;
	});

	const quality = document.getElementById("video-controls-quality");
	const defaultQuality = localStorage.getItem("quality");
	for (const [qualityIndex, qualityLevel] of globalPlayer.levels.entries()) {
		const qualityOption = document.createElement("option");
		qualityOption.value = qualityIndex;
		qualityOption.innerText = qualityLevel.name;
		if (qualityLevel.name === defaultQuality) {
			qualityOption.selected = true;
		}
		quality.appendChild(qualityOption);
	}
	localStorage.setItem("quality", quality.options[quality.options.selectedIndex].innerText);
	quality.addEventListener("change", (_event) => {
		globalPlayer.currentLevel = +quality.value;
		localStorage.setItem("quality", quality.options[quality.options.selectedIndex].innerText);
	});

	const fullscreen = document.getElementById("video-controls-fullscreen");
	fullscreen.addEventListener("click", (_event) => {
		if (document.fullscreenElement) {
			document.exitFullscreen();
		} else {
			videoElement.requestFullscreen();
		}
	});
	videoElement.addEventListener("fullscreenchange", (_event) => {
		if (document.fullscreenElement) {
			videoElement.controls = true;
		} else {
			videoElement.controls = false;
		}
	});

	const playbackPosition = document.getElementById("video-controls-playback-position");
	playbackPosition.max = videoElement.duration;
	playbackPosition.value = videoElement.currentTime;
	videoElement.addEventListener("durationchange", (_event) => {
		playbackPosition.max = videoElement.duration;
	});
	videoElement.addEventListener("timeupdate", (_event) => {
		playbackPosition.value = videoElement.currentTime;
	});
	playbackPosition.addEventListener("click", (event) => {
		const newPosition = (event.offsetX / event.target.offsetWidth) * videoElement.duration;
		videoElement.currentTime = newPosition;
		playbackPosition.value = newPosition;
	});

	/* Sometimes a mysterious issue occurs loading segments of the video when seeking.
	 * When this happens, twiddling the qualities tends to fix it. Here, we attempt to
	 * detect this situation and fix it automatically.
	 */
	videoElement.addEventListener("seeking", (_event) => {
		// If we don't get a "seeked" event soon after the "seeking" event, we assume there's
		// a loading error.
		// To handle this, we set up a timed handler to pick this up.
		if (globalSeekTimer !== null) {
			clearTimeout(globalSeekTimer);
			globalSeekTimer = null;
		}
		globalSeekTimer = setTimeout(() => {
			const currentLevel = globalPlayer.currentLevel;
			globalPlayer.currentLevel = -1;
			globalPlayer.currentLevel = currentLevel;
		}, 500);
	});
	videoElement.addEventListener("seeked", (_event) => {
		// Since we got the seek, cancel the timed twiddling of qualities
		if (globalSeekTimer !== null) {
			clearTimeout(globalSeekTimer);
			globalSeekTimer = null;
		}
	});
}

function dateTimeMathObjectFromBusTime(busTime) {
	// We need to handle inputs like "-0:10:15" in a way that consistently makes the time negative.
	// Since we can't assign the negative sign to any particular part, we'll check for the whole thing here.
	let direction = 1;
	if (busTime.startsWith("-")) {
		busTime = busTime.slice(1);
		direction = -1;
	}

	const parts = busTime.split(":", 3);
	const hours = parseInt(parts[0]) * direction;
	const minutes = (parts[1] || 0) * direction;
	const seconds = (parts[2] || 0) * direction;
	return { hours: hours, minutes: minutes, seconds: seconds };
}

function dateTimeFromBusTime(busTime) {
	return globalBusStartTime.plus(dateTimeMathObjectFromBusTime(busTime));
}

function busTimeFromDateTime(dateTime) {
	const diff = dateTime.diff(globalBusStartTime);
	return formatIntervalForDisplay(diff);
}

function formatIntervalForDisplay(interval) {
	if (interval.milliseconds < 0) {
		const negativeInterval = interval.negate();
		return `-${negativeInterval.toFormat("hh:mm:ss.SSS")}`;
	}
	return interval.toFormat("hh:mm:ss.SSS");
}

function dateTimeFromWubloaderTime(wubloaderTime) {
	return DateTime.fromISO(wubloaderTime);
}

function wubloaderTimeFromDateTime(dateTime) {
	if (!dateTime) {
		return null;
	}
	// Not using ISO here because Luxon doesn't give us a quick way to print an ISO8601 string with no offset.
	return dateTime.toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
}

function busTimeFromWubloaderTime(wubloaderTime) {
	if (wubloaderTime === "") {
		return "";
	}
	const dt = dateTimeFromWubloaderTime(wubloaderTime);
	return busTimeFromDateTime(dt);
}

function videoHumanTimeFromVideoPlayerTime(videoPlayerTime) {
	const hours = Math.floor(videoPlayerTime / 3600);
	let minutes = Math.floor((videoPlayerTime % 3600) / 60);
	let seconds = Math.floor(videoPlayerTime % 60);
	let milliseconds = Math.floor((videoPlayerTime * 1000) % 1000);

	while (minutes.toString().length < 2) {
		minutes = `0${minutes}`;
	}
	while (seconds.toString().length < 2) {
		seconds = `0${seconds}`;
	}
	while (milliseconds.toString().length < 3) {
		milliseconds = `0${milliseconds}`;
	}

	if (hours > 0) {
		return `${hours}:${minutes}:${seconds}.${milliseconds}`;
	}
	return `${minutes}:${seconds}.${milliseconds}`;
}

function videoPlayerTimeFromVideoHumanTime(videoHumanTime) {
	let timeParts = videoHumanTime.split(":", 3);
	let hours;
	let minutes;
	let seconds;

	if (timeParts.length < 2) {
		hours = 0;
		minutes = 0;
		seconds = +timeParts[0];
	} else if (timeParts.length < 3) {
		hours = 0;
		minutes = parseInt(timeParts[0]);
		seconds = +timeParts[1];
	} else {
		hours = parseInt(timeParts[0]);
		minutes = parseInt(timeParts[1]);
		seconds = +timeParts[2];
	}
	if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) {
		return null;
	}

	return hours * 3600 + minutes * 60 + seconds;
}

function dateTimeFromVideoPlayerTime(videoPlayerTime) {
	const segmentList = getSegmentList();
	let segmentStartTime;
	let segmentStartISOTime;
	for (const segment of segmentList) {
		const segmentEndTime = segment.start + segment.duration;
		if (videoPlayerTime >= segment.start && videoPlayerTime < segmentEndTime) {
			segmentStartTime = segment.start;
			segmentStartISOTime = segment.rawProgramDateTime;
			break;
		}
	}
	if (segmentStartISOTime === undefined) {
		return null;
	}
	const wubloaderDateTime = DateTime.fromISO(segmentStartISOTime);
	const offset = videoPlayerTime - segmentStartTime;
	return wubloaderDateTime.plus({ seconds: offset });
}

function videoPlayerTimeFromDateTime(dateTime) {
	const segmentTimes = getSegmentTimes();
	for (const segmentData of segmentTimes) {
		const segmentStart = segmentData.rawStart;
		const segmentEnd = segmentData.rawEnd;
		if (dateTime >= segmentStart && dateTime <= segmentEnd) {
			return segmentData.playerStart + dateTime.diff(segmentStart).as("seconds");
		}
	}
	return null;
}

function videoHumanTimeFromDateTime(dateTime) {
	const videoPlayerTime = videoPlayerTimeFromDateTime(dateTime);
	if (videoPlayerTime === null) {
		return null;
	}
	return videoHumanTimeFromVideoPlayerTime(videoPlayerTime);
}

function assembleVideoPlaylistURL(basePlaylistURL) {
	let playlistURL = basePlaylistURL;

	const queryStringParts = startAndEndTimeQueryStringParts();
	if (queryStringParts) {
		playlistURL += "?" + queryStringParts.join("&");
	}
	return playlistURL;
}

function startAndEndTimeQueryStringParts() {
	const startTime = getStartTime();
	const endTime = getEndTime();

	let queryStringParts = [];
	if (startTime) {
		queryStringParts.push(`start=${wubloaderTimeFromDateTime(startTime)}`);
	}
	if (endTime) {
		queryStringParts.push(`end=${wubloaderTimeFromDateTime(endTime)}`);
	}
	return queryStringParts;
}

function getSegmentList() {
	return globalPlayer.latencyController.levelDetails.fragments;
}

function hasSegmentList() {
	if (
		globalPlayer &&
		globalPlayer.latencyController &&
		globalPlayer.latencyController.levelDetails &&
		globalPlayer.latencyController.levelDetails.fragments
	) {
		return true;
	}
	return false;
}

function getSegmentTimes() {
	const segmentList = getSegmentList();
	const segmentTimes = [];
	for (const segment of segmentList) {
		const segmentStart = DateTime.fromISO(segment.rawProgramDateTime);
		const segmentEnd = segmentStart.plus({ seconds: segment.duration });
		segmentTimes.push({ rawStart: segmentStart, rawEnd: segmentEnd, playerStart: segment.start });
	}
	return segmentTimes;
}

function downloadFrame() {
	const videoElement = document.getElementById("video");
	const dateTime = dateTimeFromVideoPlayerTime(videoElement.currentTime);
	const url = `/frame/${globalStreamName}/source.png?timestamp=${wubloaderTimeFromDateTime(
		dateTime,
	)}`;
	// Avoid : as it causes problems on Windows
	const filename = `${dateTime.toFormat("yyyy-LL-dd'T'HH-mm-ss.SSS")}.png`;
	triggerDownload(url, filename);
}

function triggerDownload(url, filename) {
	// URL must be same-origin.
	const link = document.createElement("a");
	link.setAttribute("download", filename);
	link.href = url;
	link.setAttribute("target", "_blank");
	document.body.appendChild(link);
	link.click();
	document.body.removeChild(link);
}

function sendChatLogLoadData() {
	let startTime = getStartTime();
	let endTime = getEndTime();
	if (!startTime || !endTime) {
		return;
	}
	startTime = wubloaderTimeFromDateTime(startTime);
	endTime = wubloaderTimeFromDateTime(endTime);
	const segmentMetadata = getSegmentTimes();
	for (const segmentData of segmentMetadata) {
		segmentData.rawStart = segmentData.rawStart.toMillis();
		segmentData.rawEnd = segmentData.rawEnd.toMillis();
	}

	const message = {
		stream: globalStreamName,
		start: startTime,
		end: endTime,
		segmentMetadata: segmentMetadata,
	};
	globalLoadChatWorker.postMessage(message);
}

function updateChatDataFromWorkerResponse(chatData) {
	for (const chatLine of chatData) {
		chatLine.when = DateTime.fromMillis(chatLine.when);
	}
	globalChatData = chatData;
}

function renderChatMessage(chatMessageData) {
	const chatMessage = chatMessageData.message;
	if (chatMessage.command !== "PRIVMSG" || chatMessage.params[0] !== `#${globalStreamName}`) {
		return null;
	}

	const sendTimeElement = document.createElement("div");
	sendTimeElement.classList.add("chat-replay-message-time");
	sendTimeElement.innerText = chatMessageData.displayWhen;

	const senderNameElement = createMessageSenderElement(chatMessageData);

	const messageTextElement = document.createElement("div");
	messageTextElement.classList.add("chat-replay-message-text");

	if (chatMessage.tags.hasOwnProperty("reply-parent-msg-id")) {
		const replyParentID = chatMessage.tags["reply-parent-msg-id"];
		const replyParentSender = chatMessage.tags["reply-parent-display-name"];
		let replyParentMessageText = chatMessage.tags["reply-parent-msg-body"];
		const replyContainer = document.createElement("div");
		const replyTextContainer = document.createElement("a");

		if (replyParentMessageText.startsWith("\u0001ACTION")) {
			replyContainer.classList.add("chat-replay-message-text-action");
			const substringEnd = replyParentMessageText.endsWith("\u0001")
				? replyParentMessageText.length - 1
				: replyParentMessageText;
			replyParentMessageText = replyParentMessageText.substring(7, substringEnd);
		}

		replyTextContainer.href = `#chat-replay-message-${replyParentID}`;
		replyTextContainer.innerText = `Replying to ${replyParentSender}: ${replyParentMessageText}`;
		replyContainer.appendChild(replyTextContainer);
		replyContainer.classList.add("chat-replay-message-reply");
		messageTextElement.appendChild(replyContainer);
	}

	addChatMessageTextToElement(chatMessageData, messageTextElement);

	const messageContainer = createMessageContainer(chatMessageData, false);
	messageContainer.appendChild(sendTimeElement);
	messageContainer.appendChild(senderNameElement);
	messageContainer.appendChild(messageTextElement);
	return messageContainer;
}

function renderSystemMessages(chatMessageData) {
	const chatMessage = chatMessageData.message;
	if (chatMessage.command !== "USERNOTICE" || chatMessage.params[0] != `#${globalStreamName}`) {
		return [];
	}

	const messages = [];

	const sendTimeElement = document.createElement("div");
	sendTimeElement.classList.add("chat-replay-message-time");
	sendTimeElement.innerText = chatMessageData.displayWhen;

	const systemTextElement = document.createElement("div");
	systemTextElement.classList.add("chat-replay-message-text");
	systemTextElement.classList.add("chat-replay-message-system");
	let systemMsg = chatMessage.tags["system-msg"];
	if (!systemMsg && chatMessage.tags["msg-id"] === "announcement") {
		systemMsg = "Announcement";
	}
	systemTextElement.appendChild(document.createTextNode(systemMsg));

	const firstMessageContainer = createMessageContainer(chatMessageData, true);
	firstMessageContainer.appendChild(sendTimeElement);
	firstMessageContainer.appendChild(systemTextElement);
	messages.push(firstMessageContainer);

	if (chatMessage.params.length === 1) {
		return messages;
	}

	const emptySendTimeElement = document.createElement("div");
	emptySendTimeElement.classList.add("chat-replay-message-time");

	const senderNameElement = createMessageSenderElement(chatMessageData);

	const messageTextElement = document.createElement("div");
	messageTextElement.classList.add("chat-replay-message-text");
	addChatMessageTextToElement(chatMessageData, messageTextElement);

	const secondMessageContainer = createMessageContainer(chatMessageData, false);
	secondMessageContainer.appendChild(emptySendTimeElement);
	secondMessageContainer.appendChild(senderNameElement);
	secondMessageContainer.appendChild(messageTextElement);
	messages.push(secondMessageContainer);

	return messages;
}

function createMessageContainer(chatMessageData, isSystemMessage) {
	const chatMessage = chatMessageData.message;
	const messageContainer = document.createElement("div");
	messageContainer.classList.add("chat-replay-message");
	if (chatMessage.tags.hasOwnProperty("id")) {
		if (isSystemMessage) {
			messageContainer.id = `chat-replay-message-system-${chatMessage.tags.id}`;
		} else {
			messageContainer.id = `chat-replay-message-${chatMessage.tags.id}`;
		}
	}
	messageContainer.dataset.sender = chatMessage.sender;
	return messageContainer;
}

function getMessageDisplayName(chatMessageData) {
	const chatMessage = chatMessageData.message;
	if (chatMessage.tags.hasOwnProperty("display-name")) {
		return chatMessage.tags["display-name"];
	}
	return chatMessage.sender;
}

function createMessageSenderElement(chatMessageData) {
	const chatMessage = chatMessageData.message;
	const senderNameElement = document.createElement("div");
	senderNameElement.classList.add("chat-replay-message-sender");
	if (chatMessage.tags.hasOwnProperty("color")) {
		senderNameElement.style.color = chatMessage.tags.color;
	}
	senderNameElement.innerText = getMessageDisplayName(chatMessageData);
	return senderNameElement;
}

function addChatMessageTextToElement(chatMessageData, messageTextElement) {
	const chatMessage = chatMessageData.message;

	let chatMessageText = chatMessage.params[1];
	if (chatMessageText.startsWith("\u0001ACTION")) {
		messageTextElement.classList.add("chat-replay-message-text-action");
		const substringEnd = chatMessageText.endsWith("\u0001")
			? chatMessageText.length - 1
			: chatMessageText.length;
		chatMessageText = chatMessageText.substring(7, substringEnd);
	}

	if (chatMessage.tags.emotes) {
		const emoteDataStrings = chatMessage.tags.emotes.split("/");
		let emotePositions = [];
		for (const emoteDataString of emoteDataStrings) {
			const emoteData = emoteDataString.split(":", 2);
			const emoteID = emoteData[0];
			const emotePositionList = emoteData[1].split(",").map((val) => {
				const positions = val.split("-");
				return { emote: emoteID, start: +positions[0], end: +positions[1] };
			});
			emotePositions = emotePositions.concat(emotePositionList);
		}
		emotePositions.sort((a, b) => a.start - b.start);

		let messageText = [chatMessageText];
		while (emotePositions.length > 0) {
			const emoteData = emotePositions.pop(); // Pop the highest-index element from the array
			let text = messageText.shift();
			const textAndEmote = [text.substring(0, emoteData.start)];

			const emoteImg = document.createElement("img");
			emoteImg.src = `https://static-cdn.jtvnw.net/emoticons/v2/${emoteData.emote}/default/dark/1.0`;
			const emoteText = text.substring(emoteData.start, emoteData.end + 1);
			emoteImg.alt = emoteText;
			emoteImg.title = emoteText;
			const emoteContainer = document.createElement("span");
			emoteContainer.classList.add("chat-replay-message-emote");
			emoteContainer.appendChild(emoteImg);
			textAndEmote.push(emoteContainer);

			const remainingText = text.substring(emoteData.end + 1);
			if (remainingText !== "") {
				textAndEmote.push(remainingText);
			}
			messageText = textAndEmote.concat(messageText);
		}

		for (const messagePart of messageText) {
			if (typeof messagePart === "string") {
				const node = document.createTextNode(messagePart);
				messageTextElement.appendChild(node);
			} else {
				messageTextElement.appendChild(messagePart);
			}
		}
	} else {
		messageTextElement.appendChild(document.createTextNode(chatMessageText));
	}
}