<template>
  <div v-if="visible" class="dialog-overlay z-[60]">
    <div class="dialog relative">
      <div class="text-lg text-center font-light relative z-50">
        <div
          id="ViewHeading"
          class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none"
        >
          <span v-if="uploading"> Uploading... </span>
          <span v-else-if="blob"> Look Good? </span>
          <span v-else> Say "Cheese"! </span>
        </div>

        <div
          class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
          @click="close()"
        >
          <fa icon="xmark" class="w-[1em]"></fa>
        </div>
      </div>

      <div v-if="uploading" class="flex justify-center">
        <fa
          icon="spinner"
          class="fa-spin fa-3x text-center block px-12 py-12"
        />
      </div>
      <div v-else-if="blob">
        <div v-if="crop">
          <VuePictureCropper
            :boxStyle="{
              backgroundColor: '#f8f8f8',
              margin: 'auto',
            }"
            :img="createBlobURL(blob)"
            :options="{
              viewMode: 1,
              dragMode: 'crop',
              aspectRatio: 9 / 9,
            }"
            class="max-h-[90vh] max-w-[90vw] object-contain"
          />
          <!-- This gives a round cropper.
            :presetMode="{
              mode: 'round',
            }"
          -->
        </div>
        <div v-else>
          <div class="flex justify-center">
            <img
              :src="createBlobURL(blob)"
              class="mt-2 rounded max-h-[90vh] max-w-[90vw] object-contain"
            />
          </div>
        </div>
        <div class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
          <button
            @click="uploadImage"
            class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
          >
            <span>Upload</span>
          </button>
        </div>
        <div
          v-if="showRetry"
          class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
        >
          <button
            @click="retryImage"
            class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
          >
            <span>Retry</span>
          </button>
        </div>
      </div>
      <div v-else ref="cameraContainer">
        <!--
          Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically:
          :resolution="{ width: 375, height: 812 }"
        -->
        <camera
          facingMode="environment"
          autoplay
          ref="camera"
          @started="cameraStarted()"
        >
          <div
            class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
          >
            <button
              @click="takeImage()"
              class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
            >
              <fa icon="camera" class="w-[1em]"></fa>
            </button>
          </div>
          <div
            class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
          >
            <button
              @click="swapMirrorClass()"
              class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
            >
              <fa icon="left-right" class="w-[1em]"></fa>
            </button>
          </div>
          <div v-if="numDevices > 1" class="absolute bottom-2 right-4">
            <button
              @click="switchCamera()"
              class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
            >
              <fa icon="rotate" class="w-[1em]"></fa>
            </button>
          </div>
        </camera>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import axios from "axios";
import Camera from "simple-vue-camera";
import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";

import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { getIdentity } from "@/libs/util";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";

