// retoor <retoor@molodetz.nl>

const WRONG_WORKER_CODE = 4013;
const WRONG_WORKER_RETRY_MS = 200;
const INITIAL_BACKOFF_MS = 200;
const MAX_BACKOFF_MS = 5000;
const PING_MS = 25000;

export class ChatSocket {
    constructor(url, handlers) {
        this.url = url;
        this.handlers = handlers || {};
        this.socket = null;
        this._shouldRun = false;
        this._settled = false;
        this._backoff = INITIAL_BACKOFF_MS;
        this._pingId = null;
    }

    connect() {
        this._shouldRun = true;
        this._open();
    }

    isOpen() {
        return !!this.socket && this.socket.readyState === WebSocket.OPEN;
    }

    _open() {
        this._settled = false;
        const socket = new WebSocket(this.url);
        this.socket = socket;

        socket.addEventListener("open", () => this._startPing());

        socket.addEventListener("message", (event) => {
            let payload;
            try {
                payload = JSON.parse(event.data);
            } catch {
                return;
            }
            if (!this._settled) {
                this._settled = true;
                this._backoff = INITIAL_BACKOFF_MS;
                this._emit("onReady", payload && payload.type === "ready" ? payload : { type: "ready" });
            }
            if (payload && payload.type === "ready") return;
            this._emit("onMessage", payload);
        });

        socket.addEventListener("close", (event) => {
            this._stopPing();
            const wrongWorker = !this._settled && event.code === WRONG_WORKER_CODE;
            this._settled = false;
            if (wrongWorker) {
                if (this._shouldRun) {
                    window.setTimeout(() => this._open(), WRONG_WORKER_RETRY_MS);
                }
                return;
            }
            this._emit("onClose", event);
            if (this._shouldRun) {
                const delay = this._backoff;
                this._backoff = Math.min(this._backoff * 2, MAX_BACKOFF_MS);
                window.setTimeout(() => this._open(), delay);
            }
        });

        socket.addEventListener("error", (event) => this._emit("onError", event));
    }

    send(frame) {
        if (this.isOpen()) {
            this.socket.send(JSON.stringify(frame));
            return true;
        }
        return false;
    }

    close() {
        this._shouldRun = false;
        this._stopPing();
        if (this.socket) this.socket.close();
    }

    _startPing() {
        this._stopPing();
        this._pingId = window.setInterval(() => {
            if (this.isOpen()) this.send({ type: "ping" });
        }, PING_MS);
    }

    _stopPing() {
        if (this._pingId) {
            window.clearInterval(this._pingId);
            this._pingId = null;
        }
    }

    _emit(name, payload) {
        const handler = this.handlers[name];
        if (typeof handler === "function") handler(payload);
    }
}
