/* vim: set sts=4 sw=4 et :
 *
 * Demo Javascript app for negotiating and streaming a sendrecv webrtc stream
 * with a GStreamer app. Runs only in passive mode, i.e., responds to offers
 * with answers, exchanges ICE candidates, and streams.
 *
 * Author: Nirbheek Chauhan <nirbheek@centricular.com>
 */

// Set this to override the automatic detection in websocketServerConnect()
var ws_server;
var ws_port;
// Override with your own STUN servers if you want
var rtc_configuration = {iceServers: [{urls: "stun:stun.l.google.com:19302"},
                                       /* TODO: do not keep these static and in clear text in production,
                                        * and instead use one of the mechanisms discussed in
                                        * https://groups.google.com/forum/#!topic/discuss-webrtc/nn8b6UboqRA
                                        */
                                      {'urls': 'turn:turn.homeneural.net:3478?transport=udp',
                                       'credential': '1qaz2wsx',
                                       'username': 'test'
                                      }],
                         /* Uncomment the following line to ensure the turn server is used
                          * while testing. This should be kept commented out in production,
                          * as non-relay ice candidates should be preferred
                          */
                         // iceTransportPolicy: "relay",
                        };

var sessions = {}

/* https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript */
function getOurId() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

function Uint8ToString(u8a){
  var CHUNK_SZ = 0x8000;
  var c = [];
  for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
    c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
  }
  return c.join("");
}

