<template>
  <div
    :class="attrs.class"
    :style="attrs.style"
    @click="pickFile"
    @dragover="handleDragOver"
    @dragleave="handleDragLeave"
    @drop="handleDrop"
  >
    <div v-if="label || $slots.label" class="q-pl-xs">
      <slot name="label">
        <span>{{ label }}</span>
      </slot>
      <span v-if="required" class="text-negative q-ml-xs">*</span>
    </div>
    <q-field
      :model-value="preview"
      v-bind="inputAttrs"
      borderless
      hide-bottom-space
      :rules="rules"
      :error="error || Boolean(sizeError)"
      :error-message="sizeError || errorMessage"
      class="image-preview-container"
    >
      <div
        class="file-picker-inner"
        :class="{
          'has-image': preview,
          'is-dragging': isDragging,
        }"
      >
        <template v-if="!preview">
          <q-icon
            :name="
              isDragging
                ? 'mdi-upload'
                : type === 'image'
                  ? 'mdi-image-plus'
                  : 'mdi-file-plus'
            "
            size="48px"
            color="grey-6"
            class="absolute-center"
          />
          <div class="text-caption text-grey-6 text-center absolute-bottom q-pb-sm q-px-md">
            {{
              isDragging
                ? $t("common.ui.file.drag_and_drop")
                : type === "image"
                  ? $t("common.ui.file.click_select_image")
                  : $t("common.ui.file.click_select")
            }}
            <div>{{ $t("common.ui.file.max_size_hint", { max: maxSizeMB }) }}</div>
          </div>
        </template>

        <div
          v-else-if="isImagePreview"
          class="file-picker-image"
          :style="{ backgroundImage: `url('${preview}')` }"
        />

        <div v-else class="column flex-center" style="height: 100%">
          <q-icon name="mdi-file-check" size="48px" color="grey-6" />
          <div class="text-caption text-grey-6 text-center q-mt-sm q-px-md">
            {{ internalFile?.name }}
          </div>
        </div>
      </div>

      <q-file
        v-show="false"
        ref="fileInputRef"
        v-model="pickedFile"
        :accept="accept"
        :multiple="multiple"
      />
    </q-field>
  </div>
</template>

<script setup>
import { ref, watch, computed, onUnmounted, useTemplateRef, useAttrs } from "vue";
import { useMediaFile } from "src/composables/useMediaFile";

defineOptions({
  inheritAttrs: false,
});

const props = defineProps({
  label: {
    type: String,
    default: "Select Image",
  },
  rules: {
    type: Array,
    default: () => [],
  },
  accept: {
    type: String,
    default: "image/*",
  },
  type: {
    type: String,
    default: "image",
  },
  multiple: {
    type: Boolean,
    default: false,
  },
  initialImage: {
    type: String,
    default: null,
  },
  error: {
    type: Boolean,
    default: false,
  },
  errorMessage: {
    type: String,
    default: "",
  },
});

const emit = defineEmits(["files"]);

const attrs = useAttrs();
const fileInputRef = useTemplateRef("fileInputRef");
const { prepareFile, prepareFiles, maxSizeMB } = useMediaFile();

const model = defineModel({ type: [File, String, null], default: null });
const base64File = defineModel("base64File", { type: String, default: null });

const isDragging = ref(false);
const pickedFile = ref(null);
const internalFile = ref(null);
const sizeError = ref(null);
const objectUrl = ref(null);

const required = computed(() => props.rules.some((r) => r?.$id === "required"));

const isImagePreview = computed(() => {
  if (internalFile.value instanceof File) {
    return internalFile.value.type.startsWith("image/");
  }
  return props.type === "image";
});

const preview = computed(() => {
  if (internalFile.value instanceof File) {
    return isImagePreview.value ? objectUrl.value : "file_selected";
  }
  return props.initialImage || null;
});

const inputAttrs = computed(() => {
  // eslint-disable-next-line
  const { class: _, style: __, ...rest } = attrs;
  return rest;
});

