/** * 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 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()"
        >
          <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: 9 / 9,
            }"
            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="absolute bottom-[1rem] left-[1rem] px-2 py-1">
          <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-1 px-2 rounded-md"
            @click="uploadImage"
          >
            <span>Upload</span>
          </button>
        </div>
        <div
          v-if="showRetry"
          class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
        >
          <button
            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"
            @click="retryImage"
          >
            <span>Retry</span>
          </button>
        </div>
      </div>
      <div v-else>
        <div class="flex flex-col items-center justify-center gap-4 p-4">
          <button
            class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
            @click="takePhoto"
          >
            <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 } from "../constants/app";
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;

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

  /**
   * Lifecycle hook: Initializes component and retrieves user settings
   * @throws {Error} When settings retrieval fails
   */
  async mounted() {
    try {
      const settings = await retrieveSettingsForActiveAccount();
      this.activeDid = settings.activeDid || "";
    } 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,
      );
    }
  }

  /**
   * 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
   */
  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;
    }
  }

  /**
   * Closes the photo dialog and resets state
   */
  close() {
    this.visible = false;
    const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
    if (bottomNav) {
      bottomNav.style.display = "";
    }
    this.blob = undefined;
  }

  /**
   * 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
   */
  async retryImage() {
    this.blob = undefined;
  }

  /**
   * 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;
}
</style>
