import { platform } from "./platform.common.js";
import { rethrow } from "./prototype.common.js"; // .bracket()
import { message_symbol, pack, parse } from "./io.common.js";
import { check, defer, delay, genid, Lock, set_error_message, timeout, TimeoutError } from "./utils.common.js";
import { t } from "./i18n/instance.js";
/** 对于 request() 函数来说无意义的 headers，会自动过滤掉 */
export const drop_request_headers = new Set([
    // : 开头的 key
    // sec-*
    'accept-charset',
    'connection',
    'content-length',
    'keep-alive',
    'trailer',
    'transfer-encoding',
    'upgrade',
]);
export const WebSocketConnecting = 0;
export const WebSocketOpen = 1;
export const WebSocketClosing = 2;
export const WebSocketClosed = 3;
export const websocket_states = ['connecting', 'open', 'closing', 'closed'];
export class WebSocketConnectionError extends Error {
    name = 'WebSocketConnectionError';
    // 这里不保留 websocket 引用，防止循环引用导致 JSON 序列化失败
    url;
    protocols;
    type;
    /** close 事件时为 close code (非 1000 的 number), error 事件为 error code (可能是 string) */
    code;
    reason;
    event;
    // --- node.js error event 独有
    address;
    errno;
    port;
    syscall;
    static get_reason_string(code, reason) {
        // https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
        if (!reason)
            if (code === 1001)
                reason = t('对端关闭');
            else if (code === 1006)
                reason = t('网络中断');
        return reason ? `${reason} (${code})` : code;
    }
    constructor(url, protocols, event, message) {
        let type, reason, code, error;
        if (!message)
            if (event)
                if ((type = event.type) === 'error') {
                    ({ error } = event);
                    if (error) {
                        ({ code, reason } = error);
                        message = `${t('连接被关闭')}: ` + [
                            error.message,
                            WebSocketConnectionError.get_reason_string(code, reason),
                            error.syscall ? `${error.syscall}(${error.errno})` : ''
                        ].filter(Boolean).join(' ');
                    }
                    else
                        message = t('连接被关闭');
                }
                else {
                    ({ code, reason } = event);
                    message = `${t('连接被关闭')}: ${WebSocketConnectionError.get_reason_string(code, reason)}`;
                }
            else
                message = t('连接被关闭');
        super(new Date().to_time_str() + '  ' +
            (url || '传入') +
            (protocols ? ' ' + protocols.join(', ').bracket() : '') +
            ' ' + message);
        this.url = url;
        this.protocols = protocols;
        if (!event)
            return;
        this.type = type;
        this.code = code;
        this.reason = reason;
        if (type === 'error' && platform.nodejs) {
            this.cause = error;
            this.address = error.address;
            this.errno = error.errno;
            this.port = error.port;
            this.syscall = error.syscall;
        }
        this.event = event;
    }
}
let websocket_proxy_agents = {};
/** 连接 websocket url, 设置各种事件监听器。在 open 事件后 resolve, 返回 websocket
    遇到 error, close 事件时会创建 WebSocketConnectionError:
        - reject 掉返回的 promise (若此时未 settle)
        - 作为参数调用 on_error (已 settle 且有 on_error 回调)
    可以用 WebSocket.bufferedAmount 来查询大消息的发送进度
    https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/bufferedAmount
    - url
    - options:
        - print?: 是否打印连接、关闭信息
        - protocols?
        - max_payload?: `8 gb` (仅 nodejs 环境有效)
        - proxy?: string (仅 nodejs 环境有效)
        - on_message: 根据 websocket frame 的 opcode 不同 (text frame 或 binary frame)，event 中的 data 对应为 ArrayBuffer 或者 string
          https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
        - on_error?: 在 websocket 出错和非正常关闭 (close, error 事件) 时都调用，可以根据 error.type 来区分，error 的类型是 WebSocketConnectionError，
            type 为 'close' 时有 code 和 reason 属性
        - on_close?: 和 websocket 的 'close' 事件不相同，只在正常关闭 (close code 为 1000) 时才调用，否则都会调用 on_error
            https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes */
