'use strict'
const User = use('App/Models/User');
const UserChildUtils = use('App/Utils/UserChildUtils');
const CallUtils = use('App/Utils/CallUtils');
const Child = use('App/Models/Child');
const IceServer = use('App/Models/IceServer');
const UserChannel = use('App/Controllers/Ws/UserChannelController')
const calls = {};

class SignalingController {
  constructor({socket, request, auth, call}) {
    this.callId = socket.topic.split(':')[1];
    this.user = auth.user;
    this.socket = socket;
    this.request = request;
    this.register(call)
        .then(_ => {
          console.log(`User #${this.user.id} connected to call ${this.callId}`);
        })
        .catch(e => {
          console.error(e);
        });
  }
  async register(callModel) {
    if (!calls[this.callId])
      calls[this.callId] =
          new CallSession(callModel, this.onCallEnded.bind(this));
    else
      console.log(`Call #${this.callId} Already Found`);
    const callSession = calls[this.callId];
    const success = await callSession.registerUser(this.user, this.socket)
    if (!success) throw new Error('Invalid User');
    return true;
  }
  onCallEnded(callId) {
    console.log(`Call ${callId} Ended`);
    delete calls[callId];
  }
  onClose() {
    calls[this.callId].removeUser(this.user);
    console.log(`User #${this.user.id} left call ${this.callId}`);
  }
}

class CallSession {
  // states: NEW -> STARTED -> IN_PROGRESS -> ENDED
  constructor(callModel, onCallEndedCallback) {
    this.onCallEndedCallback = onCallEndedCallback;
    this.callId = callModel.id;
    this.callModel = callModel;
    this.callBooks = null;
    this.hostId = 2;
    this.state = callModel.state;
    this.sessionState = {page: 'lobby', activity: {type: null, model: null}};
    this.parent = {
      id: callModel.parent_id,
      socket: null,
      userModel: null,
      isParent: true
    };
    this.guest = {
      id: callModel.guest_id,
      socket: null,
      userModel: null,
      isParent: false
    };
    this.startTime = Date.now();
    this.heartbeat =
        setInterval(this.onHeartbeat.bind(this), 1000);  // Every second
    this.userMap = new Map();  // Reference to this.parent/guest by userId;
  }
  onHeartbeat() {
    const now = Date.now();
    const elapsed = ((now - this.startTime) / 60000).toPrecision(1);
    const newStateTimeout = 5;       // 5 min
    const startedStateTimeout = 10;  // 10 min
    // console.log(`Heartbeat for call #${this.callId} State: ${
    //     this.state}, time-elapsed: ${(elapsed)}min`);
    if (this.state === 'ENDED') return this.endCall();
    if (this.state === 'NEW' && elapsed >= newStateTimeout)
      return this.endCall();
    if (this.state === 'STARTED' && elapsed >= startedStateTimeout)
      return this.endCall();
  }
  removeUser(user) {
    let userToRemove = this.userMap.get(user.id);
    userToRemove.userModel = null;
    userToRemove.socket = null;
    this.userMap.delete(user.id);
    this.updateState();
    if (this.state === 'ENDED') this.endCall();
  }
  async registerUser(user, socket) {
    if (!this.child) this.child = await this.callModel.child().fetch();
    if (!this.callBooks)
      this.callBooks = await CallUtils.getBooks(
          this.callModel.parent_id, this.callModel.guest_id);
    let isParent = this.parent.id === user.id;
    let peerId = isParent ? this.guest.id : this.parent.id;
    if (isParent) {
      this.parent.userModel = user;
      this.parent.socket = socket;
      this.userMap.set(user.id, this.parent);
    } else {
      this.guest.userModel = user;
      this.guest.socket = socket;
      this.userMap.set(user.id, this.guest);
      peerId = this.parent.id;
    }

    socket.on('wrtc:sdp:offer', this.onSdpOffer.bind(this));
    socket.on('wrtc:sdp:answer', this.onSdpAnswer.bind(this));
    socket.on('wrtc:ice', this.onIceCandidate.bind(this));
    socket.on('call:host:changed', this.onHostChanged.bind(this));
    socket.on('call:view:lobby', this.onCallViewLobby.bind(this));
    socket.on('call:view:book', this.onCallViewBook.bind(this));
    socket.on('book:action:flip-page', this.onActionBookFlip.bind(this));

    await this.updateState();
    if (this.state === 'STARTED') {
      await this.sendStandby(socket, user.id, peerId);
      // Send event to other user about the call
      console.log(`trying to find peer's ${peerId} channel...`);
      const otherUserChannel = UserChannel.getUserChannel(peerId);
      if (otherUserChannel) {
        // console.log(otherUserChannel);
        console.log(`Sending notification to peer ${peerId}`);
        const payload = {callId: this.callId, child: this.child.toJSON()};
        console.dir(payload);
        otherUserChannel.emit('call:incoming', payload);
      }
    } else if (this.state === 'IN_PROGRESS') {
      await this.sendStart(socket, user.id, peerId);
    }
    return true;
  }
  endCall() {
    this.state = 'ENDED';
    if (this.callModel.state != 'ENDED') {
      this.callModel.state = this.state;
      this.callModel.save();
    }
    if (this.parent.socket) this.parent.socket.close();
    if (this.guest.socket) this.guest.socket.close();
    clearInterval(this.heartbeat);
    this.onCallEndedCallback(this.callId);
  }

