<template>
	<div class="yfudmmck">
		<nav>
			<div class="path" @contextmenu.prevent.stop="() => {}">
				<XNavFolder
					:class="{ current: folder == null }"
					:parent-folder="folder"
					@move="move"
					@upload="upload"
					@removeFile="removeFile"
					@removeFolder="removeFolder"
				/>
				<template v-for="f in hierarchyFolders">
					<span class="separator"
						><i class="ph-caret-right ph-bold ph-lg"></i
					></span>
					<XNavFolder
						:folder="f"
						:parent-folder="folder"
						@move="move"
						@upload="upload"
						@removeFile="removeFile"
						@removeFolder="removeFolder"
					/>
				</template>
				<span v-if="folder != null" class="separator"
					><i class="ph-caret-right ph-bold ph-lg"></i
				></span>
				<span v-if="folder != null" class="folder current">{{
					folder.name
				}}</span>
			</div>
			<button class="menu _button" @click="showMenu">
				<i class="ph-dots-three-outline ph-bold ph-lg"></i>
			</button>
		</nav>
		<div
			ref="main"
			class="main"
			:class="{ uploading: uploadings.length > 0, fetching }"
			@dragover.prevent.stop="onDragover"
			@dragenter="onDragenter"
			@dragleave="onDragleave"
			@drop.prevent.stop="onDrop"
			@contextmenu.stop="onContextmenu"
		>
			<div ref="contents" class="contents">
				<div
					v-show="folders.length > 0"
					ref="foldersContainer"
					class="folders"
				>
					<XFolder
						v-for="(f, i) in folders"
						:key="f.id"
						v-anim="i"
						class="folder"
						:folder="f"
						:select-mode="select === 'folder'"
						:is-selected="
							selectedFolders.some((x) => x.id === f.id)
						"
						@chosen="chooseFolder"
						@move="move"
						@upload="upload"
						@removeFile="removeFile"
						@removeFolder="removeFolder"
						@dragstart="isDragSource = true"
						@dragend="isDragSource = false"
					/>
					<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
					<div v-for="(n, i) in 16" :key="i" class="padding"></div>
					<MkButton v-if="moreFolders" ref="moreFolders">{{
						i18n.ts.loadMore
					}}</MkButton>
				</div>
				<div
					v-show="files.length > 0"
					ref="filesContainer"
					class="files"
				>
					<XFile
						v-for="(file, i) in files"
						:key="file.id"
						v-anim="i"
						class="file"
						:file="file"
						:select-mode="select === 'file'"
						:is-selected="
							selectedFiles.some((x) => x.id === file.id)
						"
						@chosen="chooseFile"
						@dragstart="isDragSource = true"
						@dragend="isDragSource = false"
					/>
					<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
					<div v-for="(n, i) in 16" :key="i" class="padding"></div>
					<MkButton
						v-show="moreFiles"
						ref="loadMoreFiles"
						@click="fetchMoreFiles"
						>{{ i18n.ts.loadMore }}</MkButton
					>
				</div>
				<div
					v-if="files.length == 0 && folders.length == 0 && !fetching"
					class="empty"
				>
					<p v-if="draghover">{{ i18n.t("empty-draghover") }}</p>
					<p v-if="!draghover && folder == null">
						<strong>{{ i18n.ts.emptyDrive }}</strong
						><br />{{ i18n.t("empty-drive-description") }}
					</p>
					<p v-if="!draghover && folder != null">
						{{ i18n.ts.emptyFolder }}
					</p>
				</div>
			</div>
			<MkLoading v-if="fetching" />
		</div>
		<div v-if="draghover" class="dropzone"></div>
		<input
			ref="fileInput"
			type="file"
			accept="*/*"
			multiple
			tabindex="-1"
			@change="onChangeFileInput"
		/>
	</div>
</template>

<script lang="ts" setup>
import {
	markRaw,
	nextTick,
	onActivated,
	onBeforeUnmount,
	onMounted,
	ref,
	watch,
} from "vue";
import * as Misskey from "iceshrimp-sdk";
import MkButton from "./MkButton.vue";
import XNavFolder from "@/components/MkDrive.navFolder.vue";
import XFolder from "@/components/MkDrive.folder.vue";
import XFile from "@/components/MkDrive.file.vue";
import * as os from "@/os";
import { stream } from "@/stream";
import { defaultStore } from "@/store";
import { i18n } from "@/i18n";
import { uploadFile, uploads } from "@/scripts/upload";

