import type { Metadata, MSEMediaPlayerInitParameters, MSEMediaPlayerParameters, StreamParams, UUID } from "@solid/types";
import { MSEMediaPlayerConnection, MSEMediaPlayerError, MSEMediaPlayerMetadataType, MSEMediaPlayerStatus, MSEMediaPlayerStream, MSEMediaPlayerTransport } from "@solid/types";
import { MetaDataRender, MetadataRenderVisual } from "@solid/player";
import { __, AjaxError, API, getSnapshotURL, idle, Logger, ptsToTimestamp, timeout, TimeoutError, timestampToPts, Utils } from "@solid/libs";
import m from "mithril";
import { VNPlayer, VNPlayerStreamState } from "@libs/vn-player";
import YUVCanvas, { FrameSink } from "yuv-canvas";
import TypedEventEmitter from "typed-emitter";
import { EventEmitter } from "events";

export type YUVPlayerParameters = MSEMediaPlayerParameters;

export type YUVPlayerInitParameters = MSEMediaPlayerInitParameters & {
  jitter_buffer_len?: number
};

export type GetUrlParameters = {
  cameraid: string,
  startTime?: number, // ms
  endTime?: number, // ms
  streamnum?: string,
  isLocal?: boolean,
  audio?: boolean
}

type LocalConnectionStatus = {
  time: number,
  fail?: {
    code: number,
    message: string
  } | null
}

type YUVPlayerEvent = {
  play: (streamParams: StreamParams) => void,
  pause: (byUser: boolean) => void,
  stop: (code?: number, message?: string) => void,
  connecting: (message: string) => void,
  initializing: () => void,
  buffering: () => void,
  frame: (time: number, metadata?: [Metadata]) => void,
  click: (width: number, height: number, x: number, y: number) => void,
  archive: () => void,
  live: () => void,
  resize: () => void,
  record: () => void,
  geoPosition: (geo: [lat: number, lng: number, alt?: number]) => void
}
type YUVPlayerEventName = keyof YUVPlayerEvent;

const JITTER_BUFFER_LENGTH_DEFAULT = 1000; // ms
const JITTER_BUFFER_LENGTH_MIN_FOR_AUDIO = 700; // ms

export class YUVPlayer {
  _logger: Logger | Console;
  _node: HTMLElement | null;
  _mediaPlayerNode: HTMLElement | null;

  _obj?: UUID;
  _avatarId?: UUID;
  _currentTime: number | undefined;
  _lastJumpTime: number | undefined;

  _metadataType: MSEMediaPlayerMetadataType
  _isMetadataVisible: boolean;
  _metadataList: Metadata[] = [];
  _metadataListDraw: MetadataRenderVisual = {};

  _url: string | undefined;

  _api: API;

  _streamid: MSEMediaPlayerStream;

  _rendered: StreamParams;

  _canvas: HTMLCanvasElement | null;

  _pixelAspectRatio: number;
  _isStretchAspectRatio: boolean;

  _yuvCanvas: FrameSink | null;

  _player: VNPlayer | null;

  _status: MSEMediaPlayerStatus = MSEMediaPlayerStatus.STOP;

  _archiveIntervalRequest: number; // ms

  _startDate: Date;
  _endDate: Date;

  _topics = new Set<YUVPlayerEventName>([
    "play",
    "pause",
    "stop",
    "connecting",
    "initializing",
    "buffering",
    "frame",
    "click",
    "archive",
    "live",
    "resize",
    "record",
    "geoPosition"
  ]);

  _topicsEvents: TypedEventEmitter<YUVPlayerEvent>;

  _stopCodeMessage: Record<number, string> = {
    "0": "Stop",
    "110": "Stop, not specified",
    "-1": "Something went wrong"
  };

  _isPrintError: boolean;

  _stoppedAfterIdle = false;
  _stopOnIdleTimeout = 10 * 60 * 1000; // ms

  _stream: StreamParams | null;

  _inProgress: boolean;
  _snapshotLoading: boolean;
  _showPauseButton: boolean;

  _isArchive: boolean = false;

  _speed: number = 1.0;
  _audio: boolean = false;
  _muted: boolean = false;
  _jitter_buffer_len: number = JITTER_BUFFER_LENGTH_DEFAULT; // ms

  _connectionType: Exclude<MSEMediaPlayerConnection, MSEMediaPlayerConnection.AUTO> = MSEMediaPlayerConnection.LOCAL;
  _preferredConnectionType: MSEMediaPlayerConnection = MSEMediaPlayerConnection.AUTO;

  _disableLocalTransport: boolean = false;

  _lastInitParameters: YUVPlayerInitParameters | null = null;
  _lastGetUrlParameters: GetUrlParameters | null = null;

  _connectionTimeout = 5 * 1000; // ms
  _checkConnectionTimeout = 15 * 60 * 1000; // ms

  _opened = false;
  _playFromServer = false;
  _newSize = false;

  _isReconnectingAllowed = true;
  _reconnectTimeoutId: number | undefined;

