// Copyright (c) 2020 by Juliusz Chroboczek.

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

'use strict';

/** @type {string} */
let group;

/** @type {ServerConnection} */
let serverConnection;

/**
 * @typedef {Object} userpass
 * @property {string} username
 * @property {string} password
 */

/* Some browsers disable session storage when cookies are disabled,
   we fall back to a global variable. */
/**
 * @type {userpass}
 */
let fallbackUserPass = null;


/**
 * @param {string} username
 * @param {string} password
 */
function storeUserPass(username, password) {
    let userpass = {username: username, password: password};
    try {
        window.sessionStorage.setItem('userpass', JSON.stringify(userpass));
        fallbackUserPass = null;
    } catch(e) {
        console.warn("Couldn't store password:", e);
        fallbackUserPass = userpass;
    }
}

/**
 * Returns null if the user hasn't logged in yet.
 *
 * @returns {userpass}
 */
function getUserPass() {
    /** @type{userpass} */
    let userpass;
    try {
        let json = window.sessionStorage.getItem('userpass');
        userpass = JSON.parse(json);
    } catch(e) {
        console.warn("Couldn't retrieve password:", e);
        userpass = fallbackUserPass;
    }
    return userpass || null;
}

/**
 * Return null if the user hasn't logged in yet.
 *
 * @returns {string}
 */
function getUsername() {
    let userpass = getUserPass();
    if(!userpass)
        return null;
    return userpass.username;
}

/**
 * @typedef {Object} settings
 * @property {boolean} [localMute]
 * @property {string} [video]
 * @property {string} [audio]
 * @property {string} [send]
 * @property {string} [request]
 * @property {boolean} [activityDetection]
 * @property {Array.<number>} [resolution]
 * @property {boolean} [blackboardMode]
 */

/** @type{settings} */
let fallbackSettings = null;

/**
 * @param {settings} settings
 */
function storeSettings(settings) {
    try {
        window.sessionStorage.setItem('settings', JSON.stringify(settings));
        fallbackSettings = null;
    } catch(e) {
        console.warn("Couldn't store password:", e);
        fallbackSettings = settings;
    }
}

/**
 * This always returns a dictionary.
 *
 * @returns {settings}
 */
function getSettings() {
    /** @type {settings} */
    let settings;
    try {
        let json = window.sessionStorage.getItem('settings');
        settings = JSON.parse(json);
    } catch(e) {
        console.warn("Couldn't retrieve password:", e);
        settings = fallbackSettings;
    }
    return settings || {};
}

/**
 * @param {settings} settings
 */
function updateSettings(settings) {
    let s = getSettings();
    for(let key in settings)
        s[key] = settings[key];
    storeSettings(s);
}

/**
 * @param {string} key
 * @param {any} value
 */
function updateSetting(key, value) {
    let s = {};
    s[key] = value;
    updateSettings(s);
}

/**
 * @param {string} key
 */
function delSetting(key) {
    let s = getSettings();
    if(!(key in s))
        return;
    delete(s[key]);
    storeSettings(s)
}

/**
 * @param {string} id
 */
function getSelectElement(id) {
    let elt = document.getElementById(id);
    if(!elt || !(elt instanceof HTMLSelectElement))
        throw new Error(`Couldn't find ${id}`);
    return elt;
}

/**
 * @param {string} id
 */
function getInputElement(id) {
    let elt = document.getElementById(id);
    if(!elt || !(elt instanceof HTMLInputElement))
        throw new Error(`Couldn't find ${id}`);
    return elt;
}

/**
 * @param {string} id
 */
function getButtonElement(id) {
    let elt = document.getElementById(id);
    if(!elt || !(elt instanceof HTMLButtonElement))
        throw new Error(`Couldn't find ${id}`);
    return elt;
}

function reflectSettings() {
    let settings = getSettings();
    let store = false;

    setLocalMute(settings.localMute);

    let videoselect = getSelectElement('videoselect');
    if(!settings.hasOwnProperty('video') ||
       !selectOptionAvailable(videoselect, settings.video)) {
        settings.video = selectOptionDefault(videoselect);
        store = true;
    }
    videoselect.value = settings.video;

    let audioselect = getSelectElement('audioselect');
    if(!settings.hasOwnProperty('audio') ||
       !selectOptionAvailable(audioselect, settings.audio)) {
        settings.audio = selectOptionDefault(audioselect);
        store = true;
    }
    audioselect.value = settings.audio;

    if(settings.hasOwnProperty('request')) {
        getSelectElement('requestselect').value = settings.request;
    } else {
        settings.request = getSelectElement('requestselect').value;
        store = true;
    }

    if(settings.hasOwnProperty('send')) {
        getSelectElement('sendselect').value = settings.send;
    } else {
        settings.send = getSelectElement('sendselect').value;
        store = true;
    }

    getInputElement('activitybox').checked = settings.activityDetection;

    getInputElement('blackboardbox').checked = settings.blackboardMode;

    if(store)
        storeSettings(settings);
}

function showVideo() {
    let width = window.innerWidth;
    let video_container = document.getElementById('video-container');
    video_container.classList.remove('no-video');
    if (width <= 768)
        document.getElementById('collapse-video').style.display = "block";
}

/**
 * @param {boolean} [force]
 */
function hideVideo(force) {
    let mediadiv = document.getElementById('peers');
    if(mediadiv.childElementCount > 0 && !force)
        return;
    let video_container = document.getElementById('video-container');
    video_container.classList.add('no-video');
    let left = document.getElementById("left");
    if (left.style.display !== "none") {
        // hide all video buttons used to switch video on mobile layout
        closeVideoControls();
    }
}

function closeVideoControls() {
    // hide all video buttons used to switch video on mobile layout
    document.getElementById('switch-video').style.display = "";
    document.getElementById('collapse-video').style.display = "";
}

function fillLogin() {
    let userpass = getUserPass();
    getInputElement('username').value =
        userpass ? userpass.username : '';
    getInputElement('password').value =
        userpass ? userpass.password : '';
}

/**
  * @param{boolean} connected
  */
function setConnected(connected) {
    let userbox = document.getElementById('profile');
    let connectionbox = document.getElementById('login-container');
    if(connected) {
        resetUsers();
        clearChat();
        userbox.classList.remove('invisible');
        connectionbox.classList.add('invisible');
        displayUsername();
    } else {
        resetUsers();
        fillLogin();
        userbox.classList.add('invisible');
        connectionbox.classList.remove('invisible');
        displayError('Disconnected', 'error');
        hideVideo();
        closeVideoControls();
    }
}

/** @this {ServerConnection} */
function gotConnected() {
    setConnected(true);
    let up = getUserPass();
    this.join(group, up.username, up.password);
}

/**
 * @this {ServerConnection}
 * @param {number} code
 * @param {string} reason
 */
function gotClose(code, reason) {
    delUpMediaKind(null);
    setConnected(false);
    if(code != 1000) {
        console.warn('Socket close', code, reason);
    }
}

/**
 * @this {ServerConnection}
 * @param {Stream} c
 */
