export module JCMGO {
    /**
     * @name Socket客户端
     * @description 包含了前后端所有的交互接口。(建议继承该类来使用)
     */
    export class SocketClient {
        private _url: string = null;
        private _ws: WebSocket = null;
        
        /**
         * @name 连接服务端
         * @param {string} url 服务端ws地址
         */
        public connect(url: string) {
            if (!url) throw "url can not be null";
            this._url = url;
            this._ws = new WebSocket(url);
            this._ws.onclose = this.handleOnClose.bind(this);
            this._ws.onmessage = (e) => this.invoke(JSON.parse(e.data));
        }
        
        /**
         * @name 重新连接
         * @deprecated 可在onDestroy\onMiss时调用
         */
        public reconnect() {
            if (this._url != null && this._ws == null) {
                this.connect(this._url);
            }
        }
        
        /**
         * @name 关闭连接
         */
        public close()
        {
            if (this._ws != null) {
                this._ws.close();
                this._ws = null;
            }
        }
        
        private _id: number = null;
        /**ID,由服务端发放 */
        public get id() {
            return this._id;
        }
        private _isValid = false;
        /** 是否有效（可通信） */
        public get isValid() {
            return this._isValid;
        }
        private _loaded = false;
        /** 是否已经载入过 */
        public get loaded() {
            return this._loaded;
        }
        
        /**监听-载入 */
        protected onLoad() {}
        /**监听-重载 */
        protected onReload() {}
        /**监听-销毁（连上后断开） */
        protected onDestroy() {}
        /**监听-迷失（请求连接但连接失败）*/
        protected onMiss() {}
        
        /**调用远程函数 */
        private call(func: string, ...args: any[]) {
            if (args == undefined) args = [];
            let data: SocketDataPack = {func: func, args: args};
            this._ws.send(JSON.stringify(data));
        }
        
        /**调用本地函数 */
        private invoke(dataPack: SocketDataPack) {
            let theFunc = this[dataPack.func] as Function;
            if (theFunc instanceof Function) theFunc.apply(this, dataPack.args);
            else console.error(`invoke fail: can not find func [${dataPack.func}]`);
        }

        /**初始化函数-被服务端远程调用 */
        private initByServer(id: number) {
            this._id = id;
            this._isValid = true;
            try {
                if (this._loaded) {
                    this.onReload();
                } else {
                    this.onLoad();
                }
            } catch (e) { console.error(e); }
            this._loaded = true;
        }
        
        /**监听处理-WebSocket关闭 */
        private handleOnClose() {
            this._ws = null;
            try {
                if (this._isValid) {
                    this._isValid = false;
                    this.onDestroy();            
                } else {
                    this.onMiss();
                }
            } catch (e) { console.error(e); }
        }

        //========================API========================
        /**单人随机匹配 */
        public singlePersonRandomMatching(param: MatchParam) {
            this.call("SinglePersonRandomMatching", JSON.stringify(param));
        }

        /**开始帧同步 */
        public startFrameSync() {
            this.call("StartFrameSync");
        }

        private _cachedInputs: object[] = [];
        /**缓存帧数据输入 */
        public cacheInputs(...inputs: object[]) {
            this._cachedInputs.push(...inputs);
        }
        /**清除缓存的帧数据输入 */
        public clearInputs() {
            this._cachedInputs = [];
        }
        /**判断有没有缓存的帧数据输入 */
        public hasInputs() {
            return this._cachedInputs.length > 0;
        }
        /**上传帧数据输入*/
        public uploadInputs(sign: string = "") {
            this.call("UploadInputs", this._cachedInputs, sign);
        }
        /**模拟服务器帧（可用于本地预测） */
        public moniServerFrame(param: MoniServerFrameParam): Frame {
            let frame: Frame = {
                id: -1,
                inputs: this._cachedInputs,
                dt: param.dt,
                sign: param.sign
            }
            return frame;
        }

        //========================Broadcast========================
        /**当房间加入玩家 */
        protected onRoomAddPlayer(e: BroadcastEvent<RoomAddPlayerBst>) {}
        /**当房间移除玩家 */
        protected onRoomRemovePlayer(e: BroadcastEvent<RoomRemovePlayerBst>) {}
        /**当房间结束匹配 */
        protected onRoomEndMatching(e: BroadcastEvent<RoomEndMatchingBst>) {}
        /**当帧同步开始 */
        protected onFrameSyncStart(e: BroadcastEvent<null>) {}
        /**当接收帧同步数据 */
        protected onRecvFrame(e: BroadcastEvent<Frame>) {}
    }
    