  constructor(parameters: YUVPlayerParameters = {}) {
    parameters = Object.assign({node: null}, parameters);

    this._logger = parameters.logger ?? console;

    this._node = parameters.node ?? null;
    this._mediaPlayerNode = null;

    this._currentTime = undefined;

    this._metadataType = MSEMediaPlayerMetadataType.ALL;
    this._isMetadataVisible = true;
    this._metadataList = [];
    this._metadataListDraw = {};

    this._url = undefined;

    this._api = new API();

    this._streamid = MSEMediaPlayerStream.FULL;

    this._rendered = {
      mime: "",
      width: 0,
      height: 0
    };

    this._canvas = null;

    this._pixelAspectRatio = 1;
    this._isStretchAspectRatio = false;

    this._yuvCanvas = null;

    this._player = null;

    this._archiveIntervalRequest = 3600 * 1000; // ms

    this._startDate = new Date();
    this._endDate = new Date();

    this._topicsEvents = new EventEmitter() as TypedEventEmitter<YUVPlayerEvent>;

    this._isPrintError = true;

    this._stream = null;

    this._inProgress = false;

    this._snapshotLoading = false;
    this._showPauseButton = false;

    if (parameters.connectionType) {
      this._preferredConnectionType = parameters.connectionType;
    } else {
      let storedConnectionType: MSEMediaPlayerConnection | null = null;
      try {
        storedConnectionType = localStorage.getItem("connection") as MSEMediaPlayerConnection | null ?? process.env.PLAYER_CONNECTION as MSEMediaPlayerConnection;
      }
      catch (e) {
      }
      this._preferredConnectionType = storedConnectionType ?? MSEMediaPlayerConnection.AUTO;
    }
  }

  /**
   * initialize media player
   */
  async init(parameters?: YUVPlayerInitParameters, isPrintError?: boolean): Promise<void> {
    this._lastInitParameters = parameters ?? {};

    if (typeof parameters?.idleTimeout !== "undefined") {
      this._stopOnIdleTimeout = parameters.idleTimeout * 60 * 1000;
    }

    let storedMetadataType: MSEMediaPlayerMetadataType | null = null;
    try {
      storedMetadataType = localStorage.getItem("metadata") as MSEMediaPlayerMetadataType | null;
    }
    catch (e) {}

    if (storedMetadataType) {
      this._metadataType = storedMetadataType;
    } else
    if (typeof parameters?.metadataType !== "undefined") {
      this._metadataType = parameters.metadataType;
    }

    if (typeof parameters?.connectionType !== "undefined") {
      this._preferredConnectionType = parameters.connectionType;
    }

    if (typeof parameters?.showPauseButton == "boolean") {
      this._showPauseButton = parameters.showPauseButton;
    }

    if (typeof parameters?.muted == "boolean") {
      this._muted = parameters.muted;
    }

    if (typeof parameters?.jitter_buffer_len == "number") {
      this._jitter_buffer_len = parameters.jitter_buffer_len;
    }

    if (typeof parameters?.disableLocalTransport == "boolean") {
      this._disableLocalTransport = parameters.disableLocalTransport;
      if (this._disableLocalTransport) {
        this._preferredConnectionType = MSEMediaPlayerConnection.CLOUD;
      }
    }

    this._addEvents();

    this._publish("initializing");

    return new Promise<void>(async (resolve, reject) => {
      this._isPrintError = isPrintError ?? this._isPrintError;

      let id = "NativeMediaPlayer_" + Math.round((Math.random() * 10000));

      if (this._node) {
        const NativeMediaPlayer: m.Component = {
          oncreate: ({dom}) => {
            this._canvas = dom.querySelector("canvas.playercanvas");

            if (!this._canvas) {
              throw new Error("'canvas.playercanvas' is undefined");
            }

            const playerParameters = {
              scale: false,
              webgl: true,
              vsync: true
            };
            this._yuvCanvas = YUVCanvas.attach(this._canvas, {webGL: playerParameters.webgl});
            this._player = new VNPlayer(this._yuvCanvas, {
              scale: playerParameters.scale,
              vsync: playerParameters.vsync,
              muted: this._muted,
              jitter_buffer_len: this._jitter_buffer_len,
              log_level: "info",
              logger: this._logger
            });
            this._mediaPlayerNode = document.querySelector("#" + id);

            this._initVNPlayer();

            resolve();

            const correctSize = Utils.throttleToDraw(() => {
              this._correctSize();
            });

            const resizeObserver = new ResizeObserver(Utils.throttleToDraw((entries) => {
              for (const entry of entries) {
                if (this._mediaPlayerNode === entry.target) {
                  correctSize();
                }
              }
            }));
            resizeObserver.observe(dom);
          },
          view: () => {
            return m(".mediaplayer", {
              id: id
            }, [
              m("canvas.playercanvas", {
                style: {
                  width: "100%",
                  height: "100%"
                }
              }),
              this._isMetadataVisible && m(".metadata", {
                onclick: (event: MouseEvent & {target: {clientWidth: number, clientHeight: number}, layerX: number, layerY: number}) => {
                  this._publish("click", event.target.clientWidth, event.target.clientHeight, event.layerX, event.layerY);
                }
              },
              m(MetaDataRender, {
                list: this._metadataListDraw,
                time: this._currentTime!,
                metadataType: this._metadataType,
                size: this._rendered
              })),
              m(".snapshot", [
                m("img")
              ]),
              (this._inProgress || this._snapshotLoading) && m(".progress", m(".indicator", m("i.fas.fa-circle-notch.fa-spin"))),
              (this._status === MSEMediaPlayerStatus.PAUSE || this._status == MSEMediaPlayerStatus.STOP || (this._showPauseButton && this._status === MSEMediaPlayerStatus.PLAY)) &&
              m(".mediaplayer-controls", {
                onclick: () => {
                  if (this._status === MSEMediaPlayerStatus.PAUSE || this._status == MSEMediaPlayerStatus.STOP) {
                    this.play();
                  }
                  if (this._showPauseButton && this._status === MSEMediaPlayerStatus.PLAY) {
                    this.pause();
                  }
                }
              }, [
                (this._status == MSEMediaPlayerStatus.PAUSE || this._status == MSEMediaPlayerStatus.STOP) && m(".play .fas .fa-play-circle"),
                this._showPauseButton && this._status === MSEMediaPlayerStatus.PLAY && m(".pause .fas .fa-pause-circle"),
              ]),
              m(".error")
            ]);
          }
        };
        m.mount(this._node, NativeMediaPlayer);
      } else
      if (!this._node) {
        this._player = new VNPlayer(undefined, {
          jitter_buffer_len: this._jitter_buffer_len,
          logger: this._logger
        });
        this._player.on("state_changed", (state, code, message) => {
          if (state === VNPlayerStreamState.STOPPED) {
            this._publish("stop", code, message);
          }
        });

        this._player.on("recording_status_changed", (state, timestamp, filename, message) => {
          this._publish("record", state, timestamp, filename, message);
        });

        resolve();
        return;
      }
    });
  }

