import * as net from "net";
import { EventEmitter } from "events";
import { CmdData, Package } from './Dto';

export type Options = {
    serverIp: string;
    serverPort: number;
    reconnectDelay?: number;
};

export interface SocketClient {
    on(event: "connect", listener: () => void): this;
    on(event: "data", listener: (data: Buffer) => void): this;

    /**
     * 收到数据后，解码成功时触发
     * @param event 
     * @param listener 
     */
    on(event: "package", listener: (pack: Package) => void): this;

    /**
     * 收到数据后，解码成功且指令为n指令时触发
     * @param event 
     * @param listener 
     */
    on(event: "cmd", listener: (data: CmdData) => void): this;

    /**
     * 调用send方法后，在发送数据时(socket.write)触发 
     * @param event 
     * @param listener 
     */
    on(event: "send", listener: (data: Buffer, cmd: string) => void): this;
    on(event: "error", listener: (err: Error) => void): this;
    on(event: "close", listener: (hadError: boolean) => void): this;
}

/**
 * 接收数据转义
 * @param data 
 * @returns 
 */
export function receive_escape(data: number[]) {
    let arr: number[] = [];

    for (let i = 0; i < data.length; i++) {
        //转义 0xff 0xbb => 0xfb 0xff 0xfc => 0xff 0xff 0xee => 0xfe
        if (data[i] === 0xff) {
            if (data[i + 1] === 0xbb) {
                arr.push(0xfb);
                i++;
            } else if (data[i + 1] === 0xfc) {
                arr.push(0xff);
                i++;
            } else if (data[i + 1] === 0xee) {
                arr.push(0xfe);
                i++;
            }
        } else {
            arr.push(data[i]);
        }
    }

    return arr;
}

/**
 * 发送数据转义
 * @param data 
 * @returns 
 */
export function send_escape(data: Buffer) {
    let arr: number[] = [];

    for (let i = 0; i < data.length; i++) {
        //转义 0xfb => 0xff 0xbb  0xff => 0xff 0xfc  0xfe => 0xff 0xee
        if (data[i] === 0xfb) {
            arr.push(0xff, 0xbb);
        } else if (data[i] === 0xff) {
            arr.push(0xff, 0xfc);
        } else if (data[i] === 0xfe) {
            arr.push(0xff, 0xee);
        }
        else {
            arr.push(data[i]);
        }
    }

    return arr;
}

/**
 * 解码（反序列化）
 * @param datas 
 * @returns 
 */
export function decode(datas: Buffer[]): {
    packs: Package[],

    /**
     * 剩余未解码
     */
    left?: Buffer[]
} {
    let packs: Package[] = [];
    let _i = 0;
    let _j = 0;

    let unescape: number[] = null;
    //[][][][][][][][][][][][]
    for (let i = 0; i < datas.length; i++) {
        for (let j = 0; j < datas[i].length; j++) {
            if (datas[i][j] === 0xfb) {
                unescape = [0xfb];
            } else {
                if (unescape) {
                    unescape.push(datas[i][j]);

                    if (datas[i][j] === 0xfe) {
                        _i = i;
                        _j = j;

                        let escape = receive_escape(unescape);
                        let data_length = Buffer.from(escape.slice(10, 12)).readUInt16BE();
                        let checksum = Buffer.from(escape.slice(12 + data_length, 12 + data_length + 2)).readUInt16BE();
                        let new_checksum = 0;

                        for (let i = 1; i < escape.length - 3; i++) {
                            new_checksum += escape[i];
                            //防溢出
                            new_checksum = new_checksum & 0xFFFF;
                        }

                        if (new_checksum === checksum) {
                            /**
                             解码
                                0,1 协议头	1byte	0xfb
                                1,5 时间戳	4byte	唯一标识，重复发送的包，时间戳需一样。
                                5,6 命令码	1byte	规定发送命令标示（回复包同发送包命令码一样)
                                6,8 总包数	2byte	
                                8,10 包序号	2byte	从0开始（如仅仅1个包，则总包为1，包序号为0
                                10,12 数据长度	2byte	最大1024。
                                数据内容	N byte	依数据长度而定
                                校验码	2byte	从时间戳开始到数据内容最后一个字节在内的所有数据累加和
                                协议尾	1byte	0xfe
                            */
                            let pack: Package = {
                                ts: Buffer.from(escape.slice(1, 5)).readUInt32BE(),
                                cmd: <any>String.fromCharCode(...escape.slice(5, 6)),
                                packageTotal: Buffer.from(escape.slice(6, 8)).readUInt16BE(),
                                packageNo: Buffer.from(escape.slice(8, 10)).readUInt16BE(),
                                dataLength: data_length,
                                data: Buffer.from(escape.slice(12, 12 + data_length))
                            };

                            packs.push(pack);
                        }

                        unescape = null;
                    }
                }
            }
        }
    }

    let left: Buffer[] = [];
    for (let i = _i; i < datas.length; i++) {
        if (i === _i) {
            if (_j === datas[i].length - 1) continue;
            left.push(datas[i].subarray(_j + 1));
        } else {
            left.push(datas[i]);
        }
    }

    return { packs, left: left.length > 0 ? left : null };
}

/**
 * 编码（序列化）
 * @param cmd 
 * @param data 
 * @returns 
 */