@Component({ components: { Camera, VuePictureCropper } })
export default class PhotoDialog extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;

  activeDeviceNumber = 0;
  activeDid = "";
  blob?: Blob;
  claimType = "";
  crop = false;
  fileName?: string;
  mirror = false;
  numDevices = 0;
  setImageCallback: (arg: string) => void = () => {};
  showRetry = true;
  uploading = false;
  visible = false;

  URL = window.URL || window.webkitURL;

  async mounted() {
    try {
      await db.open();
      const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
      this.activeDid = settings?.activeDid || "";
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (err: any) {
      console.error("Error retrieving settings from database:", err);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: err.message || "There was an error retrieving your settings.",
        },
        -1,
      );
    }
  }

  open(
    setImageFn: (arg: string) => void,
    claimType: string,
    crop?: boolean,
    blob?: Blob, // for image upload, just to use the cropping function
    inputFileName?: string,
  ) {
    this.visible = true;
    this.claimType = claimType;
    this.crop = !!crop;
    const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
    if (bottomNav) {
      bottomNav.style.display = "none";
    }
    this.setImageCallback = setImageFn;
    if (blob) {
      this.blob = blob;
      this.fileName = inputFileName;
      // doesn't make sense to retry the file upload; they can cancel if they picked the wrong one
      this.showRetry = false;
    } else {
      this.blob = undefined;
      this.fileName = undefined;
      this.showRetry = true;
    }
  }

  close() {
    this.visible = false;
    const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
    if (bottomNav) {
      bottomNav.style.display = "";
    }
    this.blob = undefined;
  }

  async cameraStarted() {
    const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
    if (cameraComponent) {
      this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
      this.mirror = cameraComponent.facingMode === "user";
      // figure out which device is active
      const currentDeviceId = cameraComponent.currentDeviceID();
      const devices = await cameraComponent.devices(["videoinput"]);
      this.activeDeviceNumber = devices.findIndex(
        (device) => device.deviceId === currentDeviceId,
      );
    }
  }

  async switchCamera() {
    const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
    this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
    const devices = await cameraComponent?.devices(["videoinput"]);
    await cameraComponent?.changeCamera(
      devices[this.activeDeviceNumber].deviceId,
    );
  }

  async takeImage(/* payload: MouseEvent */) {
    const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;

    /**
     * This logic to set the image height & width correctly.
     * Without it, the portrait orientation ends up with an image that is stretched horizontally.
     * Note that it's the same with raw browser Javascript; see the "drawImage" example below.
     * Now that I've done it, I can't explain why it works.
     */
    let imageHeight = cameraComponent?.resolution?.height;
    let imageWidth = cameraComponent?.resolution?.width;
    const initialImageRatio = imageWidth / imageHeight;
    const windowRatio = window.innerWidth / window.innerHeight;
    if (initialImageRatio > 1 && windowRatio < 1) {
      // the image is wider than it is tall, and the window is taller than it is wide
      // For some reason, mobile in portrait orientation renders a horizontally-stretched image.
      // We're gonna force it opposite.
      imageHeight = cameraComponent?.resolution?.width;
      imageWidth = cameraComponent?.resolution?.height;
    } else if (initialImageRatio < 1 && windowRatio > 1) {
      // the image is taller than it is wide, and the window is wider than it is tall
      // Haven't seen this happen, but we'll do it just in case.
      imageHeight = cameraComponent?.resolution?.width;
      imageWidth = cameraComponent?.resolution?.height;
    }
    const newImageRatio = imageWidth / imageHeight;
    if (newImageRatio < windowRatio) {
      // the image is a taller ratio than the window, so fit the height first
      imageHeight = window.innerHeight / 2;
      imageWidth = imageHeight * newImageRatio;
    } else {
      // the image is a wider ratio than the window, so fit the width first
      imageWidth = window.innerWidth / 2;
      imageHeight = imageWidth / newImageRatio;
    }

    // The resolution is only necessary because of that mobile portrait-orientation case.
    // The mobile emulation in a browser shows something stretched vertically, but real devices work fine.
    this.blob =
      (await cameraComponent?.snapshot({
        height: imageHeight,
        width: imageWidth,
      })) || undefined;
    // png is default
    this.fileName = "snapshot.png";
    if (!this.blob) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "There was an error taking the picture. Please try again.",
        },
        5000,
      );
      return;
    }
  }

  private createBlobURL(blob: Blob): string {
    return URL.createObjectURL(blob);
  }

  async retryImage() {
    this.blob = undefined;
  }

  /****

   Here's an approach to photo capture without a library. It has similar quirks.
   Now that we've fixed styling for simple-vue-camera, it's not critical to refactor. Maybe someday.

   <button id="start-camera" @click="cameraClicked">Start Camera</button>
   <video id="video" width="320" height="240" autoplay></video>
   <button id="snap-photo" @click="photoSnapped">Snap Photo</button>
   <canvas id="canvas" width="320" height="240"></canvas>

  async cameraClicked() {
    const video = document.querySelector("#video");
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: false,
    });
    if (video instanceof HTMLVideoElement) {
      video.srcObject = stream;
    }
  }
  photoSnapped() {
    const video = document.querySelector("#video");
    const canvas = document.querySelector("#canvas");
    if (
      canvas instanceof HTMLCanvasElement &&
      video instanceof HTMLVideoElement
    ) {
      canvas
        ?.getContext("2d")
        ?.drawImage(video, 0, 0, canvas.width, canvas.height);
      // ... or set the blob:
      // canvas?.toBlob(
      //   (blob) => {
      //     this.blob = blob;
      //   },
      //   "image/jpeg",
      //   1,
      // );

      // data url of the image
      const image_data_url = canvas?.toDataURL("image/jpeg");
    }
  }
  ****/

  async uploadImage() {
    this.uploading = true;

    if (this.crop) {
      this.blob = (await cropper?.getBlob()) || undefined;
    }

    const identifier = await getIdentity(this.activeDid);
    const token = await accessToken(identifier);
    const headers = {
      Authorization: "Bearer " + token,
    };
    const formData = new FormData();
    if (!this.blob) {
      // yeah, this should never happen, but it helps with subsequent type checking
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "There was an error finding the picture. Please try again.",
        },
        5000,
      );
      this.uploading = false;
      return;
    }
    formData.append("image", this.blob, this.fileName || "snapshot.png");
    formData.append("claimType", this.claimType);
    try {
      const response = await axios.post(
        DEFAULT_IMAGE_API_SERVER + "/image",
        formData,
        { headers },
      );
      this.uploading = false;

      this.close();
      this.setImageCallback(response.data.url as string);
    } catch (error) {
      console.error("Error uploading the image", error);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "There was an error saving the picture.",
        },
        5000,
      );
      this.uploading = false;
      this.blob = undefined;
    }
  }

  swapMirrorClass() {
    this.mirror = !this.mirror;
    if (this.mirror) {
      (this.$refs.cameraContainer as HTMLElement).classList.add("mirror-video");
    } else {
      (this.$refs.cameraContainer as HTMLElement).classList.remove(
        "mirror-video",
      );
    }
  }
}
</script>

<style>
.dialog-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 1.5rem;
}

.dialog {
  background-color: white;
  padding: 1rem;
  border-radius: 0.5rem;
  width: 100%;
  max-width: 700px;
}

.mirror-video {
  transform: scaleX(-1);
  -webkit-transform: scaleX(-1); /* For Safari */
  -moz-transform: scaleX(-1); /* For Firefox */
  -ms-transform: scaleX(-1); /* For IE */
  -o-transform: scaleX(-1); /* For Opera */
}
</style>