  _initVNPlayer(): void {
    if (!this._player) {
      this._logger.error("_initVNPlayer: this._player is undefined");
      return;
    }

    this._player.on("frame", (timestamp: number, metadataList?: [Metadata]) => {
      if (metadataList && metadataList[0]) {
        const metadata = metadataList[0];
        // merge new metadata elements with old one
        Object.assign(this._metadataListDraw, metadata.list);

        Object.keys(metadata.list).forEach((id) => {
          const element = this._metadataListDraw[id];
          if (!element) {
            return;
          }

          // save time for each element of metadata
          element.time = ptsToTimestamp(metadata.pts);

          // publish geo data if exist
          if (element.visual?.opt?.geo) {
            const geo = element.visual.opt.geo;
            if (geo) {
              this._publish("geoPosition", geo);
            }
          }
        });
      }

      this._currentTime = timestamp;

      m.redraw();

      this._publish("frame", timestamp);
    });

    this._player.on("buffer_changed", (...args) => {
      // this._logger.log("buffer_changed", args);
    });
    this._player.on("new_stream", (...args) => {
      // this._logger.log("new_stream", args);
    });
    this._player.on("state_changed", (state, code, message) => {
      this._logger.log("state_changed>", VNPlayerStreamState[state], code, message);
      if (state === VNPlayerStreamState.PLAY_FROM_SERVER || state === VNPlayerStreamState.PLAY_FROM_BUFFER) {
        this._logger.log("playFromServer");
        this.printErrorMessage("");
        if (!this._playFromServer) {
          this._logger.log("this._playFromServer = true")
          this._playFromServer = true;

          // TODO: correct logic of emulating new_size after resume
          if (this._status === MSEMediaPlayerStatus.PAUSE && this._stream) {
            onNewSize(this._stream);
          }
        }
      } else
      if (state === VNPlayerStreamState.STOPPED) {
        this._publish("stop", code, message);
      } else
      if (state === VNPlayerStreamState.OPENING) {
        this._publish("connecting", this.getConnectionType() === MSEMediaPlayerConnection.LOCAL ? __("local access") : __("cloud access"));
      } else
      if (state === VNPlayerStreamState.OPENED) {
        this._opened = true;
        this._logger.log("opened");
      } else
      if (state === VNPlayerStreamState.BUFFERING) {
        this._publish("buffering");
      }
    });
    this._player.on("stream_removed", (...args) => {
      // this._logger.log("stream_removed", args);
    });
    this._player.on("msg_log", (...args) => {
      // this._logger.log("msg_log", args);
    });

    const onNewSize = (size: { width: number, height: number }) => {
      // this._logger.log("msg_log", args);
      this._logger.log("new_size", size);

      this._stream = { mime: "", ...size };
      this._correctSize();

      if (this._playFromServer && !this._newSize) {
        this._logger.log("play");
        this._newSize = true;
        this._publish("play", this._stream);
      }
    };
    this._player.on("new_size", onNewSize);
  }

