import { v4 } from 'uuid'
import { Message, Options, ReadyState, RequestCode, MessageType, AsyncTask } from './types'

export default class {
    readonly host;
    /** 当前实例标识 */
    private uuid = '';
    /** WebSocket 对象 */
    private ws?: WebSocket;
    /** WebSocket 准备完成 */
    private wsReady?: Promise<WebSocket>;
    /** WebSocket 子协议 */
    private protocols?: string | string[];

    /** 信息发送异步建值对 */
    private asyncTaskMap = new Map<string, AsyncTask>();

    /** 超时定时器 */
    private timeoutTimer = 0;
    /** 消息回复超时时间 */
    private timeout = 1000 * 15;

    /** 心跳定时器 */
    private heartbeatTimer = 0;
    /** 心跳间隔时长 */
    private heartbeatInterval = 1000 * 30;

    /** 重连定时器 */
    private reconnectTimer = 0;
    /** 本次重连次数 */
    private reconnectCount = 0;
    /** 限制重连次数，0 = 无限制 */
    private reconnectLimit = 10;

    /** 是否启用心跳检测 */
    heartbeat;
    /** WebSocket 连接状态 */
    readyState = ReadyState.Closed;

    /** 连接成功的回调 */
    onOpen?: () => void;
    /** 连接断开的回调 */
    onClose?: () => void;
    /** 连接发生错误的回调 */
    onError?: (err: Event) => void;
    /** 接收推送消息的回调 */
    onPush?: (message: { requestCode: RequestCode; data: unknown; }) => void;
    /** 在重连之前回调 */
    onBeforeReconnect?: (count: number) => void;
    /** 在重连成功之后回调 */
    onReconnect?: () => void;

    constructor(host: string, options?: Options) {
        this.host = host
        if (options) {
            this.heartbeat = options.heartbeat
            this.protocols = options.protocols
        }
        if (!options?.manual) {
            this.connect()
        }
    }

    /**
     * 连接服务器
     */
    private connect() {
        if (!this.wsReady || this.readyState === ReadyState.Closed) {
            clearTimeout(this.reconnectTimer)
            this.stopHeartbeat()
            this.readyState = ReadyState.Connecting
            console.log(this.host, '正在连接')

            this.wsReady = new Promise((resolve, reject) => {
                try {
                    const ws = new WebSocket(this.host, this.protocols)
                    const uuid = v4()
                    this.uuid = uuid

                    // 连接成功
                    ws.onopen = () => {
                        console.log(this.host, '连接成功')
                        this.readyState = ReadyState.Open
                        this.startHeartbeat()
                        this.onOpen && this.onOpen()
                        resolve(ws)
                    }
                    // 连接断开(CLOSING有可能延迟)
                    ws.onclose = () => {
                        // 判断是否当前实例
                        // 如果连接断开后不等待 onclose 响应，由于 onclose 延迟的原因可能会在创建新的 ws 实例后触发，导致刚创建的实例被断开进入重连机制
                        if (this.uuid === uuid) {
                            console.warn(this.host, '连接已断开')
                            this.reset()
                            if (this.reconnectLimit === 0 || this.reconnectCount < this.reconnectLimit) {
                                this.reconnect() // 重连失败会不断尝试，直到成功为止
                            } else {
                                this.reconnectCount = 0
                            }
                        }
                    }
                    // 连接发生错误
                    ws.onerror = (e) => {
                        console.error('连接发生错误', e)
                        this.onError && this.onError(e)
                        reject(e)
                    }
                    // 接收消息
                    ws.onmessage = (e) => {
                        console.log('收到消息', e.data)
                        this.response(e.data)
                    }

                    this.ws = ws
                } catch (e) {
                    reject(e)
                }
            })
        }
        return this.wsReady
    }

    /**
     * 重置连接状态
     */
    private reset() {
        this.ws = undefined
        this.wsReady = undefined
        this.readyState = ReadyState.Closed
        this.asyncTaskMap.clear()
        this.onClose && this.onClose()
    }