function pickFile() {
  fileInputRef.value?.pickFiles();
}

function handleDragOver(event) {
  event.preventDefault();
  event.dataTransfer.dropEffect = "copy";
  isDragging.value = true;
}

function handleDragLeave() {
  isDragging.value = false;
}

function matchesAccept(file) {
  const acceptedMimes = props.accept
    .split(",")
    .map((m) => m.trim())
    .filter(Boolean);

  return acceptedMimes.some((mime) =>
    mime.endsWith("/*") ? file.type.startsWith(mime.slice(0, -1)) : mime === file.type,
  );
}

function handleDrop(event) {
  event.preventDefault();
  isDragging.value = false;

  const dropped = Array.from(event.dataTransfer?.files ?? []).filter(matchesAccept);
  if (!dropped.length) return;

  pickedFile.value = props.multiple ? dropped : dropped[0];
}

async function acceptFile(file) {
  if (!(file instanceof File)) {
    sizeError.value = null;
    internalFile.value = null;
    return;
  }

  const { file: prepared, message } = await prepareFile(file);
  sizeError.value = message;
  internalFile.value = prepared;
}

function generateBase64(file) {
  if (!file) { base64File.value = null; return; }
  const reader = new FileReader();
  reader.onload = (e) => { base64File.value = e.target.result; };
  reader.onerror = () => { base64File.value = null; };
  reader.readAsDataURL(file);
}

watch(pickedFile, async (picked) => {
  if (picked === null) return;

  pickedFile.value = null;

  if (props.multiple) {
    const selected = Array.isArray(picked) ? picked : [picked];
    const accepted = await prepareFiles(selected);
    if (accepted.length) emit("files", accepted);
    return;
  }

  await acceptFile(picked);
});

watch(internalFile, (newFile) => {
  if (objectUrl.value) {
    URL.revokeObjectURL(objectUrl.value);
    objectUrl.value = null;
  }
  if (newFile instanceof File) {
    if (newFile.type.startsWith("image/")) {
      objectUrl.value = URL.createObjectURL(newFile);
    }
    model.value = newFile;
    generateBase64(newFile);
  } else {
    model.value = null;
    base64File.value = null;
  }
});

watch(model, (val) => {
  if (!val && internalFile.value) {
    internalFile.value = null;
    sizeError.value = null;
  }
});

onUnmounted(() => {
  if (objectUrl.value) {
    URL.revokeObjectURL(objectUrl.value);
    objectUrl.value = null;
  }
});
</script>

<style lang="scss">
@use "sass:map";
@use "src/css/quasar.variables.scss";

.image-preview-container {
  display: flex;
  justify-content: center;
  align-items: stretch;

  .q-field__inner {
    .body--light & {
      --image-bg-color: #{map.get($colors, "surface")};
      --image-border-color: #{map.get($colors, "violet-normal")};
      --image-border-hover-color: #{map.get($colors, "violet-normal-hover")};
    }

    height: 100%;
    display: flex;
    flex-direction: column;
    transition: border-color 0.3s;
  }

  .q-field__control {
    height: 100%;
    min-height: 200px;
    border: 2px dashed var(--image-border-color);
    border-radius: 8px;
    cursor: pointer;
    overflow: hidden;

    &:has(.has-image) {
      border-style: solid;
      border-color: var(--image-border-color);
    }

    &:has(.is-dragging) {
      border-color: var(--image-border-hover-color);
      opacity: 0.8;
    }
  }

  .q-field__control-container {
    height: 100%;
    justify-content: stretch;
    align-items: stretch;
  }

  .q-field__append {
    position: absolute;
    top: -5px;
    right: 10px;
  }

  .file-picker-inner {
    position: relative;
    width: 100%;
    height: 100%;
    min-height: 196px;
  }

  .file-picker-image {
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    border-radius: 6px;
  }
}
</style>