function gotDownStream(c) {
    c.onclose = function() {
        delMedia(c.id);
    };
    c.onerror = function(e) {
        console.error(e);
        displayError(e);
    }
    c.ondowntrack = function(track, transceiver, label, stream) {
        setMedia(c, false);
    }
    c.onlabel = function(label) {
        setLabel(c);
    }
    c.onstatus = function(status) {
        setMediaStatus(c);
    }
    c.onstats = gotDownStats;
    if(getSettings().activityDetection)
        c.setStatsInterval(activityDetectionInterval);
}

// Store current browser viewport height in css variable
function setViewportHeight() {
    document.documentElement.style.setProperty(
        '--vh', `${window.innerHeight/100}px`,
    );
    // Ajust video component size
    resizePeers();
}
setViewportHeight();

// On resize and orientation change, we update viewport height
addEventListener('resize', setViewportHeight);
addEventListener('orientationchange', setViewportHeight);

getButtonElement('presentbutton').onclick = async function(e) {
    e.preventDefault();
    let button = this;
    if(!(button instanceof HTMLButtonElement))
        throw new Error('Unexpected type for this.');
    // there's a potential race condition here: the user might click the
    // button a second time before the stream is set up and the button hidden.
    button.disabled = true;
    try {
        let id = findUpMedia('local');
        if(!id)
            await addLocalMedia();
    } finally {
        button.disabled = false;
    }
};

getButtonElement('unpresentbutton').onclick = function(e) {
    e.preventDefault();
    delUpMediaKind('local');
    resizePeers();
};

function changePresentation() {
    let id = findUpMedia('local');
    if(id) {
        addLocalMedia(id);
    }
}

/**
 * @param {string} id
 * @param {boolean} visible
 */
function setVisibility(id, visible) {
    let elt = document.getElementById(id);
    if(visible)
        elt.classList.remove('invisible');
    else
        elt.classList.add('invisible');
}

function setButtonsVisibility() {
    let permissions = serverConnection.permissions;
    let local = !!findUpMedia('local');
    let share = !!findUpMedia('screenshare');
    let video = !!findUpMedia('video');

    // don't allow multiple presentations
    setVisibility('presentbutton', permissions.present && !local);
    setVisibility('unpresentbutton', local);

    setVisibility('mutebutton', permissions.present);

    // allow multiple shared documents
    setVisibility('sharebutton', permissions.present &&
                  ('getDisplayMedia' in navigator.mediaDevices));
    setVisibility('unsharebutton', share);

    setVisibility('stopvideobutton', video);

    setVisibility('mediaoptions', permissions.present);
    setVisibility('sendform', permissions.present);
    setVisibility('fileform', permissions.present);
}

/**
 * @param {boolean} mute
 * @param {boolean} [reflect]
 */
function setLocalMute(mute, reflect) {
    muteLocalTracks(mute);
    let button = document.getElementById('mutebutton');
    let icon = button.querySelector("span .fas");
    if(mute){
        icon.classList.add('fa-microphone-slash');
        icon.classList.remove('fa-microphone');
        button.classList.add('muted');
    } else {
        icon.classList.remove('fa-microphone-slash');
        icon.classList.add('fa-microphone');
        button.classList.remove('muted');
    }
    if(reflect)
        updateSettings({localMute: mute});
}

getSelectElement('videoselect').onchange = function(e) {
    e.preventDefault();
    if(!(this instanceof HTMLSelectElement))
        throw new Error('Unexpected type for this');
    updateSettings({video: this.value});
    changePresentation();
};

getSelectElement('audioselect').onchange = function(e) {
    e.preventDefault();
    if(!(this instanceof HTMLSelectElement))
        throw new Error('Unexpected type for this');
    updateSettings({audio: this.value});
    changePresentation();
};

getInputElement('blackboardbox').onchange = function(e) {
    e.preventDefault();
    if(!(this instanceof HTMLInputElement))
        throw new Error('Unexpected type for this');
    updateSettings({blackboardMode: this.checked});
    changePresentation();
}

document.getElementById('mutebutton').onclick = function(e) {
    e.preventDefault();
    let localMute = getSettings().localMute;
    localMute = !localMute;
    setLocalMute(localMute, true);
}

document.getElementById('sharebutton').onclick = function(e) {
    e.preventDefault();
    addShareMedia();
};

document.getElementById('unsharebutton').onclick = function(e) {
    e.preventDefault();
    delUpMediaKind('screenshare');
    resizePeers();
}

document.getElementById('stopvideobutton').onclick = function(e) {
    e.preventDefault();
    delUpMediaKind('video');
    resizePeers();
}

/** @returns {number} */
function getMaxVideoThroughput() {
    let v = getSettings().send;
    switch(v) {
    case 'lowest':
        return 150000;
    case 'low':
        return 300000;
    case 'normal':
        return 700000;
    case 'unlimited':
        return null;
    default:
        console.error('Unknown video quality', v);
        return 700000;
    }
}

getSelectElement('sendselect').onchange = async function(e) {
    if(!(this instanceof HTMLSelectElement))
        throw new Error('Unexpected type for this');
    updateSettings({send: this.value});
    let t = getMaxVideoThroughput();
    for(let id in serverConnection.up) {
        let c = serverConnection.up[id];
        await setMaxVideoThroughput(c, t);
    }
}

getSelectElement('requestselect').onchange = function(e) {
    e.preventDefault();
    if(!(this instanceof HTMLSelectElement))
        throw new Error('Unexpected type for this');
    updateSettings({request: this.value});
    serverConnection.request(this.value);
};

const activityDetectionInterval = 200;
const activityDetectionPeriod = 700;
const activityDetectionThreshold = 0.2;

getInputElement('activitybox').onchange = function(e) {
    if(!(this instanceof HTMLInputElement))
        throw new Error('Unexpected type for this');
    updateSettings({activityDetection: this.checked});
    for(let id in serverConnection.down) {
        let c = serverConnection.down[id];
        if(this.checked)
            c.setStatsInterval(activityDetectionInterval);
        else {
            c.setStatsInterval(0);
            setActive(c, false);
        }
    }
}

getInputElement('fileinput').onchange = function(e) {
    if(!(this instanceof HTMLInputElement))
        throw new Error('Unexpected type for this');
    let input = this;
    let files = input.files;
    for(let i = 0; i < files.length; i++)
        addFileMedia(files[i]);
    input.value = '';
    closeNav();
}

/**
 * @this {Stream}
 * @param {Object<string,any>} stats
 */
function gotUpStats(stats) {
    let c = this;

    let text = '';

    c.pc.getSenders().forEach(s => {
        let tid = s.track && s.track.id;
        let stats = tid && c.stats[tid];
        let rate = stats && stats['outbound-rtp'] && stats['outbound-rtp'].rate;
        if(typeof rate === 'number') {
            if(text)
                text = text + ' + ';
            text = text + Math.round(rate / 1000) + 'kbps';
        }
    });

    setLabel(c, text);
}

/**
 * @param {Stream} c
 * @param {boolean} value
 */
function setActive(c, value) {
    let peer = document.getElementById('peer-' + c.id);
    if(value)
        peer.classList.add('peer-active');
    else
        peer.classList.remove('peer-active');
}

