<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="fas fa-angle-right"></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="fas fa-angle-right"></i></span>
			<span v-if="folder != null" class="folder current">{{ folder.name }}</span>
		</div>
		<button class="menu _button" @click="showMenu"><i class="fas fa-ellipsis-h"></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 'misskey-js';
import XNavFolder from './drive.nav-folder.vue';
import XFolder from './drive.folder.vue';
import XFile from './drive.file.vue';
import MkButton from './ui/button.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: 'fas fa-upload',
		action: () => { selectLocalFile(); },
	}, {
		text: i18n.ts.fromUrl,
		icon: 'fas fa-link',
		action: () => { urlUpload(); },
	}, null, {
		text: folder.value ? folder.value.name : i18n.ts.drive,
		type: 'label',
	}, folder.value ? {
		text: i18n.ts.renameFolder,
		icon: 'fas fa-i-cursor',
		action: () => { renameFolder(folder.value); },
	} : undefined, folder.value ? {
		text: i18n.ts.deleteFolder,
		icon: 'fas fa-trash-alt',
		action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
	} : undefined, {
		text: i18n.ts.createFolder,
		icon: 'fas fa-folder-plus',
		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>
