<script setup lang="ts">
import { computed, ref } from 'vue'
import { useDropZone } from '@vueuse/core'
import { Button } from '@/components/ui/button'
import { Upload } from 'lucide-vue-next'
import { cn } from '@/lib/utils'

interface DropzoneProps {
    accept?: Record<string, string[]>
    maxFiles?: number
    maxSize?: number
    minSize?: number
    disabled?: boolean
    src?: File[]
    class?: string
}

const props = withDefaults(defineProps<DropzoneProps>(), {
    maxFiles: 1,
    accept: () => ({}),
    src: () => []
})

const emit = defineEmits<{
    drop: [acceptedFiles: File[], event: DragEvent]
    error: [error: Error]
}>()

const files = ref<File[]>(props.src || [])
const error = ref<string>('')

const renderBytes = (bytes: number): string => {
    const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
    let size = bytes
    let unitIndex = 0

    while (size >= 1024 && unitIndex < units.length - 1) {
        size /= 1024
        unitIndex++
    }

    return `${size.toFixed(2)}${units[unitIndex]}`
}

const validateFile = (file: File): string => {
    if (props.minSize && file.size < props.minSize) {
        return `File must be at least ${renderBytes(props.minSize)}`
    }

    if (props.maxSize && file.size > props.maxSize) {
        return `File must be less than ${renderBytes(props.maxSize)}`
    }

    if (props.accept && Object.keys(props.accept).length > 0) {
        const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase()
        const mimeType = file.type

        const isAccepted = Object.entries(props.accept).some(([mime, extensions]) => {
            return (
                mimeType === mime ||
                extensions.some((ext) => fileExtension === ext.toLowerCase())
            )
        })

        if (!isAccepted) {
            const acceptedTypes = Object.keys(props.accept).join(', ')
            return `File type not accepted. Accepted types: ${acceptedTypes}`
        }
    }

    return ''
}

const onDrop = (droppedFiles: File[] | null, event: DragEvent) => {
    if (!droppedFiles || props.disabled) return

    error.value = ''
    const validFiles: File[] = []
    const newFiles: File[] = []

    droppedFiles.slice(0, props.maxFiles).forEach((file) => {
        const validationError = validateFile(file)
        if (validationError) {
            error.value = validationError
            emit('error', new Error(validationError))
        } else {
            validFiles.push(file)
        }
    })

    if (validFiles.length > 0) {
        newFiles.push(...validFiles)

        if (props.maxFiles === 1) {
            files.value = newFiles
        } else {
            const remainingSlots = props.maxFiles - files.value.length
            files.value = [...files.value, ...newFiles.slice(0, remainingSlots)]
        }

        emit('drop', newFiles, event)
    }
}

const dropZoneRef = ref<HTMLElement>()
const { isOverDropZone } = useDropZone(dropZoneRef, {
    onDrop,
    dataTypes: props.accept ? Object.keys(props.accept) : []
})

const handleClick = () => {
    if (props.disabled) return

    const input = document.createElement('input')
    input.type = 'file'
    input.multiple = props.maxFiles > 1
    input.accept = props.accept ? Object.keys(props.accept).join(',') : '*/*'

    input.onchange = (e: Event) => {
        const target = e.target as HTMLInputElement
        if (target.files) {
            onDrop(Array.from(target.files), e as unknown as DragEvent)
        }
    }

    input.click()
}

const maxLabelItems = 3
const fileListLabel = computed(() => {
    if (files.value.length === 0) return ''

    if (files.value.length > maxLabelItems) {
        const firstThree = files.value.slice(0, maxLabelItems).map(f => f.name)
        const remaining = files.value.length - maxLabelItems
        return `${new Intl.ListFormat('en').format(firstThree)} and ${remaining} more`
    }

    return new Intl.ListFormat('en').format(files.value.map(f => f.name))
})

const captionText = computed(() => {
    let caption = ''

    if (Object.keys(props.accept).length > 0) {
        caption += 'Accepts '
        caption += new Intl.ListFormat('en').format(Object.keys(props.accept))
    }

    if (props.minSize && props.maxSize) {
        caption += ` between ${renderBytes(props.minSize)} and ${renderBytes(props.maxSize)}`
    } else if (props.minSize) {
        caption += ` at least ${renderBytes(props.minSize)}`
    } else if (props.maxSize) {
        caption += ` less than ${renderBytes(props.maxSize)}`
    }

    return caption
})

defineExpose({
    files,
    clearFiles: () => { files.value = [] },
    addFiles: (newFiles: File[]) => {
        const remainingSlots = props.maxFiles - files.value.length
        files.value = [...files.value, ...newFiles.slice(0, remainingSlots)]
    }
})
</script>

<template>
    <Button ref="dropZoneRef" :class="cn(
        'relative h-auto w-full flex-col overflow-hidden p-8',
        isOverDropZone && 'outline-none ring-1 ring-ring',
        props.class
    )
        " :disabled="disabled" type="button" variant="outline" @click="handleClick">
        <div v-if="files.length === 0" class="flex flex-col items-center justify-center">
            <div class="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
                <Upload :size="16" />
            </div>
            <p class="my-2 w-full truncate text-wrap font-medium text-sm">
                Upload {{ maxFiles === 1 ? 'a file' : 'files' }}
            </p>
            <p class="w-full truncate text-wrap text-muted-foreground text-xs">
                Drag and drop or click to upload
            </p>
            <p v-if="captionText" class="text-wrap text-muted-foreground text-xs">
                {{ captionText }}.
            </p>
        </div>

        <div v-else class="flex flex-col items-center justify-center">
            <div class="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
                <Upload :size="16" />
            </div>
            <p class="my-2 w-full truncate font-medium text-sm">
                {{ fileListLabel }}
            </p>
            <p class="w-full text-wrap text-muted-foreground text-xs">
                Drag and drop or click to replace
            </p>
        </div>

        <p v-if="error" class="mt-2 text-sm text-destructive">{{ error }}</p>
    </Button>
</template>
