///
/// Copyright © 2016-2026 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///

import {
  Component,
  ElementRef,
  Inject,
  Input,
  OnDestroy,
  OnInit,
  ViewChild,
  ViewEncapsulation
} from '@angular/core';

import { ImageService } from '@app/core/public-api';
import { AppState } from '@core/core.state';
import { AttributeService } from '@core/http/attribute.service';
import { UtilsService } from '@core/services/utils.service';
import { WINDOW } from '@core/services/window.service';
import { isString } from '@core/utils';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { PageComponent } from '@shared/components/page.component';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
import { map, Observable, of, switchMap } from 'rxjs';

interface PhotoCameraInputWidgetSettings {
  widgetTitle: string;
  saveToGallery: boolean;
  usePublicGalleryLink: boolean;
  imageQuality: number;
  imageFormat: string;
  maxWidth: number;
  maxHeight: number;
}

// @dynamic
@Component({
    selector: 'tb-photo-camera-widget',
    templateUrl: './photo-camera-input.component.html',
    styleUrls: ['./photo-camera-input.component.scss'],
    encapsulation: ViewEncapsulation.None,
    standalone: false
})
export class PhotoCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {

  constructor(@Inject(WINDOW) private window: Window,
              protected store: Store<AppState>,
              private imageService: ImageService,
              private utils: UtilsService,
              private attributeService: AttributeService
  ) {
    super(store);
  }

  public get videoElement() {
    return this.videoStreamRef.nativeElement;
  }

  public get canvasElement() {
    return this.canvasRef.nativeElement;
  }

  public get videoWidth() {
    const videoRatio = this.getVideoAspectRatio();
    return Math.min(this.width, this.height * videoRatio);
  }

  public get videoHeight() {
    const videoRatio = this.getVideoAspectRatio();
    return Math.min(this.height, this.width / videoRatio);
  }

  private static DEFAULT_IMAGE_TYPE = 'image/jpeg';
  private static DEFAULT_IMAGE_QUALITY = 0.92;

  @Input()
  ctx: WidgetContext;

  @ViewChild('videoStream', {static: false}) videoStreamRef: ElementRef<HTMLVideoElement>;
  @ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;

  private videoInputsIndex = 0;
  private settings: PhotoCameraInputWidgetSettings;
  private datasource: Datasource;
  private width = 640;
  private height = 480;
  private availableVideoInputs: MediaDeviceInfo[];
  private mediaStream: MediaStream;

  isEntityDetected = false;
  dataKeyDetected = false;
  isProtocolHttps = false;
  isCameraSupport = false;
  isDeviceDetect = false;
  isShowCamera = false;
  isPreviewPhoto = false;
  isHavePermissionCamera = true;
  isLoading = false;
  singleDevice = true;
  updatePhoto = false;
  previewPhoto: string;
  lastPhoto: string;
  datasourceDetected = false;

  private mimeType: string;
  private quality: number;