export async function connect_websocket(url, { print = true, protocols, max_payload = 2 ** 33, // 8 GB
proxy, on_message, on_error, on_close, }) {
    const { nodejs } = platform;
    let websocket = new (await platform.get_websocket())(url, protocols, 
    // @ts-ignore
    nodejs ?
        {
            maxPayload: max_payload,
            skipUTF8Validation: true,
            allowSynchronousEvents: true,
            ...proxy ? {
                agent: websocket_proxy_agents[proxy] ??= new (await platform.get_https_proxy_agent())(proxy)
            } : {}
        }
        :
            undefined);
    // https://stackoverflow.com/questions/11821096/what-is-the-difference-between-an-arraybuffer-and-a-blob/39951543
    websocket.binaryType = 'arraybuffer';
    return new Promise((resolve, reject) => {
        let settled = false;
        // websocket error, close 事件只选一个最详细的构造 WebSocketConnectionError 并调用 on_error
        let errored;
        websocket.addEventListener('open', event => {
            if (print)
                console.log(websocket.url +
                    (websocket.protocol ? ' ' + websocket.protocol.bracket() : '') +
                    t(' 已连接'));
            settled = true;
            resolve(websocket);
        }, { once: true });
        // error 事件会先于 close 事件
        // node.js 环境下 error 事件的错误信息比较多，浏览器环境下 close 的错误信息比较多
        // 在浏览器环境下延后处理 error 事件，放到微任务队列之后的 timers 队列中
        // https://blog.insiderattack.net/promises-next-ticks-and-immediates-nodejs-event-loop-part-3-9226cbe7a6aa
        function on_close_or_error(event) {
            websocket.removeEventListener('message', _on_message);
            const { type } = event;
            // 正常关闭
            if (type === 'close' && event.code === 1000) {
                if (on_close)
                    on_close(event, websocket);
                else if (print)
                    console.log(`${websocket.url} 已正常关闭`);
                return;
            }
            // 观察一段时间，在 nodejs 中应该没有先 close 再 error 的情况
            if (errored === 'close' && platform.nodejs) {
                console.warn('在 nodejs 中应该没有先 close 再 error 的情况', event.error);
                errored = null;
            }
            // 已经调用过 on_error 了
            if (errored)
                return;
            errored = type;
            const error = new WebSocketConnectionError(websocket.url, protocols, event);
            if (!settled) {
                settled = true;
                reject(error);
                return;
            }
            if (on_error)
                on_error(error, websocket);
            else if (print)
                console.log(error);
        }
        // 检查实际情况下 error 事件会先于 close 事件，观察一段时间后可删去
        let closed = false;
        websocket.addEventListener('error', (event) => {
            if (closed)
                console.warn('close 事件先于 error 事件发生，奇怪！');
            if (nodejs)
                on_close_or_error(event);
            else
                setTimeout(() => { on_close_or_error(event); });
        }, { once: true });
        websocket.addEventListener('close', event => {
            closed = true;
            on_close_or_error(event);
        }, { once: true });
        function _on_message(event) {
            on_message(event.data, websocket);
        }
        websocket.addEventListener('message', _on_message);
    });
}
/** 通过创建 remote 对象对 websocket rpc 进行抽象
    使用 remote.call() 进行一元 rpc
    使用 remote.send() 结合 message.id 进行复杂 rpc
    
    可传入 funcs 注册函数，使得对端能调用，传入后，
    连接发起方在检测到连接断开后间隔 2s (首次) / 10s 尝试重连
    
    可启用 probe 选项启用主动探测连接，进一步确保连接可靠性
    
    有两种创建方法:
    - 传入 url 主动创建 websocket 连接
        创建后等到首个 remote.call 或 remote.send 时建立实际 websocket 连接
    - 传入 websocket，监听事件，将已有的 websocket 连接转为 rpc 通道
    
    rpc 状态与底层连接的状态无关，如果是发起方，remote.send 时检测到断线会自动尝试建立新的 websocket 连接 */