  async play(obj?: UUID, fromTime?: number, toTime?: number, doNotStart: boolean = false): Promise<boolean> {
    if (!this._node) {
      this._logger.warn(this._node, "play: player node is not created");
      return false;
    }

    if (!this._player) {
      this._logger.error("play: this._player is undefined");
      return false;
    }

    this.clearTimers();

    if (this._status === MSEMediaPlayerStatus.INITIALIZING) {
      return false;
    }

    if (this._status === MSEMediaPlayerStatus.PLAY) {
      this.stop();
    }

    if (!obj && !fromTime && this._obj && this._isArchive) {
      fromTime = this._currentTime;
    }
    if (!!obj) this._obj = obj;
    if (!this._obj) throw new Error("obj not specified");

    if (this._obj && !fromTime) {
      this._isArchive = false;
      this._currentTime = undefined;
    } else
    if (this._obj && fromTime) {
      this._isArchive = true;
      this._currentTime = fromTime;

      let date = new Date();
      date.setTime(fromTime);
      this._startDate.setTime(date.getTime());
      let time = toTime ? toTime : this._startDate.getTime() + this._archiveIntervalRequest;
      this._endDate.setTime(time);
    }

    // pause player to prevent playing old chunks before receiving new one
    // if (this._status === MSEMediaPlayerStatus.PLAY) {
    //   this.pause();
    // this._player && this._player.pause();
    // }

    if (this._status === MSEMediaPlayerStatus.STOP) {
      this._status = MSEMediaPlayerStatus.INITIALIZING;
      this._publish("initializing");
    }

    m.redraw();

    const isLocal = await this.checkIsLocal(this._obj);
    this._connectionType = isLocal ? MSEMediaPlayerConnection.LOCAL : MSEMediaPlayerConnection.CLOUD;

    const parameters: GetUrlParameters = {
      cameraid: this._obj,
      isLocal,
      audio: true
    };
    if (this._isArchive) {
      parameters.startTime = this._startDate.getTime();
      parameters.endTime = this._endDate.getTime();
      this._publish("archive");
    } else {
      this._publish("live");
    }

    this._inProgress = true;
    m.redraw();

    this._showSnapshot();

    this._opened = false;
    this._playFromServer = false;
    this._newSize = false;

    // Live
    if (!obj && !this._isArchive && this._status === MSEMediaPlayerStatus.PAUSE) {
      this._player.resume(1);
      return true;
    } else
    // Archive
    if (!obj && this._isArchive && this._status === MSEMediaPlayerStatus.PAUSE && !!this._lastGetUrlParameters?.startTime) {
      if (this._currentTime && this._lastJumpTime !== this._currentTime) {
        this._lastJumpTime = this._currentTime;
        this._player.jump(Math.trunc(this._currentTime / 1000));
      }
      this._player.resume(Math.sign(this._speed));
      return true;
    } else {
      this._lastGetUrlParameters = {...parameters};
      const {url, audio} = await this.getURL(parameters);

      this._logger.log(`--> play ${this._obj}, speed=${this._speed}, url=${url}`);

      await this._player.play(this._obj, url, this._speed);
      this._audio = audio;
      if (this._audio && this._jitter_buffer_len < JITTER_BUFFER_LENGTH_MIN_FOR_AUDIO) {
        this.setJitterBufferLen(JITTER_BUFFER_LENGTH_MIN_FOR_AUDIO);
      }
    }

    return true;
  }

  setSpeed(speed: number) {
    this._speed = speed;

    if (!this._player) {
      this._logger.error("setSpeed: this._player is undefined");
      return;
    }

    this._player.speed(speed);
  }

  getSpeed(): number {
    return this._speed;
  }

  muteAudio(muted: boolean): void {
    if (!this._player) {
      this._logger.error("muteSound: this._player is undefined");
      return;
    }

    this._muted = muted;

    if (!this._audio) {
      this._logger.warn("muteSound: camera has no sound");
      return;
    }

    this._player.muteAudio(muted);
  }

  getAudio(): boolean {
    return this._muted;
  }

  setJitterBufferLen(length: number): void {
    if (!this._player) {
      this._logger.error("setJitterBufferLen: this._player is undefined");
      return;
    }

    this._jitter_buffer_len = this._audio && length < JITTER_BUFFER_LENGTH_MIN_FOR_AUDIO ? JITTER_BUFFER_LENGTH_MIN_FOR_AUDIO : length;
    this._player.setJitterBufferLen(this._jitter_buffer_len);
  }

  getJitterBufferLen(): number {
    return this._jitter_buffer_len;
  }