    //========================传输协议========================
    /**匹配参数 */
    export interface MatchParam {
        /**玩家基本信息 */
        playerInfo: object;
        /**匹配房间配置 */
        matchRoomConfig: MatchRoomConfig
    }
    /**匹配房间配置 */
    export interface MatchRoomConfig {
        /**同一类型的房间才会匹配到一起 */
        type: string;
        /**最多容纳多少人 */
        maxMembers: number;
        /**服务端下发同步帧的频率 */
        syncFrameRate: number;
        /**匹配多久结束（毫秒） */
        maxMatchingTime: number;
    }
    /**同步帧 */
    export interface Frame {
        /**帧序列号 */
        id: number;
        /**帧操作（房间内所有玩家上传的操作会合并在内） */
        inputs: object[];
        /**时间间隔（毫秒） */
        dt: number;
        /**服务端返回的最新记号（需要在上传帧数据输入时，填入参数sign） */
        sign: string;
    }
    /**房间信息 */
    export interface RoomInfo {
        id: number;
        playerInfos: Map<number, object>;
    }
    /**RPC协议 */
    interface SocketDataPack {
        func: string;
        args: any[];
    }

    //========================广播事件========================
    /**房间结束匹配 */
    export interface RoomEndMatchingBst {
        roomInfo: RoomInfo;
        timestamp: number;
    }
    /**房间加入玩家 */
    export interface RoomAddPlayerBst {
        /**最新的房间信息 */
        roomInfo: RoomInfo;
        playerIndex: number;
        isSelf: boolean;
    }
    /**房间移除玩家 */
    export interface RoomRemovePlayerBst {
        /**最新的房间信息（目标已离开房间，所以里面没目标的信息） */
        roomInfo: RoomInfo;
        playerIndex: number;
        isSelf: boolean;
    }
    /**广播事件 */
    export interface BroadcastEvent<T> {
        /**序列号 */
        seq: number;
        /**数据 */
        data: T;
    }

    //========================参数定义========================
    export interface MoniServerFrameParam {
        sign: string;
        dt: number;
    }

    //========================Object========================
    /**对象处理工具 */
    export class ObjectUtils {
        /**
         * 将sources合并到target，该合并全部是深拷贝
         * @param target
         * @param sources
         * @returns {object}
         */
        public static merge(target: object, ...sources: object[]): object {
            for (let i = 0; i < sources.length; ++i) {
                let source = sources[i];
                if (typeof source != 'object' || source == null) {
                    continue;
                }
                for (let skey in source) {
                    //只处理自身的key 这里可能来自于外部prototype的扩展
                    if (!source.hasOwnProperty(skey)) {
                        continue;
                    }
                    if (source[skey] instanceof Date) {
                        //Date类型 要克隆一份 保证深拷贝
                        target[skey] = new Date(source[skey]);
                        continue;
                    }
                    else if (typeof (target[skey]) == 'object' && target[skey] != null && typeof (source[skey]) == 'object' && source[skey] != null) {
                        // 两个都是Object 递归merge之
                        this.merge(target[skey], source[skey]);
                    }
                    else {
                        if (Array.isArray(source[skey])) {
                            // 数组merge后还是数组
                            target[skey] = this.merge([], source[skey]);
                        }
                        else if (typeof (source[skey]) == 'object' && source[skey] !== null) {
                            // Object要克隆一份以确保深拷贝
                            target[skey] = this.merge({}, source[skey]);
                        }
                        else {
                            // 基本类型 直接赋值即可
                            target[skey] = source[skey];
                        }
                    }
                }
            }
            return target;
        }
        public static values(obj: object): any[] {
            let output = [];
            for (let k in obj) {
                obj.hasOwnProperty(k) && output.push(obj[k]);
            }
            return output;
        }
        public static entries(obj: object): any[][] {
            let output = [];
            for (let key in obj) {
                if (!obj.hasOwnProperty(key)) {
                    continue;
                }
                output.push([key, obj[key]]);
            }
            return output;
        }
        public static forEach(obj: object, handler: (value: any, key: string, obj: object) => void): void {
            for (let key in obj) {
                if (!obj.hasOwnProperty(key)) {
                    return;
                }
                handler(obj[key], key, obj);
            }
        }
    }

    //========================Math========================
    /**
     * 多平台一致精确计算
     * [Math.round、Math.min、Math.max，Math.floor、Math.ceil，这些系统方法一般情况下是可以放心使用的]
     */
    export class ExactMath {
        // 计算精度
        // sin、cos、tan方法的误差小数点后16位
        public static readonly ACCURACY_SIN_ERROR = 1e-16;
        public static readonly ACCURACY_COS_ERROR = 1e-16;
        public static readonly ACCURACY_TAN_ERROR = 1e-16;

        // 角度弧度常量
        public static readonly DEG = 57.29577951308232;
        public static readonly RAD = 0.017453292519943295;