/**
 * @this {Stream}
 * @param {Object<string,any>} stats
 */
function gotDownStats(stats) {
    if(!getInputElement('activitybox').checked)
        return;

    let c = this;

    let maxEnergy = 0;

    c.pc.getReceivers().forEach(r => {
        let tid = r.track && r.track.id;
        let s = tid && stats[tid];
        let energy = s && s['track'] && s['track'].audioEnergy;
        if(typeof energy === 'number')
            maxEnergy = Math.max(maxEnergy, energy);
    });

    // totalAudioEnergy is defined as the integral of the square of the
    // volume, so square the threshold.
    if(maxEnergy > activityDetectionThreshold * activityDetectionThreshold) {
        c.userdata.lastVoiceActivity = Date.now();
        setActive(c, true);
    } else {
        let last = c.userdata.lastVoiceActivity;
        if(!last || Date.now() - last > activityDetectionPeriod)
            setActive(c, false);
    }
}

/**
 * @param {HTMLSelectElement} select
 * @param {string} label
 * @param {string} [value]
 */
function addSelectOption(select, label, value) {
    if(!value)
        value = label;
    for(let i = 0; i < select.children.length; i++) {
        let child = select.children[i];
        if(!(child instanceof HTMLOptionElement)) {
            console.warn('Unexpected select child');
            continue;
        }
        if(child.value === value) {
            if(child.label !== label) {
                child.label = label;
            }
            return;
        }
    }

    let option = document.createElement('option');
    option.value = value;
    option.textContent = label;
    select.appendChild(option);
}

/**
 * @param {HTMLSelectElement} select
 * @param {string} value
 */
function selectOptionAvailable(select, value) {
    let children = select.children;
    for(let i = 0; i < children.length; i++) {
        let child = select.children[i];
        if(!(child instanceof HTMLOptionElement)) {
            console.warn('Unexpected select child');
            continue;
        }
        if(child.value === value)
            return true;
    }
    return false;
}

/**
 * @param {HTMLSelectElement} select
 * @returns {string}
 */
function selectOptionDefault(select) {
    /* First non-empty option. */
    for(let i = 0; i < select.children.length; i++) {
        let child = select.children[i];
        if(!(child instanceof HTMLOptionElement)) {
            console.warn('Unexpected select child');
            continue;
        }
        if(child.value)
            return child.value;
    }
    /* The empty option is always available. */
    return '';
}

/* media names might not be available before we call getDisplayMedia.  So
   we call this twice, the second time to update the menu with user-readable
   labels. */
/** @type {boolean} */
let mediaChoicesDone = false;

/**
 * @param{boolean} done
 */
async function setMediaChoices(done) {
    if(mediaChoicesDone)
        return;

    let devices = [];
    try {
        devices = await navigator.mediaDevices.enumerateDevices();
    } catch(e) {
        console.error(e);
        return;
    }

    let cn = 1, mn = 1;

    devices.forEach(d => {
        let label = d.label;
        if(d.kind === 'videoinput') {
            if(!label)
                label = `Camera ${cn}`;
            addSelectOption(getSelectElement('videoselect'),
                            label, d.deviceId);
            cn++;
        } else if(d.kind === 'audioinput') {
            if(!label)
                label = `Microphone ${mn}`;
            addSelectOption(getSelectElement('audioselect'),
                            label, d.deviceId);
            mn++;
        }
    });

    mediaChoicesDone = done;
}


/**
 * @param {string} [id]
 */
function newUpStream(id) {
    let c = serverConnection.newUpStream(id);
    c.onstatus = function(status) {
        setMediaStatus(c);
    }
    c.onerror = function(e) {
        console.error(e);
        displayError(e);
        delUpMedia(c);
    }
    c.onabort = function() {
        delUpMedia(c);
    }
    c.onnegotiationcompleted = function() {
        setMaxVideoThroughput(c, getMaxVideoThroughput())
    }
    return c;
}

/**
 * @param {Stream} c
 * @param {number} [bps]
 */
async function setMaxVideoThroughput(c, bps) {
    let senders = c.pc.getSenders();
    for(let i = 0; i < senders.length; i++) {
        let s = senders[i];
        if(!s.track || s.track.kind !== 'video')
            continue;
        let p = s.getParameters();
        if(!p.encodings)
            p.encodings = [{}];
        p.encodings.forEach(e => {
            if(bps > 0)
                e.maxBitrate = bps;
            else
                delete e.maxBitrate;
        });
        try {
            await s.setParameters(p);
        } catch(e) {
            console.error(e);
        }
    }
}

function isSafari() {
    let ua = navigator.userAgent.toLowerCase();
    return ua.indexOf('safari') >= 0 && ua.indexOf('chrome') < 0;
}

/**
 * @param {string} [id]
 */
async function addLocalMedia(id) {
    let settings = getSettings();

    let audio = settings.audio ? {deviceId: settings.audio} : false;
    let video = settings.video ? {deviceId: settings.video} : false;

    if(video) {
        let resolution = settings.resolution;
        if(resolution) {
            video.width = { ideal: resolution[0] };
            video.height = { ideal: resolution[1] };
        } else if(settings.blackboardMode) {
            video.width = { min: 640, ideal: 1920 };
            video.height = { min: 400, ideal: 1080 };
        }
    }

    let old = id && serverConnection.up[id];

    if(!audio && !video) {
        if(old)
            delUpMedia(old);
        return;
    }

    if(old)
        stopUpMedia(old);

    let constraints = {audio: audio, video: video};
    /** @type {MediaStream} */
    let stream = null;
    try {
        stream = await navigator.mediaDevices.getUserMedia(constraints);
    } catch(e) {
        displayError(e);
        if(old)
            delUpMedia(old);
        return;
    }

    setMediaChoices(true);

    let c = newUpStream(id);

    c.kind = 'local';
    c.stream = stream;
    let mute = getSettings().localMute;
    stream.getTracks().forEach(t => {
        c.labels[t.id] = t.kind
        if(t.kind == 'audio') {
            if(mute)
                t.enabled = false;
        } else if(t.kind == 'video') {
            if(settings.blackboardMode) {
                /** @ts-ignore */
                t.contentHint = 'detail';
            }
        }
        c.pc.addTrack(t, stream);
    });

    c.onstats = gotUpStats;
    c.setStatsInterval(2000);
    await setMedia(c, true, true);
    setButtonsVisibility();
}

let safariScreenshareDone = false;

async function addShareMedia() {
    /** @type {MediaStream} */
    let stream = null;
    try {
        if(!('getDisplayMedia' in navigator.mediaDevices))
            throw new Error('Your browser does not support screen sharing');
        /** @ts-ignore */
        stream = await navigator.mediaDevices.getDisplayMedia({video: true});
    } catch(e) {
        console.error(e);
        displayError(e);
        return;
    }

    if(!safariScreenshareDone) {
        if(isSafari())
            displayWarning('Screen sharing under Safari is experimental.  ' +
                           'Please use a different browser if possible.');
        safariScreenshareDone = true;
    }

    let c = newUpStream();
    c.kind = 'screenshare';
    c.stream = stream;
    stream.getTracks().forEach(t => {
        c.pc.addTrack(t, stream);
        t.onended = e => {
            delUpMedia(c);
        };
        c.labels[t.id] = 'screenshare';
    });
    c.onstats = gotUpStats;
    c.setStatsInterval(2000);
    await setMedia(c, true);
    setButtonsVisibility()
}

