/*
 Copyright (C) 2024 3NSoft Inc.

 This program is free software: you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at your option) any later
 version.

 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { Peer, StreamType, StreamsStore } from 'src-video/store/streams';
import type { VueEventBus } from '@v1nt1248/3nclient-lib/plugins';
import type { PeerEvents } from './events';
import { OffBandSignalingChannel, WebRTCPeerChannel } from './webrtc-peer-channel';
import { sleep } from '@v1nt1248/3nclient-lib/utils';

interface InfoMsg {
  type: 'info',
  msg: any;
}

interface StreamInfoMsg {
  type: 'stream',
  streamId: string;
  streamType: StreamType;
}

interface StreamStateMsg {
  type: 'stream-state',
  streamId: string;
  audio: boolean;
  video: boolean;
}

interface DisconnectMsg {
  type: 'disconnect';
}

type Msg = InfoMsg | StreamInfoMsg | StreamStateMsg | DisconnectMsg;

export class PeerChannelWithStreams extends WebRTCPeerChannel {

  private readonly streams: Record<string, MediaStream> = {};
  private readonly ownTrackSenders = new Map<string, RTCRtpSender>();

  private constructor(
    private readonly peerAddr: string,
    private readonly store: StreamsStore,
    private readonly eventBus: VueEventBus<PeerEvents>,
    rtcConfig: RTCConfiguration, offBandComm: OffBandSignalingChannel,
    isPolite: boolean,
  ) {
    super(rtcConfig, offBandComm, isPolite);
    Object.seal(this);
  }

  public static makeWith(
    peerAddr: string,
    store: StreamsStore, eventBus: VueEventBus<PeerEvents>,
    rtcConfig: RTCConfiguration, offBandComm: OffBandSignalingChannel,
    isPolite: boolean,
  ): PeerChannelWithStreams {
    const peer = new PeerChannelWithStreams(
      peerAddr, store, eventBus, rtcConfig, offBandComm, isPolite
    );
    return peer;
  }

  private get peerStore(): Peer {
    return this.store.getPeer(this.peerAddr);
  }

  protected onConnectionTrack(ev: RTCTrackEvent): void {
    const { track, streams: [ stream ] } = ev;
    const { ui } = this.peerStore;
    this.streams[stream.id] = stream;
    track.onunmute = () => {
      const info = ui.streams.find(s => (s.streamId === stream.id));
      if (info) {
        this.store.syncUIWithTracksState(this.peerAddr);
        this.emitStreamAdditionEvent(stream.id, info.type);
      }
    };
    track.onmute = () => {
      this.store.syncUIWithTracksState(this.peerAddr);
    };
    track.onended = () => {
      this.store.syncUIWithTracksState(this.peerAddr);
    };
  }

  protected onWebRTCConnectionDisconnected(): void {
    this.peerStore.webRTCConnected = false;
  }

  protected onWebRTCConnected(): void {
    this.peerStore.webRTCConnected = true;
    this.emitPeerConnectedEvent();
  }

  getStream(id: string): MediaStream|undefined {
    return this.streams[id];
  }

  sendUserMediaStream(stream: MediaStream): void {
    this.sendStreamInfo(stream.id, 'camera+mic');
    this.sendStream(stream);
  }

  sendScreenMediaStream(type: 'window'|'screen', stream: MediaStream): void {
    this.sendStreamInfo(stream.id, type);
    this.sendStream(stream);
  }

  private sendStream(stream: MediaStream): void {
    console.log(`🧐 start sending stream ${stream.id}`);
    for (const track of stream.getTracks()) {
      const sender = this.conn.addTrack(track, stream);
      this.ownTrackSenders.set(track.id, sender);
      console.log(`🧐 sending track ${track.id}`);
    }
  }

  private sendStreamInfo(streamId: string, streamType: StreamType): void {
    const json: StreamInfoMsg = { type: 'stream', streamId, streamType };
    this.dataChannel?.send(JSON.stringify(json));
    console.log(`🧐 sent track info message with !!this.dataChannel:`, !!this.dataChannel, json);
  }

  signalOwnStreamState(streamId: string, audio: boolean, video: boolean): void {
    const json: StreamStateMsg = {
      type: 'stream-state',
      streamId, audio, video
    };
    this.dataChannel?.send(JSON.stringify(json));
  }

  stopSendingStream(stream: MediaStream): void {
    console.log(`🧐 stop sending stream ${stream.id}`);
    for (const track of stream.getTracks()) {
      const sender = this.ownTrackSenders.get(track.id);
      if (sender) {
        this.ownTrackSenders.delete(track.id);
        this.conn.removeTrack(sender);
        console.log(`🧐 removing track ${track.id} from sending`);
      }
    }
  }

  sendInfoMsg<T>(msg: T): void {
    const json: InfoMsg = {
      type: 'info', msg
    };
    this.dataChannel?.send(JSON.stringify(json));
    console.log(`🧐 sent info message with !!this.dataChannel:`, !!this.dataChannel);
  }

  protected onDataChannelMessage(ev: { data: string; }): void {
    try {
      console.log(`📡 data channel message`, ev.data);
      const json: Msg = JSON.parse(ev.data);
      if (json.type === 'info') {
        this.emitPeerInfoMsgEvent(json.msg);
      } else if (json.type === 'stream') {
        this.absorbStreamInfo(json);
      } else if (json.type === 'stream-state') {
        this.absorbStreamStateEvent(json);
      } else if (json.type === 'disconnect') {
        console.log(`🧐 received disconnect msg from peer`);
        this.emitPeerDisconnectedEvent();
        this.performClosing();
      } else {
        console.error(
          `Unknown type in data channel message:`, (json as any).type
        );
      }
    } catch (err) {
      console.error(`Captured error in handling data channel message`, err);
    }
  }

  private emitPeerInfoMsgEvent(msg: any): void {
    this.eventBus.emit('peer:info-msg', {
      peerAddr: this.peerAddr,
      msg
    });
  }

  private absorbStreamInfo({ streamType, streamId }: StreamInfoMsg): void {
    console.log(`🧐 received and added stream info`, { streamType, streamId });
    const { ui } = this.peerStore;
    ui.streams.push({ streamId, type: streamType });
    if (this.getStream(streamId)) {
      this.store.syncUIWithTracksState(this.peerAddr);
      this.emitStreamAdditionEvent(streamId, streamType);
    }
  }

  private emitStreamAdditionEvent(
    streamId: string, streamType: StreamType
  ): void {
    console.log(`📢 emitting stream:added`, streamType);
    this.eventBus.emit('stream:added', {
      peerAddr: this.peerAddr,
      streamId, streamType
    });
  }

  private absorbStreamStateEvent({
    streamId, audio, video
  }: StreamStateMsg): void {
    const stream = this.getStream(streamId);
    if (!stream) {
      return;
    }
    stream.getAudioTracks().forEach(t => { t.enabled = audio; });
    stream.getVideoTracks().forEach(t => { t.enabled = video; });
    this.store.syncUIWithTracksState(this.peerAddr);
    this.eventBus.emit('stream:track-event', {
      peerAddr: this.peerAddr,
      streamId, audio, video
    });
  }

  private emitPeerConnectedEvent(): void {
    this.eventBus.emit('peer:connected', {
      peerAddr: this.peerAddr
    });
  }

  private emitPeerDisconnectedEvent(): void {
    this.eventBus.emit('peer:disconnected', {
      peerAddr: this.peerAddr
    });
  }

  private stopSendingOwnTracks(): void {
    this.dataChannel?.close();
    for (const sender of this.ownTrackSenders.values()) {
      this.conn.removeTrack(sender);
    }
    this.ownTrackSenders.clear()
  }

  private async performClosing(): Promise<void> {
    try {
      this.stopSendingOwnTracks();
      await sleep(200);
    } finally {
      super.close();
    }
  }

  /**
   * Close should be called by one side.
   * When this is called after arrival of peer-disconnected message, a new call
   * may get started in a race condition.
   */
  async close(): Promise<void> {
    if (this.isConnected) {
      const json: DisconnectMsg = {
        type: 'disconnect'
      };
      this.dataChannel?.send(JSON.stringify(json));
    }
    await sleep(50);
    this.performClosing();
  }

  get vaStream(): MediaStream|undefined {
    const { ui } = this.peerStore;
    const vaStreamId = ui.streams.find(
      ({ type }) => (type === 'camera+mic')
    )?.streamId;
    return (vaStreamId ? this.getStream(vaStreamId) : undefined);
  }

}
