import { Controller } from '@hotwired/stimulus'
import Client from 'models/client'
import WebrtcNegotiation from 'models/webrtc_negotiation'
import VideoRoomSubscription from 'subscriptions/video_room_subscription'
import Signaller from 'subscriptions/signaling_subscription'

export default class VideoRoomController extends Controller {
  connect() {
    this.clients = {}
    this.client = new Client(this.clientIdValue)

    this.subscription = new VideoRoomSubscription({
      delegate: this,
      id: this.idValue,
      clientId: this.client.id
    })

    this.signaller = new Signaller({
      delegate: this,
      id: this.idValue,
      clientId: this.client.id
    })

    this.client.on('iceConnection:checking', ({ detail: { otherClient } }) => {
      this.startStreamingTo(otherClient)
    })
  }

  async enter () {
    try {
      const constraints = { audio: true, video: true }
      this.client.stream = await navigator.mediaDevices.getUserMedia(constraints)
      this.localMediumTarget.srcObject = this.client.stream
      this.localMediumTarget.muted = true // Keep muted on Firefox
      this.enterTarget.hidden = true

      this.subscription.start()
      this.signaller.start()
    } catch (error) {
      console.error(error)
    }
  }

  greetNewClient ({ from }) {
    const otherClient = this.findOrCreateClient(from)
    otherClient.newcomer = true
    this.subscription.greet({ to: otherClient.id, from: this.client.id })
  }

  remoteMediumTargetConnected (element) {
    const clientId = element.id.replace('medium_', '')
    this.negotiateConnection(clientId)
  }

  remoteMediumTargetDisconnected (element) {
    const clientId = element.id.replace('medium_', '')
    this.teardownClient(clientId)
  }

  negotiateConnection (clientId) {
    const otherClient = this.findOrCreateClient(clientId)

    // Be polite to newcomers!
    const polite = !!otherClient.newcomer

    otherClient.negotiation = this.createNegotiation({ otherClient, polite })

    // The polite client sets up the negotiation last, so we can start streaming
    // The impolite client signals to the other client that it's ready
    if (polite) {
      this.startStreamingTo(otherClient)
    } else {
      this.subscription.greet({ to: otherClient.id, from: this.client.id })
    }
  }

  teardownClient (clientId) {
    this.clients[clientId].stop()
    delete this.clients[clientId]
  }

  createNegotiation ({ otherClient, polite }) {
    const negotiation = new WebrtcNegotiation({
      signaller: this.signaller,
      client: this.client,
      otherClient: otherClient,
      polite
    })

    otherClient.on('track', ({ detail }) => {
      this.startStreamingFrom(otherClient.id, detail)
    })

    return negotiation
  }

  startStreamingTo (otherClient) {
    this.client.streamTo(otherClient)
  }

  startStreamingFrom (id, { track, streams: [stream] }) {
    const remoteMediaElement = this.findRemoteMediaElement(id)
    if (!remoteMediaElement.srcObject) {
      remoteMediaElement.srcObject = stream
    }
  }

  findOrCreateClient (id) {
    return this.clients[id] || (this.clients[id] = new Client(id))
  }

  findRemoteMediaElement (clientId) {
    const target = this.remoteMediumTargets.find(
      target => target.id === `medium_${clientId}`
    )
    return target ? target.querySelector('video') : null
  }

  negotiationFor (id) {
    return this.clients[id].negotiation
  }

  // VideoRoomSubscription Delegate

  videoRoomPinged (data) {
    this.greetNewClient(data)
  }

  // Signaler Delegate

  sdpDescriptionReceived ({ from, description }) {
    this.negotiationFor(from).setDescription(description)
  }

  iceCandidateReceived ({ from, candidate }) {
    this.negotiationFor(from).addCandidate(candidate)
  }

  negotiationRestarted ({ from }) {
    const negotiation = this.negotiationFor(from)
    negotiation.restart()
    negotiation.createOffer()
  }
}

VideoRoomController.values = { id: String, clientId: String }
VideoRoomController.targets = ['localMedium', 'remoteMedium', 'enter']
