"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
function _export(target, all) {
    for(var name in all)Object.defineProperty(target, name, {
        enumerable: true,
        get: all[name]
    });
}
_export(exports, {
    urlQuery: function() {
        return urlQuery;
    },
    /**
 * Misskey stream connection
 */ default: function() {
        return Stream;
    },
    Connection: function() {
        return Connection;
    }
});
const _eventemitter3 = require("eventemitter3");
const _reconnectingwebsocket = /*#__PURE__*/ _interop_require_default(require("reconnecting-websocket"));
function _interop_require_default(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
function autobind(instance) {
    const prototype = Object.getPrototypeOf(instance);
    const propertyNames = Object.getOwnPropertyNames(prototype);
    for (const key of propertyNames){
        const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
        if (typeof descriptor?.value === "function" && key !== "constructor") {
            Object.defineProperty(instance, key, {
                value: instance[key].bind(instance),
                enumerable: descriptor.enumerable,
                configurable: descriptor.configurable,
                writable: descriptor.writable
            });
        }
    }
}
function urlQuery(obj) {
    const params = Object.entries(obj).filter(([, v])=>Array.isArray(v) ? v.length : v !== undefined)// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    .reduce((a, [k, v])=>(a[k] = v, a), {});
    return Object.entries(params).map((e)=>`${e[0]}=${encodeURIComponent(e[1])}`).join("&");
}
class Stream extends _eventemitter3.EventEmitter {
    stream;
    state = "initializing";
    sharedConnectionPools = [];
    sharedConnections = [];
    nonSharedConnections = [];
    idCounter = 0;
    constructor(origin, user, options){
        super();
        autobind(this);
        options = options || {};
        const query = urlQuery({
            i: user?.token,
            // To prevent cache of an HTML such as error screen
            _t: Date.now()
        });
        const wsOrigin = origin.replace("http://", "ws://").replace("https://", "wss://");
        this.stream = new _reconnectingwebsocket.default(`${wsOrigin}/streaming?${query}`, "", {
            minReconnectionDelay: 1,
            WebSocket: options.WebSocket
        });
        this.stream.addEventListener("open", this.onOpen);
        this.stream.addEventListener("close", this.onClose);
        this.stream.addEventListener("message", this.onMessage);
    }
    genId() {
        return (++this.idCounter).toString();
    }
    useChannel(channel, params, name) {
        if (params) {
            return this.connectToChannel(channel, params);
        } else {
            return this.useSharedConnection(channel, name);
        }
    }
    useSharedConnection(channel, name) {
        let pool = this.sharedConnectionPools.find((p)=>p.channel === channel);
        if (pool == null) {
            pool = new Pool(this, channel, this.genId());
            this.sharedConnectionPools.push(pool);
        }
        const connection = new SharedConnection(this, channel, pool, name);
        this.sharedConnections.push(connection);
        return connection;
    }
    removeSharedConnection(connection) {
        this.sharedConnections = this.sharedConnections.filter((c)=>c !== connection);
    }
    removeSharedConnectionPool(pool) {
        this.sharedConnectionPools = this.sharedConnectionPools.filter((p)=>p !== pool);
    }
    connectToChannel(channel, params) {
        const connection = new NonSharedConnection(this, channel, this.genId(), params);
        this.nonSharedConnections.push(connection);
        return connection;
    }
    disconnectToChannel(connection) {
        this.nonSharedConnections = this.nonSharedConnections.filter((c)=>c !== connection);
    }
    /**
	 * Callback of when open connection
	 */ onOpen() {
        const isReconnect = this.state === "reconnecting";
        this.state = "connected";
        this.emit("_connected_");
        // チャンネル再接続
        if (isReconnect) {
            for (const p of this.sharedConnectionPools)p.connect();
            for (const c of this.nonSharedConnections)c.connect();
        }
    }
    /**
	 * Callback of when close connection
	 */ onClose() {
        if (this.state === "connected") {
            this.state = "reconnecting";
            this.emit("_disconnected_");
        }
    }
    /**
	 * Callback of when received a message from connection
	 */ onMessage(message) {
        const { type, body } = JSON.parse(message.data);
        if (type === "channel") {
            const id = body.id;
            let connections;
            connections = this.sharedConnections.filter((c)=>c.id === id);
            if (connections.length === 0) {
                const found = this.nonSharedConnections.find((c)=>c.id === id);
                if (found) {
                    connections = [
                        found
                    ];
                }
            }
            for (const c of connections){
                c.emit(body.type, body.body);
                c.inCount++;
            }
        } else {
            this.emit(type, body);
        }
    }
    /**
	 * Send a message to connection
	 */ send(typeOrPayload, payload) {
        const data = payload === undefined ? typeOrPayload : {
            type: typeOrPayload,
            body: payload
        };
        this.stream.send(JSON.stringify(data));
    }
    /**
	 * Close this connection
	 */ close() {
        this.stream.close();
    }
}
// TODO: これらのクラスを Stream クラスの内部クラスにすれば余計なメンバをpublicにしないで済むかも？
// もしくは @internal を使う？ https://www.typescriptlang.org/tsconfig#stripInternal
class Pool {
    channel;
    id;
    stream;
    users = 0;
    disposeTimerId;
    isConnected = false;
    constructor(stream, channel, id){
        this.channel = channel;
        this.stream = stream;
        this.id = id;
        this.stream.on("_disconnected_", this.onStreamDisconnected);
    }
    onStreamDisconnected() {
        this.isConnected = false;
    }
    inc() {
        if (this.users === 0 && !this.isConnected) {
            this.connect();
        }
        this.users++;
        // タイマー解除
        if (this.disposeTimerId) {
            clearTimeout(this.disposeTimerId);
            this.disposeTimerId = null;
        }
    }
    dec() {
        this.users--;
        // そのコネクションの利用者が誰もいなくなったら
        if (this.users === 0) {
            // また直ぐに再利用される可能性があるので、一定時間待ち、
            // 新たな利用者が現れなければコネクションを切断する
            this.disposeTimerId = setTimeout(()=>{
                this.disconnect();
            }, 3000);
        }
    }
    connect() {
        if (this.isConnected) return;
        this.isConnected = true;
        this.stream.send("connect", {
            channel: this.channel,
            id: this.id
        });
    }
    disconnect() {
        this.stream.off("_disconnected_", this.onStreamDisconnected);
        this.stream.send("disconnect", {
            id: this.id
        });
        this.stream.removeSharedConnectionPool(this);
    }
}
class Connection extends _eventemitter3.EventEmitter {
    channel;
    stream;
    name;
    inCount = 0;
    outCount = 0;
    constructor(stream, channel, name){
        super();
        this.stream = stream;
        this.channel = channel;
        this.name = name;
    }
    send(type, body) {
        this.stream.send("ch", {
            id: this.id,
            type: type,
            body: body
        });
        this.outCount++;
    }
}
class SharedConnection extends Connection {
    pool;
    get id() {
        return this.pool.id;
    }
    constructor(stream, channel, pool, name){
        super(stream, channel, name);
        this.pool = pool;
        this.pool.inc();
    }
    dispose() {
        this.pool.dec();
        this.removeAllListeners();
        this.stream.removeSharedConnection(this);
    }
}
class NonSharedConnection extends Connection {
    id;
    params;
    constructor(stream, channel, id, params){
        super(stream, channel);
        this.params = params;
        this.id = id;
        this.connect();
    }
    connect() {
        this.stream.send("connect", {
            channel: this.channel,
            id: this.id,
            params: this.params
        });
    }
    dispose() {
        this.removeAllListeners();
        this.stream.send("disconnect", {
            id: this.id
        });
        this.stream.disconnectToChannel(this);
    }
}