/**
 * @param {File} file
 */
async function addFileMedia(file) {
    let url = URL.createObjectURL(file);
    let video = document.createElement('video');
    video.src = url;
    video.controls = true;
    /** @ts-ignore */
    let stream = video.captureStream();

    let c = newUpStream();
    c.kind = 'video';
    c.stream = stream;
    stream.onaddtrack = function(e) {
        let t = e.track;
        if(t.kind === 'audio') {
            let presenting = !!findUpMedia('local');
            let muted = getSettings().localMute;
            if(presenting && !muted) {
                setLocalMute(true, true);
                displayWarning('You have been muted');
            }
        }
        c.pc.addTrack(t, stream);
        c.labels[t.id] = t.kind;
        c.onstats = gotUpStats;
        c.setStatsInterval(2000);
    };
    stream.onremovetrack = function(e) {
        let t = e.track;
        delete(c.labels[t.id]);

        /** @type {RTCRtpSender} */
        let sender;
        c.pc.getSenders().forEach(s => {
            if(s.track === t)
                sender = s;
        });
        if(sender) {
            c.pc.removeTrack(sender)
        } else {
            console.warn('Removing unknown track');
        }

        if(Object.keys(c.labels).length === 0) {
            stream.onaddtrack = null;
            stream.onremovetrack == null;
            delUpMedia(c);
        }
    };
    await setMedia(c, true, false, video);
    c.userdata.play = true;
    setButtonsVisibility()
}

/**
 * @param {Stream} c
 */
function stopUpMedia(c) {
    if(!c.stream)
        return;
    c.stream.getTracks().forEach(t => {
        try {
            t.stop();
        } catch(e) {
        }
    });
}

/**
 * @param {Stream} c
 */
function delUpMedia(c) {
    stopUpMedia(c);
    try {
        delMedia(c.id);
    } catch(e) {
        console.warn(e);
    }
    c.close();
    delete(serverConnection.up[c.id]);
    setButtonsVisibility()
}

/**
 * delUpMediaKind reoves all up media of the given kind.  If kind is
 * falseish, it removes all up media.
 * @param {string} kind
*/
function delUpMediaKind(kind) {
    for(let id in serverConnection.up) {
        let c = serverConnection.up[id];
        if(kind && c.kind != kind)
            continue
        c.close();
        delMedia(id);
        delete(serverConnection.up[id]);
    }

    setButtonsVisibility();
    hideVideo();
}

/**
 * @param {string} kind
 */
function findUpMedia(kind) {
    for(let id in serverConnection.up) {
        if(serverConnection.up[id].kind === kind)
            return id;
    }
    return null;
}

/**
 * @param {boolean} mute
 */
function muteLocalTracks(mute) {
    if(!serverConnection)
        return;
    for(let id in serverConnection.up) {
        let c = serverConnection.up[id];
        if(c.kind === 'local') {
            let stream = c.stream;
            stream.getTracks().forEach(t => {
                if(t.kind === 'audio') {
                    t.enabled = !mute;
                }
            });
        }
    }
}

/**
 * setMedia adds a new media element corresponding to stream c.
 *
 * @param {Stream} c
 * @param {boolean} isUp
 *     - indicates whether the stream goes in the up direction
 * @param {boolean} [mirror]
 *     - whether to mirror the video
 * @param {HTMLVideoElement} [video]
 *     - the video element to add.  If null, a new element with custom
 *       controls will be created.
 */
async function setMedia(c, isUp, mirror, video) {
    let peersdiv = document.getElementById('peers');

    let div = document.getElementById('peer-' + c.id);
    if(!div) {
        div = document.createElement('div');
        div.id = 'peer-' + c.id;
        div.classList.add('peer');
        peersdiv.appendChild(div);
    }

    let media = /** @type {HTMLVideoElement} */
        (document.getElementById('media-' + c.id));
    if(media) {
        if(video) {
            throw new Error("Duplicate video");
        }
    } else {
        if(video) {
            media = video;
        } else {
            media = document.createElement('video');
            if(isUp)
                media.muted = true;
        }

        media.classList.add('media');
        media.autoplay = true;
        /** @ts-ignore */
        media.playsinline = true;
        media.id = 'media-' + c.id;
        div.appendChild(media);
        if(!video)
            addCustomControls(media, div, c);
        if(mirror)
            media.classList.add('mirror');
    }

    if(!video)
        media.srcObject = c.stream;

    let label = document.getElementById('label-' + c.id);
    if(!label) {
        label = document.createElement('div');
        label.id = 'label-' + c.id;
        label.classList.add('label');
        div.appendChild(label);
    }

    setLabel(c);
    setMediaStatus(c);

    showVideo();
    resizePeers();

    if(!isUp && isSafari() && !findUpMedia('local')) {
        // Safari doesn't allow autoplay unless the user has granted media access
        try {
            let stream = await navigator.mediaDevices.getUserMedia({audio: true});
            stream.getTracks().forEach(t => t.stop());
        } catch(e) {
        }
    }
}

/**
 * @param {Element} elt
 */
function cloneHTMLElement(elt) {
    if(!(elt instanceof HTMLElement))
        throw new Error('Unexpected element type');
    return /** @type{HTMLElement} */(elt.cloneNode(true));
}

/**
 * @param {HTMLVideoElement} media
 * @param {HTMLElement} container
 * @param {Stream} c
 */
function addCustomControls(media, container, c) {
    media.controls = false;
    let controls = document.getElementById('controls-' + c.id);
    if(controls) {
        console.warn('Attempted to add duplicate controls');
        return;
    }

    let template =
        document.getElementById('videocontrols-template').firstElementChild;
    controls = cloneHTMLElement(template);
    controls.id = 'controls-' + c.id;

    let volume = getVideoButton(controls, 'volume');
    if(c.kind === 'local') {
        volume.remove();
    } else {
        setVolumeButton(media.muted,
                        getVideoButton(controls, "volume-mute"),
                        getVideoButton(controls, "volume-slider"));
    }

    container.appendChild(controls);
    registerControlHandlers(media, container);
}

/**
 * @param {HTMLElement} container
 * @param {string} name
 */
function getVideoButton(container, name) {
    return /** @type {HTMLElement} */(container.getElementsByClassName(name)[0]);
}

/**
 * @param {boolean} muted
 * @param {HTMLElement} button
 * @param {HTMLElement} slider
 */
function setVolumeButton(muted, button, slider) {
    if(!muted) {
        button.classList.remove("fa-volume-mute");
        button.classList.add("fa-volume-up");
    } else {
        button.classList.remove("fa-volume-up");
        button.classList.add("fa-volume-mute");
    }

    if(!(slider instanceof HTMLInputElement))
        throw new Error("Couldn't find volume slider");
    slider.disabled = muted;
}

/**
 * @param {HTMLVideoElement} media
 * @param {HTMLElement} container
 */