    /**
     * 主动断开连接，断开后不会自动重连
     * @param forced 是否强制断开
     */
    close(forced = false) {
        return new Promise<void>((resolve) => {
            clearTimeout(this.reconnectTimer)
            this.stopHeartbeat()
            this.uuid = v4() // 改变实例标识，取消重连状态
            this.reconnectCount = 0

            if (!forced && this.ws) {
                const listener = () => {
                    console.warn(this.host, '主动断开')
                    this.ws?.removeEventListener('close', listener)
                    this.reset()
                    resolve()
                }
                this.ws.addEventListener('close', listener)
                this.ws.close()
            } else {
                if (this.ws) {
                    console.warn(this.host, '主动断开')
                    this.ws.close()
                }
                this.reset()
                resolve()
            }
        })
    }

    /**
     * 发送消息
     * @param message 
     */
    async send(params: Message) {
        const ws = await this.connect()
        return new Promise((resolve, reject) => {
            const { requestId } = params.headers

            // 消息超时回调
            const timeoutId = window.setTimeout(() => {
                reject('业务超时')
                this.asyncTaskMap.delete(requestId)
            }, this.timeout)

            this.asyncTaskMap.set(requestId, {
                timeoutId,
                callback: (data) => resolve(data),
            })

            ws.send(JSON.stringify(params))
        })
    }

    /**
     * 响应消息
     * @param data 
     */
    private response(message: string) {
        const { headers, data }: Message = JSON.parse(message)
        const asyncTask = this.asyncTaskMap.get(headers.requestId)

        switch (headers.messageType) {
            case MessageType.Heartbeat:
                this.stopHeartbeat()
                this.startHeartbeat()
                break
            case MessageType.Request:
                asyncTask?.callback && asyncTask.callback(data)
                break
            case MessageType.Push:
                this.onPush && this.onPush({
                    requestCode: headers.requestCode,
                    data
                })
                break
        }

        if (asyncTask) {
            clearTimeout(asyncTask.timeoutId)
            this.asyncTaskMap.delete(headers.requestId)
        }
    }

    /** 
     * 发送心跳检测
     */
    private startHeartbeat() {
        if (this.heartbeat) {
            this.heartbeatTimer = window.setTimeout(() => {
                this.send({
                    headers: {
                        requestId: v4(),
                        requestCode: RequestCode.Heartbeat,
                        messageType: MessageType.Heartbeat,
                        timestamp: new Date().getTime()
                    },
                    data: '心跳检测'
                })
                // 没有收到心跳回复，则认为网络已经异常，进行断网重连
                this.timeoutTimer = window.setTimeout(() => {
                    console.warn(this.host, '心跳超时')
                    this.reconnect()
                }, this.timeout)
            }, this.heartbeatInterval)
        }
    }

    /** 
     * 停止心跳检测
     */
    private stopHeartbeat() {
        clearTimeout(this.timeoutTimer)
        clearTimeout(this.heartbeatTimer)
    }

    /**
     * 断开重连，在重连尝试失败时会再次重试
     */
    private reconnect() {
        this.stopHeartbeat() // 重连之前停止心跳检测
        if (this.readyState !== ReadyState.Connecting) {
            this.reconnectCount++
            this.onBeforeReconnect && this.onBeforeReconnect(this.reconnectCount)

            // 自动计算每次重试的延时，重试次数越多，延时越大
            const delay = this.reconnectCount * 3000
            console.log(this.host, `${delay / 1000}秒后将进行第${this.reconnectCount}次重连`)

            this.reconnectTimer = window.setTimeout(() => {
                this.connect().then(() => {
                    console.log(this.host, '重连成功，可开始进行业务操作')
                    this.reconnectCount = 0
                    this.onReconnect && this.onReconnect()
                }).catch(() => {
                    // 重连失败会进入 ws.onclose 再次发起重连
                    if (this.reconnectCount) {
                        console.warn(this.host, `第${this.reconnectCount}次重连失败`)
                    }
                })
            }, delay)
        }
    }
}