const props = withDefaults(
	defineProps<{
		initialFolder?: Misskey.entities.DriveFolder;
		type?: string;
		multiple?: boolean;
		select?: "file" | "folder" | null;
	}>(),
	{
		multiple: false,
		select: null,
	},
);

const emit = defineEmits<{
	(
		ev: "selected",
		v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder,
	): void;
	(
		ev: "change-selection",
		v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[],
	): void;
	(ev: "move-root"): void;
	(ev: "cd", v: Misskey.entities.DriveFolder | null): void;
	(ev: "open-folder", v: Misskey.entities.DriveFolder): void;
}>();

const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
const fileInput = ref<HTMLInputElement>();

const folder = ref<Misskey.entities.DriveFolder | null>(null);
const files = ref<Misskey.entities.DriveFile[]>([]);
const folders = ref<Misskey.entities.DriveFolder[]>([]);
const moreFiles = ref(false);
const moreFolders = ref(false);
const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
const uploadings = uploads;
const connection = stream.useChannel("drive");
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい

// ドロップされようとしているか
const draghover = ref(false);

// 自身の所有するアイテムがドラッグをスタートさせたか
// (自分自身の階層にドロップできないようにするためのフラグ)
const isDragSource = ref(false);

const fetching = ref(true);

const ilFilesObserver = new IntersectionObserver(
	(entries) =>
		entries.some((entry) => entry.isIntersecting) &&
		!fetching.value &&
		moreFiles.value &&
		fetchMoreFiles(),
);

watch(folder, () => emit("cd", folder.value));

function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
	addFile(file, true);
}

function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
	const current = folder.value ? folder.value.id : null;
	if (current !== file.folderId) {
		removeFile(file);
	} else {
		addFile(file, true);
	}
}

function onStreamDriveFileDeleted(fileId: string) {
	removeFile(fileId);
}

function onStreamDriveFolderCreated(
	createdFolder: Misskey.entities.DriveFolder,
) {
	addFolder(createdFolder, true);
}

function onStreamDriveFolderUpdated(
	updatedFolder: Misskey.entities.DriveFolder,
) {
	const current = folder.value ? folder.value.id : null;
	if (current !== updatedFolder.parentId) {
		removeFolder(updatedFolder);
	} else {
		addFolder(updatedFolder, true);
	}
}

function onStreamDriveFolderDeleted(folderId: string) {
	removeFolder(folderId);
}

function onDragover(ev: DragEvent): any {
	if (!ev.dataTransfer) return;

	// ドラッグ元が自分自身の所有するアイテムだったら
	if (isDragSource.value) {
		// 自分自身にはドロップさせない
		ev.dataTransfer.dropEffect = "none";
		return;
	}

	const isFile = ev.dataTransfer.items[0].kind === "file";
	const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
	const isDriveFolder =
		ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
	if (isFile || isDriveFile || isDriveFolder) {
		ev.dataTransfer.dropEffect =
			ev.dataTransfer.effectAllowed === "all" ? "copy" : "move";
	} else {
		ev.dataTransfer.dropEffect = "none";
	}

	return false;
}

function onDragenter() {
	if (!isDragSource.value) draghover.value = true;
}

function onDragleave() {
	draghover.value = false;
}

function onDrop(ev: DragEvent): any {
	draghover.value = false;

	if (!ev.dataTransfer) return;

	// ドロップされてきたものがファイルだったら
	if (ev.dataTransfer.files.length > 0) {
		for (const file of Array.from(ev.dataTransfer.files)) {
			upload(file, folder.value);
		}
		return;
	}

	//#region ドライブのファイル
	const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
	if (driveFile != null && driveFile !== "") {
		const file = JSON.parse(driveFile);
		if (files.value.some((f) => f.id === file.id)) return;
		removeFile(file.id);
		os.api("drive/files/update", {
			fileId: file.id,
			folderId: folder.value ? folder.value.id : null,
		});
	}
	//#endregion

	//#region ドライブのフォルダ
	const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
	if (driveFolder != null && driveFolder !== "") {
		const droppedFolder = JSON.parse(driveFolder);

		// 移動先が自分自身ならreject
		if (folder.value && droppedFolder.id === folder.value.id) return false;
		if (folders.value.some((f) => f.id === droppedFolder.id)) return false;
		removeFolder(droppedFolder.id);
		os.api("drive/folders/update", {
			folderId: droppedFolder.id,
			parentId: folder.value ? folder.value.id : null,
		})
			.then(() => {
				// noop
			})
			.catch((err) => {
				switch (err) {
					case "detected-circular-definition":
						os.alert({
							title: i18n.ts.unableToProcess,
							text: i18n.ts.circularReferenceFolder,
						});
						break;
					default:
						os.alert({
							type: "error",
							text: i18n.ts.somethingHappened,
						});
				}
			});
	}
	//#endregion
}

