// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview The avatar-camera component displays a camera interface to
 * allow the user to take a selfie.
 */

import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';

import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import {assertInstanceof, assertNotReached} from 'chrome://resources/js/assert_ts.js';
import {html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {WithPersonalizationStore} from '../personalization_store.js';

import {saveCameraImage} from './user_controller.js';
import {getUserProvider} from './user_interface_provider.js';
import {GetUserMediaProxy, getWebcamUtils} from './webcam_utils_proxy.js';


export const enum AvatarCameraMode {
  CAMERA = 'camera',
  VIDEO = 'video',
}

function getNumFrames(mode: AvatarCameraMode): number {
  switch (mode) {
    case AvatarCameraMode.CAMERA:
      return 1;
    case AvatarCameraMode.VIDEO:
      const webcamUtils = getWebcamUtils();
      return webcamUtils.CAPTURE_DURATION_MS / webcamUtils.CAPTURE_INTERVAL_MS;
    default:
      assertNotReached(`Called with impossible AvatarCameraMode: ${mode}`);
  }
}

function getCaptureSize(mode: AvatarCameraMode):
    {height: number, width: number} {
  const webcamUtils = getWebcamUtils();
  switch (mode) {
    case AvatarCameraMode.CAMERA:
      return webcamUtils.CAPTURE_SIZE;
    case AvatarCameraMode.VIDEO:
      return {
        height: webcamUtils.CAPTURE_SIZE.height / 2,
        width: webcamUtils.CAPTURE_SIZE.width / 2,
      };
    default:
      assertNotReached(`Called with impossible AvatarCameraMode: ${mode}`);
  }
}

export interface AvatarCamera {
  $: {dialog: CrDialogElement; webcamVideo: HTMLVideoElement;};
}

export class AvatarCamera extends WithPersonalizationStore {
  static get is() {
    return 'avatar-camera';
  }

  static get template() {
    return html`{__html_template__}`;
  }

  static get properties() {
    return {
      /**
       * Set mode property to switch between camera and video.
       */
      mode: {
        type: String,
        observer: 'onModeChanged_',
        value: AvatarCameraMode.CAMERA,
      },

      /** Keep track of the open handle to the webcam. */
      cameraStream_: {
        type: Object,
        value: null,
      },

      /**
       * Store a reference to the captured png data to know if the user has
       * captured an image yet.
       */
      pngBinary_: {
        type: Object,
        value: null,
      },

      /** Show the image as a blob to avoid URL length limits. */
      previewBlobUrl_: {
        type: String,
        computed: 'computePreviewBlobUrl_(pngBinary_)',
        observer: 'onPreviewBlobUrlChanged_',
      }
    };
  }

  mode: AvatarCameraMode;
  private cameraStream_: MediaStream|null;
  private pngBinary_: Uint8Array|null;
  private previewBlobUrl_: string|null;

  connectedCallback() {
    super.connectedCallback();
    this.startCamera_();
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.stopCamera_();
  }

  private computePreviewBlobUrl_(): string|null {
    if (!this.pngBinary_) {
      return null;
    }

    assertInstanceof(
        this.pngBinary_, Uint8Array,
        'Preview binary should be a png uint8array');

    const blob = new Blob([this.pngBinary_], {type: 'image/png'});
    return URL.createObjectURL(blob);
  }

  private onPreviewBlobUrlChanged_(_: string|null, old: string|null) {
    if (old) {
      // Revoke the last one to free memory.
      URL.revokeObjectURL(old);
    }
  }

  private async startCamera_() {
    this.stopCamera_();

    try {
      this.cameraStream_ = await GetUserMediaProxy.getInstance().getUserMedia();
      if (!this.isConnected) {
        // User closed the camera UI while waiting for the camera to start.
        this.stopCamera_();
        return;
      }
      const video = this.$.webcamVideo;
      // Display the webcam feed to the user by binding it to |video|.
      video.srcObject = this.cameraStream_;
    } catch (e) {
      console.error('Unable to start camera', e);
      this.stopCamera_();
    }
  }

  /**
   * If the camera is active, stop all the active media. Safe to call even if
   * the camera is off.
   */
  private stopCamera_() {
    getWebcamUtils().stopMediaTracks(this.cameraStream_);
    this.cameraStream_ = null;
    this.pngBinary_ = null;
  }

  private async takePhoto_() {
    const webcamUtils = getWebcamUtils();

    const frames = await webcamUtils.captureFrames(
        this.$.webcamVideo, getCaptureSize(this.mode),
        webcamUtils.CAPTURE_INTERVAL_MS, getNumFrames(this.mode));

    this.pngBinary_ = webcamUtils.convertFramesToPngBinary(frames);
  }

  private confirmPhoto_() {
    assertInstanceof(
        this.pngBinary_, Uint8Array,
        'Preview image binary must be set to confirm photo');

    saveCameraImage(this.pngBinary_, getUserProvider());
    this.pngBinary_ = null;
    // Close the camera interface when an image is confirmed.
    this.$.dialog.close();
  }

  private clearPhoto_() {
    this.pngBinary_ = null;
  }

  private isVideoHidden_(): boolean {
    return !this.cameraStream_ || !!this.previewBlobUrl_;
  }


  private getTakePhotoText_(mode: AvatarCameraMode): string {
    return mode === AvatarCameraMode.VIDEO ? this.i18n('takeWebcamVideo') :
                                             this.i18n('takeWebcamPhoto');
  }

  private getConfirmText_(mode: AvatarCameraMode): string {
    return mode === AvatarCameraMode.VIDEO ? this.i18n('confirmWebcamVideo') :
                                             this.i18n('confirmWebcamPhoto');
  }
}

customElements.define(AvatarCamera.is, AvatarCamera);

declare global {
  interface HTMLElementTagNameMap {
    'avatar-camera': AvatarCamera;
  }
}