import {inject, Injectable} from '@angular/core'
import {Camera, CameraResultType, CameraSource, PermissionStatus, Photo} from '@capacitor/camera'
import {Preferences} from '@capacitor/preferences'
import {Capacitor} from '@capacitor/core'
import {Directory, Filesystem} from '@capacitor/filesystem'
import {ErrorService} from './error.service'

@Injectable({
  providedIn: 'root'
})
export class PhotoService {

  // The const keyword isn't allowed here, const and let can only be used within
  // the scope of a method or function.
  //
  // The readonly modifier ensures the same result. A variable
  // modified as readonly can only be assigned as a class variable
  // or in the constructor. This keyword has the same function as readonly in C#
  // or final in Java.

  // List of photo objects, more info about the interface can be found in the
  // Capacitor documentation: https://capacitorjs.com/docs/apis/camera#photo
  readonly photos: Photo[] = []

  // The key under which the URIs for the photos will be saved.
  readonly #storageKey = 'photos'

  // The filesystem URIs for each photo.
  // Can't be a readonly variable because the Storage plug-in is asynchronous,
  // therefore, it is impossible to guarantee that the variable can be initialized
  // before the constructor has finished.
  #photoURIs: string[] = []

  // The permissions that have been granted, for the camera and filesystem.
  // See https://capacitorjs.com/docs/apis/camera#permissionstatus
  // When deploying to the web platform, permissions will be handled by
  // browser and not by Capacitor. These standard permissions will be overwritten
  // when working on a native platform and don't matter on a PWA.
  #permissionGranted: PermissionStatus = {camera: 'granted', photos: 'granted'}
  #errorService = inject(ErrorService)

  constructor() {
    this.#loadData().then(() => console.log('Data loaded'))
  }