function selectLocalFile() {
	fileInput.value?.click();
}

function urlUpload() {
	os.inputText({
		title: i18n.ts.uploadFromUrl,
		type: "url",
		placeholder: i18n.ts.uploadFromUrlDescription,
	}).then(({ canceled, result: url }) => {
		if (canceled || !url) return;
		os.api("drive/files/upload-from-url", {
			url: url,
			folderId: folder.value ? folder.value.id : undefined,
		});

		os.alert({
			title: i18n.ts.uploadFromUrlRequested,
			text: i18n.ts.uploadFromUrlMayTakeTime,
		});
	});
}

function createFolder() {
	os.inputText({
		title: i18n.ts.createFolder,
		placeholder: i18n.ts.folderName,
	}).then(({ canceled, result: name }) => {
		if (canceled) return;
		os.api("drive/folders/create", {
			name: name,
			parentId: folder.value ? folder.value.id : undefined,
		}).then((createdFolder) => {
			addFolder(createdFolder, true);
		});
	});
}

function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
	os.inputText({
		title: i18n.ts.renameFolder,
		placeholder: i18n.ts.inputNewFolderName,
		default: folderToRename.name,
	}).then(({ canceled, result: name }) => {
		if (canceled) return;
		os.api("drive/folders/update", {
			folderId: folderToRename.id,
			name: name,
		}).then((updatedFolder) => {
			// FIXME: 画面を更新するために自分自身に移動
			move(updatedFolder);
		});
	});
}

function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
	os.api("drive/folders/delete", {
		folderId: folderToDelete.id,
	})
		.then(() => {
			// 削除時に親フォルダに移動
			move(folderToDelete.parentId);
		})
		.catch((err) => {
			switch (err.id) {
				case "b0fc8a17-963c-405d-bfbc-859a487295e1":
					os.alert({
						type: "error",
						title: i18n.ts.unableToDelete,
						text: i18n.ts.hasChildFilesOrFolders,
					});
					break;
				default:
					os.alert({
						type: "error",
						text: i18n.ts.unableToDelete,
					});
			}
		});
}

function onChangeFileInput() {
	if (!fileInput.value?.files) return;
	for (const file of Array.from(fileInput.value.files)) {
		upload(file, folder.value);
	}
}

function upload(
	file: File,
	folderToUpload?: Misskey.entities.DriveFolder | null,
) {
	uploadFile(
		file,
		folderToUpload && typeof folderToUpload === "object"
			? folderToUpload.id
			: null,
		undefined,
		keepOriginal.value,
	).then((res) => {
		addFile(res, true);
	});
}

function chooseFile(file: Misskey.entities.DriveFile) {
	const isAlreadySelected = selectedFiles.value.some((f) => f.id === file.id);
	if (props.multiple) {
		if (isAlreadySelected) {
			selectedFiles.value = selectedFiles.value.filter(
				(f) => f.id !== file.id,
			);
		} else {
			selectedFiles.value.push(file);
		}
		emit("change-selection", selectedFiles.value);
	} else {
		if (isAlreadySelected) {
			emit("selected", file);
		} else {
			selectedFiles.value = [file];
			emit("change-selection", [file]);
		}
	}
}

function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
	const isAlreadySelected = selectedFolders.value.some(
		(f) => f.id === folderToChoose.id,
	);
	if (props.multiple) {
		if (isAlreadySelected) {
			selectedFolders.value = selectedFolders.value.filter(
				(f) => f.id !== folderToChoose.id,
			);
		} else {
			selectedFolders.value.push(folderToChoose);
		}
		emit("change-selection", selectedFolders.value);
	} else {
		if (isAlreadySelected) {
			emit("selected", folderToChoose);
		} else {
			selectedFolders.value = [folderToChoose];
			emit("change-selection", [folderToChoose]);
		}
	}
}

