/** * PhotoDialog.vue - Cross-platform photo capture and selection component * *
This component provides a unified interface for taking photos and selecting
images * across different platforms (web, mobile) using the PlatformService. It
supports: * - Taking photos using device camera * - Selecting images from device
gallery * - Image cropping functionality * - Image upload to server * - Error
handling and user feedback * * Features: * - Responsive design with mobile-first
approach * - Cross-platform compatibility through PlatformService * - Image
cropping with aspect ratio control * - Progress feedback during upload * -
Comprehensive error handling * * @author Matthew Raymer * @version 1.0.0 * @file
PhotoDialog.vue */

<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 inset-x-0 px-4 py-2 bg-black/50 text-white leading-none pointer-events-none"
        >
          <span v-if="uploading"> Uploading... </span>
          <span v-else-if="blob"> Look Good? </span>
          <span v-else-if="showCameraPreview"> Take Photo </span>
          <span v-else> Say "Cheese"! </span>
        </div>

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

      <div v-if="uploading" class="flex justify-center">
        <font-awesome
          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
            :box-style="{
              backgroundColor: '#f8f8f8',
              margin: 'auto',
            }"
            :img="createBlobURL(blob)"
            :options="{
              viewMode: 1,
              dragMode: 'crop',
              aspectRatio: 1 / 1,
            }"
            class="max-h-[90vh] max-w-[90vw] object-contain"
          />
        </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="grid grid-cols-2 gap-2 mt-2">
          <button
            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-2 px-3 rounded-md"
            @click="uploadImage"
          >
            <span>Upload</span>
          </button>
          <button
            v-if="showRetry"
            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-2 px-3 rounded-md"
            @click="retryImage"
          >
            <span>Retry</span>
          </button>
        </div>
      </div>
      <div v-else-if="showCameraPreview" class="camera-preview">
        <div class="camera-container">
          <video
            ref="videoElement"
            class="camera-video"
            autoplay
            playsinline
            muted
          ></video>
          <button
            class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
            @click="capturePhoto"
          >
            <font-awesome icon="camera" class="w-[1em]" />
          </button>
        </div>
      </div>
      <div v-else>
        <div class="flex flex-col items-center justify-center gap-4 p-4">
          <button
            v-if="isRegistered"
            class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
            @click="startCameraPreview"
          >
            <font-awesome icon="camera" class="w-[1em]" />
          </button>
          <button
            class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
            @click="pickPhoto"
          >
            <font-awesome icon="image" class="w-[1em]" />
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";
import {
  DEFAULT_IMAGE_API_SERVER,
  NotificationIface,
  USE_DEXIE_DB,
} from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";

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

  /** Active DID for user authentication */
  activeDid = "";

  /** Current image blob being processed */
  blob?: Blob;

  /** Type of claim for the image */
  claimType = "";

  /** Whether to show cropping interface */
  crop = false;

  /** Name of the selected file */
  fileName?: string;

  /** Callback function to set image URL after upload */
  setImageCallback: (arg: string) => void = () => {};

  /** Whether to show retry button */
  showRetry = true;

  /** Upload progress state */
  uploading = false;

  /** Dialog visibility state */
  visible = false;

  /** Whether to show camera preview */
  showCameraPreview = false;

  /** Camera stream reference */
  private cameraStream: MediaStream | null = null;

  private platformService = PlatformServiceFactory.getInstance();
  URL = window.URL || window.webkitURL;

  isRegistered = false;
  private platformCapabilities = this.platformService.getCapabilities();

  /**
   * Lifecycle hook: Initializes component and retrieves user settings
   * @throws {Error} When settings retrieval fails
   */
  async mounted() {
    // logger.log("PhotoDialog mounted");
    try {
      let settings = await databaseUtil.retrieveSettingsForActiveAccount();
      if (USE_DEXIE_DB) {
        settings = await retrieveSettingsForActiveAccount();
      }
      this.activeDid = settings.activeDid || "";
      this.isRegistered = !!settings.isRegistered;
      logger.log("isRegistered:", this.isRegistered);
    } catch (error: unknown) {
      logger.error("Error retrieving settings from database:", error);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text:
            error instanceof Error
              ? error.message
              : "There was an error retrieving your settings.",
        },
        -1,
      );
    }
  }

  /**
   * Lifecycle hook: Cleans up camera stream when component is destroyed
   */
  beforeDestroy() {
    this.stopCameraPreview();
  }

  /**
   * Opens the photo dialog with specified configuration
   * @param setImageFn - Callback function to handle image URL after upload
   * @param claimType - Type of claim for the image
   * @param crop - Whether to enable cropping
   * @param blob - Optional existing image blob
   * @param inputFileName - Optional filename for the image
   */
  async open(
    setImageFn: (arg: string) => void,
    claimType: string,
    crop?: boolean,
    blob?: Blob,
    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;
      this.showRetry = false;
    } else {
      this.blob = undefined;
      this.fileName = undefined;
      this.showRetry = true;
      // Start camera preview automatically if no blob is provided
      if (!this.platformCapabilities.isMobile) {
        await this.startCameraPreview();
      }
    }
  }

  /**
   * Closes the photo dialog and resets state
   */
  close() {
    logger.debug(
      "Dialog closing, current showCameraPreview:",
      this.showCameraPreview,
    );
    this.visible = false;
    this.stopCameraPreview();
    const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
    if (bottomNav) {
      bottomNav.style.display = "";
    }
    this.blob = undefined;
  }

  /**
   * Starts the camera preview
   */
  async startCameraPreview() {
    logger.debug("startCameraPreview called");
    logger.debug("Current showCameraPreview state:", this.showCameraPreview);
    logger.debug("Platform capabilities:", this.platformCapabilities);

    // If we're on a mobile device or using Capacitor, use the platform service
    if (this.platformCapabilities.isMobile) {
      logger.debug("Using platform service for mobile device");
      try {
        const result = await this.platformService.takePicture();
        this.blob = result.blob;
        this.fileName = result.fileName;
      } catch (error) {
        logger.error("Error taking picture:", error);
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error",
            text: "Failed to take picture. Please try again.",
          },
          5000,
        );
      }
      return;
    }

    // For desktop web browsers, use our custom preview
    logger.debug("Starting camera preview for desktop browser");
    try {
      // Set state before requesting camera access
      this.showCameraPreview = true;
      logger.debug("showCameraPreview set to:", this.showCameraPreview);

      // Force a re-render
      await this.$nextTick();
      logger.debug(
        "After nextTick, showCameraPreview is:",
        this.showCameraPreview,
      );

      logger.debug("Requesting camera access...");
      const stream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: "environment" },
      });
      logger.debug("Camera access granted, setting up video element");
      this.cameraStream = stream;

      // Force another re-render after getting the stream
      await this.$nextTick();
      logger.debug(
        "After getting stream, showCameraPreview is:",
        this.showCameraPreview,
      );

      const videoElement = this.$refs.videoElement as HTMLVideoElement;
      if (videoElement) {
        logger.debug("Video element found, setting srcObject");
        videoElement.srcObject = stream;
        // Wait for video to be ready
        await new Promise((resolve) => {
          videoElement.onloadedmetadata = () => {
            logger.debug("Video metadata loaded");
            videoElement.play().then(() => {
              logger.debug("Video playback started");
              resolve(true);
            });
          };
        });
      } else {
        logger.error("Video element not found");
      }
    } catch (error) {
      logger.error("Error starting camera preview:", error);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "Failed to access camera. Please try again.",
        },
        5000,
      );
      this.showCameraPreview = false;
    }
  }

  /**
   * Stops the camera preview and cleans up resources
   */
  stopCameraPreview() {
    logger.debug(
      "Stopping camera preview, current showCameraPreview:",
      this.showCameraPreview,
    );
    if (this.cameraStream) {
      this.cameraStream.getTracks().forEach((track) => track.stop());
      this.cameraStream = null;
    }
    this.showCameraPreview = false;
    logger.debug(
      "After stopping, showCameraPreview is:",
      this.showCameraPreview,
    );
  }

  /**
   * Captures a photo from the camera preview
   */
  async capturePhoto() {
    if (!this.cameraStream) return;

    try {
      const videoElement = this.$refs.videoElement as HTMLVideoElement;
      const canvas = document.createElement("canvas");
      canvas.width = videoElement.videoWidth;
      canvas.height = videoElement.videoHeight;
      const ctx = canvas.getContext("2d");
      ctx?.drawImage(videoElement, 0, 0, canvas.width, canvas.height);

      canvas.toBlob(
        (blob) => {
          if (blob) {
            this.blob = blob;
            this.fileName = `photo_${Date.now()}.jpg`;
            this.stopCameraPreview();
          }
        },
        "image/jpeg",
        0.95,
      );
    } catch (error) {
      logger.error("Error capturing photo:", error);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "Failed to capture photo. Please try again.",
        },
        5000,
      );
    }
  }

  /**
   * Captures a photo using device camera
   * @throws {Error} When camera access fails
   */
  async takePhoto() {
    try {
      const result = await this.platformService.takePicture();
      this.blob = result.blob;
      this.fileName = result.fileName;
    } catch (error: unknown) {
      logger.error("Error taking picture:", error);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "Failed to take picture. Please try again.",
        },
        5000,
      );
    }
  }

  /**
   * Selects an image from device gallery
   * @throws {Error} When gallery access fails
   */
  async pickPhoto() {
    try {
      const result = await this.platformService.pickImage();
      this.blob = result.blob;
      this.fileName = result.fileName;
    } catch (error: unknown) {
      logger.error("Error picking image:", error);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "Failed to pick image. Please try again.",
        },
        5000,
      );
    }
  }

  /**
   * Creates a blob URL for image preview
   * @param blob - Image blob to create URL for
   * @returns {string} Blob URL for the image
   */
  private createBlobURL(blob: Blob): string {
    return URL.createObjectURL(blob);
  }

  /**
   * Resets the current image selection and restarts camera preview
   */
  async retryImage() {
    this.blob = undefined;
    if (!this.platformCapabilities.isMobile) {
      await this.startCameraPreview();
    }
  }

  /**
   * Uploads the current image to the server
   * Handles cropping if enabled and manages upload state
   * @throws {Error} When upload fails or server returns error
   */
  async uploadImage() {
    this.uploading = true;

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

    const token = await accessToken(this.activeDid);
    const headers = {
      Authorization: "Bearer " + token,
    };
    const formData = new FormData();
    if (!this.blob) {
      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 || "photo.jpg");
    formData.append("claimType", this.claimType);
    try {
      if (
        window.location.hostname === "localhost" &&
        !DEFAULT_IMAGE_API_SERVER.includes("localhost")
      ) {
        logger.log(
          "Using shared image API server, so only users on that server can play with images.",
        );
      }
      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: unknown) {
      // Log the raw error first
      logger.error("Raw error object:", JSON.stringify(error, null, 2));

      let errorMessage = "There was an error saving the picture.";

      if (axios.isAxiosError(error)) {
        const status = error.response?.status;
        const statusText = error.response?.statusText;
        const data = error.response?.data;

        // Log detailed error information
        logger.error("Upload error details:", {
          status,
          statusText,
          data: JSON.stringify(data, null, 2),
          message: error.message,
          config: {
            url: error.config?.url,
            method: error.config?.method,
            headers: error.config?.headers,
          },
        });

        if (status === 401) {
          errorMessage = "Authentication failed. Please try logging in again.";
        } else if (status === 413) {
          errorMessage = "Image file is too large. Please try a smaller image.";
        } else if (status === 415) {
          errorMessage =
            "Unsupported image format. Please try a different image.";
        } else if (status && status >= 500) {
          errorMessage = "Server error. Please try again later.";
        } else if (data?.message) {
          errorMessage = data.message;
        }
      } else if (error instanceof Error) {
        // Log non-Axios error with full details
        logger.error("Non-Axios error details:", {
          name: error.name,
          message: error.message,
          stack: error.stack,
          error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
        });
      } else {
        // Log any other type of error
        logger.error("Unknown error type:", {
          error: JSON.stringify(error, null, 2),
          type: typeof error,
        });
      }

      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: errorMessage,
        },
        5000,
      );
      this.uploading = false;
      this.blob = undefined;
    }
  }
}
</script>

<style>
/* Dialog overlay styling */
.dialog-overlay {
  z-index: 60;
  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 container styling */
.dialog {
  background-color: white;
  padding: 1rem;
  border-radius: 0.5rem;
  width: 100%;
  max-width: 700px;
  max-height: 90vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Camera preview styling */
.camera-preview {
  flex: 1;
  background-color: #000;
  overflow: hidden;
  position: relative;
}

.camera-container {
  width: 100%;
  height: 100%;
  position: relative;
}

.camera-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.capture-button {
  position: absolute;
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  background: linear-gradient(to bottom, #60a5fa, #2563eb);
  color: white;
  padding: 0.75rem 1.5rem;
  border-radius: 9999px;
  box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, 0.5);
  border: none;
  cursor: pointer;
}
</style>
