<!--
Assistant de création d'image

@see https://norserium.github.io/vue-advanced-cropper/
-->
<template>
  <div class="input-image">
    <UiImage
      ref="uiImage"
      :image-id="modelValue"
      :default-image="defaultImage"
      :width="width"
      :height="height"
      class="image"
      overlay-icon="fas fa-upload"
      @overlay-clicked="openModal()"
    />

    <LazyLayoutDialog :show="showModal">
      <template #dialogType>{{ $t('image_assistant') }}</template>
      <template #dialogTitle>{{ $t('modif_picture') }}</template>
      <template #dialogText>
        <div class="upload">
          <v-row
            v-if="pending"
            class="fill-height ma-0 loading"
            align="center"
            justify="center"
          >
            <v-progress-circular :indeterminate="true" color="neutral">
            </v-progress-circular>
          </v-row>

          <div v-else>
            <div class="upload__cropper-wrapper">
              <Cropper
                ref="cropper"
                class="upload__cropper"
                check-orientation
                :src="currentImage.src"
                :default-position="defaultPosition"
                :default-size="defaultSize"
                @change="onCropperChange"
              />

              <div
                v-if="currentImage.src"
                class="upload__reset-button"
                title="Reset Image"
                @click="reset()"
              >
                <v-icon>fas fa-trash</v-icon>
              </div>
            </div>

            <div class="upload__buttons-wrapper">
              <button class="upload__button" @click="fileInput?.click()">
                <input
                  ref="fileInput"
                  type="file"
                  accept="image/*"
                  @change="uploadImage($event)"
                />
                {{ $t('upload_image') }}
              </button>
            </div>

            <span class="max-size-label">{{ $t('max_size_4_mb') }}</span>
          </div>
        </div>
      </template>
      <template #dialogBtn>
        <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="cancel">
          {{ $t('cancel') }}
        </v-btn>
        <v-btn
          class="submitBtn theme-primary"
          @click="save"
          :disabled="pending"
        >
          {{ $t('save') }}
        </v-btn>
      </template>
    </LazyLayoutDialog>
  </div>
</template>

<script setup lang="ts">
import { Cropper } from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
import { type Ref, ref } from '@vue/reactivity'
import File from '~/models/Core/File'
import type { PropType } from '@vue/runtime-core'
import { useEntityManager } from '~/composables/data/useEntityManager'
import { useImageManager } from '~/composables/data/useImageManager'
import { FILE_VISIBILITY, TYPE_ALERT } from '~/types/enum/enums'
import { usePageStore } from '~/stores/page'
import FileUtils from '~/services/utils/fileUtils'

const props = defineProps({
  /**
   * Id de l'objet File, ou null
   */
  modelValue: {
    type: Number as PropType<number | null>,
    required: false,
    default: null,
  },
  /**
   * Label du champ
   * Si non défini, c'est le nom de propriété qui est utilisé
   */
  field: {
    type: String,
    required: false,
    default: null,
  },
  /**
   * Image par défaut en cas d'absence d'une image uploadée
   */
  defaultImage: {
    type: String,
    required: false,
  },
  /**
   * Hauteur de l'image à l'écran (en px)
   */
  height: {
    type: Number,
    required: false,
  },
  /**
   * Largeur de l'image à l'écran (en px)
   */
  width: {
    type: Number,
    required: false,
  },
  /**
   * TODO: completer
   */
  ownerId: {
    type: Number,
    required: false,
  },
})

const { em } = useEntityManager()
const { imageManager } = useImageManager()
const pageStore = usePageStore()

const emit = defineEmits(['update:modelValue'])

/**
 * Références à des composants
 */
const fileInput: Ref<null | any> = ref(null)
const cropper: Ref<any> = ref(null)
const uiImage: Ref<any> = ref(null)

/**
 * L'objet File ou l'image sont en cours de chargement
 */
const pending: Ref<boolean> = ref(false)

