// @ts-check
"use strict";
import * as Types from './types.js';
///// Status message /////
/** @typedef {"error" | "success" | "progress"} StatusType */
/** @type {HTMLElement | null} */
let statusElementVal = null;
/** @returns {HTMLElement} */
function getStatusElement() {
    if (statusElementVal === null) {
        const r = document.getElementById("statusMessage");
        if (!(r instanceof HTMLElement)) {
            throw new Error("Can't find status message area");
        }
        statusElementVal = r;
        statusElementVal.innerText = "Скрипт загружен впервые";
    }
    return statusElementVal;
}
/**
 * @param {string} msg
 * @param {StatusType} type
 * @returns {null}
 */
function logStatus(msg, type) {
    console.log(msg);
    const e = getStatusElement();
    e.innerText = msg;
    switch (type) {
        case "error":
            e.style.backgroundColor = "#ffaaaa";
            e.style.borderColor = "#7f5555";
            return null;
        case "success":
            e.style.backgroundColor = "#aaffaa";
            e.style.borderColor = "#557f55";
            return null;
        case "progress":
            e.style.backgroundColor = "#ffffaa";
            e.style.borderColor = "#7f7f55";
            return null;
    }
}
/**
 * @param {string} s
 * @returns {never}
 */
function panic(s) {
    logStatus(s, "error");
    throw new Error("Panic: " + s);
}
/**
 * @template A
 * @param {A | null} x
 * @param {string} s - error message
 * @returns {A}
 */
function expect(x, s) {
    if (x == null) {
        panic("unwrap: " + s);
    }
    return x;
}
///// Main logic /////
/** Open a connection to a websocket server and wait for the connection to
 * succeed
 *
 * @param {string} url
 * @returns {Promise<WebSocket>}
 */
function connectWs(url) {
    return new Promise(resolve => {
        const ws = new WebSocket(url);
        /** @param {Event} _ev */
        function onOpen(_ev) {
            ws.removeEventListener("open", onOpen);
            resolve(ws);
        }
        ws.addEventListener("open", onOpen);
    });
}
/** Receive one message from the websocket
 * @param {WebSocket} ws
 * @returns {Promise<MessageEvent>}
 */
function recv(ws) {
    return new Promise(resolve => {
        /** @param {MessageEvent} msg */
        function onMsg(msg) {
            ws.removeEventListener("message", onMsg);
            resolve(msg);
        }
        ws.addEventListener("message", onMsg);
    });
}
/**
 * @param {string} url
 * @returns {Promise<{ws: WebSocket, roomId: string, myId: string}>}
 */
async function connectToServer(url) {
    logStatus("Подключение к серверу..", "progress");
    const ws = await connectWs(url);
    logStatus("Подключение установлено, ожидание инициализации..", "progress");
    const msgEvent = await recv(ws);
    const firstMsg = Types.decodeServerMessage(msgEvent.data);
    if (firstMsg === null || firstMsg.body.t != "identity") {
        panic("Ошибка инициализации: некорректное сообщение от сервера");
    }
    logStatus("Соединение с сервером установлено", "success");
    return {
        ws: ws,
        roomId: firstMsg.body.roomId,
        myId: firstMsg.senderId,
    };
}
/**
 * @param {MediaStream} stream
 * @param {string} watcherId
 * @param {WebSocket} serverConnection
 * @returns {Promise<RTCPeerConnection>}
 */
async function createRtc(stream, watcherId, serverConnection) {
    logStatus(`Создаётся новое подключение к ${watcherId}`, "progress");
    // Create webrtc connection
    const configuration = {"iceServers": [{"urls": "stun:stun.l.google.com:19302"}]};
    const peerConnection = new RTCPeerConnection(configuration);
    // Set to send local stream
    const constraint = qualityConstraint(activeQuality);
    for (const track of stream.getTracks()) {
        try {
            await track.applyConstraints(constraint);
        } catch (e) {
            logStatus(`${ e?.toString?.() }`, "error");
        }
        peerConnection.addTrack(track, stream);
    }
    // Initialize webrtc
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    peerConnection.onicecandidate = ev => {
        serverConnection.send(Types.encodeServerMessage({
            targetId: watcherId,
            body: {
                t: "ice",
                ice: ev.candidate,
            },
        }));
    };
    peerConnection.onconnectionstatechange = _ev => {
        if (peerConnection.connectionState === "connected") {
            logStatus(`Подключение к ${watcherId} успешно`, "success");
        }
    };
    // Send offer through signaling
    serverConnection.send(Types.encodeServerMessage({
        targetId: watcherId,
        body: {
            t: "sdp",
            sdp: expect(peerConnection.localDescription, "Local description not set"),
        }
    }));
    return peerConnection;
}
/** @typedef {"max" | "1440p" | "1080p60" | "1080p30" | "720p" | "360p"} Quality */
/** Get constraint corresponding to human-redable quality
 * @param {Quality} q
 * @returns {MediaTrackConstraints}
 */