  /**
   * Load the photo URIs from storage and read the photos from the filesystem.
   *
   * @private
   */
  async #loadData() {
    await this.#retrievePhotoURIs()
    await this.#retrievePermissions()
    await this.#loadPhotos()
  }

  async takePhoto(): Promise<void> {
    // It's always best to request permissions explicitly.
    // On some devices, the permissions won't be requested automatically.
    // Furthermore, when you request permissions, and it isn't immediately obvious why,
    // it's good practice to include a message to the user that explains why the permissions are required.
    if (!this.#haveCameraPermission() || !this.#havePhotosPermission()) {
      await this.#requestPermissions()
    }

    if (!this.#haveCameraPermission() || !this.#havePhotosPermission()) {
      this.#errorService.enqueueErrorMessage(`Can't take a photo because the right to do so has not been granted.`)
      return
    }

    const image = await Camera.getPhoto({
      quality: 90,
      // The result must be a base64 string because capacitor implements the filesystem using indexedDB for PWA.
      // This is not a "real" filesystem, and as such, the files can't be read from it directly but must first be loaded
      // in memory as a base64 string. It is possible to also implement this using the CameraResultType.Uri.
      // However, if persistence is required, using Uri requires downloading the resulting blob
      // and then converting it to base64 and writing it to the filesystem
      resultType: CameraResultType.Base64,
      source: this.#determinePhotoSource(),
    })

    // Save the image to the filesystem and save the uri so that the image can be retrieved later.
    const uri = await this.#saveImageToFileSystem(image)
    this.#photoURIs.push(uri)
    image.path = uri
    this.#persistPhotoURIs().then()

    // An <img> element can easily display a dataURL.
    // In this case, the url contains image data. But this needed always be the case.
    // Any and all binary data can be represented in base64 format, and if a website can handle
    // the data type, then it can read a base64 string representing the data.
    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
    image.dataUrl = `data:image/${image.format};base64,${image.base64String}`
    this.photos.push(image)
  }

  /**
   * Delete the photo at the given index.
   *
   * @param index The index at which the photo must be deleted.
   */
  async deleteImage(index: number): Promise<void> {
    const deletedFile = this.photos[index]
    this.photos.splice(index, 1)
    this.#photoURIs = this.#photoURIs.filter(u => u !== deletedFile?.path)
    await Promise.all([
      this.#persistPhotoURIs(),
      this.#deleteImageFromFilesystem(deletedFile),
    ])
  }

  /**
   * Save a photo to the file system.
   *
   * @param photo The photo that is to be saved.
   * @return Promise<string> The filesystem URI of the saved photo
   * @private
   */
  async #saveImageToFileSystem(photo: Photo): Promise<string> {
    if (!photo.base64String) {
      throw new Error('No photo data available')
    }

    const fileName = `${new Date().getTime()}.${photo.format}`
    const savedFile = await Filesystem.writeFile({
      path: fileName,
      data: photo.base64String,
      directory: Directory.Data,
    })
    return savedFile.uri
  }

  /**
   * Delete a given photo from the filesystem.
   *
   * @param photo The photo to delete.
   */
  async #deleteImageFromFilesystem(photo: Photo): Promise<void> {
    if (!photo.path) {
      throw new Error(`The photo can be deleted because the path attribute isn't set`)
    }

    await Filesystem.deleteFile({
      path: photo.path, // The URI already contains the DATA directory.
    })
  }

  /**
   * Retrieve the filesystem URIs for each photo in the gallery app. If no stored
   * URIs are available, initialize the class variable photoURIs with an empty array.
   *
   * @private
   */
  async #retrievePhotoURIs(): Promise<void> {
    const {value} = await Preferences.get({key: this.#storageKey})
    this.#photoURIs = value ? JSON.parse(value) : []
  }

  /**
   * Persist the list of filesystem URIs for the stored photos.
   *
   * @private
   */
  async #persistPhotoURIs(): Promise<void> {
    await Preferences.set({
      key: this.#storageKey,
      value: JSON.stringify(this.#photoURIs),
    })
  }

  /**
   * Request permission to use the camera and gallery.
   *
   * @private
   */
  async #requestPermissions(): Promise<void> {
    try {
      this.#permissionGranted = await Camera.requestPermissions({permissions: ['photos', 'camera']})
    } catch (error) {
      console.error(`Permissions aren't available on this device: ${Capacitor.getPlatform()} platform.`)
    }
  }

  /**
   * Retrieve the permissions that have been granted for the camera and gallery (filesystem on PWA).
   *
   * @private
   */
  async #retrievePermissions(): Promise<void> {
    try {
      this.#permissionGranted = await Camera.checkPermissions()
    } catch (error) {
      console.error(`Permissions aren't available on this device: ${Capacitor.getPlatform()} platform.`)
    }
  }

  /**
   * Determine, based on the granted permissions and the platform, what the best source for the photo is.
   *
   * @private
   */
  #determinePhotoSource(): CameraSource {
    if (!Capacitor.isNativePlatform()) {
      return CameraSource.Camera
    }

    if (this.#havePhotosPermission() && this.#haveCameraPermission()) {
      return CameraSource.Prompt
    } else {
      return this.#havePhotosPermission() ?
        CameraSource.Photos : CameraSource.Camera
    }
  }

  /**
   * Check if permission has been granted to use the camera.
   *
   * @return boolean True if permission has been granted, false otherwise.
   * @private
   */
  #haveCameraPermission(): boolean {
    return this.#permissionGranted.camera === 'granted'
  }

  /**
   * Check if permission has been granted to read from, and write to the gallery.
   * This is only relevant on native platforms.
   *
   * @return boolean True if permission has been granted, false otherwise.
   * @private
   */
  #havePhotosPermission(): boolean {
    return this.#permissionGranted.photos === 'granted'
  }

  /**
   * Load the photos from the filesystem as base64 and use this to create a dataURL, so that the photos can be shown on
   * an HTML page.
   *
   * @private
   */
  async #loadPhotos(): Promise<void> {
    for (const uri of this.#photoURIs) {

      const data = await Filesystem.readFile({
        path: uri,
      })

      const format = this.#getPhotoFormat(uri)
      this.photos.push({
        dataUrl: `data:image/${format};base64,${data.data}`,
        format,
        path: uri,
        saved: false,
      })
    }
  }

  /**
   * Extract the image format (extension) from a given filesystem URI.
   * This is required because the Photo interface includes the format as a non-optional attribute.
   *
   * @param uri The uri from which the format must be extracted.
   * @return string The extension of the given image.
   * @private
   */
  #getPhotoFormat(uri: string): string {
    const splitUri = uri.split('.')
    return splitUri[splitUri.length - 1]
  }
}