function registerControlHandlers(media, container) {
    let play = getVideoButton(container, 'video-play');
    if(play) {
        play.onclick = function(event) {
            event.preventDefault();
            media.play();
        };
    }

    let volume = getVideoButton(container, 'volume');
    if (volume) {
        volume.onclick = function(event) {
            let target = /** @type{HTMLElement} */(event.target);
            if(!target.classList.contains('volume-mute'))
                // if click on volume slider, do nothing
                return;
            event.preventDefault();
            media.muted = !media.muted;
            setVolumeButton(media.muted, target,
                            getVideoButton(volume, "volume-slider"));
        };
        volume.oninput = function() {
          let slider = /** @type{HTMLInputElement} */
              (getVideoButton(volume, "volume-slider"));
          media.volume = parseInt(slider.value, 10)/100;
        };
    }

    let pip = getVideoButton(container, 'pip');
    if(pip) {
        /** @ts-ignore */
        if(HTMLVideoElement.prototype.requestPictureInPicture) {
            pip.onclick = function(e) {
                e.preventDefault();
                /** @ts-ignore */
                if(media.requestPictureInPicture) {
                    /** @ts-ignore */
                    media.requestPictureInPicture();
                } else {
                    displayWarning('Picture in Picture not supported.');
                }
            };
        } else {
            pip.style.display = 'none';
        }
    }

    let fs = getVideoButton(container, 'fullscreen');
    if(fs) {
        if(HTMLVideoElement.prototype.requestFullscreen ||
           /** @ts-ignore */
           HTMLVideoElement.prototype.webkitRequestFullscreen) {
            fs.onclick = function(e) {
                e.preventDefault();
                if(media.requestFullscreen) {
                    media.requestFullscreen();
                /** @ts-ignore */
                } else if(media.webkitRequestFullscreen) {
                    /** @ts-ignore */
                    media.webkitRequestFullscreen();
                } else {
                    displayWarning('Full screen not supported!');
                }
            };
        } else {
            fs.style.display = 'none';
        }
    }
}

/**
 * @param {string} id
 */
function delMedia(id) {
    let mediadiv = document.getElementById('peers');
    let peer = document.getElementById('peer-' + id);
    if(!peer)
        throw new Error('Removing unknown media');

    let media = /** @type{HTMLVideoElement} */
        (document.getElementById('media-' + id));

    if(media.src) {
        URL.revokeObjectURL(media.src);
        media.src = null;
    }

    media.srcObject = null;
    mediadiv.removeChild(peer);

    resizePeers();
    hideVideo();
}

/**
 * @param {Stream} c
 */
function setMediaStatus(c) {
    let state = c && c.pc && c.pc.iceConnectionState;
    let good = state === 'connected' || state === 'completed';

    let media = document.getElementById('media-' + c.id);
    if(!media) {
        console.warn('Setting status of unknown media.');
        return;
    }
    if(good) {
        media.classList.remove('media-failed');
        if(c.userdata.play) {
            if(media instanceof HTMLMediaElement)
                media.play().catch(e => {
                    console.error(e);
                    displayError(e);
                });
            delete(c.userdata.play);
        }
    } else {
        media.classList.add('media-failed');
    }
}


/**
 * @param {Stream} c
 * @param {string} [fallback]
 */
function setLabel(c, fallback) {
    let label = document.getElementById('label-' + c.id);
    if(!label)
        return;
    let l = c.label;
    if(l) {
        label.textContent = l;
        label.classList.remove('label-fallback');
    } else if(fallback) {
        label.textContent = fallback;
        label.classList.add('label-fallback');
    } else {
        label.textContent = '';
        label.classList.remove('label-fallback');
    }
}

function resizePeers() {
    // Window resize can call this method too early
    if (!serverConnection)
        return;
    let count =
        Object.keys(serverConnection.up).length +
        Object.keys(serverConnection.down).length;
    let peers = document.getElementById('peers');
    let columns = Math.ceil(Math.sqrt(count));
    if (!count)
        // No video, nothing to resize.
        return;
    let container = document.getElementById("video-container");
    // Peers div has total padding of 40px, we remove 40 on offsetHeight
    // Grid has row-gap of 5px
    let rows = Math.ceil(count / columns);
    let margins = (rows - 1) * 5 + 40;

    if (count <= 2 && container.offsetHeight > container.offsetWidth) {
        peers.style['grid-template-columns'] = "repeat(1, 1fr)";
        rows = count;
    } else {
        peers.style['grid-template-columns'] = `repeat(${columns}, 1fr)`;
    }
    if (count === 1)
        return;
    let max_video_height = (peers.offsetHeight - margins) / rows;
    let media_list = peers.querySelectorAll(".media");
    for(let i = 0; i < media_list.length; i++) {
        let media = media_list[i];
        if(!(media instanceof HTMLMediaElement)) {
            console.warn('Unexpected media');
            continue;
        }
        media.style['max-height'] = max_video_height + "px";
    }
}

/** @type{Object<string,string>} */
let users = {};

/**
 * Lexicographic order, with case differences secondary.
 * @param{string} a
 * @param{string} b
 */
function stringCompare(a, b) {
    let la = a.toLowerCase()
    let lb = b.toLowerCase()
    if(la < lb)
        return -1;
    else if(la > lb)
        return +1;
    else if(a < b)
        return -1;
    else if(a > b)
        return +1;
    return 0
}

/**
 * @param {string} id
 * @param {string} name
 */
function addUser(id, name) {
    if(!name)
        name = null;
    if(id in users)
        throw new Error('Duplicate user id');
    users[id] = name;

    let div = document.getElementById('users');
    let user = document.createElement('div');
    user.id = 'user-' + id;
    user.classList.add("user-p");
    user.textContent = name ? name : '(anon)';

    if(name) {
        let us = div.children;
        for(let i = 0; i < us.length; i++) {
            let child = us[i];
            let childname = users[child.id.slice('user-'.length)] || null;
            if(!childname || stringCompare(childname, name) > 0) {
                div.insertBefore(user, child);
                return;
            }
        }
    }
    div.appendChild(user);
}

/**
 * @param {string} id
 * @param {string} name
 */
function delUser(id, name) {
    if(!name)
        name = null;
    if(!(id in users))
        throw new Error('Unknown user id');
    if(users[id] !== name)
        throw new Error('Inconsistent user name');
    delete(users[id]);
    let div = document.getElementById('users');
    let user = document.getElementById('user-' + id);
    div.removeChild(user);
}

function resetUsers() {
    for(let id in users)
        delUser(id, users[id]);
}

/**
 * @param {string} id
 * @param {string} kind
 * @param {string} name
 */
function gotUser(id, kind, name) {
    switch(kind) {
    case 'add':
        addUser(id, name);
        break;
    case 'delete':
        delUser(id, name);
        break;
    default:
        console.warn('Unknown user kind', kind);
        break;
    }
}

function displayUsername() {
    let userpass = getUserPass();
    let text = '';
    if(userpass && userpass.username)
        document.getElementById('userspan').textContent = userpass.username;
    if(serverConnection.permissions.op && serverConnection.permissions.present)
        text = '(op, presenter)';
    else if(serverConnection.permissions.op)
        text = 'operator';
    else if(serverConnection.permissions.present)
        text = 'presenter';
    document.getElementById('permspan').textContent = text;
}