export function encode(cmd: string, data: Buffer) {
    let ts_buf = Buffer.alloc(4);
    ts_buf.writeInt32BE(Math.floor(Date.now() / 1000));

    let cmd_buf = Buffer.alloc(1);
    cmd_buf.writeUInt8(cmd.charCodeAt(0));

    let pack_total = Math.ceil(data.length / 1024) || 1;
    let pack_total_buf = Buffer.alloc(2);
    pack_total_buf.writeInt16BE(pack_total);

    let pack_no_buf = Buffer.alloc(2);
    let data_length_buf = Buffer.alloc(2);
    let checksum_buf = Buffer.alloc(2);

    let bufs: Buffer[] = [];

    for (let i = 0; i < pack_total; i++) {
        let data_buf = data.subarray(i * 1024, (i + 1) * 1024);

        pack_no_buf.writeInt16BE(i);
        data_length_buf.writeUInt16BE(data_buf.length);

        let unescape = Buffer.concat([
            ts_buf,
            cmd_buf,
            pack_total_buf,
            pack_no_buf,
            data_length_buf,
            data_buf,
            checksum_buf
        ]);

        let checksum = 0;
        for (let i = 0; i < unescape.length - 2; i++) {
            checksum += unescape[i];
        }

        unescape.writeUInt16BE(checksum, unescape.length - 2);

        let escape = send_escape(unescape);
        let buf = Buffer.concat([Uint8Array.from([0xfb]), Buffer.from(escape), Uint8Array.from([0xfe])]);
        bufs.push(buf);
    }

    return bufs;
}

/**
 * 视频服务程序SOCKET客户端
 */
export class SocketClient {
    private socket: net.Socket = null;
    readonly opts: Options = null;
    private emitter: EventEmitter;
    private decode_cache: Buffer[] = null;
    private disposed = null;
    private heartbeatTimer?: NodeJS.Timeout;

    constructor(opts: Options) {
        if (!net.isIP(opts.serverIp)) throw new Error('Invalid ip.');
        if (opts.serverPort) {
            if (!Number.isInteger(opts.serverPort) ||
                opts.serverPort < 0 ||
                opts.serverPort > 65535) throw new Error('Invalid port.');
        }

        this.opts = Object.assign({}, opts);
        this.emitter = new EventEmitter();
        this._connect();
    }

    getSocketInstance() {
        return this.socket;
    }

    on(event: string, listener: (...args: any[]) => void): this {
        this.emitter.on(event, listener);
        return this;
    }

    /**
     * @param cmd 'A' | 'C' | 'D' | 'F' | 'n' | 'Z' | 'P' | 'V' 等
     * @param data 
     */
    send(cmd: string, data: Buffer | object) {
        this._send(cmd, Buffer.isBuffer(data) ? data : Buffer.from(JSON.stringify(data)));
    }

    destroy() {
        this._dispose();
    }

    private _connect() {
        let socket = net.connect({
            host: this.opts.serverIp,
            port: this.opts.serverPort,
        });

        socket.on('connect', () => {
            this.emitter.emit('connect');
            this._heartbeat();
        });

        socket.on('data', data => {
            this.emitter.emit('data', data);
            this._decode(data);
        });

        socket.on('error', err => {
            this.emitter.emit('error', err);
        });

        socket.on('close', async hadError => {
            this.emitter.emit('close', hadError);
            clearInterval(this.heartbeatTimer);
            this.decode_cache = null;
            this._reconnect();
        });

        this.socket = socket;
    }

    private async _reconnect() {
        if (!this.disposed) {
            await new Promise(resolve => {
                setTimeout(resolve, this.opts.reconnectDelay || 1000 * 10);
            });

            this._connect();
        }
    }

    private _heartbeat() {
        if (!this.disposed) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = setInterval(() => {
                this.send('F', Buffer.alloc(0));
            }, 1000 * 5);
        }
    }

    private _decode(data: Buffer) {
        if (!data || data.length === 0) return;

        try {
            if (data[0] === 0xfb) {
                this.decode_cache = [data];
            } else {
                if (this.decode_cache) {
                    if (this.decode_cache.length > 20) {
                        this.decode_cache.shift();
                    }

                    this.decode_cache.push(data);
                } else {
                    this.decode_cache = [data];
                }
            }

            if (!data.includes(0xfe)) return;

            let dec = decode(this.decode_cache);
            dec.packs.forEach(pack => {
                this.emitter.emit('package', pack);

                if (pack.cmd === 'n') {
                    try {
                        let data = JSON.parse(Buffer.from(pack.data).toString()) as CmdData;
                        if (data.cmd) {
                            this.emitter.emit('cmd', data);
                        }
                    } catch (error) { }
                }
            });

            this.decode_cache = dec.left;
        } catch (error) {
            this.emitter.emit('error', error);
        }
    }

    private _send(cmd: string, data: Buffer) {
        if (this.socket.destroyed) return;

        let bufs = encode(cmd, data);
        for (const buf of bufs) {
            this.socket.write(buf);
            this.emitter.emit('send', buf, cmd);
        }
    }

    private _dispose() {
        this.disposed = true;
        clearInterval(this.heartbeatTimer);
        this.socket.destroy();
        this.decode_cache = null;
    }
}