  pause(closeTransport: boolean = true): void {
    if (!this._player) {
      this._logger.error("pause: this._player is undefined");
      return;
    }

    this.clearTimers();

    if (this._status !== MSEMediaPlayerStatus.STOP) {
      if (!closeTransport) {
        if (this._status !== MSEMediaPlayerStatus.PAUSE) {
          this._player.pause();
          this._publish("pause");
        }
      } else {
        this.stop(0, "Pause");
      }
    } else {
      this._publish("pause");
      this.stop(0, "Pause");
    }
  }

  stop(code?: number, message?: string): boolean {
    if (this._status === MSEMediaPlayerStatus.STOP) return false;

    this._publish("stop", code ?? 0, message ?? "");

    return true;
  }

  async getURL({cameraid, startTime, endTime, streamnum = "1", isLocal = false, audio = false}: GetUrlParameters): Promise<{ url: string, audio: boolean }> {
    try {
      let parameters: GetUrlParameters = {
        cameraid,
        streamnum,
        isLocal,
        audio
      };
      if (startTime && endTime) {
        parameters.startTime = timestampToPts(startTime);
        parameters.endTime = timestampToPts(endTime);
      }
      const mediaUrl = await this._api.getMobileMediaURL<{url: string, audio?: boolean}>(parameters);
      return {url: mediaUrl.url, audio: mediaUrl.audio ?? false};
    }
    catch (e: any) {
      throw new Error(e.message);
    }
  }

  destroy(): void {
    this._player && this._player.teardown();

    if (this._node) {
      m.mount(this._node, null);
      this._node = null;
    }
  }

  /**
   * subscribe for event
   *
   * @param {string} eventName event name
   * @param {Function} callback function
   */
  subscribe(eventName: YUVPlayerEventName, callback: YUVPlayerEvent[YUVPlayerEventName]): boolean {
    if (!eventName) return false;

    if (!this._topics.has(eventName)) {
      this._logger.error(`Unknown topic '${eventName}'`);
      return false;
    }

    this._topicsEvents.on(eventName, callback.bind(this));

    return true;
  }

  unsubscribe(eventName: YUVPlayerEventName): boolean {
    if (!eventName) return false;

    if (!this._topics.has(eventName)) {
      this._logger.error(`Unknown topic '${eventName}'`);
      return false;
    }

    this._topicsEvents.removeAllListeners(eventName);

    return true;
  }

  /**
   * subscribe for single event
   *
   * @param {string} eventName event name
   * @param {Function} callback function
   */
  once(eventName: YUVPlayerEventName, callback: YUVPlayerEvent[YUVPlayerEventName]): boolean {
    if (!eventName) return false;

    if (!this._topics.has(eventName)) {
      this._logger.error(`Unknown topic '${eventName}'`);
      return false;
    }

    this._topicsEvents.once(eventName, callback.bind(this));

    return true;
  }

  /**
   * publish event from media player object
   *
   * @param {string} eventName
   * @param {...*} data
   */
  _publish(eventName: YUVPlayerEventName, ...data: Parameters<YUVPlayerEvent[YUVPlayerEventName]>): boolean {
    // if (!this._player) return false;
    if (!eventName) return false;

    if (!this._topics.has(eventName)) {
      this._logger.error(`Unknown topic '${eventName}'`);
      return false;
    }

    this._topicsEvents.emit(eventName, ...data);

    return true;
  }

  getMetadataDrawType(): MSEMediaPlayerMetadataType {
    return this._metadataType;
  }

  setMetadataDrawType(metadata: MSEMediaPlayerMetadataType) {
    this._metadataType = metadata;

    m.redraw();
  }

  showMetaData() {
    this._isMetadataVisible = true;

    m.redraw();
  };

  hideMetaData() {
    this._isMetadataVisible = false;

    m.redraw();
  };

  getConnectionType(): Exclude<MSEMediaPlayerConnection, MSEMediaPlayerConnection.AUTO> {
    return this._connectionType;
  };

  getTransportType(): MSEMediaPlayerTransport {
    return MSEMediaPlayerTransport.WEBSOCKET;
  }

  getPreferredConnectionType(): MSEMediaPlayerConnection {
    return this._preferredConnectionType;
  };

  async setPreferredConnectionType(type: MSEMediaPlayerConnection = MSEMediaPlayerConnection.AUTO, isPlay: boolean = true): Promise<void> {
    if (![MSEMediaPlayerConnection.LOCAL, MSEMediaPlayerConnection.CLOUD, MSEMediaPlayerConnection.AUTO].includes(type)) {
      throw new Error("incorrect connection type");
    }

    this._preferredConnectionType = this._disableLocalTransport ? MSEMediaPlayerConnection.CLOUD : type;

    if (this.getConnectionType() == this._preferredConnectionType) {
      return;
    }

    // TODO: reinit connection with new url

    // reinit connection if it closed with current parameters
    this.stop();

    if (isPlay) {
      await this.play();
    }
  }

