import { listen } from "./framework.js";

let loginStorage = JSON.parse(localStorage.getItem("rkgk.login") ?? "{}");

function saveLoginStorage() {
    localStorage.setItem("rkgk.login", JSON.stringify(loginStorage));
}

let resolveLoggedInPromise;
let loggedInPromise = new Promise((resolve) => (resolveLoggedInPromise = resolve));

export function isUserLoggedIn() {
    return loginStorage.userId != null;
}

export function getUserId() {
    return loginStorage.userId;
}

export function getLoginSecret() {
    return loginStorage.secret;
}

export function waitForLogin() {
    return loggedInPromise;
}

if (isUserLoggedIn()) {
    resolveLoggedInPromise();
}

export async function registerUser(nickname) {
    try {
        let response = await fetch("/api/login", {
            method: "POST",
            body: JSON.stringify({ nickname }),
            headers: {
                "Content-Type": "application/json",
            },
        });

        if (response.status == 500) {
            console.error("login service returned 500 status", response);
            return {
                status: "error",
                message:
                    "We're sorry, but we ran into some trouble registering your account. Please try again.",
            };
        }

        let responseText = await response.text();
        let responseJson = JSON.parse(responseText);
        if (responseJson.status != "ok") {
            console.error("registering user failed", responseJson);
            return {
                status: "error",
                message: "Something seems to have gone wrong. Please try again.",
            };
        }

        loginStorage.userId = responseJson.userId;
        loginStorage.secret = responseJson.secret;
        console.info("user registered", loginStorage.userId);
        saveLoginStorage();
        resolveLoggedInPromise();

        return { status: "ok" };
    } catch (error) {
        console.error("registering user failed", error);
        return {
            status: "error",
            message: "Something seems to have gone wrong. Please try again.",
        };
    }
}

class Session extends EventTarget {
    #sentPing = false;

    constructor(userId, secret) {
        super();
        this.userId = userId;
        this.secret = secret;
    }

    async #recvJson() {
        let event = await listen([this.ws, "message"]);
        if (typeof event.data == "string") {
            return JSON.parse(event.data);
        } else {
            throw new Error("received a binary message where a JSON text message was expected");
        }
    }

    async #recvBinary() {
        let event = await listen([this.ws, "message"]);
        if (event.data instanceof Blob) {
            return event.data;
        } else {
            throw new Error("received a text message where a binary message was expected");
        }
    }

    #sendJson(object) {
        this.ws.send(JSON.stringify(object));
    }

    #dispatchError(source, kind, message) {
        this.dispatchEvent(
            Object.assign(new Event("error"), {
                source,
                errorKind: kind,
                message,
            }),
        );
    }

    async join(wallId, userInit) {
        console.info("joining wall", wallId);
        this.wallId = wallId;

        this.ws = new WebSocket("/api/wall");

        this.ws.addEventListener("error", (event) => {
            console.error("WebSocket connection error", error);
            this.dispatchEvent(Object.assign(new Event("error"), event));
        });

        this.ws.addEventListener("message", (event) => {
            if (typeof event.data == "string") {
                let json = JSON.parse(event.data);
                if (json.error != null) {
                    console.error("received error from server:", json.error);
                    this.#dispatchError(json, "protocol", json.error);
                }
            }
        });

        this.ws.addEventListener("close", (_) => {
            console.info("connection closed");
            this.dispatchEvent(new Event("disconnect"));
        });

        try {
            await listen([this.ws, "open"]);
            await this.joinInner(wallId, userInit);
        } catch (error) {
            this.#dispatchError(error, "connection", `communication failed: ${error.toString()}`);
        }
    }

    async joinInner(wallId, userInit) {
        let secret = this.secret;
        this.secret = null;

        let version = await this.#recvJson();
        console.info("protocol version", version.version);
        // TODO: This should probably verify that the version is compatible.
        // We don't have a way of sending Rust stuff to JavaScript just yet, so we don't care about it.

        let init = {
            brush: userInit.brush,
        };
        if (this.wallId == null) {
            this.#sendJson({
                user: this.userId,
                secret,
                init,
            });
        } else {
            this.#sendJson({
                user: this.userId,
                secret,
                wall: wallId,
                init,
            });
        }

        let loginResponse = await this.#recvJson();
        if (loginResponse.response == "loggedIn") {
            this.wallId = loginResponse.wall;
            this.wallInfo = loginResponse.wallInfo;
            this.sessionId = loginResponse.sessionId;

            console.info("logged in", this.wallId, this.sessionId);
            console.info("wall info:", this.wallInfo);
        } else {
            this.#dispatchError(
                loginResponse,
                loginResponse.response,
                "login failed; check error kind for details",
            );
            return;
        }
    }

    async eventLoop() {
        this.#pingLoop();

        try {
            while (true) {
                let event = await listen([this.ws, "message"]);
                if (typeof event.data == "string") {
                    await this.#processNotify(JSON.parse(event.data));
                } else {
                    console.warn("unhandled binary event", event.data);
                }
            }
        } catch (error) {
            this.#dispatchError(error, "protocol", `error in event loop: ${error.toString()}`);
        }
    }

    #pingLoop() {
        // Send small ping packets every 30 seconds to prevent reverse proxies from reaping the
        // connection if the tab is in the background.
        // (Browsers don't seem to send standard WebSocket pings if the tab is unfocused.)
        // We don't actually use this packet for anything else, like establishing whether
        // we still have a connection to the server, because the browser can handle that for us.
        setInterval(() => this.sendPing(), 30000);
    }

    async #processNotify(notify) {
        if (notify.notify == "pong") {
            console.debug("pong received");
            this.#sentPing = false;
        }

        if (notify.notify == "wall") {
            this.dispatchEvent(
                Object.assign(new Event("wallEvent"), {
                    sessionId: notify.sessionId,
                    wallEvent: notify.wallEvent,
                }),
            );
        }

        if (notify.notify == "chunks") {
            let chunkData = await this.#recvBinary();
            this.dispatchEvent(
                Object.assign(new Event("chunks"), {
                    chunkInfo: notify.chunks,
                    chunkData,
                    hasMore: notify.hasMore,
                }),
            );
        }
    }

    sendPing() {
        if (!this.#sentPing) {
            console.debug("ping sent; waiting for pong");
            this.#sendJson({
                request: "ping",
            });
            this.#sentPing = true;
        }
    }

    sendCursor(x, y) {
        this.#sendJson({
            request: "wall",
            wallEvent: {
                event: "cursor",
                position: { x, y },
            },
        });
    }

    sendPlot(points) {
        this.#sendJson({
            request: "wall",
            wallEvent: {
                event: "plot",
                points,
            },
        });
    }

    sendSetBrush(brush) {
        this.#sendJson({
            request: "wall",
            wallEvent: {
                event: "setBrush",
                brush,
            },
        });
    }

    sendViewport({ left, top, right, bottom }) {
        this.#sendJson({
            request: "viewport",
            topLeft: { x: left, y: top },
            bottomRight: { x: right, y: bottom },
        });
    }

    sendMoreChunks() {
        this.#sendJson({
            request: "moreChunks",
        });
    }
}

export async function newSession({ userId, secret, wallId, userInit, onError, onDisconnect }) {
    let session = new Session(userId, secret);
    session.addEventListener("error", onError);
    session.addEventListener("disconnect", onDisconnect);
    await session.join(wallId, userInit);
    return session;
}