// Written by retoor@molodetz.nl

// This project implements a client-server communication system using WebSockets and REST APIs. 
// It features a chat system, a notification sound system, and interaction with server endpoints.

// No additional imports were used beyond standard JavaScript objects and constructors.

// MIT License

class RESTClient {
    debug = false;

    async get(url, params = {}) {
        const encodedParams = new URLSearchParams(params);
        if (encodedParams) url += '?' + encodedParams;
        const response = await fetch(url, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
            },
        });
        const result = await response.json();
        if (this.debug) {
            console.debug({ url, params, result });
        }
        return result;
    }

    async post(url, data) {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(data),
        });

        const result = await response.json();
        if (this.debug) {
            console.debug({ url, data, result });
        }
        return result;
    }
}

class EventHandler {
    constructor() {
        this.subscribers = {};
    }

    addEventListener(type, handler) {
        if (!this.subscribers[type]) this.subscribers[type] = [];
        this.subscribers[type].push(handler);
    }

    emit(type, ...data) {
        if (this.subscribers[type]) this.subscribers[type].forEach(handler => handler(...data));
    }
}

class Chat extends EventHandler {
    constructor() {
        super();
        this._url = window.location.hostname === 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws';
        this._socket = null;
        this._waitConnect = null;
        this._promises = {};
    }

    connect() {
        if (this._waitConnect) {
            return this._waitConnect;
        }
        return new Promise((resolve) => {
            this._waitConnect = resolve;
            console.debug("Connecting..");

            try {
                this._socket = new WebSocket(this._url);
            } catch (e) {
                console.warn(e);
                setTimeout(() => {
                    this.ensureConnection();
                }, 1000);
            }

            this._socket.onconnect = () => {
                this._connected();
                this._waitSocket();
            };
        });
    }

    generateUniqueId() {
        return 'id-' + Math.random().toString(36).substr(2, 9);
    }

    call(method, ...args) {
        return new Promise((resolve, reject) => {
            try {
                const command = { method, args, message_id: this.generateUniqueId() };
                this._promises[command.message_id] = resolve;
                this._socket.send(JSON.stringify(command));
            } catch (e) {
                reject(e);
            }
        });
    }

    _connected() {
        this._socket.onmessage = (event) => {
            const message = JSON.parse(event.data);
            if (message.message_id && this._promises[message.message_id]) {
                this._promises[message.message_id](message);
                delete this._promises[message.message_id];
            } else {
                this.emit("message", message);
            }
        };
        this._socket.onclose = () => {
            this._waitSocket = null;
            this._socket = null;
            this.emit('close');
        };
    }

    async privmsg(room, text) {
        await rest.post("/api/privmsg", {
            room,
            text,
        });
    }
}

class Socket extends EventHandler {
    ws = null;
    isConnected = null;
    isConnecting = null;
    url = null;
    connectPromises = [];
    ensureTimer = null;

    constructor() {
        super();
        this.url = window.location.hostname === 'localhost' ? 'ws://localhost:8081/rpc.ws' : 'wss://' + window.location.hostname + '/rpc.ws';
        this.ensureConnection();
    }

    _camelToSnake(str) {
        return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
    }

    get client() {
        const me = this;
        return new Proxy({}, {
            get(_, prop) {
                return (...args) => {
                    const functionName = me._camelToSnake(prop);
                    return me.call(functionName, ...args);
                };
            },
        });
    }

    ensureConnection() {
        if (this.ensureTimer) {
            return this.connect();
        }
        const me = this;
        this.ensureTimer = setInterval(() => {
            if (me.isConnecting) me.isConnecting = false;
            me.connect();
        }, 5000);
        return this.connect();
    }

    generateUniqueId() {
        return 'id-' + Math.random().toString(36).substr(2, 9);
    }

