import { FaceMatcher } from "@vladmandic/face-api";
import { IRoom } from "./config";
import {
  Rtsp,
  IStreamEventArgs,
  ICloseEventArgs,
  IErrorEventArgs,
  IMessageEventArgs,
} from "./rtsp/rtsp";
import canvas from "canvas";
import * as faceapi from "@vladmandic/face-api";
import { getFaceDetectorOptions, saveFile } from "./common";
import { nets } from "@vladmandic/face-api";
import { Logger } from "homebridge";
import { Event } from "./events";
import { IConfig } from "./config";
const { Canvas, Image, ImageData } = canvas;

export type MonitorState = { [label: string]: string | null };
export interface IStateChangeEventArgs {
  label: string;
  old: string | null;
  new: string;
}

export class Monitor {
  private _state: MonitorState = {};
  private _streamsByRoom: { [roomName: string]: Array<Rtsp> } = {};
  private _faceDetectionNet = nets.ssdMobilenetv1;
  private _stateChangedEvent: Event<this, IStateChangeEventArgs>;

  constructor(
    private _rooms: Array<IRoom>,
    private _matcher: FaceMatcher,
    private _logger: Logger,
    private _config: IConfig
  ) {
    this._stateChangedEvent = new Event();

    //Initialize state
    for (const room of this._rooms) {
      this._streamsByRoom[room.name] = [
        ...room.rtspConnectionStrings.map((connectionString) => {
          const rtsp = new Rtsp(connectionString, {
            rate: 0.7,
            image: true,
          });
          rtsp.dataEvent.push((sender: Rtsp, args: IStreamEventArgs) =>
            this.onData(room.name, args)
          );
          rtsp.closeEvent.push((sender: Rtsp, args: ICloseEventArgs) =>
            this.onExit(connectionString, args)
          );
          rtsp.errorEvent.push((sender: Rtsp, args: IErrorEventArgs) =>
            this.onError(args, connectionString)
          );
          if (this._config.debug) {
            rtsp.messageEvent.push((sender: Rtsp, args: IMessageEventArgs) => {
              this._logger.info(`[${connectionString}] ${args.message}`);
            });
          }

          return rtsp;
        }),
      ];

      _matcher.labeledDescriptors.forEach((descriptor) => {
        this._state[descriptor.label] = null;
      });
    }
  }

  /**
   * @method getState
   *
   * @param label The name of the label to retrieve state for
   *
   * The last known room of the requested label
   */
  public getState(label: string): string | null {
    return this._state[label];
  }

  /**
   * @property labels
   *
   * Gets the list of labels associated with the monitor
   */
  public get labels(): Array<string> {
    return this._matcher.labeledDescriptors
      .map((descriptor) => descriptor.label)
      .filter(
        (label: string, index: number, array: Array<string>) =>
          array.indexOf(label) === index
      );
  }

  public get stateChangedEvent(): Event<this, IStateChangeEventArgs> {
    return this._stateChangedEvent;
  }

  /**
   * @method startStreams
   *
   * Starts monitoring rtsp streams
   */
  public startStreams() {
    for (const key in this._streamsByRoom) {
      for (const stream of this._streamsByRoom[key]) {
        stream.start();
      }
    }
  }

  /**
   * @method closeStreams
   *
   * Stops monitoring rtsp streams
   */
  public closeStreams() {
    for (const key in this._streamsByRoom) {
      for (const stream of this._streamsByRoom[key]) {
        stream.close();
      }
    }
  }

  private onData = async (room: string, args: IStreamEventArgs) => {
    const input = ((await canvas.loadImage(args.data)) as unknown) as ImageData;
    const out = faceapi.createCanvasFromMedia(input);
    const resultsQuery = await faceapi
      .detectAllFaces(out, getFaceDetectorOptions(this._faceDetectionNet))
      .withFaceLandmarks()
      .withFaceDescriptors();

    //Write to output image
    if (this._config.writeOutput) {
      await saveFile(this._config.outputDirectory, room + ".jpg", args.data);
    }

    for (const res of resultsQuery) {
      const bestMatch = this._matcher.matchDescriptor(res.descriptor);
      const old = this._state[bestMatch.label];
      this._state[bestMatch.label] = room;
      this._stateChangedEvent.fire(this, {
        old: old,
        new: room,
        label: bestMatch.label,
      });

      this._logger.info(`Face Detected: ${bestMatch.label} in room ${room}`);
    }
  };

  private onError = (args: IErrorEventArgs, streamName: string) => {
    this._logger.info(`[${streamName}] ${args.message}`);
  };
  private onExit = (streamName: string, args: ICloseEventArgs) => {
    this._logger.info(`[${streamName}] Stream has exited: ${args.message}`);
  };
}