export class Remote {
    /** 对端名称，方便接收端提供更明确的报错信息 */
    name;
    /** 作为 websocket 连接发起方，对端的 url 地址 */
    url;
    /** 能被对端调用的函数 */
    funcs;
    /** `false` 是否启用主动探测连接，在连接状态下以 30s 间隔发送心跳包确保连接状态 */
    probe = false;
    /** websocket 连接建立成功时调用对端 register 函数的参数 (仅发起方有效) */
    args;
    /** `true` 是否打印连接信息、错误信息 */
    print = true;
    /** `false` 打印所有交互的 rpc messages */
    verbose = false;
    // --- states
    /** 防止作为 websocket 连接发起方时并发创建 websocket 连接 */
    lwebsocket = new Lock();
    /** map<id, message handler>: 通过 (rpc message).id 找到对应的 handler
        一元 rpc 接收方不需要设置 handlers, 发送方需要 */
    handlers = new Map();
    /** 是否正在定时主动检测重连 */
    probing = false;
    /** websocket 检测到错误，_on_error 被调用，此时会尝试一段时间后重连 */
    reconnecting;
    /** 用户是否手动调用了 disconnect() 主动断开了连接 (仅发起方有效) */
    disconnected = false;
    /** on_error 接收到的，正常 websocket 连接发生的首个非 TimeoutError 错误 */
    error;
    /** 作为 websocket 连接发起方，传入 url
        作为 websocket 连接接收方，传入 websocket + name */
    constructor({ name, url, websocket, funcs, print, verbose, probe, args, on_error, on_reconnect } = {}) {
        if (name)
            this.name = name;
        if (url) {
            check(!websocket, '构建 Remote 时 url 和 websocket 只能传其中一个');
            this.url = url;
            if (on_reconnect)
                this.on_reconnect = on_reconnect;
        }
        else { // 连接接收方，一定是 nodejs 环境
            check(websocket, '构建 Remote 时需传入 url 或者 websocket');
            this.lwebsocket.resource = websocket;
            websocket.binaryType = 'arraybuffer';
            // 参考 connect_websocket 实现
            let errored = false;
            const on_close_or_error = (event) => {
                websocket.removeEventListener('message', _on_message);
                // 正常关闭
                if (event.type === 'close' && event.code === 1000) {
                    if (print)
                        console.log(`${this.name || websocket.url || '传入连接'} 已正常关闭`);
                    return;
                }
                // 已经调用过 on_error 了
                if (errored)
                    return;
                errored = true;
                this._on_error(new WebSocketConnectionError(this.name || websocket.url, websocket.protocol ? [websocket.protocol] : undefined, event));
            };
            // 检查实际情况下 error 事件会先于 close 事件，观察一段时间后可删去
            let closed = false;
            websocket.addEventListener('error', (event) => {
                if (closed)
                    console.warn('close 事件先于 error 事件发生，奇怪！');
                on_close_or_error(event);
            }, { once: true });
            websocket.addEventListener('close', event => {
                closed = true;
                on_close_or_error(event);
            }, { once: true });
            const _on_message = (event) => {
                this._on_message(event.data);
            };
            websocket.addEventListener('message', _on_message);
        }
        if (funcs) {
            check(!funcs.echo);
            this.funcs = funcs;
        }
        if (probe !== undefined)
            this.probe = probe;
        if (args)
            this.args = args;
        if (print !== undefined)
            this.print = print;
        if (verbose !== undefined)
            this.verbose = verbose;
        if (on_error)
            this.on_error = on_error;
    }
    /** 统一处理首次连接和连接后的 websocket 错误 */
    _on_error = (error) => {
        if (this.disconnected)
            return;
        // 去重
        if (error === this.error)
            return;
        // 重连
        if (error instanceof WebSocketConnectionError &&
            !this.reconnecting &&
            this.url &&
            (this.funcs || this.probe)) {
            this.reconnecting = setTimeout(this.reconnect, this.error ? 20_000 : 2_000);
            this.error = error;
        }
        this.on_error(error, this);
    };
    on_reconnect;
    reconnect = async () => {
        this.reconnecting = null;
        if (this.on_reconnect)
            await this.on_reconnect();
        this.try_connect();
    };
    _on_message = (data) => {
        this.handle(new Uint8Array(data));
    };
    /** 使用者自定义的在 websocket 连接出错时，或者 handlers 出错时的处理
        用户设置后会覆盖默认的 print 错误功能 */
    on_error(error, remote) {
        // 使用者未定义 Remote 如何处理 error 时，一般来说简单打印即可，因为 handlers 中报错了也会返回给对端
        if (this.print)
            this.print_error(error);
        // 这里继续往上层抛没有太大意义，上面一般都是 websocket on_message 这些
    }
    print_error(error) {
        console.warn(error instanceof WebSocketConnectionError ?
            error.message
            :
                error);
    }
    /** 检查是否已经有 error, websocket 状态是否正常 (open | connecting)
        返回 true: 连接正常，false: 连接异常 */
    check_connection() {
        if (this.error)
            return false;
        const { readyState } = this.lwebsocket.resource || {};
        return readyState === WebSocketOpen || readyState === WebSocketConnecting;
    }
    /** 幂等，保证 websocket 已连接，否则抛出异常
        通常用于手动建立 websocket 连接并确保成功
        连接断开后，作为发起方会自动创建新的 websocket 连接;
        作为接收方只会检查连接状态，在断开时抛出异常
        保存的 rpc 状态在 this.handlers, 与 websocket 无关，因此即使断开重连也不影响 rpc 的运行，即
        底层连接断开后自动重连对上层应该是无感知的，除非再次连接时失败 */
    async connect() {
        if (this.disconnected)
            throw new Error(`remote (${this.name || this.url}) 已调用过 disconnect 断开连接，无法再次 connect`);
        // 首次 connect 开始探测
        if (this.probe && !this.probing)
            this.probe_connection();
        if (this.check_connection())
            return;
        try {
            if (!this.url) {
                if (this.error)
                    throw this.error;
                const { resource: websocket } = this.lwebsocket;
                throw new WebSocketConnectionError(this.name || websocket.url, websocket.protocol ? [websocket.protocol] : undefined, undefined, `连接状态异常: ${websocket_states[this.lwebsocket.resource.readyState]}`);
            }
            // 假设有多个请求想要并发连接 websocket, 且此时 websocket 是断开的状态
            // 应该排队依次连接，而不是后续的连接直接使用第一次连接的 promise，后续调用还是应该尝试重连（不止连接一次）
            await this.lwebsocket.request(this._connect);
        }
        catch (error) {
            this._on_error(error);
            throw error;
        }
    }
    _connect = async () => {
        if (this.check_connection())
            return;
        // 重连
        this.lwebsocket.resource = await connect_websocket(this.url, {
            on_message: this._on_message,
            on_error: this._on_error,
            print: this.print
        });
        this.error = null;
        if (this.args)
            await this.call('register', this.args);
    };
    /** 尝试建立连接，通常用于开始保活
        重连错误会在 this.connect 里面调用 this._on_error 处理 */
    async try_connect() {
        try {
            await this.connect();
        }
        catch { }
    }
    /** 开始心跳保活 */
    async probe_connection(print_timeout = true) {
        // 手动调用该函数时也会启用 probe
        this.probe = true;
        this.probing = true;
        for (let timeouted = false;;) {
            await delay(timeouted ? 5_000 : 30_000);
            if (this.disconnected || !this.url && !this.check_connection() || !this.probe)
                break;
            if (this.reconnecting)
                continue;
            // 产生的非 TimeoutError 应该都通过 _on_error 输出了，同时可能触发了重连
            timeouted = (await this.test(print_timeout)) instanceof TimeoutError;
        }
    }
    /** 检测连接，返回连接错误，出错时触发 on_error 打印错误信息
        返回值:
        - 连接正常返回: undefined
        - 连接错误返回: 实际的连接错误，通常是 WebSocketConnectionError | TimeoutError */
    async test(print_timeout = true) {
        try {
            await timeout(5_000, this.call('echo'), undefined, false);
        }
        catch (error) {
            if (error instanceof TimeoutError) {
                let message;
                const websocket = this.lwebsocket.resource;
                if (websocket?.bufferedAmount)
                    message = `连接正忙，待发 ${websocket.bufferedAmount.to_fsize_str()}`;
                else
                    message = '连接正在收数据或无响应';
                set_error_message(error, message = `${new Date().to_time_str()}  ${this.name || this.url} ${message}`);
                if (this.print && print_timeout)
                    console.warn(message);
            }
            return error;
        }
    }
    /** 手动关闭到对端的 websocket 连接 */
    disconnect() {
        this.disconnected = true;
        this.lwebsocket.resource?.close(1000);
    }
    /** 发送 message 到对端 remote
        发送或连接出错时自动清理 message.id 对应的 handler */
    async send(message) {
        if (this.verbose)
            console.log('remote.send:', message);
        try {
            await this.connect();
            message[message_symbol] = true;
            // 不需要独占 websocket
            this.lwebsocket.resource.send(pack(message));
        }
        catch (error) {
            if (message.id)
                this.handlers.delete(message.id);
            throw error;
        }
    }
    /** 能发就发一下，忽略发送失败, best effort */
    try_send(message) {
        message[message_symbol] = true;
        // websocket.send 本身通常不会 throw error
        this.lwebsocket.resource?.send(pack(message));
    }
    /** 处理接收到的 websocket message 并解析, 根据 message.id 或 message.func 分发到对应的 handler 进行处理，
        handler 处理完成后:
        - 传了 func: 调用函数的情况下 (通常是一元 rpc)，总是将返回值包装为 message 回传
        - 未传 func: 通过 id 调用，如果 handler 返回非 undefined 的值，也包装为 message 回传
        
        如果 message.done == true 则对端指示当前 remote 可以清理 handler
        使用 Uint8Array 作为参数更灵活 https://stackoverflow.com/a/74505197/7609214
        这个方法一般不会抛出错误，也不需要 await，一般在 websocket on_message 时使用 */
    async handle(data) {
        let message;
        try {
            check(data[0] === 0xcc, 'message 格式错误');
            message = parse(data);
        }
        catch (error) {
            this.on_error(error, this);
            return;
        }
        const { id, func, done } = message;
        if (this.verbose)
            console.log('remote.handle:', message);
        try {
            if (func === 'echo') {
                await this.send({ id, data: message.data });
                return;
            }
            let handler;
            if (func) {
                handler = this.funcs[func];
                // 传了 func 调用函数的情况下，如果 message.data 为 undefined, 默认为 [ ]
                if (message.data === undefined)
                    message.data = [];
            }
            else {
                handler = this.handlers.get(id);
                if (done && handler)
                    this.handlers.delete(id);
            }
            if (handler) {
                const data = await handler(message, this);
                if (func || data !== undefined)
                    await this.send({ id, data });
            }
            else if (message.error)
                throw message.error;
            else if (func)
                throw new Error(`找不到 func: ${func.quote()}`);
            else
                console.log(`忽略找不到的 handler id: ${id}`);
        }
        catch (error) {
            // handler 出错并不意味着 rpc 一定会结束，可能 error 是运行中的正常数据，所以不能清理 handler
            // 这里继续往上层抛没有太大意义，上面一般都是 websocket on_message 这些，交给自定义或默认的 on_error 处理
            this._on_error(error);
            if (this.check_connection() &&
                !message.error // 防止无限循环往对方发送 error, 只有在对方无错误时才可以发送
            )
                try {
                    await this.send({ id, error /* 不能设置 done 清理对面 handler, 理由同上 */ });
                }
                catch { }
        }
    }
    /** 调用对端 remote 中的 func, 只适用于最简单的一元 rpc (请求, 响应) */
    async call(func, args) {
        let promise = defer();
        const id = genid();
        this.handlers.set(id, ({ error, data }) => {
            if (error)
                promise.reject(error);
            else
                promise.resolve(data);
            this.handlers.delete(id);
        });
        await this.send({ id, func, data: args }); // 不需要 done: true, 因为对面的 remote.handlers 中不会有这个 id 的 handler
        return promise;
    }
    /** 调用对端 remote 中的 func, 开始订阅并接收连续的消息 (订阅流)
        - func: 订阅处理函数
        - on_data: 接收开始订阅后的数据
        - options?:
            - on_error?: 处理开始订阅后的错误 */
    async subscribe(func, on_data, on_error = rethrow) {
        const id = genid();
        let psubscribed = new Promise((resolve, reject) => {
            let first = true;
            this.handlers.set(id, ({ error, data }) => {
                if (error) {
                    if (first) {
                        first = false;
                        this.handlers.delete(id);
                        reject(error);
                    }
                    else
                        on_error(error, this);
                    return;
                }
                if (first) {
                    first = false;
                    resolve({ id, data: data });
                    return;
                }
                on_data(data);
            });
        });
        try {
            await this.send({ id, func });
        }
        catch (error) {
            this.handlers.delete(id);
            throw error;
        }
        return psubscribed;
    }
}
//# sourceMappingURL=net.common.js.map