function Session(our_id, peer_id, closed_callback) {
    this.id = null;
    this.peer_connection = null;
    this.ws_conn = null;
    this.peer_id = peer_id;
    this.our_id = our_id;
    this.closed_callback = closed_callback;
    this.data_channel = null;
    this.input = null;

    this.getVideoElement = function() {
        return document.getElementById("stream-" + this.our_id);
    };

    this.resetState = function() {
        if (this.peer_connection) {
            this.peer_connection.close();
            this.peer_connection = null;
        }
        var videoElement = this.getVideoElement();
        if (videoElement) {
            videoElement.pause();
            videoElement.src = "";
        }

        var session_div = document.getElementById("session-" + this.our_id);
        if (session_div) {
            session_div.parentNode.removeChild(session_div);
        }
        if (this.ws_conn) {
            this.ws_conn.close();
            this.ws_conn = null;
        }

        this.input && this.input.detach();
        this.data_channel = null;
    };

    this.handleIncomingError = function(error) {
        this.resetState();
        this.closed_callback(this.our_id);
    };

    this.setStatus = function(text) {
        console.log(text);
        var span = document.getElementById("status-" + this.our_id);
        // Don't set the status if it already contains an error
        if (!span.classList.contains('error'))
            span.textContent = text;
    };

    this.setError = function(text) {
        console.error(text);
        var span = document.getElementById("status-" + this.our_id);
        span.textContent = text;
        span.classList.add('error');
        this.resetState();
        this.closed_callback(this.our_id);
    };

    // Local description was set, send it to peer
    this.onLocalDescription = function(desc) {
        console.log("Got local description: " + JSON.stringify(desc), this);
        var thiz = this;
        this.peer_connection.setLocalDescription(desc).then(() => {
            this.setStatus("Sending SDP answer");
            var sdp = {
                'type': 'peer',
                'sessionId': this.id,
                'sdp': this.peer_connection.localDescription.toJSON()
            };
            this.ws_conn.send(JSON.stringify(sdp));
        }).catch(function(e) {
            thiz.setError(e);
        });
    };

    this.onRemoteDescriptionSet = function() {
        this.setStatus("Remote SDP set");
        this.setStatus("Got SDP offer");
        this.peer_connection.createAnswer()
        .then(this.onLocalDescription.bind(this)).catch(this.setError);
    }

    // SDP offer received from peer, set remote description and create an answer
    this.onIncomingSDP = function(sdp) {
        var thiz = this;
        this.peer_connection.setRemoteDescription(sdp)
            .then(this.onRemoteDescriptionSet.bind(this))
            .catch(function(e) {
                thiz.setError(e)
            });
    };

    // ICE candidate received from peer, add it to the peer connection
    this.onIncomingICE = function(ice) {
        var candidate = new RTCIceCandidate(ice);
        var thiz = this;
        this.peer_connection.addIceCandidate(candidate).catch(function(e) {
            thiz.setError(e)
        });
    };

    this.onServerMessage = function(event) {
        console.log("Received " + event.data);
        try {
            msg = JSON.parse(event.data);
        } catch (e) {
            if (e instanceof SyntaxError) {
                this.handleIncomingError("Error parsing incoming JSON: " + event.data);
            } else {
                this.handleIncomingError("Unknown error parsing response: " + event.data);
            }
            return;
        }

        if (msg.type == "registered") {
            this.setStatus("Registered with server");
            this.connectPeer();
        } else if (msg.type == "sessionStarted") {
            this.setStatus("Registered with server");
            this.id = msg.sessionId;
        } else if (msg.type == "error") {
            this.handleIncomingError(msg.details);
        } else if (msg.type == "endSession") {
            this.resetState();
            this.closed_callback(this.our_id);
        } else if (msg.type == "peer") {
            // Incoming peer message signals the beginning of a call
            if (!this.peer_connection)
                this.createCall(msg);

            if (msg.sdp != null) {
                this.onIncomingSDP(msg.sdp);
            } else if (msg.ice != null) {
                this.onIncomingICE(msg.ice);
            } else {
                this.handleIncomingError("Unknown incoming JSON: " + msg);
            }
        }
    };

    this.streamIsPlaying = function(e) {
        this.setStatus("Streaming");
    };

    this.onServerClose = function(event) {
        this.resetState();
        this.closed_callback(this.our_id);
    };

    this.onServerError = function(event) {
        this.handleIncomingError('Server error');
    };

    this.websocketServerConnect = function() {
        // Clear errors in the status span
        var span = document.getElementById("status-" + this.our_id);
        span.classList.remove('error');
        span.textContent = '';
        console.log("Our ID:", this.our_id);
        var ws_port = ws_port || '8443';
        if (window.location.protocol.startsWith ("file")) {
            var ws_server = ws_server || "127.0.0.1";
        } else if (window.location.protocol.startsWith ("http")) {
            var ws_server = ws_server || window.location.hostname;
        } else {
            throw new Error ("Don't know how to connect to the signalling server with uri" + window.location);
        }
        var ws_url = 'ws://' + ws_server + ':' + ws_port
        this.setStatus("Connecting to server " + ws_url);
        this.ws_conn = new WebSocket(ws_url);
        /* When connected, immediately register with the server */
        this.ws_conn.addEventListener('open', (event) => {
            this.setStatus("Connecting to the peer");
            this.connectPeer();
        });
        this.ws_conn.addEventListener('error', this.onServerError.bind(this));
        this.ws_conn.addEventListener('message', this.onServerMessage.bind(this));
        this.ws_conn.addEventListener('close', this.onServerClose.bind(this));
    };

    this.connectPeer = function() {
        this.setStatus("Connecting " + this.peer_id);

        this.ws_conn.send(JSON.stringify({
            "type": "startSession",
            "peerId": this.peer_id
        }));
    };

    this.onRemoteStreamAdded = function(event) {
        var videoTracks = event.stream.getVideoTracks();
        var audioTracks = event.stream.getAudioTracks();

        console.log(videoTracks);

        if (videoTracks.length > 0) {
            console.log('Incoming stream: ' + videoTracks.length + ' video tracks and ' + audioTracks.length + ' audio tracks');
            this.getVideoElement().srcObject = event.stream;
            this.getVideoElement().play();
        } else {
            this.handleIncomingError('Stream with unknown tracks added, resetting');
        }
    };

    this.createCall = function(msg) {
        console.log('Creating RTCPeerConnection');

        this.peer_connection = new RTCPeerConnection(rtc_configuration);
        this.peer_connection.onaddstream = this.onRemoteStreamAdded.bind(this);

        this.peer_connection.ondatachannel = (event) => {
            console.log(`Data channel created: ${event.channel.label}`);
            this.data_channel = event.channel;

            video_element = this.getVideoElement();
            if (video_element) {
                this.input = new Input(video_element, (data) => {
                    if (this.data_channel) {
                        console.log(`Navigation data: ${data}`);
                        this.data_channel.send(JSON.stringify(data));
                    }
                });
            }

            this.data_channel.onopen = (event) => {
                console.log("Receive channel opened, attaching input");
                this.input.attach();
            }
            this.data_channel.onclose = (event) => {
                console.info("Receive channel closed");
                this.input && this.input.detach();
                this.data_channel = null;
            }
            this.data_channel.onerror = (event) => {
                this.input && this.input.detach();
                console.warn("Error on receive channel", event.data);
                this.data_channel = null;
            }

            let buffer = [];
            this.data_channel.onmessage = (event) => {
                if (typeof event.data === 'string' || event.data instanceof String) {
                    if (event.data == 'BEGIN_IMAGE')
                        buffer = [];
                    else if (event.data == 'END_IMAGE') {
                        var decoder = new TextDecoder("ascii");
                        var array_buffer = new Uint8Array(buffer);
                        var str = decoder.decode(array_buffer);
                        let img = document.getElementById("image");
                        img.src = 'data:image/png;base64, ' + str;
                    }
                } else {
                    var i, len = buffer.length
                    var view = new DataView(event.data);
                    for (i = 0; i < view.byteLength; i++) {
                        buffer[len + i] = view.getUint8(i);
                    }
                }
            }
        }

        this.peer_connection.onicecandidate = (event) => {
            if (event.candidate == null) {
                console.log("ICE Candidate was null, done");
                return;
            }
            this.ws_conn.send(JSON.stringify({
                "type": "peer",
                "sessionId": this.id,
                "ice": event.candidate.toJSON()
            }));
        };

        this.setStatus("Created peer connection for call, waiting for SDP");
    };

    document.getElementById("stream-" + this.our_id).addEventListener("playing", this.streamIsPlaying.bind(this), false);

    this.websocketServerConnect();
}