        public static readonly PI = 3.141592653589793;
        public static readonly E = 2.718281828459045;
        public static readonly LN2 = 0.6931471805599453;
        public static readonly LN10 = 2.302585092994046;
        public static readonly LOG2E = 1.4426950408889634;
        public static readonly LOG10E = 0.4342944819032518;
        public static readonly SQRT1_2 = 0.7071067811865476;
        public static readonly SQRT2 = 1.4142135623730951;

        /****************************************************基础****************************************************/
        /**
         * 获得小数位数
         * @param {Number} num 浮点数
         * @returns {Number}
         */
        public static getDecimalPlace(num: number) {
            if (num && num !== Math.floor(num)) {
                for (let n = 1, m = 10, temp = 0; n < 20; n += 1, m *= 10) {
                    temp = num * m;
                    if (temp == Math.floor(temp)) return n;
                }
                return 20;
            } else {
                return 0;
            }
        }
        /**
         * 保留n为小数，并四舍五入
         * @param {Number} num 浮点数
         * @param {Number} n 整数
         * @returns {Number}
         */
        public static toFixed(num: number, n: number = 0) {
            if (n == 0) {
                return Math.round(num);
            } else {
                const m = Math.pow(10, n);
                return Math.round(num * (m * 10) / 10) / m;
            }
        }
        public static abs(x: number) {
            return Math.abs(x);
        }
        public static round(x: number) {
            return Math.round(x);
        }
        public static ceil(x: number) {
            return Math.ceil(x)
        }
        public static floor(x: number) {
            return Math.floor(x)
        }
        public static min(...args: number[]) {
            return Math.min(...args);
        }
        public static max(...args: number[]) {
            return Math.max(...args);
        }