  /**
   * @private
   */
  _addEvents(): void {
    this.subscribe("stop", (code?: number, message?: string) => {
      this.clearTimers();

      this._player && this._player.teardown();
      const previousStatus = this._status;
      this._status = MSEMediaPlayerStatus.STOP;

      let errorMessage;
      if (message) {
        errorMessage = message;
      } else {
        const message = typeof code !== "undefined" && code in this._stopCodeMessage ? this._stopCodeMessage[code] : undefined;
        const defaultMessage = this._stopCodeMessage["-1"] ?? "";
        errorMessage = message ? __(message) : __(defaultMessage);
      }

      const errorMessageWithCode = `[${code}] ${errorMessage}`;
      this.printErrorMessage(errorMessageWithCode);

      this._showSnapshot();

      this._inProgress = false;
      this._snapshotLoading = false;

      m.redraw();

      const errorCodeList: number[] = [
        MSEMediaPlayerError.NOT_AUTHORIZED,
        MSEMediaPlayerError.AVATAR_OFFLINE,
        MSEMediaPlayerError.BROKEN_ON_AVATAR_SIDE,
        MSEMediaPlayerError.LIVE_NOT_OPENED_NOT_ENOUGH_AVATAR_VSAAS_BANDWIDTH,
        MSEMediaPlayerError.LIVE_STOPPED_NOT_ENOUGH_AVATAR_VSAAS_BANDWIDTH,
        MSEMediaPlayerError.STREAM_STOPPED_NOT_ENOUGH_AVATAR_LOCAL_BANDWIDTH,
        MSEMediaPlayerError.CRITICAL
      ];

      if (previousStatus === MSEMediaPlayerStatus.PAUSE) {
        // do not restart
      } else
      if (typeof code !== "undefined" && code > 59000 && code < 60000) {
        // do not restart
      } else
      if (code === MSEMediaPlayerError.STREAM_STOPPED_UNEXPECTEDLY
        || code === MSEMediaPlayerError.INTERNAL_ERROR
        || code === MSEMediaPlayerError.VIDEO_UNAVAILABLE && !this._isArchive) {
        const delay = 2 * 1000; // ms
        this._logger.log(`reconnecting in ${delay/1000} sec`);
        if (this._isReconnectingAllowed) {
          this._reconnectTimeoutId = window.setTimeout(() => this.play(), delay);
        }
      } else
      if (typeof code !== "undefined" && errorCodeList.includes(code)) {
        const delay = 5 * 1000; // ms // TODO: exponential rise to 1min
        this._logger.log(`reconnecting in ${delay/1000} sec`);
        if (this._isReconnectingAllowed) {
          this._reconnectTimeoutId = window.setTimeout(() => this.play(), delay);
        }
      } else
      // play is called in aod request callback
      // TODO: and not in local stream
      if (code === MSEMediaPlayerError.VIDEO_UNAVAILABLE && this._isArchive) {
        // do not restart
      } else
      if (code === MSEMediaPlayerError.END_OF_ARCHIVE || code === MSEMediaPlayerError.END_OF_STREAM || code === MSEMediaPlayerError.END_OF_CHUNK) {
        // do not restart
      } else
      if (code !== 0) {
        const delay = 5 * 1000; // ms // TODO: exponential rise to 1min
        this._logger.log(`reconnecting in ${delay/1000} sec`);
        if (this._isReconnectingAllowed) {
          this._reconnectTimeoutId = window.setTimeout(() => this.play(), delay);
        }
      }
    });

    this.subscribe("play", () => {
      this._logger.log("PLAY");
      this._status = MSEMediaPlayerStatus.PLAY;
      this._inProgress = false;

      this._correctSize();

      this._hideSnapshot();

      // this._render();

      m.redraw();
    });

    this.subscribe("pause", () => {
      this.clearTimers();

      this._status = MSEMediaPlayerStatus.PAUSE;
      this.printErrorMessage(__("Pause"));

      m.redraw();
    });

    this.subscribe("connecting", (message: string) => {
      this.printErrorMessage(__("Opening video") + " / " + message);

      m.redraw();
    });

    this.subscribe("initializing", () => {
      this.printErrorMessage(__("Initializing"));

      m.redraw();
    });

    this.subscribe("buffering", () => {
      this.printErrorMessage(__("Buffering"));

      m.redraw();
    });

    if (this._node) {
      this._stopOnIdleTimeout !== 0 && idle({
        onIdle: () => {
          // if Player is working with a local stream, DO NOT STOP it
          if (this.getConnectionType() !== MSEMediaPlayerConnection.LOCAL
              && this._status === MSEMediaPlayerStatus.PLAY) {
            this._stoppedAfterIdle = true;
            this._logger.log("idle: stop");
            this.stop();
          }
        },
        onActive: () => {
          if (this._stoppedAfterIdle && this._status === MSEMediaPlayerStatus.STOP) {
            this._stoppedAfterIdle = false;
            this.play();
          }
        },
        idle: this._stopOnIdleTimeout
      }).start();
    }
  }

  printErrorMessage(message: string): boolean {
    if (!this._isPrintError) return false;
    // if (!this._player) return false;
    if (!this._node) return false;

    const errorNode = this._node.querySelector(".error");
    if (!errorNode) {
      this._logger.error("printErrorMessage: errorNode is undefined");
      return false;
    }
    errorNode.innerHTML = message;

    return true;
  }