function startSession() {
    var peer_id = document.getElementById("camera-id").value;

    if (peer_id === "") {
        return;
    }

    sessions[peer_id] = new Session(peer_id);
}

function session_closed(peer_id) {
    sessions[peer_id] = null;
}

function addPeer(peer_id, meta) {
    console.log("Meta: ", JSON.stringify(meta));

    var nav_ul = document.getElementById("camera-list");

    meta = meta ? meta : {"display-name": peer_id};
    let display_html = `${meta["display-name"] ? meta["display-name"] : peer_id}<ul>`;
    for (const key in meta) {
        if (key != "display-name") {
            display_html += `<li>- ${key}: ${meta[key]}</li>`;
        }
    }
    display_html += "</ul>"

    var li_str = '<li id="peer-' + peer_id + '"><button class="button button1">' + display_html + '</button></li>';

    nav_ul.insertAdjacentHTML('beforeend', li_str);
    var li = document.getElementById("peer-" + peer_id);
    li.onclick = function(e) {
        var sessions_div = document.getElementById('sessions');
        var our_id = getOurId();
        var session_div_str = '<div class="session" id="session-' + our_id + '"><video preload="none" class="stream" id="stream-' + our_id + '"></video><p>Status: <span id="status-' + our_id + '">unknown</span></p></div>'
        sessions_div.insertAdjacentHTML('beforeend', session_div_str);
        sessions[peer_id] = new Session(our_id, peer_id, session_closed);
    }
}

function clearPeers() {
    var nav_ul = document.getElementById("camera-list");

    while (nav_ul.firstChild) {
        nav_ul.removeChild(nav_ul.firstChild);
    }
}

function onServerMessage(event) {
    console.log("Received " + event.data);

    try {
        msg = JSON.parse(event.data);
    } catch (e) {
        if (e instanceof SyntaxError) {
            console.error("Error parsing incoming JSON: " + event.data);
        } else {
            console.error("Unknown error parsing response: " + event.data);
        }
        return;
    }

    if (msg.type == "welcome") {
        console.info(`Got welcomed with ID ${msg.peer_id}`);
        ws_conn.send(JSON.stringify({
            "type": "list"
        }));
    } else if (msg.type == "list") {
        clearPeers();
        for (i = 0; i < msg.producers.length; i++) {
            addPeer(msg.producers[i].id, msg.producers[i].meta);
        }
    } else if (msg.type == "peerStatusChanged") {
        var li = document.getElementById("peer-" + msg.peerId);
        if (msg.roles.includes("producer")) {
            if (li == null) {
                console.error('Adding peer');
                addPeer(msg.peerId, msg.meta);
            }
        } else if (li != null) {
            li.parentNode.removeChild(li);
        }
    } else {
        console.error("Unsupported message: ", msg);
    }
};

function clearConnection() {
    ws_conn.removeEventListener('error', onServerError);
    ws_conn.removeEventListener('message', onServerMessage);
    ws_conn.removeEventListener('close', onServerClose);
    ws_conn = null;
}

function onServerClose(event) {
    clearConnection();
    clearPeers();
    console.log("Close");
    window.setTimeout(connect, 1000);
};

function onServerError(event) {
    clearConnection();
    clearPeers();
    console.log("Error", event);
    window.setTimeout(connect, 1000);
};

function connect() {
    var ws_port = ws_port || '8443';
    if (window.location.protocol.startsWith ("file")) {
        var ws_server = ws_server || "127.0.0.1";
    } else if (window.location.protocol.startsWith ("http")) {
        var ws_server = ws_server || window.location.hostname;
    } else {
        throw new Error ("Don't know how to connect to the signalling server with uri" + window.location);
    }
    var ws_url = 'ws://' + ws_server + ':' + ws_port
    console.log("Connecting listener");
    ws_conn = new WebSocket(ws_url);
    ws_conn.addEventListener('open', (event) => {
        ws_conn.send(JSON.stringify({
            "type": "setPeerStatus",
            "roles": ["listener"]
        }));
    });
    ws_conn.addEventListener('error', onServerError);
    ws_conn.addEventListener('message', onServerMessage);
    ws_conn.addEventListener('close', onServerClose);
}

function setup() {
    connect();
}