        /**
         * 小数相加
         * @param {Number} num1 浮点数
         * @param {Number} num2 浮点数
         * @returns {Number}
         */
        public static add(...args: number[]) {
            if (args.length === 2) {
                const num1 = args[0];
                const num2 = args[1];
                const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));
                return (this.toFixed(num1 * m) + this.toFixed(num2 * m)) / m;
            } else {
                return args.reduce((a, b) => this.add(a, b))
            }
        };
        /**
         * 小数相减
         * @param {Number} num1 浮点数
         * @param {Number} num2 浮点数
         * @returns {Number}
         */
        public static sub(...args: number[]) {
            if (args.length === 2) {
                const num1 = args[0];
                const num2 = args[1];
                const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));

                return (this.toFixed(num1 * m) - this.toFixed(num2 * m)) / m;
            } else {
                return args.reduce((a, b) => this.sub(a, b))
            }
        };
        /**
         * 小数相乘
         * @param {Number} num1 浮点数
         * @param {Number} num2 浮点数
         * @returns {Number}
         */
        public static mul(...args: number[]) {
            if (args.length === 2) {
                let num1 = args[0];
                let num2 = args[1];

                // 方案1：
                // 直接相乘，但是相乘两数小数点过多会导致中间值[(n1 * m1) * (n2 * m2)]过大
                // const n1 = this.getDecimalPlace(num1);
                // const n2 = this.getDecimalPlace(num2);
                // const m1 = Math.pow(10, n1);
                // const m2 = Math.pow(10, n2);
                // return (n1 * m1) * (n2 * m2) / (m1 * m2);

                // 方案2：
                // 用除法实现乘法，不会存在过大中间值
                let n1 = this.getDecimalPlace(num1);
                let n2 = this.getDecimalPlace(num2);

                let m = Math.pow(10, n2);
                num2 = m / this.toFixed(num2 * m);

                m = Math.pow(10, Math.max(n1, this.getDecimalPlace(num2)));
                m = this.toFixed(num1 * m) / this.toFixed(num2 * m);

                let n = Math.min(this.getDecimalPlace(m), n1 + n2);
                return this.toFixed(m, n);
            } else {
                return args.reduce((a, b) => this.mul(a, b))
            }
        };
        /**
         * 小数相除法
         * @param {Number} num1 浮点数
         * @param {Number} num2 浮点数
         * @returns {Number}
         */
        public static div(...args: number[]) {
            if (args.length === 2) {
                const num1 = args[0];
                const num2 = args[1];

                const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));
                return this.toFixed(num1 * m) / this.toFixed(num2 * m);
            } else {
                return args.reduce((a, b) => this.div(a, b))
            }
        };
        /**
         * 取余
         * @param {Number} num1 浮点数
         * @param {Number} num2 浮点数
         * @returns {Number}
         */
        public static rem(...args: number[]) {
            if (args.length === 2) {
                const num1 = args[0];
                const num2 = args[1];
                const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));

                return this.toFixed(num1 * m) % this.toFixed(num2 * m) / m;
            } else {
                return args.reduce((a, b) => this.rem(a, b))
            }
        };

        /**
         * n次方，仅支持整数次方(正负都可以)
         * @param {Number} num 浮点数
         * @param {Number} n 整数
         */
        public static pow(num: number, n: number) {
            if (num == 0 && n == 0) {
                return 1;
            }
            if (num == 0 && n > 0) {
                return 0
            }
            if (num == 0 && n < 0) {
                return Infinity;
            }
            // num为负数，n为负小数，返回NaN
            if (num < 0 && n < 0 && Math.round(n) != n) {
                return NaN;
            }

            if (Math.round(n) != n) {
                throw new Error('n must be an integer');
            }

            let result = 1;

            if (n > 0) {
                for (let index = 0; index < n; index++) {
                    result = this.mul(result, num);
                }
            } else if (n < 0) {
                for (let index = 0, len = Math.abs(n); index < len; index++) {
                    result = this.div(result, num);
                }
            }

            return result;
        };
        /**
         * 开方运算【牛顿迭代法】
         * 
         * @param {Number} n
         * @returns 
         */
        public static sqrt(n: number) {
            if (n < 0) return NaN;
            if (n === 0) return 0;
            if (n === 1) return 1;
            let last = 0;
            let res = 1;
            let c = 50;
            while (res != last && --c >= 0) {
                last = res;
                res = this.div(this.add(res, this.div(n, res)), 2)
            }
            return res;

            // float InvSqrt(float x)
            // {
            //     float xhalf = 0.5f * x;
            //     int i = * (int *) & x; // get bits for floating VALUE 
            //     i = 0x5f375a86 - (i >> 1); // gives initial guess y0
            //     x = * (float *) & i; // convert bits BACK to float
            //     x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
            //     x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
            //     x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
            //     return 1 / x;
            // }
        };

        /****************************************************随机****************************************************/
        private static getSeed(seed: number) {
            if (typeof seed !== "number" || isNaN(seed)) throw "seed invalid";
            seed = Math.floor(seed % 233280);
            return seed;
        }

        private static randomSeed: number;

        /**
         * 设置随机种子
         */
        public static setSeed(seed: number) {
            this.randomSeed = this.getSeed(seed);
        };

        /**
         * 随机
         */
        public static random() {
            if (typeof this.randomSeed !== "number" || isNaN(this.randomSeed)) throw "seed invalid";
            this.randomSeed = (this.randomSeed * 9301 + 49297) % 233280;
            return this.randomSeed / 233280.0;
        };

        /**
         * 根据随机种子随机
         * @param {number} seed
         */
        public static randomBySeed(seed: number) {
            seed = this.getSeed(seed);
            seed = (seed * 9301 + 49297) % 233280;
            return seed / 233280.0;
        };

        /****************************************************角度弧度转换****************************************************/
        /**
         * 弧度数转角度数
         * @param {Number} radians 浮点数
         * @returns {Numbe} 浮点数
         */
        public static radiansToDegrees(radians: number) {
            return this.div(radians, this.RAD);
        };
        /**
         * 角度数转弧度数
         * @param {Number} degrees 浮点数
         * @returns {Numbe} 浮点数
         */
        public static degreesToRadians(degrees: number) {
            return this.div(degrees, this.DEG);
        };
        /**
         * 将角度值转换到[0, 360)范围内
         * @param {Number} angle 浮点数
         * @returns {Number} 整数
         */
        public static get0To360Angle(angle: number) {
            if (angle === 0) {
                return 0;
            } else if (angle < 0) {
                return this.add(this.rem(angle, 360), 360);
            } else {
                return this.rem(angle, 360);
            }
        };
        /****************************************************三角函数****************************************************/
        /**
         * 查表
         */
        private static _sin = {};
        private static _cos = {};
        private static _tan = {};

        /**
         * 3个三角函数，根据需求自行添加
         * 为了效率，应该尽量使用查表法
         * 表内查不到的，目前使用系统方法的结果并取前4位小数
         */
        public static sin(x: number) {
            // if (x == 0) {
            //     return 0;
            // } else if (x == 90) {
            //     return 1;
            // }

            // let n = x, sum = 0, i = 1;
            // do {
            //     i++;
            //     sum = this.add(sum, n);
            //     // n = -n * x * x / (2 * i - 1) / (2 * i - 2);
            //     n = this.div(this.mul(-1, n, x, x), this.sub(this.mul(2, i), 1), this.sub(this.mul(2, i), 2));
            // } while (Math.abs(n) >= ACCURACY_SIN_ERROR);
            // return sum;
            if (this._sin.hasOwnProperty(x)) {
                return this._sin[x];
            }
            return this.toFixed(Math.sin(x), 4);
        };
        public static cos(x: number) {
            if (this._cos.hasOwnProperty(x)) {
                return this._cos[x];
            }
            return this.toFixed(Math.cos(x), 4);
        };
        public static tan(x: number) {
            if (this._tan.hasOwnProperty(x)) {
                return this._tan[x];
            }
            return this.toFixed(Math.tan(x), 4);
        };
    }
}