<template>
	<div>
		<Transition name="upload-status" @enter="onUploadStatusEnter" @leave="onUploadStatusLeave">
			<div v-show="isUploading" ref="uploadStatusRef" class="upload-status">
				<div
					ref="statusContentRef"
					v-bind="$attrs"
					:class="['flex flex-col p-4 text-sm text-contrast']"
				>
					<div class="flex items-center justify-between">
						<div class="flex items-center gap-2 font-bold">
							<FolderOpenIcon class="size-4" />
							<span>
								<span class="capitalize">
									{{ props.fileType ? props.fileType : 'File' }} uploads
								</span>
								<span>{{ activeUploads.length > 0 ? ` - ${activeUploads.length} left` : '' }}</span>
							</span>
						</div>
					</div>

					<div class="mt-2 space-y-2">
						<div
							v-for="item in uploadQueue"
							:key="item.file.name"
							class="flex h-6 items-center justify-between gap-2 text-xs"
						>
							<div class="flex flex-1 items-center gap-2 truncate">
								<transition-group name="status-icon" mode="out-in">
									<PanelSpinner
										v-show="item.status === 'uploading'"
										key="spinner"
										class="absolute !size-4"
									/>
									<CheckCircleIcon
										v-show="item.status === 'completed'"
										key="check"
										class="absolute size-4 text-green"
									/>
									<XCircleIcon
										v-show="
											item.status.includes('error') ||
											item.status === 'cancelled' ||
											item.status === 'incorrect-type'
										"
										key="error"
										class="absolute size-4 text-red"
									/>
								</transition-group>
								<span class="ml-6 truncate">{{ item.file.name }}</span>
								<span class="text-secondary">{{ item.size }}</span>
							</div>
							<div class="flex min-w-[80px] items-center justify-end gap-2">
								<template v-if="item.status === 'completed'">
									<span>Done</span>
								</template>
								<template v-else-if="item.status === 'error-file-exists'">
									<span class="text-red">Failed - File already exists</span>
								</template>
								<template v-else-if="item.status === 'error-generic'">
									<span class="text-red"
										>Failed - {{ item.error?.message || 'An unexpected error occured.' }}</span
									>
								</template>
								<template v-else-if="item.status === 'incorrect-type'">
									<span class="text-red">Failed - Incorrect file type</span>
								</template>
								<template v-else>
									<template v-if="item.status === 'uploading'">
										<span>{{ item.progress }}%</span>
										<div class="h-1 w-20 overflow-hidden rounded-full bg-bg">
											<div
												class="h-full bg-contrast transition-all duration-200"
												:style="{ width: item.progress + '%' }"
											/>
										</div>
										<ButtonStyled color="red" type="transparent" @click="cancelUpload(item)">
											<button>Cancel</button>
										</ButtonStyled>
									</template>
									<template v-else-if="item.status === 'cancelled'">
										<span class="text-red">Cancelled</span>
									</template>
									<template v-else>
										<span>{{ item.progress }}%</span>
										<div class="h-1 w-20 overflow-hidden rounded-full bg-bg">
											<div
												class="h-full bg-contrast transition-all duration-200"
												:style="{ width: item.progress + '%' }"
											/>
										</div>
									</template>
								</template>
							</div>
						</div>
					</div>
				</div>
			</div>
		</Transition>
	</div>
</template>

<script setup lang="ts">
import { CheckCircleIcon, FolderOpenIcon, XCircleIcon } from '@modrinth/assets'
import { ButtonStyled, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
import { computed, nextTick, ref, watch } from 'vue'

import PanelSpinner from './PanelSpinner.vue'

const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()

interface UploadItem {
	file: File
	progress: number
	status:
		| 'pending'
		| 'uploading'
		| 'completed'
		| 'error-file-exists'
		| 'error-generic'
		| 'cancelled'
		| 'incorrect-type'
	size: string
	uploader?: ReturnType<typeof client.kyros.files_v0.uploadFile>
	error?: Error
}

interface Props {
	currentPath: string
	fileType?: string
	marginBottom?: number
	acceptedTypes?: Array<string>
}

defineOptions({
	inheritAttrs: false,
})

const props = defineProps<Props>()

const emit = defineEmits<{
	(e: 'uploadComplete'): void
}>()

const uploadStatusRef = ref<HTMLElement | null>(null)
const statusContentRef = ref<HTMLElement | null>(null)
const uploadQueue = ref<UploadItem[]>([])

const isUploading = computed(() => uploadQueue.value.length > 0)
const activeUploads = computed(() =>
	uploadQueue.value.filter((item) => item.status === 'pending' || item.status === 'uploading'),
)

const onUploadStatusEnter = (el: Element) => {
	const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0)
	;(el as HTMLElement).style.height = '0'

	void (el as HTMLElement).offsetHeight
	;(el as HTMLElement).style.height = `${height}px`
}

