<template>
	<div ref="chat" class="chat" tabindex="-1">
		<div v-show="channel.moreHistoryAvailable" class="show-more">
			<button
				ref="loadMoreButton"
				:disabled="channel.historyLoading || !$store.state.isConnected"
				class="btn"
				@click="onShowMoreClick"
			>
				<span v-if="channel.historyLoading">Loading…</span>
				<span v-else>Show older messages</span>
			</button>
		</div>
		<div
			class="messages"
			role="log"
			aria-live="polite"
			aria-relevant="additions"
			@copy="onCopy"
		>
			<template v-for="(message, id) in condensedMessages">
				<DateMarker
					v-if="shouldDisplayDateMarker(message, id)"
					:key="message.id + '-date'"
					:message="message"
					:focused="message.id == focused"
				/>
				<div
					v-if="shouldDisplayUnreadMarker(message.id)"
					:key="message.id + '-unread'"
					class="unread-marker"
				>
					<span class="unread-marker-text" />
				</div>

				<MessageCondensed
					v-if="message.type === 'condensed'"
					:key="message.messages[0].id"
					:network="network"
					:keep-scroll-position="keepScrollPosition"
					:messages="message.messages"
					:focused="message.id == focused"
				/>
				<Message
					v-else
					:key="message.id"
					:channel="channel"
					:network="network"
					:message="message"
					:keep-scroll-position="keepScrollPosition"
					:is-previous-source="isPreviousSource(message, id)"
					:focused="message.id == focused"
					@toggle-link-preview="onLinkPreviewToggle"
				/>
			</template>
		</div>
	</div>
</template>

<script>
const constants = require("../js/constants");
import eventbus from "../js/eventbus";
import clipboard from "../js/clipboard";
import socket from "../js/socket";
import Message from "./Message.vue";
import MessageCondensed from "./MessageCondensed.vue";
import DateMarker from "./DateMarker.vue";

let unreadMarkerShown = false;