function move(target?: Misskey.entities.DriveFolder) {
	if (!target) {
		goRoot();
		return;
	} else if (typeof target === "object") {
		target = target.id;
	}

	fetching.value = true;

	os.api("drive/folders/show", {
		folderId: target,
	}).then((folderToMove) => {
		folder.value = folderToMove;
		hierarchyFolders.value = [];

		const dive = (folderToDive) => {
			hierarchyFolders.value.unshift(folderToDive);
			if (folderToDive.parent) dive(folderToDive.parent);
		};

		if (folderToMove.parent) dive(folderToMove.parent);

		emit("open-folder", folderToMove);
		fetch();
	});
}

function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
	const current = folder.value ? folder.value.id : null;
	if (current !== folderToAdd.parentId) return;

	if (folders.value.some((f) => f.id === folderToAdd.id)) {
		const exist = folders.value.map((f) => f.id).indexOf(folderToAdd.id);
		folders.value[exist] = folderToAdd;
		return;
	}

	if (unshift) {
		folders.value.unshift(folderToAdd);
	} else {
		folders.value.push(folderToAdd);
	}
}

function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
	const current = folder.value ? folder.value.id : null;
	if (current !== fileToAdd.folderId) return;

	if (files.value.some((f) => f.id === fileToAdd.id)) {
		const exist = files.value.map((f) => f.id).indexOf(fileToAdd.id);
		files.value[exist] = fileToAdd;
		return;
	}

	if (unshift) {
		files.value.unshift(fileToAdd);
	} else {
		files.value.push(fileToAdd);
	}
}

function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
	const folderIdToRemove =
		typeof folderToRemove === "object" ? folderToRemove.id : folderToRemove;
	folders.value = folders.value.filter((f) => f.id !== folderIdToRemove);
}

function removeFile(file: Misskey.entities.DriveFile | string) {
	const fileId = typeof file === "object" ? file.id : file;
	files.value = files.value.filter((f) => f.id !== fileId);
}

function appendFile(file: Misskey.entities.DriveFile) {
	addFile(file);
}

function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
	addFolder(folderToAppend);
}
/*
function prependFile(file: Misskey.entities.DriveFile) {
	addFile(file, true);
}

function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) {
	addFolder(folderToPrepend, true);
}
*/
function goRoot() {
	// 既にrootにいるなら何もしない
	if (folder.value == null) return;

	folder.value = null;
	hierarchyFolders.value = [];
	emit("move-root");
	fetch();
}

async function fetch() {
	folders.value = [];
	files.value = [];
	moreFolders.value = false;
	moreFiles.value = false;
	fetching.value = true;

	const foldersMax = 30;
	const filesMax = 30;

	const foldersPromise = os
		.api("drive/folders", {
			folderId: folder.value ? folder.value.id : null,
			limit: foldersMax + 1,
		})
		.then((fetchedFolders) => {
			if (fetchedFolders.length === foldersMax + 1) {
				moreFolders.value = true;
				fetchedFolders.pop();
			}
			return fetchedFolders;
		});

	const filesPromise = os
		.api("drive/files", {
			folderId: folder.value ? folder.value.id : null,
			type: props.type,
			limit: filesMax + 1,
		})
		.then((fetchedFiles) => {
			if (fetchedFiles.length === filesMax + 1) {
				moreFiles.value = true;
				fetchedFiles.pop();
			}
			return fetchedFiles;
		});

	const [fetchedFolders, fetchedFiles] = await Promise.all([
		foldersPromise,
		filesPromise,
	]);

	for (const x of fetchedFolders) appendFolder(x);
	for (const x of fetchedFiles) appendFile(x);

	fetching.value = false;
}

function fetchMoreFiles() {
	fetching.value = true;

	const max = 30;

	// ファイル一覧取得
	os.api("drive/files", {
		folderId: folder.value ? folder.value.id : null,
		type: props.type,
		untilId: files.value[files.value.length - 1].id,
		limit: max + 1,
	}).then((files) => {
		if (files.length === max + 1) {
			moreFiles.value = true;
			files.pop();
		} else {
			moreFiles.value = false;
		}
		for (const x of files) appendFile(x);
		fetching.value = false;
	});
}