function qualityConstraint(q) {
    switch (q) {
        case "max":
            return {};
        case "1440p":
            return {
                height: {max: 1440, ideal: 1440},
                frameRate: {ideal: 60},
            }
        case "1080p60":
            return {
                height: {max: 1080, ideal: 1080},
                frameRate: {ideal: 60},
            }
        case "1080p30":
            return {
                height: {max: 1080, ideal: 1080},
                frameRate: {max: 30},
            }
        case "720p":
            return {
                height: {max: 720, ideal: 720},
                frameRate: {ideal: 30},
            }
        case "360p":
            return {
                height: {max: 360, ideal: 360},
                frameRate: {ideal: 30},
            }
    }
}
/** @param senderId {string} */
function addViewerList(senderId) {
    const viewersElement = document.getElementById("viewerList");
    if (viewersElement instanceof HTMLElement) {
        const p = document.createElement("p");
        p.innerText = senderId;
        viewersElement.appendChild(p);
    } else {
        console.error("Error getting viewer list");
    }
}
/** @type {WebSocket | null} */
let serverConnection = null;
/** @type {string[]}
 * Watchers who joined before RTC started */
let oldWatchers = [];
/** @type {{[id: string]: RTCPeerConnection}}
 * Watchers and their established RTC channels */
let watchers = {};
/** @type {MediaStream | null} */
let activeStream = null;
/** @type {Quality} */
let activeQuality = "max";
/** @param {Quality} quality */
async function changeQuality(quality) {
    // a bit of ts crime because I don't want to double parse
    /** @type {MediaTrackConstraints | undefined} */
    const constraint = qualityConstraint(quality);
    if (!constraint) {
        panic("Wrong quality");
    }
    activeQuality = quality;
    if (activeStream) {
        for (const track of activeStream.getTracks()) {
            try {
                await track.applyConstraints(constraint);
            } catch (e) {
                logStatus(`${ e?.toString?.() }`, "error");
            }
        }
    }
}
/** @ts-ignore */
window.changeQuality = changeQuality;
async function makeCall() {
    if (serverConnection === null) {
        panic("Звонок невозможен: нет соединения с сервером");
    }
    // Initialize stream to send
    const video = document.getElementById("video");
    if (!(video instanceof HTMLVideoElement)) {
        panic("Ошибка при поиске видеоэлемента на странице");
    }
    const stream = await navigator
        .mediaDevices
        .getDisplayMedia({ video: true, audio: true });
    activeStream = stream;
    video.srcObject = stream;
    video.play();
    for (const watcher of oldWatchers) {
        const peerConnection = await createRtc(stream, watcher, serverConnection);
        watchers[watcher] = peerConnection;
    }
    serverConnection.onmessage = async message => {
        const signal = Types.decodeServerMessage(message.data);
        if (!signal) {
            console.error("Malformed message");
            return;
        }
        if (signal.body.t === "sdp") {
            console.debug("responder sdp signal");
            const peerConnection = watchers[signal.senderId];
            if (!peerConnection) {
                console.warn("Sdp signal references unknown sender: " + signal.senderId);
                return;
            }
            await peerConnection.setRemoteDescription(signal.body.sdp);
            if (signal.body.sdp.type === "offer") {
                console.warn("Got an offer from the responder");
                // Create answer to offer
                if (peerConnection.localDescription === null) {
                    console.warn("Can't respond as there is no local description");
                } else {
                    const desc = await peerConnection.createAnswer();
                    await peerConnection.setLocalDescription(desc);
                    const conn = expect(serverConnection, "соединение неожиданно потеряно");
                    conn.send(Types.encodeServerMessage({
                        targetId: signal.senderId,
                        body: {
                            t: "sdp",
                            sdp: peerConnection.localDescription,
                        },
                    }));
                }
            }
        } else if (signal.body.t === "ice") {
            console.debug(`responder ${signal.senderId} ice signal`);
            const watcher = watchers[signal.senderId];
            if (!watcher) {
                console.warn("Ice signal references unknown sender: " + signal.senderId);
                return;
            }
            console.debug("Add ice candidate with arg", signal.body.ice);
            const ice = signal.body.ice
                ? new RTCIceCandidate(signal.body.ice)
                : { candidate: "" };
            watcher.addIceCandidate(ice);
        } else if (signal.body.t === "partyJoins") {
            logStatus(`Новый зритель: ${signal.senderId}`, "success");
            addViewerList(signal.senderId);
            // Save for restarting stream
            oldWatchers.push(signal.senderId);
            // Don't attempt creating RTC while the stream is on pause
            if (activeStream != null) {
                const conn = expect(serverConnection, "соединение неожиданно потеряно");
                const peerConnection = await createRtc(stream, signal.senderId, conn);
                watchers[signal.senderId] = peerConnection;
            }
        } else if (signal.body.t === "leaveRoom") {
            logStatus(`Зритель ушёл: ${signal.senderId}`, "success");
            // Filter the viewer list widget for the participant that left
            const viewersElement = document.getElementById("viewerList");
            if (viewersElement instanceof HTMLElement) {
                viewersElement.childNodes.forEach(viewer => {
                    if (viewer instanceof HTMLElement && viewer.innerText === signal.senderId) {
                        viewersElement.removeChild(viewer);
                    }
                });
            } else {
                console.error("Error getting viewer list");
            }
        }
    }
}
async function stopCall() {
    if (activeStream === null) {
        logStatus("Попытка остановить остановленную трансляцию", "error");
        return;
    }
    // Stop the streams
    activeStream.getTracks().forEach(track => track.stop());
    activeStream = null;
    // Stop the connections
    for (const stream of Object.values(watchers)) {
        stream.close();
        console.debug("stream closed");
    }
    watchers = {};
    // Remove video
    const video = document.getElementById("video");
    if (!(video instanceof HTMLVideoElement)) {
        panic("Ошибка при поиске видеоэлемента на странице");
    }
    video.srcObject = null;
    logStatus("Трансляция остановлена", "success");
}
/** @param target {HTMLButtonElement} */
async function callClicked(target) {
    if (target.textContent === "Начать") {
        target.textContent = "Остановить";
        await makeCall();
    } else if (target.textContent === "Остановить") {
        target.textContent = "Начать";
        await stopCall();
    } else {
        panic(`Неверное состояние звонка: ${target.textContent}`);
    }
}
/** @ts-ignore */
window.callClicked = callClicked;
async function copyRoom() {
    const roomElement = document.getElementById("roomName");
    if (!(roomElement instanceof HTMLElement)) {
        panic("Ошибка при поиске подписи комнаты на странице");
    }
    const roomCopyStatusElement = document.getElementById("roomNameCopyStatus");
    if (!(roomCopyStatusElement instanceof HTMLElement)) {
        panic("Ошибка при поиске подписи комнаты на странице");
    }
    const blob = new Blob([roomElement.innerText], {type: "text/plain"});
    const data = [new ClipboardItem({ ["text/plain"]: blob })];
    await navigator.clipboard.write(data);
    roomCopyStatusElement.innerText = "[скопировано]";
    await new Promise(r => setTimeout(r, 2000));
    roomCopyStatusElement.innerText = "[копировать]";
}
/** @ts-ignore */
window.copyRoom = copyRoom;
window.onload = async () => {
    // Create connection to signaling server
    const urlPort = window.location.port === "" ? "" : ":" + window.location.port;
    const proto = window.location.protocol === "http:" ? "ws" : "wss";
    const initMessage = await connectToServer(proto + "://" + window.location.hostname + urlPort);
    serverConnection = initMessage.ws;
    const myId = initMessage.myId;
    const roomId = initMessage.roomId;
    const roomElement = document.getElementById("roomName");
    if (!(roomElement instanceof HTMLElement)) {
        panic("Ошибка при поиске подписи комнаты на странице");
    }
    roomElement.innerText = `${window.location}watch?room=${roomId}`
    console.log(`myId = ${myId}, my roomId = ${roomId}`);
    serverConnection.onmessage = async message => {
        const signal = Types.decodeServerMessage(message.data);
        if (!signal) {
            console.error("Malformed message");
            return;
        }
        if (signal.body.t === "partyJoins") {
            logStatus(`Новый зритель: ${signal.senderId}`, "success");
            oldWatchers.push(signal.senderId);
            addViewerList(signal.senderId);
        }
    }
}