const onUploadStatusLeave = (el: Element) => {
	const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0)
	;(el as HTMLElement).style.height = `${height}px`

	void (el as HTMLElement).offsetHeight
	;(el as HTMLElement).style.height = '0'
}

watch(
	uploadQueue,
	() => {
		if (!uploadStatusRef.value) return
		const el = uploadStatusRef.value
		const itemsHeight = uploadQueue.value.length * 32
		const headerHeight = 12
		const gap = 8
		const padding = 32
		const totalHeight = padding + headerHeight + gap + itemsHeight + (props.marginBottom || 0)
		el.style.height = `${totalHeight}px`
	},
	{ deep: true },
)

const formatFileSize = (bytes: number): string => {
	if (bytes < 1024) return bytes + ' B'
	if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + ' KB'
	if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + ' MB'
	return (bytes / 1024 ** 3).toFixed(1) + ' GB'
}

const cancelUpload = (item: UploadItem) => {
	if (item.uploader && item.status === 'uploading') {
		item.uploader.cancel()
		item.status = 'cancelled'

		setTimeout(async () => {
			const index = uploadQueue.value.findIndex((qItem) => qItem.file.name === item.file.name)
			if (index !== -1) {
				uploadQueue.value.splice(index, 1)
				await nextTick()
			}
		}, 5000)
	}
}

const badFileTypeMsg = 'Upload had incorrect file type'

const uploadFile = async (file: File) => {
	const uploadItem: UploadItem = {
		file,
		progress: 0,
		status: 'pending',
		size: formatFileSize(file.size),
	}

	uploadQueue.value.push(uploadItem)

	try {
		if (
			props.acceptedTypes &&
			!props.acceptedTypes.includes(file.type) &&
			!props.acceptedTypes.some((type) => file.name.endsWith(type))
		) {
			throw new Error(badFileTypeMsg)
		}

		uploadItem.status = 'uploading'
		const filePath = `${props.currentPath}/${file.name}`.replace('//', '/')

		const uploader = client.kyros.files_v0.uploadFile(filePath, file, {
			onProgress: ({ progress }) => {
				const index = uploadQueue.value.findIndex((item) => item.file.name === file.name)
				if (index !== -1) {
					uploadQueue.value[index].progress = Math.round(progress)
				}
			},
		})
		uploadItem.uploader = uploader

		await uploader.promise
		const index = uploadQueue.value.findIndex((item) => item.file.name === file.name)
		if (index !== -1 && uploadQueue.value[index].status !== 'cancelled') {
			uploadQueue.value[index].status = 'completed'
			uploadQueue.value[index].progress = 100
		}

		await nextTick()

		setTimeout(async () => {
			const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name)
			if (removeIndex !== -1) {
				uploadQueue.value.splice(removeIndex, 1)
				await nextTick()
			}
		}, 5000)

		emit('uploadComplete')
	} catch (error) {
		console.error('Error uploading file:', error)
		const index = uploadQueue.value.findIndex((item) => item.file.name === file.name)
		if (index !== -1 && uploadQueue.value[index].status !== 'cancelled') {
			const target = uploadQueue.value[index]

			if (error instanceof Error) {
				if (error.message === badFileTypeMsg) {
					target.status = 'incorrect-type'
				} else if (target.progress === 100 && error.message.includes('401')) {
					target.status = 'error-file-exists'
				} else {
					target.status = 'error-generic'
					target.error = error
				}
			}
		}

		setTimeout(async () => {
			const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name)
			if (removeIndex !== -1) {
				uploadQueue.value.splice(removeIndex, 1)
				await nextTick()
			}
		}, 5000)

		if (error instanceof Error && error.message !== 'Upload cancelled') {
			addNotification({
				title: 'Upload failed',
				text: `Failed to upload ${file.name}`,
				type: 'error',
			})
		}
	}
}

defineExpose({
	uploadFile,
	cancelUpload,
})
</script>

<style scoped>
.upload-status {
	overflow: hidden;
	transition: height 0.2s ease;
}

.upload-status-enter-active,
.upload-status-leave-active {
	transition: height 0.2s ease;
	overflow: hidden;
}

.upload-status-enter-from,
.upload-status-leave-to {
	height: 0 !important;
}

.status-icon-enter-active,
.status-icon-leave-active {
	transition: all 0.25s ease;
}

.status-icon-enter-from,
.status-icon-leave-to {
	transform: scale(0);
	opacity: 0;
}

.status-icon-enter-to,
.status-icon-leave-from {
	transform: scale(1);
	opacity: 1;
}
</style>