let presentRequested = null;

/**
 * @this {ServerConnection}
 * @param {string} group
 * @param {Object<string,boolean>} perms
 */
async function gotJoined(kind, group, perms, message) {
    let present = presentRequested;
    presentRequested = null;

    switch(kind) {
    case 'fail':
        displayError('The server said: ' + message);
        this.close();
        return;
    case 'redirect':
        this.close();
        document.location = message;
        return;
    case 'leave':
        this.close();
        return;
    case 'join':
    case 'change':
        displayUsername();
        setButtonsVisibility();
        if(kind === 'change')
            return;
        break;
    default:
        displayError('Unknown join message');
        this.close();
        return;
    }

    let input = /** @type{HTMLTextAreaElement} */
        (document.getElementById('input'));
    input.placeholder = 'Type /help for help';
    setTimeout(() => {input.placeholder = '';}, 8000);

    this.request(getSettings().request);

    if(serverConnection.permissions.present && !findUpMedia('local')) {
        if(present) {
            if(present === 'mike')
                updateSettings({video: ''});
            else if(present === 'both')
                delSetting('video');
            reflectSettings();

            let button = getButtonElement('presentbutton');
            button.disabled = true;
            try {
                await addLocalMedia();
            } finally {
                button.disabled = false;
            }
        } else {
            displayMessage(
                "Press Ready to enable your camera or microphone"
            );
        }
    }
}

