import { ChildProcess, spawn } from "child_process";
import { Writable } from "stream";
import { IOptions } from "./options";
import { Event } from "../events";

const ef1 = "ff";
const ef2 = "d9";

export interface IStreamEventArgs {
  data: Buffer;
}

export interface ICloseEventArgs {
  message: string;
}
export interface IErrorEventArgs {
  message?: string;
  err?: Error;
}

export interface IMessageEventArgs {
  message: string;
}

export class Rtsp {
  private _connecteionString: string;
  private _childProcess: ChildProcess | undefined;
  private _started: boolean;
  private _buffer: Buffer;
  private _options: IOptions;
  private _paused: boolean;

  private _dataEvent: Event<this, IStreamEventArgs>;
  private _closeEvent: Event<this, ICloseEventArgs>;
  private _errorEvent: Event<this, IErrorEventArgs>;
  private _messageEvent: Event<this, IMessageEventArgs>;

  constructor(connectionString: string, options: IOptions) {
    this._started = false;
    this._connecteionString = connectionString;
    this._childProcess = undefined;
    this._buffer = Buffer.from("");
    this._options = options;
    this._paused = false;
    this._dataEvent = new Event();
    this._closeEvent = new Event();
    this._errorEvent = new Event();
    this._messageEvent = new Event();

    this.onData = this.onData.bind(this);
  }

  public get isStarted(): boolean {
    return this._started;
  }

  public get isPaused(): boolean {
    return this._paused;
  }

  public get dataEvent(): Event<this, IStreamEventArgs> {
    return this._dataEvent;
  }

  public get messageEvent(): Event<this, IMessageEventArgs> {
    return this._messageEvent;
  }

  public get closeEvent(): Event<this, ICloseEventArgs> {
    return this._closeEvent;
  }

  public get errorEvent(): Event<this, IErrorEventArgs> {
    return this._errorEvent;
  }

  public start(): void {
    const argStrings = [
      `-i ${this._connecteionString}`,
      `-r ${this._options.rate ?? 10}`,
      `-vf mpdecimate,setpts=N/FRAME_RATE/TB`,
      this._options.image
        ? `-f image2`
        : `-codec:v ${this._options.codec ?? "libx264"}`,
      `-update 1 -`,
    ];
    const args = argStrings.join(" ");
    this._childProcess = spawn("ffmpeg", args.split(/\s+/));

    if (!this._childProcess) {
      return;
    }

    this._childProcess.stdout?.on("data", this.onData);
    this._childProcess.stdout?.on("error", (err) =>
      console.log("And error occurred" + err)
    );
    this._childProcess.stdout?.on("close", () => console.log("Stream closed"));
    this._childProcess.stdout?.on("end", () => console.log("Stream ended"));

    //Only register this event if there are subscribers
    if (this._childProcess.stderr && this._messageEvent.length > 0) {
      this._childProcess.stderr.on("data", this.onMessage);
    }

    this._childProcess.on("close", (code: number, signal: NodeJS.Signals) =>
      this._closeEvent.fire(this, {
        message: "FFmpeg exited with code: " + code + " and signal: " + signal,
      })
    );

    this._childProcess.on("exit", (code: number, signal: NodeJS.Signals) =>
      this._closeEvent.fire(this, {
        message: "FFmpeg exited with code: " + code + " and signal: " + signal,
      })
    );

    this._childProcess.on("error", (error: Error) =>
      this._errorEvent.fire(this, { err: error })
    );
  }

  public close(): void {
    this._childProcess && this._childProcess.kill("SIGKILL");
    this._closeEvent.fire(this, { message: "Process killed by user" });
  }

  public pause(): void {
    this._paused = true;
  }

  public resume(): void {
    this._paused = false;
  }

  public getStdin(): Writable | null {
    return this._childProcess ? this._childProcess.stdin : null;
  }

  private onMessage = (data: any): void => {
    if (!this._started) {
      this._started = true;
    }
    let msg = "";
    data
      .toString()
      .split(/\n/)
      .forEach((line: string) => {
        msg += `${line}\n`;
      });

    this._messageEvent.fire(this, { message: msg });
  };

  private onData(data: Buffer): void {
    if (!this._paused && data.length > 1) {
      this._buffer = this._buffer
        ? Buffer.concat([this._buffer, data])
        : (this._buffer = Buffer.from(data));
      //End of image
      if (
        data[data.length - 2].toString(16) == ef1 &&
        data[data.length - 1].toString(16) == ef2
      ) {
        this._dataEvent.fire(this, { data: this._buffer });
        this._buffer = Buffer.from("");
      }
    }
  }
}