  private static hasGetUserMedia(): boolean {
    return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia);
  }

  private static getAvailableVideoInputs(): Promise<MediaDeviceInfo[]> {
    return new Promise((resolve, reject) => {
      navigator.mediaDevices.enumerateDevices()
        .then((devices: MediaDeviceInfo[]) => {
          resolve(devices.filter((device: MediaDeviceInfo) => device.kind === 'videoinput'));
        })
        .catch(err => {
          reject(err.message || err);
        });
    });
  }

  ngOnInit(): void {
    this.ctx.$scope.photoCameraInputWidget = this;
    this.isLoading = true;
    this.settings = this.ctx.settings;
    this.datasource = this.ctx.datasources[0];

    if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
      this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle);
    } else {
      this.ctx.widgetTitle = this.ctx.widgetConfig.title;
    }

    this.width = this.settings.maxWidth ? this.settings.maxWidth : 640;
    this.height = this.settings.maxHeight ? this.settings.maxWidth : 480;
    this.datasourceDetected = this.ctx.datasources?.length !== 0;
    if (this.datasourceDetected) {
      if (this.datasource.type === DatasourceType.entity) {
        if (this.datasource.entityType && this.datasource.entityId) {
          this.isEntityDetected = true;
        }
      }
      if (this.datasource.dataKeys.length) {
        this.dataKeyDetected = true;
      }
    }
    this.mimeType = this.settings.imageFormat || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
    this.quality = this.settings.imageQuality || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;

    this.detectAvailableDevices();
  }

  ngOnDestroy(): void {
    this.stopMediaTracks();
  }

  private updateWidgetData(data: Array<DatasourceData>) {
    const keyData = data[0].data;
    if (keyData?.length && isString(keyData[0][1])) {
      this.lastPhoto = keyData[0][1];
    }
  }

  public onDataUpdated() {
    this.updateWidgetData(this.ctx.defaultSubscription.data);
    this.ctx.detectChanges();
  }


  private detectAvailableDevices(): void {
    if (this.window.location.protocol === 'https:' || this.window.location.hostname === 'localhost') {
      this.isProtocolHttps = true;

      if (PhotoCameraInputWidgetComponent.hasGetUserMedia()) {
        this.isCameraSupport = true;
        PhotoCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
            this.isLoading = false;
            this.isDeviceDetect = !!devices.length;
            this.singleDevice = devices.length < 2;
            this.availableVideoInputs = devices;
            this.ctx.detectChanges();
          }, () => {
            this.isLoading = false;
            this.availableVideoInputs = [];
          }
        );
      } else {
        this.isLoading = false;
      }
    } else {
      this.isLoading = false;
    }
  }

  private getVideoAspectRatio(): number {
    if (this.videoElement.videoWidth && this.videoElement.videoWidth > 0 &&
      this.videoElement.videoHeight && this.videoElement.videoHeight > 0) {
      return this.videoElement.videoWidth / this.videoElement.videoHeight;
    }
    return this.width / this.height;
  }

  private stopMediaTracks() {
    if (this.mediaStream && this.mediaStream.getTracks) {
      this.mediaStream.getTracks()
        .forEach((track: MediaStreamTrack) => track.stop());
    }
  }

  takePhoto() {
    this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId, true);
  }

  closeCamera() {
    this.stopMediaTracks();
    this.videoElement.srcObject = null;
    this.isShowCamera = false;
  }

  cancelPhoto() {
    this.isPreviewPhoto = false;
    this.previewPhoto = '';
  }

  savePhoto() {
    this.updatePhoto = true;
    const entityId: EntityId = {
      entityType: this.datasource.entityType,
      id: this.datasource.entityId
    };

    const uploadImage$ = this.settings.saveToGallery ? this.saveImageToGallery() : of(this.previewPhoto);

    uploadImage$.pipe(
      switchMap((image) => {
        const saveData = [{
          key: this.datasource.dataKeys[0].name,
          value: image
        }];
        if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
          return this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData);
        } else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
          return this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData);
        }
      })
    ).subscribe({
      next: () => {
        this.isPreviewPhoto = false;
        this.updatePhoto = false;
        this.closeCamera();
      },
      error: () => {
        this.updatePhoto = false;
      }
    });
  }

  switchWebCamera() {
    this.videoInputsIndex = (this.videoInputsIndex + 1) % this.availableVideoInputs.length;
    this.stopMediaTracks();
    this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId);
  }

  createPhoto() {
    this.canvasElement.width = this.videoWidth;
    this.canvasElement.height = this.videoHeight;
    this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight);

    this.previewPhoto = this.canvasElement.toDataURL(this.mimeType, this.quality);
    this.isPreviewPhoto = true;
  }

  private saveImageToGallery(): Observable<string> {
    return new Observable<Blob>((observer) => {
      this.canvasElement.toBlob(
        (blob) => {
          if (blob) {
            observer.next(blob);
            observer.complete();
          } else {
            observer.error('Failed to create image blob');
          }
        },
        this.mimeType,
        this.quality
      );
    }).pipe(
      switchMap((blob) => {
        const fileName = this.datasource.dataKeys[0].name;
        const file = new File([blob], fileName, { type: this.mimeType });
        return this.imageService.uploadImage(file, fileName);
      }),
      map((imageInfo) =>
        this.settings.usePublicGalleryLink ? imageInfo.publicLink : imageInfo.link
      )
    );
  }

  private inititedVideoStream(deviceId?: string, init = false) {
    if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
      const videoTrackConstraints = {
        video: {deviceId: deviceId !== '' ? {exact: deviceId} : undefined}
      };

      window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => {
        if (init) {
          this.isShowCamera = true;
          if (this.availableVideoInputs.find((device) => device.deviceId === '')) {
            PhotoCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
              this.singleDevice = devices.length < 2;
              this.availableVideoInputs = devices;
              this.ctx.detectChanges();
            });
          }
        }
        this.mediaStream = stream;
        this.videoElement.srcObject = stream;
        this.ctx.detectChanges();
      }, () => {
        this.isHavePermissionCamera = false;
      });
    }
  }

  get textMessage() {
    if (this.isLoading) {
      return '';
    }
    if (!this.isProtocolHttps) {
      return 'widgets.input-widgets.enable-https-use-widget';
    }
    if (!this.isCameraSupport) {
      return 'widgets.input-widgets.no-support-web-camera';
    }
    if (!this.isEntityDetected) {
      return 'widgets.input-widgets.no-entity-selected';
    }
    if (!this.dataKeyDetected) {
      return 'widgets.input-widgets.no-datakey-selected';
    }
    if (!this.isDeviceDetect) {
      return 'widgets.input-widgets.no-found-your-camera';
    }
    if (!this.isHavePermissionCamera) {
      return 'widgets.input-widgets.no-permission-camera';
    }
    return null;
  }
}