const urlRegexp = /https?:\/\/[-a-zA-Z0-9@:%/._\\+~#&()=?]+[-a-zA-Z0-9@:%/_\\+~#&()=]/g;

/**
 * @param {string} line
 * @returns {Array.<Text|HTMLElement>}
 */
function formatLine(line) {
    let r = new RegExp(urlRegexp);
    let result = [];
    let pos = 0;
    while(true) {
        let m = r.exec(line);
        if(!m)
            break;
        result.push(document.createTextNode(line.slice(pos, m.index)));
        let a = document.createElement('a');
        a.href = m[0];
        a.textContent = m[0];
        a.target = '_blank';
        a.rel = 'noreferrer noopener';
        result.push(a);
        pos = m.index + m[0].length;
    }
    result.push(document.createTextNode(line.slice(pos)));
    return result;
}

/**
 * @param {string[]} lines
 * @returns {HTMLElement}
 */
function formatLines(lines) {
    let elts = [];
    if(lines.length > 0)
        elts = formatLine(lines[0]);
    for(let i = 1; i < lines.length; i++) {
        elts.push(document.createElement('br'));
        elts = elts.concat(formatLine(lines[i]));
    }
    let elt = document.createElement('p');
    elts.forEach(e => elt.appendChild(e));
    return elt;
}

/**
 * @param {number} time
 * @returns {string}
 */
function formatTime(time) {
    let delta = Date.now() - time;
    let date = new Date(time);
    let m = date.getMinutes();
    if(delta > -30000)
        return date.getHours() + ':' + ((m < 10) ? '0' : '') + m;
    return date.toLocaleString();
}

/**
 * @typedef {Object} lastMessage
 * @property {string} [nick]
 * @property {string} [peerId]
 * @property {string} [dest]
 * @property {number} [time]
 */

/** @type {lastMessage} */
let lastMessage = {};

/**
 * @param {string} peerId
 * @param {string} nick
 * @param {number} time
 * @param {string} kind
 * @param {string} message
 */
function addToChatbox(peerId, dest, nick, time, privileged, kind, message) {
    let userpass = getUserPass();
    let row = document.createElement('div');
    row.classList.add('message-row');
    let container = document.createElement('div');
    container.classList.add('message');
    row.appendChild(container);
    let footer = document.createElement('p');
    footer.classList.add('message-footer');
    if(!peerId)
        container.classList.add('message-system');
    if(userpass.username === nick)
        container.classList.add('message-sender');
    if(dest)
        container.classList.add('message-private');

    if(kind !== 'me') {
        let p = formatLines(message.split('\n'));
        let doHeader = true;
        if(!peerId && !dest && !nick) {
            doHeader = false;
        } else if(lastMessage.nick !== (nick || null) ||
                  lastMessage.peerId !== peerId ||
                  lastMessage.dest !== (dest || null) ||
                  !time || !lastMessage.time) {
            doHeader = true;
        } else {
            let delta = time - lastMessage.time;
            doHeader = delta < 0 || delta > 60000;
        }

        if(doHeader) {
            let header = document.createElement('p');
            if(peerId || nick || dest) {
                let user = document.createElement('span');
                user.textContent = dest ?
                    `${nick||'(anon)'} \u2192 ${users[dest]||'(anon)'}` :
                    (nick || '(anon)');
                user.classList.add('message-user');
                header.appendChild(user);
            }
            header.classList.add('message-header');
            container.appendChild(header);
            if(time) {
                let tm = document.createElement('span');
                tm.textContent = formatTime(time);
                tm.classList.add('message-time');
                header.appendChild(tm);
            }
        }

        p.classList.add('message-content');
        container.appendChild(p);
        lastMessage.nick = (nick || null);
        lastMessage.peerId = peerId;
        lastMessage.dest = (dest || null);
        lastMessage.time = (time || null);
        container.appendChild(footer);
    } else {
        let asterisk = document.createElement('span');
        asterisk.textContent = '*';
        asterisk.classList.add('message-me-asterisk');
        let user = document.createElement('span');
        user.textContent = nick || '(anon)';
        user.classList.add('message-me-user');
        let content = document.createElement('span');
        formatLine(message).forEach(elt => {
            content.appendChild(elt);
        });
        content.classList.add('message-me-content');
        container.appendChild(asterisk);
        container.appendChild(user);
        container.appendChild(content);
        container.classList.add('message-me');
        lastMessage = {};
    }

    let box = document.getElementById('box');
    box.appendChild(row);
    if(box.scrollHeight > box.clientHeight) {
        box.scrollTop = box.scrollHeight - box.clientHeight;
    }

    return message;
}

function clearChat() {
    lastMessage = {};
    document.getElementById('box').textContent = '';
}

/**
 * A command known to the command-line parser.
 *
 * @typedef {Object} command
 * @property {string} [parameters]
 *     - A user-readable list of parameters.
 * @property {string} [description]
 *     - A user-readable description, null if undocumented.
 * @property {() => string} [predicate]
 *     - Returns null if the command is available.
 * @property {(c: string, r: string) => void} f
 */

/**
 * The set of commands known to the command-line parser.
 *
 * @type {Object.<string,command>}
 */
let commands = {}

function operatorPredicate() {
    if(serverConnection && serverConnection.permissions &&
       serverConnection.permissions.op)
        return null;
    return 'You are not an operator';
}

function recordingPredicate() {
    if(serverConnection && serverConnection.permissions &&
       serverConnection.permissions.record)
        return null;
    return 'You are not allowed to record';
}

commands.help = {
    description: 'display this help',
    f: (c, r) => {
        /** @type {string[]} */
        let cs = [];
        for(let cmd in commands) {
            let c = commands[cmd];
            if(!c.description)
                continue;
            if(c.predicate && c.predicate())
                continue;
            cs.push(`/${cmd}${c.parameters?' ' + c.parameters:''}: ${c.description}`);
        }
        cs.sort();
        let s = '';
        for(let i = 0; i < cs.length; i++)
            s = s + cs[i] + '\n';
        addToChatbox(null, null, null, Date.now(), false, null, s);
    }
};

commands.me = {
    f: (c, r) => {
        // handled as a special case
        throw new Error("this shouldn't happen");
    }
};

commands.set = {
    f: (c, r) => {
        if(!r) {
            let settings = getSettings();
            let s = "";
            for(let key in settings)
                s = s + `${key}: ${JSON.stringify(settings[key])}\n`;
            addToChatbox(null, null, null, Date.now(), false, null, s);
            return;
        }
        let p = parseCommand(r);
        let value;
        if(p[1]) {
            value = JSON.parse(p[1])
        } else {
            value = true;
        }
        updateSetting(p[0], value);
        reflectSettings();
    }
};

commands.unset = {
    f: (c, r) => {
        delSetting(r.trim());
        return;
    }
};

commands.leave = {
    description: "leave group",
    f: (c, r) => {
        if(!serverConnection)
            throw new Error('Not connected');
        serverConnection.close();
    }
};

commands.clear = {
    predicate: operatorPredicate,
    description: 'clear the chat history',
    f: (c, r) => {
        serverConnection.groupAction(getUsername(), 'clearchat');
    }
};

commands.lock = {
    predicate: operatorPredicate,
    description: 'lock this group',
    parameters: '[message]',
    f: (c, r) => {
        serverConnection.groupAction(getUsername(), 'lock', r);
    }
};

commands.unlock = {
    predicate: operatorPredicate,
    description: 'unlock this group, revert the effect of /lock',
    f: (c, r) => {
        serverConnection.groupAction(getUsername(), 'unlock');
    }
};

commands.record = {
    predicate: recordingPredicate,
    description: 'start recording',
    f: (c, r) => {
        serverConnection.groupAction(getUsername(), 'record');
    }
};

commands.unrecord = {
    predicate: recordingPredicate,
    description: 'stop recording',
    f: (c, r) => {
        serverConnection.groupAction(getUsername(), 'unrecord');
    }
};

commands.subgroups = {
    predicate: operatorPredicate,
    description: 'list subgroups',
    f: (c, r) => {
        serverConnection.groupAction(getUsername(), 'subgroups');
    }
};

commands.renegotiate = {
    description: 'renegotiate media streams',
    f: (c, r) => {
        for(let id in serverConnection.up)
            serverConnection.up[id].restartIce();
        for(let id in serverConnection.down)
            serverConnection.down[id].restartIce();
    }
};

/**
 * parseCommand splits a string into two space-separated parts.  The first
 * part may be quoted and may include backslash escapes.
 *
 * @param {string} line
 * @returns {string[]}
 */
function parseCommand(line) {
    let i = 0;
    while(i < line.length && line[i] === ' ')
        i++;
    let start = ' ';
    if(i < line.length && line[i] === '"' || line[i] === "'") {
        start = line[i];
        i++;
    }
    let first = "";
    while(i < line.length) {
        if(line[i] === start) {
            if(start !== ' ')
                i++;
            break;
        }
        if(line[i] === '\\' && i < line.length - 1)
            i++;
        first = first + line[i];
        i++;
    }

    while(i < line.length && line[i] === ' ')
        i++;
    return [first, line.slice(i)];
}

/**
 * @param {string} user
 */
function findUserId(user) {
    if(user in users)
        return user;

    for(let id in users) {
        if(users[id] === user)
            return id;
    }
    return null;
}

commands.msg = {
    parameters: 'user message',
    description: 'send a private message',
    f: (c, r) => {
        let p = parseCommand(r);
        if(!p[0])
            throw new Error('/msg requires parameters');
        let id = findUserId(p[0]);
        if(!id)
            throw new Error(`Unknown user ${p[0]}`);
        let username = getUsername();
        serverConnection.chat(username, '', id, p[1]);
        addToChatbox(serverConnection.id, id, username,
                     Date.now(), false, '', p[1]);
    }
};

/**
   @param {string} c
   @param {string} r
*/
function userCommand(c, r) {
    let p = parseCommand(r);
    if(!p[0])
        throw new Error(`/${c} requires parameters`);
    let id = findUserId(p[0]);
    if(!id)
        throw new Error(`Unknown user ${p[0]}`);
    serverConnection.userAction(getUsername(), c, id, p[1]);
}

function userMessage(c, r) {
    let p = parseCommand(r);
    if(!p[0])
        throw new Error(`/${c} requires parameters`);
    let id = findUserId(p[0]);
    if(!id)
        throw new Error(`Unknown user ${p[0]}`);
    serverConnection.userMessage(getUsername(), c, id, p[1]);
}

commands.kick = {
    parameters: 'user [message]',
    description: 'kick out a user',
    predicate: operatorPredicate,
    f: userCommand,
};

commands.op = {
    parameters: 'user',
    description: 'give operator status',
    predicate: operatorPredicate,
    f: userCommand,
};

commands.unop = {
    parameters: 'user',
    description: 'revoke operator status',
    predicate: operatorPredicate,
    f: userCommand,
};

commands.present = {
    parameters: 'user',
    description: 'give user the right to present',
    predicate: operatorPredicate,
    f: userCommand,
};

commands.unpresent = {
    parameters: 'user',
    description: 'revoke the right to present',
    predicate: operatorPredicate,
    f: userCommand,
};

commands.mute = {
    parameters: 'user',
    description: 'mute a remote user',
    predicate: operatorPredicate,
    f: userMessage,
};

commands.warn = {
    parameters: 'user message',
    description: 'send a warning to a user',
    predicate: operatorPredicate,
    f: (c, r) => {
        userMessage('warning', r);
    },
};

commands.wall = {
    parameters: 'message',
    description: 'send a warning to all users',
    predicate: operatorPredicate,
    f: (c, r) => {
        if(!r)
            throw new Error('empty message');
        serverConnection.userMessage(getUsername(), 'warning', '', r);
    },
};

function handleInput() {
    let input = /** @type {HTMLTextAreaElement} */
        (document.getElementById('input'));
    let data = input.value;
    input.value = '';

    let message, me;

    if(data === '')
        return;

    if(data[0] === '/') {
        if(data.length > 1 && data[1] === '/') {
            message = data.slice(1);
            me = false;
        } else {
            let cmd, rest;
            let space = data.indexOf(' ');
            if(space < 0) {
                cmd = data.slice(1);
                rest = '';
            } else {
                cmd = data.slice(1, space);
                rest = data.slice(space + 1);
            }

            if(cmd === 'me') {
                message = rest;
                me = true;
            } else {
                let c = commands[cmd];
                if(!c) {
                    displayError(`Uknown command /${cmd}, type /help for help`);
                    return;
                }
                if(c.predicate) {
                    let s = c.predicate();
                    if(s) {
                        displayError(s);
                        return;
                    }
                }
                try {
                    c.f(cmd, rest);
                } catch(e) {
                    displayError(e);
                }
                return;
            }
        }
    } else {
        message = data;
        me = false;
    }

    if(!serverConnection || !serverConnection.socket) {
        displayError("Not connected.");
        return;
    }

    let username = getUsername();
    try {
        serverConnection.chat(username, me ? 'me' : '', '', message);
    } catch(e) {
        console.error(e);
        displayError(e);
    }
}

document.getElementById('inputform').onsubmit = function(e) {
    e.preventDefault();
    handleInput();
};

document.getElementById('input').onkeypress = function(e) {
    if(e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
        e.preventDefault();
        handleInput();
    }
};

function chatResizer(e) {
    e.preventDefault();
    let full_width = document.getElementById("mainrow").offsetWidth;
    let left = document.getElementById("left");
    let right = document.getElementById("right");

    let start_x = e.clientX;
    let start_width = left.offsetWidth;

    function start_drag(e) {
        let left_width = (start_width + e.clientX - start_x) * 100 / full_width;
        // set min chat width to 300px
        let min_left_width = 300 * 100 / full_width;
        if (left_width < min_left_width) {
          return;
        }
        left.style.flex = left_width.toString();
        right.style.flex = (100 - left_width).toString();
    }
    function stop_drag(e) {
        document.documentElement.removeEventListener(
            'mousemove', start_drag, false,
        );
        document.documentElement.removeEventListener(
            'mouseup', stop_drag, false,
        );
    }

    document.documentElement.addEventListener(
        'mousemove', start_drag, false,
    );
    document.documentElement.addEventListener(
        'mouseup', stop_drag, false,
    );
}

document.getElementById('resizer').addEventListener('mousedown', chatResizer, false);

/**
 * @param {unknown} message
 * @param {string} [level]
 */
function displayError(message, level) {
    if(!level)
        level = "error";

    var background = 'linear-gradient(to right, #e20a0a, #df2d2d)';
    var position = 'center';
    var gravity = 'top';

    switch(level) {
    case "info":
        background = 'linear-gradient(to right, #529518, #96c93d)';
        position = 'right';
        gravity = 'bottom';
        break;
    case "warning":
        background = "linear-gradient(to right, #bdc511, #c2cf01)";
        break;
    }

    /** @ts-ignore */
    Toastify({
        text: message,
        duration: 4000,
        close: true,
        position: position,
        gravity: gravity,
        backgroundColor: background,
        className: level,
    }).showToast();
}

/**
 * @param {unknown} message
 */
function displayWarning(message) {
    return displayError(message, "warning");
}

/**
 * @param {unknown} message
 */
function displayMessage(message) {
    return displayError(message, "info");
}

let connecting = false;

document.getElementById('userform').onsubmit = async function(e) {
    e.preventDefault();
    if(connecting)
        return;
    connecting = true;
    try {
        let username = getInputElement('username').value.trim();
        let password = getInputElement('password').value;
        storeUserPass(username, password);
        serverConnect();
    } finally {
        connecting = false;
    }

    if(getInputElement('presentboth').checked)
        presentRequested = 'both';
    else if(getInputElement('presentmike').checked)
        presentRequested = 'mike';
    else
        presentRequested = null;

    getInputElement('presentoff').checked = true;
};

document.getElementById('disconnectbutton').onclick = function(e) {
    serverConnection.close();
    closeNav();
};

function openNav() {
    document.getElementById("sidebarnav").style.width = "250px";
}

function closeNav() {
    document.getElementById("sidebarnav").style.width = "0";
}

document.getElementById('sidebarCollapse').onclick = function(e) {
    document.getElementById("left-sidebar").classList.toggle("active");
    document.getElementById("mainrow").classList.toggle("full-width-active");
};

document.getElementById('openside').onclick = function(e) {
      e.preventDefault();
      let sidewidth = document.getElementById("sidebarnav").style.width;
      if (sidewidth !== "0px" && sidewidth !== "") {
          closeNav();
          return;
      } else {
          openNav();
      }
};


document.getElementById('clodeside').onclick = function(e) {
    e.preventDefault();
    closeNav();
};

document.getElementById('collapse-video').onclick = function(e) {
    e.preventDefault();
    if(!(this instanceof HTMLElement))
        throw new Error('Unexpected type for this');
    let width = window.innerWidth;
    let left = document.getElementById("left");
    if (left.style.display === "" || left.style.display === "none") {
      //left chat is hidden, we show the chat and hide collapse button
      left.style.display = "block";
      this.style.display = "";
    }
    if (width <= 768) {
      // fixed div for small screen
      this.style.display = "";
      hideVideo(true);
      document.getElementById('switch-video').style.display = "block";
    }
};

document.getElementById('switch-video').onclick = function(e) {
    e.preventDefault();
    if(!(this instanceof HTMLElement))
        throw new Error('Unexpected type for this');
    showVideo();
    this.style.display = "";
    document.getElementById('collapse-video').style.display = "block";
};

document.getElementById('close-chat').onclick = function(e) {
  e.preventDefault();
  let left = document.getElementById("left");
  left.style.display = "none";
  document.getElementById('collapse-video').style.display = "block";
};

async function serverConnect() {
    if(serverConnection && serverConnection.socket)
        serverConnection.close();
    serverConnection = new ServerConnection();
    serverConnection.onconnected = gotConnected;
    serverConnection.onclose = gotClose;
    serverConnection.ondownstream = gotDownStream;
    serverConnection.onuser = gotUser;
    serverConnection.onjoined = gotJoined;
    serverConnection.onchat = addToChatbox;
    serverConnection.onclearchat = clearChat;
    serverConnection.onusermessage = function(id, dest, username, time, privileged, kind, message) {
        switch(kind) {
        case 'error':
        case 'warning':
        case 'info':
            let from = id ? (username || 'Anonymous') : 'The Server';
            if(privileged)
                displayError(`${from} said: ${message}`, kind);
            else
                console.error(`Got unprivileged message of kind ${kind}`);
            break;
        case 'mute':
            console.log(id, dest, username);
            if(privileged) {
                setLocalMute(true, true);
                let by = username ? ' by ' + username : '';
                displayWarning(`You have been muted${by}`);
            } else {
                console.error(`Got unprivileged message of kind ${kind}`);
            }
            break;
        default:
            console.warn(`Got unknown user message ${kind}`);
            break;
        }
    };
    let url = `ws${location.protocol === 'https:' ? 's' : ''}://${location.host}/ws`;
    try {
        await serverConnection.connect(url);
    } catch(e) {
        console.error(e);
        displayError(e.message ? e.message : "Couldn't connect to " + url);
    }
}

function start() {
    group = decodeURIComponent(location.pathname.replace(/^\/[a-z]*\//, ''));
    let title = group.charAt(0).toUpperCase() + group.slice(1);
    if(group !== '') {
        document.title = title;
        document.getElementById('title').textContent = title;
    }

    setMediaChoices(false).then(e => reflectSettings());

    fillLogin();
    document.getElementById("login-container").classList.remove('invisible');
}

start();