export default {
	name: "MessageList",
	components: {
		Message,
		MessageCondensed,
		DateMarker,
	},
	props: {
		network: Object,
		channel: Object,
		focused: String,
	},
	computed: {
		condensedMessages() {
			if (this.channel.type !== "channel") {
				return this.channel.messages;
			}

			// If actions are hidden, just return a message list with them excluded
			if (this.$store.state.settings.statusMessages === "hidden") {
				return this.channel.messages.filter(
					(message) => !constants.condensedTypes.has(message.type)
				);
			}

			// If actions are not condensed, just return raw message list
			if (this.$store.state.settings.statusMessages !== "condensed") {
				return this.channel.messages;
			}

			const condensed = [];
			let lastCondensedContainer = null;

			for (const message of this.channel.messages) {
				// If this message is not condensable, or its an action affecting our user,
				// then just append the message to container and be done with it
				if (
					message.self ||
					message.highlight ||
					!constants.condensedTypes.has(message.type)
				) {
					lastCondensedContainer = null;

					condensed.push(message);

					continue;
				}

				if (lastCondensedContainer === null) {
					lastCondensedContainer = {
						time: message.time,
						type: "condensed",
						messages: [],
					};

					condensed.push(lastCondensedContainer);
				}

				lastCondensedContainer.messages.push(message);

				// Set id of the condensed container to last message id,
				// which is required for the unread marker to work correctly
				lastCondensedContainer.id = message.id;

				// If this message is the unread boundary, create a split condensed container
				if (message.id === this.channel.firstUnread) {
					lastCondensedContainer = null;
				}
			}

			return condensed.map((message) => {
				// Skip condensing single messages, it doesn't save any
				// space but makes useful information harder to see
				if (message.type === "condensed" && message.messages.length === 1) {
					return message.messages[0];
				}

				return message;
			});
		},
	},
	watch: {
		"channel.id"() {
			this.channel.scrolledToBottom = true;

			// Re-add the intersection observer to trigger the check again on channel switch
			// Otherwise if last channel had the button visible, switching to a new channel won't trigger the history
			if (this.historyObserver) {
				this.historyObserver.unobserve(this.$refs.loadMoreButton);
				this.historyObserver.observe(this.$refs.loadMoreButton);
			}
		},
		"channel.messages"() {
			this.keepScrollPosition();
		},
		"channel.pendingMessage"() {
			this.$nextTick(() => {
				// Keep the scroll stuck when input gets resized while typing
				this.keepScrollPosition();
			});
		},
	},
	created() {
		this.$nextTick(() => {
			if (!this.$refs.chat) {
				return;
			}

			if (window.IntersectionObserver) {
				this.historyObserver = new window.IntersectionObserver(this.onLoadButtonObserved, {
					root: this.$refs.chat,
				});
			}

			this.jumpToBottom();
		});
	},
	mounted() {
		this.$refs.chat.addEventListener("scroll", this.handleScroll, {passive: true});

		eventbus.on("resize", this.handleResize);

		this.$nextTick(() => {
			if (this.historyObserver) {
				this.historyObserver.observe(this.$refs.loadMoreButton);
			}
		});
	},
	beforeUpdate() {
		unreadMarkerShown = false;
	},
	beforeDestroy() {
		eventbus.off("resize", this.handleResize);
		this.$refs.chat.removeEventListener("scroll", this.handleScroll);
	},
	destroyed() {
		if (this.historyObserver) {
			this.historyObserver.disconnect();
		}
	},
	methods: {
		shouldDisplayDateMarker(message, id) {
			const previousMessage = this.condensedMessages[id - 1];

			if (!previousMessage) {
				return true;
			}

			const oldDate = new Date(previousMessage.time);
			const newDate = new Date(message.time);

			return (
				oldDate.getDate() !== newDate.getDate() ||
				oldDate.getMonth() !== newDate.getMonth() ||
				oldDate.getFullYear() !== newDate.getFullYear()
			);
		},
		shouldDisplayUnreadMarker(id) {
			if (!unreadMarkerShown && id > this.channel.firstUnread) {
				unreadMarkerShown = true;
				return true;
			}

			return false;
		},
		isPreviousSource(currentMessage, id) {
			const previousMessage = this.condensedMessages[id - 1];
			return (
				previousMessage &&
				currentMessage.type === "message" &&
				previousMessage.type === "message" &&
				previousMessage.from &&
				currentMessage.from.nick === previousMessage.from.nick
			);
		},
		onCopy() {
			clipboard(this.$el);
		},
		onLinkPreviewToggle(preview, message) {
			this.keepScrollPosition();

			// Tell the server we're toggling so it remembers at page reload
			socket.emit("msg:preview:toggle", {
				target: this.channel.id,
				msgId: message.id,
				link: preview.link,
				shown: preview.shown,
			});
		},
		onShowMoreClick() {
			if (!this.$store.state.isConnected) {
				return;
			}

			let lastMessage = -1;

			// Find the id of first message that isn't showInActive
			// If showInActive is set, this message is actually in another channel
			for (const message of this.channel.messages) {
				if (!message.showInActive) {
					lastMessage = message.id;
					break;
				}
			}

			this.channel.historyLoading = true;

			socket.emit("more", {
				target: this.channel.id,
				lastId: lastMessage,
				condensed: this.$store.state.settings.statusMessages !== "shown",
			});
		},
		onLoadButtonObserved(entries) {
			entries.forEach((entry) => {
				if (!entry.isIntersecting) {
					return;
				}

				this.onShowMoreClick();
			});
		},
		keepScrollPosition() {
			// If we are already waiting for the next tick to force scroll position,
			// we have no reason to perform more checks and set it again in the next tick
			if (this.isWaitingForNextTick) {
				return;
			}

			const el = this.$refs.chat;

			if (!el) {
				return;
			}

			if (!this.channel.scrolledToBottom) {
				if (this.channel.historyLoading) {
					const heightOld = el.scrollHeight - el.scrollTop;

					this.isWaitingForNextTick = true;
					this.$nextTick(() => {
						this.isWaitingForNextTick = false;
						this.skipNextScrollEvent = true;
						el.scrollTop = el.scrollHeight - heightOld;
					});
				}

				return;
			}

			this.isWaitingForNextTick = true;
			this.$nextTick(() => {
				this.isWaitingForNextTick = false;
				this.jumpToBottom();
			});
		},
		handleScroll() {
			// Setting scrollTop also triggers scroll event
			// We don't want to perform calculations for that
			if (this.skipNextScrollEvent) {
				this.skipNextScrollEvent = false;
				return;
			}

			const el = this.$refs.chat;

			if (!el) {
				return;
			}

			this.channel.scrolledToBottom = el.scrollHeight - el.scrollTop - el.offsetHeight <= 30;
		},
		handleResize() {
			// Keep message list scrolled to bottom on resize
			if (this.channel.scrolledToBottom) {
				this.jumpToBottom();
			}
		},
		jumpToBottom() {
			this.skipNextScrollEvent = true;
			this.channel.scrolledToBottom = true;

			const el = this.$refs.chat;
			el.scrollTop = el.scrollHeight;
		},
	},
};
</script>