function getMenu() {
	return [
		{
			type: "switch",
			text: i18n.ts.keepOriginalUploading,
			ref: keepOriginal,
		},
		null,
		{
			text: i18n.ts.addFile,
			type: "label",
		},
		{
			text: i18n.ts.upload,
			icon: "ph-upload-simple ph-bold ph-lg",
			action: () => {
				selectLocalFile();
			},
		},
		{
			text: i18n.ts.fromUrl,
			icon: "ph-link-simple ph-bold ph-lg",
			action: () => {
				urlUpload();
			},
		},
		null,
		{
			text: folder.value ? folder.value.name : i18n.ts.drive,
			type: "label",
		},
		folder.value
			? {
					text: i18n.ts.renameFolder,
					icon: "ph-cursor-text ph-bold ph-lg",
					action: () => {
						renameFolder(folder.value);
					},
			  }
			: undefined,
		folder.value
			? {
					text: i18n.ts.deleteFolder,
					icon: "ph-trash ph-bold ph-lg",
					action: () => {
						deleteFolder(
							folder.value as Misskey.entities.DriveFolder,
						);
					},
			  }
			: undefined,
		{
			text: i18n.ts.createFolder,
			icon: "ph-folder-notch-plus ph-bold ph-lg",
			action: () => {
				createFolder();
			},
		},
	];
}

function showMenu(ev: MouseEvent) {
	os.popupMenu(
		getMenu(),
		(ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined,
	);
}

function onContextmenu(ev: MouseEvent) {
	os.contextMenu(getMenu(), ev);
}

onMounted(() => {
	if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
		nextTick(() => {
			ilFilesObserver.observe(loadMoreFiles.value?.$el);
		});
	}

	connection.on("fileCreated", onStreamDriveFileCreated);
	connection.on("fileUpdated", onStreamDriveFileUpdated);
	connection.on("fileDeleted", onStreamDriveFileDeleted);
	connection.on("folderCreated", onStreamDriveFolderCreated);
	connection.on("folderUpdated", onStreamDriveFolderUpdated);
	connection.on("folderDeleted", onStreamDriveFolderDeleted);

	if (props.initialFolder) {
		move(props.initialFolder);
	} else {
		fetch();
	}
});

onActivated(() => {
	if (defaultStore.state.enableInfiniteScroll) {
		nextTick(() => {
			ilFilesObserver.observe(loadMoreFiles.value?.$el);
		});
	}
});

onBeforeUnmount(() => {
	connection.dispose();
	ilFilesObserver.disconnect();
});
</script>

<style lang="scss" scoped>
.yfudmmck {
	display: flex;
	flex-direction: column;
	height: 100%;

	> nav {
		display: flex;
		z-index: 2;
		width: 100%;
		padding: 0 8px;
		box-sizing: border-box;
		overflow: auto;
		font-size: 0.9em;
		box-shadow: 0 1px 0 var(--divider);

		&,
		* {
			user-select: none;
		}

		> .path {
			display: inline-block;
			vertical-align: bottom;
			line-height: 42px;
			white-space: nowrap;

			> * {
				display: inline-block;
				margin: 0;
				padding: 0 8px;
				line-height: 42px;
				cursor: pointer;

				* {
					pointer-events: none;
				}

				&:hover {
					text-decoration: underline;
				}

				&.current {
					font-weight: bold;
					cursor: default;

					&:hover {
						text-decoration: none;
					}
				}

				&.separator {
					margin: 0;
					padding: 0;
					opacity: 0.5;
					cursor: default;

					> i {
						margin: 0;
					}
				}
			}
		}

		> .menu {
			margin-left: auto;
			padding: 0 12px;
		}
	}

	> .main {
		flex: 1;
		overflow: auto;
		padding: var(--margin);

		&,
		* {
			user-select: none;
		}

		&.fetching {
			cursor: wait !important;

			* {
				pointer-events: none;
			}

			> .contents {
				opacity: 0.5;
			}
		}

		&.uploading {
			height: calc(100% - 38px - 100px);
		}

		> .contents {
			> .folders,
			> .files {
				display: flex;
				flex-wrap: wrap;

				> .folder,
				> .file {
					flex-grow: 1;
					width: 128px;
					margin: 4px;
					box-sizing: border-box;
				}

				> .padding {
					flex-grow: 1;
					pointer-events: none;
					width: 128px + 8px;
				}
			}

			> .empty {
				padding: 16px;
				text-align: center;
				pointer-events: none;
				opacity: 0.5;

				> p {
					margin: 0;
				}
			}
		}
	}

	> .dropzone {
		position: absolute;
		left: 0;
		top: 38px;
		width: 100%;
		height: calc(100% - 38px);
		border: dashed 2px var(--focus);
		pointer-events: none;
	}

	> input {
		display: none;
	}
}
</style>