  getDrawNode(): HTMLCanvasElement | null {
    if (!this._node) {
      return null;
    }
    return this._node.querySelector("canvas");
  }

  getMetaDataNode(): HTMLDivElement | null {
    if (!this._node) {
      return null;
    }
    return this._node.querySelector(".metadata");
  }

  _correctSize(): void {
    if (!this._node) return;

    /*
    let width = this._transport._stream.width / this._pixelAspectRatio;
    let height = this._transport._stream.height;
    if (this._pixelAspectRatio > 1) {
      width = this._transport._stream.width;
      height = this._transport._stream.height * this._pixelAspectRatio;
    }
    */

    let imageAspectRatio = !this._stream ? 0 : this._stream.width / this._stream.height;
    let playerBlock = this._node;

    let playerWidth = playerBlock.offsetWidth;
    let playerHeight = playerBlock.offsetHeight;
    let playerAspectRatio = playerWidth / playerHeight;

    if (this._isStretchAspectRatio) {
      imageAspectRatio = playerAspectRatio;
    } else {
      imageAspectRatio /= this._pixelAspectRatio;
    }

    let cssWidth = "";
    let cssHeight = "";

    let width = 0;
    let height = 0;

    if (imageAspectRatio <= playerAspectRatio) {
      width = Math.floor(playerHeight * imageAspectRatio + 0.5);
      height = playerHeight;

      cssWidth = width + "px";
      cssHeight = 100 + "%";
    } else {
      width = playerWidth;
      height = Math.floor(playerWidth / imageAspectRatio + 0.5);

      cssWidth = 100 + "%";
      cssHeight = height + "px";
    }

    const metadataBlock = this._node.querySelector<HTMLElement>(".metadata");
    if (metadataBlock) {
      metadataBlock.style.width = cssWidth;
      metadataBlock.style.height = cssHeight;
    }

    if (!this._canvas) {
      this._logger.error("_correctSize: this._canvas is undefined");
    }

    if (this._stream && this._canvas) {
      this._canvas.width = !this._stream ? 0 : this._stream.width;
      this._canvas.height = !this._stream ? 0 : this._stream.height;
      this._canvas.style.width = cssWidth;
      this._canvas.style.height = cssHeight;
    }

    this._rendered.width = width;
    this._rendered.height = height;

    this._publish("resize");
  }

  /**
   * change player current archive time when player in pause
   * this call do not start playing at new time, play method should be called
   *
   * @param {string} obj
   * @param {number} time ms
   * @param {boolean} isPlay
   */
  async setTime(obj: UUID, time: number, isPlay: boolean = false): Promise<void> {
    if (!this._node) {
      this._logger.error("setTime: this._node is undefined");
      return;
    }

    if (!this._player) {
      this._logger.error("setTime: this._player is undefined");
      return;
    }

    this._obj = obj;
    this._currentTime = time;

    if (this._status != MSEMediaPlayerStatus.PLAY) {
      this._showSnapshot();
    }

    if (!this._isArchive) {
      this._isArchive = true;

      if (isPlay) {
        this.stop();
        await this.play(obj, time);
      }
    } else {
      if (this._status == MSEMediaPlayerStatus.PLAY) {
        this._player.jump(Math.trunc(time / 1000));
      } else
      if (this._status == MSEMediaPlayerStatus.PAUSE) {
        if (isPlay) {
          if (this._currentTime && this._lastJumpTime !== this._currentTime) {
            this._lastJumpTime = this._currentTime;
            this._player.jump(Math.trunc(this._currentTime / 1000));
          }
          this._player.resume(Math.sign(this._speed));
        }
      }
    }
  }

  _showSnapshot(): void {
    if (!this._node) return;
    if (!this._obj) {
      this._logger.error("_showSnapshot: this._obj is undefined");
      return;
    }

    let snapshotBlock = this._node.querySelector<HTMLElement>(".snapshot");
    if (!snapshotBlock) {
      this._logger.error("_showSnapshot: snapshotBlock is undefined");
      return;
    }
    snapshotBlock.style.display = "flex";

    this._snapshotLoading = true;
    m.redraw();

    let snapshotImgBlock = snapshotBlock.querySelector<HTMLImageElement>("img");
    if (!snapshotImgBlock) {
      this._logger.error("_showSnapshot: snapshotImgBlock is undefined");
      return;
    }
    snapshotImgBlock.onload = snapshotImgBlock.onerror = snapshotImgBlock.onabort = () => {
      this._snapshotLoading = false;
      m.redraw();
    };
    snapshotImgBlock.src = getSnapshotURL(this._obj, this._currentTime ? Math.round(this._currentTime / 1000) : undefined, true, true);

    // let imageBlock = this._node.querySelector(".image");
    let metadataBlock = this._node.querySelector<HTMLElement>(".metadata");
    // imageBlock.style.display = "none";
    metadataBlock && (metadataBlock.style.display = "none");
  }