    connect() {
        
        const me = this 
        if (this.isConnected || this.isConnecting) {
            return new Promise((resolve) => {
                me.connectPromises.push(resolve);
                if (!me.isConnecting) resolve(me);
            });
        }
        this.isConnecting = true;
        return new Promise((resolve) => {
            this.connectPromises.push(resolve);
            console.debug("Connecting..");

            const ws = new WebSocket(this.url);
            ws.onopen = () => {
                this.ws = ws;
                this.isConnected = true;
                this.isConnecting = false;
                ws.onmessage = (event) => {
                    this.onData(JSON.parse(event.data));
                };
                ws.onclose = () => {
                    this.onClose();
                };
                ws.onerror = () => {
                    this.onClose();
                };
                this.onConnect()
                this.connectPromises.forEach(resolver => resolver(this));
            };
        });
    }
    onConnect(){
        this.emit("connected")
    }
    onData(data) {
        if (data.success !== undefined && !data.success) {
            console.error(data);
        }
        if (data.callId) {
            this.emit(data.callId, data.data);
        }
        if (data.channel_uid) {
            this.emit(data.channel_uid, data.data);
            this.emit("channel-message", data);
        }
    }

    async sendJson(data) {
        await this.connect().then(api => {
            api.ws.send(JSON.stringify(data));
        });
    }

    async call(method, ...args) {
        const call = {
            callId: this.generateUniqueId(),
            method,
            args,
        };
        const me = this 
        return new Promise((resolve) => {
            me.addEventListener(call.callId, data => resolve(data));
            me.sendJson(call);
        });
    }

    onClose() {
        console.info("Connection lost. Reconnecting.");
        this.isConnected = false;
        this.isConnecting = false;
        this.ws.close();
        this.ws = null;
        this.ensureConnection().then(() => {
            console.info("Reconnected.");
        });
    }
}

class NotificationAudio {
    constructor(timeout = 500) {
        this.schedule = new Schedule(timeout);
    }

    sounds = ["/audio/soundfx.d_beep3.mp3"];

    play(soundIndex = 0) {
        this.schedule.delay(() => {
            new Audio(this.sounds[soundIndex]).play()
                .then(() => {
                    console.debug("Gave sound notification");
                })
                .catch(error => {
                    console.error("Notification failed:", error);
                });
        });
    }
}

class App extends EventHandler {
    rest = new RESTClient();
    ws = null;
    rpc = null;
    audio = null;
    user = {};

    async ping(...args) {
        if(this.is_pinging)return false 
        this.is_pinging = true
        await this.rpc.ping(...args);
        this.is_pinging = false
    }
    async forcePing(...arg) {
        await this.rpc.ping(...args);
    }

    constructor() {
        super();
        this.ws = new Socket();
        this.rpc = this.ws.client;
        this.audio = new NotificationAudio(500);
        this.is_pinging = false 
        this.ping_interval = setInterval(()=>{
            this.ping("active")
        }, 15000)
        

        const me = this 
        this.ws.addEventListener("connected", (data)=> {
            this.ping("online")
        })
        this.ws.addEventListener("channel-message", (data) => {
            me.emit("channel-message", data);
        });

        this.rpc.getUser(null).then(user => {
            me.user = user;
        });
    }

    playSound(index) {
        this.audio.play(index);
    }
    timeDescription(isoDate) {
        const date = new Date(isoDate);
        const hours = String(date.getHours()).padStart(2, "0");
        const minutes = String(date.getMinutes()).padStart(2, "0");
        let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
        return timeStr;
    }
    timeAgo(date1, date2) {
        const diffMs = Math.abs(date2 - date1);
        const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
        const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
        const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
        const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);

        if (days) {
            return `${days} ${days > 1 ? 'days' : 'day'} ago`;
        }
        if (hours) {
            return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
        }
        if (minutes) {
            return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
        }
        return 'just now';
    }
    async benchMark(times = 100, message = "Benchmark Message") {
        const promises = [];
        const me = this; 
        for (let i = 0; i < times; i++) {
            promises.push(this.rpc.getChannels().then(channels => {
                channels.forEach(channel => {
                    me.rpc.sendMessage(channel.uid, `${message} ${i}`);
                });
            }));
        }
    }
}

const app = new App();