/**
 * Affiche la modale d'upload / modification de l'image
 */
const showModal = ref(false)

/**
 * L'objet File contenant les informations de l'image
 */
const file: Ref<File | null> = ref(null)

/**
 * Données d'une nouvelle image uploadée par l'utilisateur
 */
const currentImage: Ref<{
  id: string | number | null
  src: string | null
  content: string | null
  name: string | null
}> = ref({
  id: null,
  src: null,
  content: null,
  name: null,
})

/**
 * Taille maximale autorisée pour les images uploadées (en bytes)
 */
const MAX_FILE_SIZE = 4 * 1024 * 1024

/**
 * Coordonnées du cropper
 */
const cropperConfig: Ref<{
  left?: number
  top?: number
  height?: number
  width?: number
}> = ref({})

/**
 * @see https://advanced-cropper.github.io/vue-advanced-cropper/components/cropper.html#defaultposition
 */
const defaultPosition = () => {
  return { left: cropperConfig.value.left, top: cropperConfig.value.top }
}

/**
 * @see https://advanced-cropper.github.io/vue-advanced-cropper/components/cropper.html#defaultsize
 */
const defaultSize = (params: any): { width: number; height: number } | null => {
  if (!params) {
    return null
  }
  const { imageSize, visibleArea } = params

  return {
    width: cropperConfig.value.width ?? (visibleArea || imageSize).width,
    height: cropperConfig.value.height ?? (visibleArea || imageSize).height,
  }
}

/**
 * Charge l'image correspondant au fichier dans le cropper
 *
 * @param fileId
 */
const loadImage = async (fileId: number) => {
  file.value = (await em.fetch(File, fileId)) as File

  if (file.value.config) {
    const fileConfig = JSON.parse(file.value.config)
    cropperConfig.value.left = fileConfig.x
    cropperConfig.value.top = fileConfig.y
    cropperConfig.value.height = fileConfig.height
    cropperConfig.value.width = fileConfig.width
  }

  currentImage.value.name = file.value.name
  currentImage.value.id = file.value.id
  currentImage.value.src = (await imageManager.get(fileId)) as string
}

/**
 * Affiche la modale d'upload / modification de l'image
 */
const openModal = async () => {
  showModal.value = true
  pending.value = true

  if (props.modelValue !== null) {
    // Un objet File existe déjà: on le récupère
    await loadImage(props.modelValue)
  } else {
    // Nouveau File
    file.value = em.newInstance(File) as File
  }

  pending.value = false
}

/**
 * Réinitialise l'image actuellement chargée dans le cropper
 */
const reset = () => {
  if (currentImage.value.src !== null) {
    URL.revokeObjectURL(currentImage.value.src)
  }

  currentImage.value = {
    src: null,
    content: null,
    name: null,
    id: null,
  }

  cropperConfig.value = {
    left: undefined,
    height: undefined,
    top: undefined,
    width: undefined,
  }
}

/**
 * Upload une image depuis le poste client
 * @param event
 */
const uploadImage = async (event: any) => {
  const { files } = event.target

  if (!files || !files[0]) {
    return
  }

  const uploadedFile = files[0]

  if (uploadedFile.size > MAX_FILE_SIZE) {
    pageStore.alerts.push({
      type: TYPE_ALERT.ALERT,
      messages: ['file_too_large'],
    })
    return
  }

  // Met à jour l'image dans le cropper
  currentImage.value.id = null
  currentImage.value.name = uploadedFile.name
  currentImage.value.src = URL.createObjectURL(uploadedFile)
  currentImage.value.content = await FileUtils.blobToBase64(uploadedFile)

  // Met à jour la configuration du cropper
  cropperConfig.value.top = 0
  cropperConfig.value.left = 0
  cropperConfig.value.height = uploadedFile.height
  cropperConfig.value.width = uploadedFile.width
}