  _hideSnapshot(): void {
    if (!this._node) return;

    let snapshotBlock = this._node.querySelector<HTMLElement>(".snapshot");
    if (snapshotBlock) {
      snapshotBlock.style.display = "none";
    }

    let metadataBlock = this._node.querySelector<HTMLElement>(".metadata");
    if (metadataBlock) {
      metadataBlock.style.display = "";
    }
  }

  async setStreamId(streamid: MSEMediaPlayerStream): Promise<void> {
    this._streamid = streamid;

    if (this._status === MSEMediaPlayerStatus.PLAY) {
      this.pause();

      this.printErrorMessage(__("Init stream"));

      // wait for connection close
      // TODO: add once subscription for close event
      await Utils.wait(2000);
      this.play();
    }
  }

  showProgressIndicator(): void {
    this._inProgress = true;

    m.redraw();
  }

  hideProgressIndicator(): void {
    this._inProgress = false;

    m.redraw();
  }

  private async checkIsLocal(id: UUID): Promise<boolean> {
    if (this._preferredConnectionType == MSEMediaPlayerConnection.CLOUD) {
      return false;
    }

    if (this._preferredConnectionType == MSEMediaPlayerConnection.LOCAL) {
      return true;
    }

    try {
      const avatarId = await this.getAvatarId(id);
      if (!avatarId) {
        return false;
      }

      await this.checkLocalConnection(id, avatarId);
      return true;
    }
    catch {
    }

    return false;
  }

  private async checkLocalConnection(id: UUID, avatarId: UUID): Promise<void> {
    const isPreviousLocalConnectionSuccess = this.isPreviousLocalConnectionSuccess(avatarId);
    let localConnectionStatus = this.getLocalConnectionStatus(avatarId);

    if (!isPreviousLocalConnectionSuccess && localConnectionStatus.fail) {
      throw {code: localConnectionStatus.fail.code, message: localConnectionStatus.fail.message};
    }

    let localConnectionException = null;
    try {
      const parameters: GetUrlParameters = {
        cameraid: id,
        isLocal: true,
        audio: true
      };

      const {url} = await this.getURL(parameters);
      await timeout(this.checkWS(url, !!parameters.isLocal), this._connectionTimeout);

      localConnectionStatus.time = Date.now();
      localConnectionStatus.fail = null;
    }
    catch (e: any) {
      let message = e.message;
      if (e instanceof TimeoutError) {
        message = __("Failed to open local stream: Connection timed out");
        e.message = message;
        this._logger.warn(message);
      }
      localConnectionException = e;

      localConnectionStatus.time = 0;
      localConnectionStatus.fail = {
        code: e.code,
        message
      };
    }

    this.setLocalConnectionStatus(avatarId, localConnectionStatus);

    if (localConnectionException) {
      throw localConnectionException;
    }
  }

  private async checkWS(url: string, isLocal: boolean): Promise<void> {
    const protocols = isLocal ? "media.videonext.com" : undefined;
    const websocket = new WebSocket(url, protocols);
    websocket.binaryType = "arraybuffer";

    return new Promise((resolve) => {
      websocket.addEventListener("open", () => {
        resolve();

        websocket.close();
      });
    });
  }

  private isPreviousLocalConnectionSuccess(avatarId: UUID) {
    const localConnectionStatus = this.getLocalConnectionStatus(avatarId);
    return Date.now() - localConnectionStatus.time < this._checkConnectionTimeout;
  }

  private getLocalConnectionStatus(avatarId: UUID): LocalConnectionStatus {
    const localConnectionStatus = localStorage.getItem(`local_connection_status:${avatarId}`);

    let webRTCStatus = {time: 0};
    try {
      if (localConnectionStatus) {
        webRTCStatus = JSON.parse(localConnectionStatus);
      }
    }
    catch (e) {}

    return webRTCStatus;
  }

  private setLocalConnectionStatus(avatarId: UUID, webRTCStatus: LocalConnectionStatus) {
    const localConnectionStatus = JSON.stringify(webRTCStatus);
    localStorage.setItem(`local_connection_status:${avatarId}`, localConnectionStatus);
  }

  private async getAvatarId(obj: UUID | undefined = this._obj): Promise<string | undefined> {
    if (this._avatarId) {
      return this._avatarId;
    }

    let avatarId: UUID;
    try {
      const response = await this._api.getAttributes({obj: obj});
      avatarId = response.list.AVATARID;
    }
    catch (e) {
      if (e instanceof AjaxError) {
        this._logger.error(e.message);
      }
      return;
    }

    this._avatarId = avatarId;

    return this._avatarId;
  }

  clearTimers(): void {
    if (!this._reconnectTimeoutId) {
      return;
    }

    clearTimeout(this._reconnectTimeoutId);
    this._reconnectTimeoutId = undefined;
  }

  getTime(): number | undefined {
    return this._currentTime;
  }
}