  async sendStandby(socket, userId, peerId) {
    console.log(`Call #${this.callId} sendStandby -> ${userId}`);
    const iceServers = (await IceServer.all()).rows.map(i => i.toJSON());
    console.log(await this.callModel.parent().fetch());
    socket.emit('call:standby', {
      iceServers,
      peerId,
      books: this.callBooks,
      child: this.child.toJSON(),
      users: await Promise.all(
          [this.callModel.parent().fetch(), this.callModel.guest().fetch()]),
      hostId: this.hostId
    });
  }
  async sendStart(socket, userId, peerId) {
    console.log(`Call #${this.callId} sendStart -> ${userId}`);
    const iceServers = (await IceServer.all()).rows.map(i => i.toJSON());
    socket.emit('call:start', {
      iceServers,
      peerId,
      books: this.callBooks,
      child: this.child.toJSON(),
      users: await Promise.all(
          [this.callModel.parent().fetch(), this.callModel.guest().fetch()]),
      hostId: this.hostId
    });
  }
  async updateState() {
    console.log(`Call #${this.callId} state=${this.state}`);
    switch (this.state) {
      case 'NEW':
        if (this.areAllPartnersConnected())
          this.state = 'IN_PROGRESS';
        else
          this.state = 'STARTED';
        break;
      case 'STARTED':
        if (this.areAllPartnersConnected()) this.state = 'IN_PROGRESS';
        break;
      case 'IN_PROGRESS':
        if (!this.areAllPartnersConnected()) this.state = 'ENDED';
        break;
    }
    this.callModel.state = this.state;
    await this.callModel.save();
    console.log(`Call #${this.callId} state=${this.state}`);
  }
  areAllPartnersConnected() {
    return !!this.parent.socket && !!this.guest.socket;
  }
  async onIceCandidate(payload) {
    const {peerId, userId, ice} = payload;
    this.userMap.get(peerId).socket.emit('wrtc:ice', {ice});
    console.log(`[Signal] [onIceCandidate] ${userId} -> ${peerId}`);
    return true;
  }
  async onSdpOffer(payload) {
    const {peerId, userId, sdp} = payload;
    this.userMap.get(peerId).socket.emit('wrtc:sdp:offer', {sdp});
    console.log(`[Signal] [onSdpOffer] ${userId} -> ${peerId}`);
    return true;
  }

  async onSdpAnswer(payload) {
    const {peerId, userId, sdp} = payload;
    this.userMap.get(peerId).socket.emit('wrtc:sdp:answer', {sdp});
    console.log(`[Signal] [onSdpAnswer] ${userId} -> ${peerId}`);
    return true;
  }

  onActionBookFlip(payload) {
    const {peerId, userId, direction} = payload;
    this.userMap.get(peerId).socket.emit('book:action:flip-page', {direction});
    console.log(`[Signal] [book] [action] [flip] [${direction}] ${userId} -> ${
        peerId}`);
    return true;
  }
  onCallViewLobby(payload) {
    const {peerId, userId, direction} = payload;
    this.userMap.get(peerId).socket.emit('call:view:lobby', {});
    console.log(
        `[Signal] [call] [view] [lobby] [${direction}] ${userId} -> ${peerId}`);
    return true;
  }

  onCallViewBook(payload) {
    const {peerId, userId, direction, bookId} = payload;
    this.userMap.get(peerId).socket.emit('call:view:book', {bookId});
    console.log(
        `[Signal] [call] [view] [book] [${direction}] ${userId} -> ${peerId}`);
    return true;
  }

  async onHostChanged(payload) {
    const {peerId, userId} = payload;
    this.hostId = this.hostId === userId ? peerId : userId;
    console.log('Host: ', this.hostId);
    this.userMap.get(userId).socket.emit(
        'call:host:changed', {hostId: this.hostId});
    try {
      this.userMap.get(peerId).socket.emit(
          'call:host:changed', {hostId: this.hostId});
    } catch (e) {
    }
    return true;
  }
}

module.exports = SignalingController