/**
 * Lorsque le cropper change de position / taille, on met à jour les coordonnées
 * @param newCoordinates
 */
const onCropperChange = ({ coordinates: newCoordinates }: any) => {
  cropperConfig.value = newCoordinates
}

/**
 * Annule l'upload et les modifications, et ferme la modale
 */
const cancel = () => {
  reset()
  showModal.value = false
}

/**
 * Enregistre une nouvelle image et retourne l'id du fichier nouvellement créé
 */
const saveNewImage = async (): Promise<number> => {
  if (!file.value) {
    throw new Error('No File object defined')
  }
  if (!currentImage.value.name) {
    throw new Error("Missing file's name")
  }
  if (!currentImage.value.content) {
    throw new Error("Missing file's content")
  }

  const config = JSON.stringify({
    x: cropperConfig.value.left,
    y: cropperConfig.value.top,
    height: cropperConfig.value.height,
    width: cropperConfig.value.width,
  })

  const response = (await imageManager.upload(
    currentImage.value.name,
    currentImage.value.content,
    FILE_VISIBILITY.EVERYBODY,
    config,
  )) as any

  return response.fileId
}

/**
 * Met à jour l'image existante
 */
const saveExistingImage = async () => {
  if (!file.value) {
    throw new Error('No File object defined')
  }

  file.value.config = JSON.stringify({
    x: cropperConfig.value.left,
    y: cropperConfig.value.top,
    height: cropperConfig.value.height,
    width: cropperConfig.value.width,
  })

  await em.persist(file.value)
}

/**
 * Lorsque l'on sauvegarde l'image
 */
// TODO: Voir si tout ou partie de cette fonction peut passer dans le useImageFetch, imageManager ou imageUtils
const save = async () => {
  pageStore.loading = true

  if (currentImage.value.src && currentImage.value.id === null) {
    // Une nouvelle image a été uploadée
    const fileId = await saveNewImage()
    emit('update:modelValue', fileId)
  } else if (currentImage.value.id) {
    // L'image existante a été modifiée
    await saveExistingImage()
    uiImage.value.refresh()
  } else {
    // On a reset l'image
    emit('update:modelValue', null)
  }

  showModal.value = false
  pageStore.loading = false
}

/**
 * Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
 */
onUnmounted(() => {
  if (currentImage.value && currentImage.value.src) {
    URL.revokeObjectURL(currentImage.value.src)
  }
})
</script>

<style scoped lang="scss">
:deep(.vue-advanced-cropper__stretcher) {
  height: auto !important;
  width: auto !important;
}

.loading {
  height: 300px;
}

.upload {
  user-select: none;
  padding: 20px;
  display: block;
  &__cropper {
    border: solid 1px rgb(var(--v-theme-on-neutral-strong));
    min-height: 300px;
    max-height: 300px;
  }
  &__cropper-wrapper {
    position: relative;
  }
  &__reset-button {
    position: absolute;
    right: 20px;
    bottom: 20px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 42px;
    width: 42px;
    background: rgb(var(--v-theme-neutral));
    transition: background 0.5s;
    &:hover {
      background: rgb(var(--v-theme-primary-alt));
    }
  }
  &__buttons-wrapper {
    display: flex;
    justify-content: center;
    margin-top: 17px;
  }
  &__button {
    border: none;
    outline: solid transparent;
    color: rgb(var(--v-theme-on-neutral));
    font-size: 16px;
    padding: 10px 20px;
    background: rgb(var(--v-theme-neutral));
    cursor: pointer;
    transition: background 0.5s;
    margin: 0 16px;
    &:hover,
    &:focus {
      background: rgb(var(--v-theme-primary-alt));
    }
    input {
      display: none;
    }
  }
}

.max-size-label {
  display: block;
  width: 100%;
  text-align: center;
  font-size: 13px;
  color: rgb(var(--v-theme-on-neutral-soft));
  margin-top: 6px;
}
</style>
