<template>
<div class="yfudmmck">
	<nav>
		<div class="path" @contextmenu.prevent.stop="() => {}">
			<XNavFolder :class="{ current: folder == null }"/>
			<template v-for="f in hierarchyFolders">
				<span class="separator"><i class="fas fa-angle-right"></i></span>
				<XNavFolder :folder="f"/>
			</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"/>
				<!-- 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">{{ $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"/>
				<!-- 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">{{ $ts.loadMore }}</MkButton>
			</div>
			<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
				<p v-if="draghover">{{ $t('empty-draghover') }}</p>
				<p v-if="!draghover && folder == null"><strong>{{ $ts.emptyDrive }}</strong><br/>{{ $t('empty-drive-description') }}</p>
				<p v-if="!draghover && folder != null">{{ $ts.emptyFolder }}</p>
			</div>
		</div>
		<MkLoading v-if="fetching"/>
	</div>
	<div v-if="draghover" class="dropzone"></div>
	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
</div>
</template>

<script lang="ts">
import { defineComponent, markRaw } from 'vue';
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';

export default defineComponent({
	components: {
		XNavFolder,
		XFolder,
		XFile,
		MkButton,
	},

	props: {
		initialFolder: {
			type: Object,
			required: false
		},
		type: {
			type: String,
			required: false,
			default: undefined
		},
		multiple: {
			type: Boolean,
			required: false,
			default: false
		},
		select: {
			type: String,
			required: false,
			default: null
		}
	},

	emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'],

	data() {
		return {
			/**
			 * 現在の階層(フォルダ)
			 * * null でルートを表す
			 */
			folder: null,

			files: [],
			folders: [],
			moreFiles: false,
			moreFolders: false,
			hierarchyFolders: [],
			selectedFiles: [],
			selectedFolders: [],
			uploadings: os.uploads,
			connection: null,

			/**
			 * ドロップされようとしているか
			 */
			draghover: false,

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

			fetching: true,

			ilFilesObserver: new IntersectionObserver(
				(entries) => entries.some((entry) => entry.isIntersecting)
				&& !this.fetching && this.moreFiles &&
					this.fetchMoreFiles()
			),
			moreFilesElement: null as Element,

		};
	},

	watch: {
		folder() {
			this.$emit('cd', this.folder);
		}
	},

	mounted() {
		if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) {
			this.$nextTick(() => {
				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
			});
		}

		this.connection = markRaw(stream.useChannel('drive'));

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

		if (this.initialFolder) {
			this.move(this.initialFolder);
		} else {
			this.fetch();
		}
	},

	activated() {
		if (this.$store.state.enableInfiniteScroll) {
			this.$nextTick(() => {
				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
			});
		}
	},

	beforeUnmount() {
		this.connection.dispose();
		this.ilFilesObserver.disconnect();
	},

	methods: {
		onStreamDriveFileCreated(file) {
			this.addFile(file, true);
		},

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

		onStreamDriveFileDeleted(fileId) {
			this.removeFile(fileId);
		},

		onStreamDriveFolderCreated(folder) {
			this.addFolder(folder, true);
		},

		onStreamDriveFolderUpdated(folder) {
			const current = this.folder ? this.folder.id : null;
			if (current != folder.parentId) {
				this.removeFolder(folder);
			} else {
				this.addFolder(folder, true);
			}
		},

		onStreamDriveFolderDeleted(folderId) {
			this.removeFolder(folderId);
		},

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

			const isFile = e.dataTransfer.items[0].kind == 'file';
			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;

			if (isFile || isDriveFile || isDriveFolder) {
				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
			} else {
				e.dataTransfer.dropEffect = 'none';
			}

			return false;
		},

		onDragenter(e) {
			if (!this.isDragSource) this.draghover = true;
		},

		onDragleave(e) {
			this.draghover = false;
		},

		onDrop(e): any {
			this.draghover = false;

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

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

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

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

		selectLocalFile() {
			(this.$refs.fileInput as any).click();
		},

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

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

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

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

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

		onChangeFileInput() {
			for (const file of Array.from((this.$refs.fileInput as any).files)) {
				this.upload(file, this.folder);
			}
		},

		upload(file, folder) {
			if (folder && typeof folder == 'object') folder = folder.id;
			os.upload(file, folder).then(res => {
				this.addFile(res, true);
			});
		},

		chooseFile(file) {
			const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id);
			if (this.multiple) {
				if (isAlreadySelected) {
					this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
				} else {
					this.selectedFiles.push(file);
				}
				this.$emit('change-selection', this.selectedFiles);
			} else {
				if (isAlreadySelected) {
					this.$emit('selected', file);
				} else {
					this.selectedFiles = [file];
					this.$emit('change-selection', [file]);
				}
			}
		},

		chooseFolder(folder) {
			const isAlreadySelected = this.selectedFolders.some(f => f.id == folder.id);
			if (this.multiple) {
				if (isAlreadySelected) {
					this.selectedFolders = this.selectedFolders.filter(f => f.id != folder.id);
				} else {
					this.selectedFolders.push(folder);
				}
				this.$emit('change-selection', this.selectedFolders);
			} else {
				if (isAlreadySelected) {
					this.$emit('selected', folder);
				} else {
					this.selectedFolders = [folder];
					this.$emit('change-selection', [folder]);
				}
			}
		},

		move(target) {
			if (target == null) {
				this.goRoot();
				return;
			} else if (typeof target == 'object') {
				target = target.id;
			}

			this.fetching = true;

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

				const dive = folder => {
					this.hierarchyFolders.unshift(folder);
					if (folder.parent) dive(folder.parent);
				};

				if (folder.parent) dive(folder.parent);

				this.$emit('open-folder', folder);
				this.fetch();
			});
		},

		addFolder(folder, unshift = false) {
			const current = this.folder ? this.folder.id : null;
			if (current != folder.parentId) return;

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

			if (unshift) {
				this.folders.unshift(folder);
			} else {
				this.folders.push(folder);
			}
		},

		addFile(file, unshift = false) {
			const current = this.folder ? this.folder.id : null;
			if (current != file.folderId) return;

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

			if (unshift) {
				this.files.unshift(file);
			} else {
				this.files.push(file);
			}
		},

		removeFolder(folder) {
			if (typeof folder == 'object') folder = folder.id;
			this.folders = this.folders.filter(f => f.id != folder);
		},

		removeFile(file) {
			if (typeof file == 'object') file = file.id;
			this.files = this.files.filter(f => f.id != file);
		},

		appendFile(file) {
			this.addFile(file);
		},

		appendFolder(folder) {
			this.addFolder(folder);
		},

		prependFile(file) {
			this.addFile(file, true);
		},

		prependFolder(folder) {
			this.addFolder(folder, true);
		},

		goRoot() {
			// 既にrootにいるなら何もしない
			if (this.folder == null) return;

			this.folder = null;
			this.hierarchyFolders = [];
			this.$emit('move-root');
			this.fetch();
		},

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

			let fetchedFolders = null;
			let fetchedFiles = null;

			const foldersMax = 30;
			const filesMax = 30;

			// フォルダ一覧取得
			os.api('drive/folders', {
				folderId: this.folder ? this.folder.id : null,
				limit: foldersMax + 1
			}).then(folders => {
				if (folders.length == foldersMax + 1) {
					this.moreFolders = true;
					folders.pop();
				}
				fetchedFolders = folders;
				complete();
			});

			// ファイル一覧取得
			os.api('drive/files', {
				folderId: this.folder ? this.folder.id : null,
				type: this.type,
				limit: filesMax + 1
			}).then(files => {
				if (files.length == filesMax + 1) {
					this.moreFiles = true;
					files.pop();
				}
				fetchedFiles = files;
				complete();
			});

			let flag = false;
			const complete = () => {
				if (flag) {
					for (const x of fetchedFolders) this.appendFolder(x);
					for (const x of fetchedFiles) this.appendFile(x);
					this.fetching = false;
				} else {
					flag = true;
				}
			};
		},

		fetchMoreFiles() {
			this.fetching = true;

			const max = 30;

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

		getMenu() {
			return [{
				text: this.$ts.addFile,
				type: 'label'
			}, {
				text: this.$ts.upload,
				icon: 'fas fa-upload',
				action: () => { this.selectLocalFile(); }
			}, {
				text: this.$ts.fromUrl,
				icon: 'fas fa-link',
				action: () => { this.urlUpload(); }
			}, null, {
				text: this.folder ? this.folder.name : this.$ts.drive,
				type: 'label'
			}, this.folder ? {
				text: this.$ts.renameFolder,
				icon: 'fas fa-i-cursor',
				action: () => { this.renameFolder(this.folder); }
			} : undefined, this.folder ? {
				text: this.$ts.deleteFolder,
				icon: 'fas fa-trash-alt',
				action: () => { this.deleteFolder(this.folder); }
			} : undefined, {
				text: this.$ts.createFolder,
				icon: 'fas fa-folder-plus',
				action: () => { this.createFolder(); }
			}];
		},

		showMenu(ev) {
			os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
		},

		onContextmenu(ev) {
			os.contextMenu(this.getMenu(), ev);
		},
	}
});
</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: 50px;
			white-space: nowrap;

			> * {
				display: inline-block;
				margin: 0;
				padding: 0 8px;
				line-height: 50px;
				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>