"use strict";

import {update as updateCursor} from "undate";

import socket from "./socket";
import store from "./store";

class Uploader {
	init() {
		this.xhr = null;
		this.fileQueue = [];
		this.tokenKeepAlive = null;

		document.addEventListener("dragenter", (e) => this.dragEnter(e));
		document.addEventListener("dragover", (e) => this.dragOver(e));
		document.addEventListener("dragleave", (e) => this.dragLeave(e));
		document.addEventListener("drop", (e) => this.drop(e));
		document.addEventListener("paste", (e) => this.paste(e));

		socket.on("upload:auth", (token) => this.uploadNextFileInQueue(token));
	}

	mounted() {
		this.overlay = document.getElementById("upload-overlay");
		this.uploadProgressbar = document.getElementById("upload-progressbar");
	}

	dragOver(event) {
		if (event.dataTransfer.types.includes("Files")) {
			// Prevent dragover event completely and do nothing with it
			// This stops the browser from trying to guess which cursor to show
			event.preventDefault();
		}
	}

	dragEnter(event) {
		// relatedTarget is the target where we entered the drag from
		// when dragging from another window, the target is null, otherwise its a DOM element
		if (!event.relatedTarget && event.dataTransfer.types.includes("Files")) {
			event.preventDefault();

			this.overlay.classList.add("is-dragover");
		}
	}

	dragLeave(event) {
		// If relatedTarget is null, that means we are no longer dragging over the page
		if (!event.relatedTarget) {
			event.preventDefault();
			this.overlay.classList.remove("is-dragover");
		}
	}

	drop(event) {
		if (!event.dataTransfer.types.includes("Files")) {
			return;
		}

		event.preventDefault();
		this.overlay.classList.remove("is-dragover");

		let files;

		if (event.dataTransfer.items) {
			files = Array.from(event.dataTransfer.items)
				.filter((item) => item.kind === "file")
				.map((item) => item.getAsFile());
		} else {
			files = Array.from(event.dataTransfer.files);
		}

		this.triggerUpload(files);
	}

	paste(event) {
		const items = event.clipboardData.items;
		const files = [];

		for (const item of items) {
			if (item.kind === "file") {
				files.push(item.getAsFile());
			}
		}

		if (files.length === 0) {
			return;
		}

		event.preventDefault();
		this.triggerUpload(files);
	}

	triggerUpload(files) {
		if (!files.length) {
			return;
		}

		if (!store.state.isConnected) {
			this.handleResponse({
				error: `You are currently disconnected, unable to initiate upload process.`,
			});

			return;
		}

		const wasQueueEmpty = this.fileQueue.length === 0;
		const maxFileSize = store.state.serverConfiguration.fileUploadMaxFileSize;

		for (const file of files) {
			if (maxFileSize > 0 && file.size > maxFileSize) {
				this.handleResponse({
					error: `File ${file.name} is over the maximum allowed size`,
				});

				continue;
			}

			this.fileQueue.push(file);
		}

		// if the queue was empty and we added some files to it, and there currently
		// is no upload in process, request a token to start the upload process
		if (wasQueueEmpty && this.xhr === null && this.fileQueue.length > 0) {
			this.requestToken();
		}
	}

	requestToken() {
		socket.emit("upload:auth");
	}

	setProgress(value) {
		this.uploadProgressbar.classList.toggle("upload-progressbar-visible", value > 0);
		this.uploadProgressbar.style.width = value + "%";
	}

	uploadNextFileInQueue(token) {
		const file = this.fileQueue.shift();

		// Tell the server that we are still upload to this token
		// so it does not become invalidated and fail the upload.
		// This issue only happens if The Lounge is proxied through other software
		// as it may buffer the upload before the upload request will be processed by The Lounge.
		this.tokenKeepAlive = setInterval(() => socket.emit("upload:ping", token), 40 * 1000);

		if (
			store.state.settings.uploadCanvas &&
			file.type.startsWith("image/") &&
			!file.type.includes("svg") &&
			file.type !== "image/gif"
		) {
			this.renderImage(file, (newFile) => this.performUpload(token, newFile));
		} else {
			this.performUpload(token, file);
		}
	}

	renderImage(file, callback) {
		const fileReader = new FileReader();

		fileReader.onabort = () => callback(file);
		fileReader.onerror = () => fileReader.abort();

		fileReader.onload = () => {
			const img = new Image();

			img.onerror = () => callback(file);

			img.onload = () => {
				const canvas = document.createElement("canvas");
				canvas.width = img.width;
				canvas.height = img.height;
				const ctx = canvas.getContext("2d");
				ctx.drawImage(img, 0, 0);

				canvas.toBlob((blob) => {
					callback(new File([blob], file.name));
				}, file.type);
			};

			img.src = fileReader.result;
		};

		fileReader.readAsDataURL(file);
	}

	performUpload(token, file) {
		this.xhr = new XMLHttpRequest();

		this.xhr.upload.addEventListener(
			"progress",
			(e) => {
				const percent = Math.floor((e.loaded / e.total) * 1000) / 10;
				this.setProgress(percent);
			},
			false
		);

		this.xhr.onreadystatechange = () => {
			if (this.xhr.readyState === XMLHttpRequest.DONE) {
				let response;

				try {
					response = JSON.parse(this.xhr.responseText);
				} catch (err) {
					// This is just a safe guard and should not happen if server doesn't throw any errors.
					// Browsers break the HTTP spec by aborting the request without reading any response data,
					// if there is still data to be uploaded. Servers will only error in extreme cases like bad
					// authentication or server-side errors.
					response = {
						error: `Upload aborted: HTTP ${this.xhr.status}`,
					};
				}

				this.handleResponse(response);

				this.xhr = null;

				// this file was processed, if we still have files in the queue, upload the next one
				if (this.fileQueue.length > 0) {
					this.requestToken();
				}
			}
		};

		const formData = new FormData();
		formData.append("file", file);
		this.xhr.open("POST", `uploads/new/${token}`);
		this.xhr.send(formData);
	}

	handleResponse(response) {
		this.setProgress(0);

		if (this.tokenKeepAlive) {
			clearInterval(this.tokenKeepAlive);
			this.tokenKeepAlive = null;
		}

		if (response.error) {
			store.commit("currentUserVisibleError", response.error);
			return;
		}

		if (response.url) {
			this.insertUploadUrl(response.url);
		}
	}

	insertUploadUrl(url) {
		const fullURL = new URL(url, location).toString();
		const textbox = document.getElementById("input");
		const initStart = textbox.selectionStart;

		// Get the text before the cursor, and add a space if it's not in the beginning
		const headToCursor = initStart > 0 ? textbox.value.substr(0, initStart) + " " : "";

		// Get the remaining text after the cursor
		const cursorToTail = textbox.value.substr(initStart);

		// Construct the value until the point where we want the cursor to be
		const textBeforeTail = headToCursor + fullURL + " ";

		updateCursor(textbox, textBeforeTail + cursorToTail);

		// Set the cursor after the link and a space
		textbox.selectionStart = textbox.selectionEnd = textBeforeTail.length;
	}

	// TODO: This is a temporary hack while Vue porting is finalized
	abort() {
		this.fileQueue = [];

		if (this.xhr) {
			this.xhr.abort();
			this.xhr = null;
		}
	}
}

const instance = new Uploader();

export default {
	abort: () => instance.abort(),
	initialize: () => instance.init(),
	mounted: () => instance.mounted(),
	triggerUpload: (files) => instance.triggerUpload(files),
};