// @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 /////
async function enterFullscreen() {
    if (document.fullscreenElement === null) {
        const videoPlayer = document.getElementById("videoPlayer");
        if (!(videoPlayer instanceof HTMLElement)) {
            panic("Unexpected element");
        }
        await videoPlayer.requestFullscreen();
    } else {
        await document.exitFullscreen();
    }
}
/** @ts-ignore */
window.enterFullscreen = enterFullscreen;
/** Set message below the video
 * @param message {string}
 * @param live {boolean} - whether to add the red circle
 */
function setLiveIndicator(message, live) {
    const liveIndicator = document.getElementById("liveIndicator");
    if (!(liveIndicator instanceof HTMLElement) || liveIndicator === null) {
        console.error("Ошибка при получении индикатора трансляции")
        return;
    }
    const label = live
        ? "<span style=\"color: #f00\">●</span>" + message
        : message;
    liveIndicator.innerHTML = label;
}
/** 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 {HTMLVideoElement} video
 * @param {WebSocket} serverConnection
 * @returns {Promise<RTCPeerConnection>}
 */
async function createRtc(video, serverConnection) {
    const configuration = {"iceServers": [{"urls": "stun:stun.l.google.com:19302"}]};
    const peerConnection = new RTCPeerConnection(configuration);
    peerConnection.onicecandidate = ev => {
        serverConnection.send(Types.encodeServerMessage({
            targetId: null,
            body: {
                t: "ice",
                ice: ev.candidate,
            }
        }));
    };
    peerConnection.onconnectionstatechange = _ev => {
        if (peerConnection.connectionState === "connected") {
            logStatus("Видеосоединение установлено", "success");
        } else if (peerConnection.connectionState === "disconnected") {
            // FIXME in theory in case of pausing this runs in parallel with
            // restarting the connection, and after the timeout of the old
            // connection the indicator should be updated to the pause. In
            // practice it doesn't. Why?
            // Since the current behaviour is desired, I can't be arsed to
            // explore and fix it.
            setLiveIndicator("Трансляция приостановлена", false);
            peerConnection.close();
        }
    };
    peerConnection.ontrack = ev => {
        // Set local video to whatever track we get
        const firstStream = ev.streams[0];
        if (firstStream != undefined) {
            video.srcObject = firstStream;
            // even though there is no audio stream, we need to mute it or
            // chrome doesn't play it
            video.muted = true;
            video.play();
        }
    }
    return peerConnection;
}
/**
 * @param {string} roomId
 */
async function awaitCall(roomId) {
    const video = document.getElementById("video");
    if (!(video instanceof HTMLVideoElement)) {
        panic("Ошибка при поиске видеоэлемента на странице");
    }
    // Create connection to signaling server
    const urlPort = window.location.port === "" ? "" : ":" + window.location.port;
    const initMessage = await connectToServer("wss://" + window.location.hostname + urlPort);
    const serverConnection = initMessage.ws;
    serverConnection.send(Types.encodeServerMessage({
        targetId: null,
        body: {
            t: "joinRoom",
            roomId,
        },
    }));
    const roomMessageEvent = await recv(serverConnection);
    const roomMessage = Types.decodeServerMessage(roomMessageEvent.data);
    if (roomMessage === null || roomMessage.body.t != "joinRoom") {
        panic("Некорректное сообщение от сервера; невозможно подключиться к комнате");
    }
    if (roomMessage.body.status != "success") {
        panic(`Невозможно подключиться к комнате. Ошибка: ${roomMessage.body.status}`);
    }
    logStatus("Подключение к комнате успешно, создание видеосвязи", "progress");
    // Create initial webrtc connection
    /** @type {RTCPeerConnection | null} */
    let peerConnection = null;
    // Handle signaling events
    serverConnection.onmessage = async message => {
        const signal = Types.decodeServerMessage(message.data);
        if (!signal) {
            console.error("Malformed message");
            return;
        }
        if (!signal.senderId) {
            console.error("Malformed message");
            return;
        }
        if (signal.body.t === "sdp") {
            console.debug("initiator sdp signal");
            if (signal.body.sdp.type !== "offer") {
                console.warn("didn't get offer sdp", signal.body.sdp);
                // We're the responder, we're only interested in offers
                return
            }
            // (Re)open rtc
            if (peerConnection) {
                peerConnection.close();
            }
            peerConnection = await createRtc(video, serverConnection);
            setLiveIndicator("Прямая трансляция", true);
            // Create answer
            await peerConnection.setRemoteDescription(new RTCSessionDescription(signal.body.sdp));
            const desc = await peerConnection.createAnswer();
            await peerConnection.setLocalDescription(desc);
            serverConnection.send(Types.encodeServerMessage({
                targetId: null,
                body: {
                    t: "sdp",
                    sdp: expect(peerConnection.localDescription, "localDescription not set"),
                },
            }));
        } else if (signal.body.t === "ice") {
            console.debug("initiator ice signal", signal.body.ice);
            const ice = signal.body.ice
                ? new RTCIceCandidate(signal.body.ice)
                : { candidate: "" };
            if (peerConnection) {
                peerConnection.addIceCandidate(ice);
            } else {
                console.warn("got ICE while the connection was never opened");
            }
        } else if (signal.body.t === "leaveRoom") {
            logStatus("Трансляция закончена", "success");
            setLiveIndicator("Конец трансляции", false);
        }
    }
}
window.onload = async () => {
    const query = new URLSearchParams(window.location.search);
    const roomId = query.get("room");
    if (roomId === null) {
        panic("Комната не указана");
    }
    const roomElement = document.getElementById("roomName");
    if (!(roomElement instanceof HTMLElement)) {
        console.error("Ошибка при поиске подписи комнаты на странице");
    } else {
        roomElement.innerText = roomId;
    }
    await awaitCall(roomId);
}