///<reference path="../../src/types/declaration/core.d.ts" />

/*
utils.js 工具类

 */

'use strict';

function utils() {
    this._init();
    this.scan = {
        up: { x: 0, y: -1 },
        left: { x: -1, y: 0 },
        down: { x: 0, y: 1 },
        right: { x: 1, y: 0 }
    };
    this.scan2 = {
        up: { x: 0, y: -1 },
        left: { x: -1, y: 0 },
        down: { x: 0, y: 1 },
        right: { x: 1, y: 0 },
        leftup: { x: -1, y: -1 },
        leftdown: { x: -1, y: 1 },
        rightup: { x: 1, y: -1 },
        rightdown: { x: 1, y: 1 }
    };
}

utils.prototype._init = function () {
    //
};

////// 将文字中的${和}（表达式）进行替换 //////
utils.prototype.replaceText = function (text, prefix) {
    if (typeof text !== 'string') return text;
    const length = text.length;
    let pointer = -1;
    let res = '';
    let expression = '';
    let blockDepth = 0;
    let inExpression = false;

    while (++pointer < length) {
        const char = text[pointer];

        if (inExpression) {
            if (char === '}') {
                blockDepth--;
            } else if (char === '{') {
                blockDepth++;
            }
            if (blockDepth === 0) {
                inExpression = false;
                res += String(core.utils.calValue(expression, prefix));
                expression = '';
            } else {
                expression += char;
            }
            continue;
        }

        if (char === '$' && text[pointer + 1] === '{') {
            pointer++;
            inExpression = true;
            blockDepth++;
            continue;
        }

        res += char;
    }

    return res;
};

utils.prototype.replaceValue = function (value) {
    if (typeof value !== 'string') return value;
    if (
        value.includes(':') ||
        value.includes('flag：') ||
        value.includes('global：')
    ) {
        if (value.includes('status:'))
            value = value.replace(
                /status:([a-zA-Z0-9_]+)/g,
                "core.getStatus('$1')"
            );
        if (value.includes('buff:'))
            value = value.replace(
                /buff:([a-zA-Z0-9_]+)/g,
                "core.getBuff('$1')"
            );
        if (value.includes('item:'))
            value = value.replace(
                /item:([a-zA-Z0-9_]+)/g,
                "core.itemCount('$1')"
            );
        if (value.includes('flag:') || value.includes('flag：'))
            value = value.replace(
                /flag[:：]([a-zA-Z0-9_\u4E00-\u9FCC\u3040-\u30FF\u2160-\u216B\u0391-\u03C9]+)/g,
                "core.getFlag('$1', 0)"
            );
        if (value.includes('global:') || value.includes('global：'))
            value = value.replace(
                /global[:：]([a-zA-Z0-9_\u4E00-\u9FCC\u3040-\u30FF\u2160-\u216B\u0391-\u03C9]+)/g,
                "core.getGlobal('$1', 0)"
            );
        if (value.includes('enemy:'))
            value = value.replace(
                /enemy:([a-zA-Z0-9_]+)[\.:]([a-zA-Z0-9_]+)/g,
                "core.material.enemys['$1'].$2"
            );
        if (value.includes('blockId:'))
            value = value.replace(
                /blockId:(\d+),(\d+)/g,
                'core.getBlockId($1, $2)'
            );
        if (value.includes('blockNumber:'))
            value = value.replace(
                /blockNumber:(\d+),(\d+)/g,
                'core.getBlockNumber($1, $2)'
            );
        if (value.includes('blockCls:'))
            value = value.replace(
                /blockCls:(\d+),(\d+)/g,
                'core.getBlockCls($1, $2)'
            );
        if (value.includes('equip:'))
            value = value.replace(/equip:(\d)/g, 'core.getEquip($1)');
        if (value.includes('temp:'))
            value = value.replace(
                /temp:([a-zA-Z0-9_]+)/g,
                "core.getFlag('@temp@$1', 0)"
            );
        // if (value.indexOf('switch:') >= 0)
        //     value = value.replace(
        //         /switch:([a-zA-Z0-9_]+)/g,
        //         "core.getFlag('" + (prefix || ':f@x@y') + "@$1', 0)"
        //     );
    }
    return value;
};

////// 计算表达式的值 //////
utils.prototype.calValue = function (value, prefix) {
    if (!core.isset(value)) return null;
    if (typeof value === 'string') {
        value = this.replaceValue(value);
        return eval(value);
    }
    if (value instanceof Function) {
        return value();
    }
    return value;
};

////// 向某个数组前插入另一个数组或元素 //////
utils.prototype.unshift = function (a, b) {
    if (!(a instanceof Array) || b == null) return;
    if (b instanceof Array) {
        a.unshift(...b);
    } else a.unshift(b);
    return a;
};

////// 向某个数组后插入另一个数组或元素 //////
utils.prototype.push = function (a, b) {
    if (!(a instanceof Array) || b == null) return;
    if (b instanceof Array) {
        a.push(...b);
    } else a.push(b);
    return a;
};

utils.prototype.decompress = function (value) {
    try {
        var output = lzw_decode(value);
        if (output) return JSON.parse(output);
    } catch (e) {}
    try {
        var output = LZString.decompress(value);
        if (output) return JSON.parse(output);
    } catch (e) {}
    try {
        return JSON.parse(value);
    } catch (e) {
        console.error(e);
    }
    return null;
};

////// 设置本地存储 //////
utils.prototype.setLocalStorage = function (key, value) {
    try {
        if (value == null) {
            this.removeLocalStorage(key);
            return;
        }

        var str = JSON.stringify(value).replace(
            /[\u007F-\uFFFF]/g,
            function (chr) {
                return (
                    '\\u' + ('0000' + chr.charCodeAt(0).toString(16)).substr(-4)
                );
            }
        );
        localStorage.setItem(core.firstData.name + '_' + key, str);

        if (key == 'autoSave') core.saves.ids[0] = true;
        else if (/^save\d+$/.test(key))
            core.saves.ids[parseInt(key.substring(4))] = true;

        return true;
    } catch (e) {
        console.error(e);
        return false;
    }
};

////// 获得本地存储 //////
utils.prototype.getLocalStorage = function (key, defaultValue) {
    try {
        var value = JSON.parse(
            localStorage.getItem(core.firstData.name + '_' + key)
        );
        if (value == null) return defaultValue;
        return value;
    } catch (e) {
        return defaultValue;
    }
};

////// 移除本地存储 //////
utils.prototype.removeLocalStorage = function (key) {
    localStorage.removeItem(core.firstData.name + '_' + key);
    if (key == 'autoSave') delete core.saves.ids[0];
    else if (/^save\d+$/.test(key))
        delete core.saves.ids[parseInt(key.substring(4))];
};

utils.prototype.setLocalForage = function (
    key,
    value,
    successCallback,
    errorCallback
) {
    if (value == null) {
        this.removeLocalForage(key);
        return;
    }

    var name = core.firstData.name + '_' + key;
    var str = JSON.stringify(value).replace(/[\u007F-\uFFFF]/g, function (chr) {
        return '\\u' + ('0000' + chr.charCodeAt(0).toString(16)).substr(-4);
    });
    var callback = function (err) {
        if (err) {
            if (errorCallback) errorCallback(err);
        } else {
            if (key == 'autoSave') core.saves.ids[0] = true;
            else if (/^save\d+$/.test(key))
                core.saves.ids[parseInt(key.substring(4))] = true;
            if (successCallback) successCallback();
        }
    };
    this._setLocalForage_set(name, str, callback);
};

utils.prototype._setLocalForage_set = function (name, str, callback) {
    if (window.jsinterface && window.jsinterface.setLocalForage) {
        var id = setTimeout(null);
        core['__callback' + id] = callback;
        core.saves.cache[name] = str;
        window.jsinterface.setLocalForage(id, name, str);
    } else {
        var compressed =
            str.length > 100000 ? LZString.compress(str) : lzw_encode(str);
        core.saves.cache[name] = compressed;
        localforage.setItem(name, compressed, callback);
    }
};

utils.prototype.getLocalForage = function (
    key,
    defaultValue,
    successCallback,
    errorCallback
) {
    var name = core.firstData.name + '_' + key;
    var callback = function (err, value) {
        if (err) {
            if (errorCallback) errorCallback(err);
        } else {
            core.saves.cache[name] = value;
            if (!successCallback) return;
            if (value != null) {
                var res = core.utils.decompress(value);
                successCallback(res == null ? defaultValue : res);
                return;
            }
            successCallback(defaultValue);
        }
    };
    if (core.saves.cache[name] != null) {
        return callback(null, core.saves.cache[name]);
    }
    this._getLocalForage_get(name, callback);
};

utils.prototype._getLocalForage_get = function (name, callback) {
    if (window.jsinterface && window.jsinterface.getLocalForage) {
        var id = setTimeout(null);
        core['__callback' + id] = callback;
        window.jsinterface.getLocalForage(id, name);
    } else {
        localforage.getItem(name, callback);
    }
};

utils.prototype.removeLocalForage = function (
    key,
    successCallback,
    errorCallback
) {
    var name = core.firstData.name + '_' + key;
    var callback = function (err) {
        if (err) {
            if (errorCallback) errorCallback(err);
        } else {
            if (key == 'autoSave') delete core.saves.ids[0];
            else if (/^save\d+$/.test(key))
                delete core.saves.ids[parseInt(key.substring(4))];
            if (successCallback) successCallback();
        }
    };
    delete core.saves.cache[name];
    this._removeLocalForage_remove(name, callback);
};

utils.prototype._removeLocalForage_remove = function (name, callback) {
    if (window.jsinterface && window.jsinterface.removeLocalForage) {
        var id = setTimeout(null);
        core['__callback' + id] = callback;
        window.jsinterface.removeLocalForage(id, name);
    } else {
        localforage.removeItem(name, callback);
    }
};

utils.prototype.clearLocalForage = function (callback) {
    core.saves.cache = {};
    if (window.jsinterface && window.jsinterface.clearLocalForage) {
        var id = setTimeout(null);
        core['__callback' + id] = callback;
        window.jsinterface.clearLocalForage(id);
    } else {
        localforage.clear(callback);
    }
};

utils.prototype.iterateLocalForage = function (iter, callback) {
    if (window.jsinterface && window.jsinterface.iterateLocalForage) {
        var id = setTimeout(null);
        core['__iter' + id] = iter;
        core['__callback' + id] = callback;
        window.jsinterface.iterateLocalForage(id);
    } else {
        localforage.iterate(iter, callback);
    }
};

utils.prototype.keysLocalForage = function (callback) {
    if (window.jsinterface && window.jsinterface.keysLocalForage) {
        var id = setTimeout(null);
        core['__callback' + id] = callback;
        window.jsinterface.keysLocalForage(id);
    } else {
        localforage.keys(callback);
    }
};

utils.prototype.lengthLocalForage = function (callback) {
    if (window.jsinterface && window.jsinterface.lengthLocalForage) {
        var id = setTimeout(null);
        core['__callback' + id] = callback;
        window.jsinterface.lengthLocalForage(id);
    } else {
        localforage.length(callback);
    }
};

utils.prototype.setGlobal = function (key, value) {
    if (core.isReplaying()) return;
    core.setLocalStorage(key, value);
};

utils.prototype.getGlobal = function (key, defaultValue) {
    var value;
    if (core.isReplaying()) {
        // 不考虑key不一致的情况
        var action = core.status.replay.toReplay.shift();
        if (action.indexOf('input2:') == 0) {
            value = JSON.parse(core.decodeBase64(action.substring(7)));
            core.setFlag('__global__' + key, value);
            core.status.route.push(
                'input2:' + core.encodeBase64(JSON.stringify(value))
            );
        } else {
            // 录像兼容性：尝试从flag和localStorage获得
            // 注意这里不再二次记录 input2: 到录像
            core.status.replay.toReplay.unshift(action);
            value = core.getFlag(
                '__global__' + key,
                core.getLocalStorage(key, defaultValue)
            );
        }
    } else {
        value = core.getLocalStorage(key, defaultValue);
        core.setFlag('__global__' + key, value);
        core.status.route.push(
            'input2:' + core.encodeBase64(JSON.stringify(value))
        );
    }
    return value;
};

////// 深拷贝一个对象 //////
utils.prototype.clone = function (data, filter, recursion) {
    if (!core.isset(data)) return null;
    // date
    if (data instanceof Date) {
        var copy = new Date();
        copy.setTime(data.getTime());
        return copy;
    }
    // array
    if (data instanceof Array) {
        return data.map((v, i) => {
            if (!filter || filter(String(i), v)) {
                return this.clone(v, recursion ? filter : null, recursion);
            }
        });
    }
    // 函数
    if (data instanceof Function) {
        return data;
    }
    // object
    if (data instanceof Object) {
        var copy = {};
        for (const [key, value] of Object.entries(data)) {
            if (!filter || filter(key, value)) {
                copy[key] = this.clone(
                    value,
                    recursion ? filter : null,
                    recursion
                );
            }
        }
        return copy;
    }
    return data;
};

////// 深拷贝1D/2D数组优化 //////
utils.prototype.cloneArray = function (data) {
    if (!(data instanceof Array)) return this.clone(data);
    if (data[0] instanceof Array) {
        return data.map(function (one) {
            return one.slice();
        });
    } else {
        return data.slice();
    }
};

////// 裁剪图片 //////
utils.prototype.splitImage = function (image, width, height) {
    if (typeof image == 'string') {
        image = core.getMappedName(image);
        image = core.material.images.images[image];
    }
    if (!image) return [];
    width = width || 32;
    height = height || width;
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');
    var ans = [];
    for (var j = 0; j < image.height; j += height) {
        for (var i = 0; i < image.width; i += width) {
            var w = Math.min(width, image.width - i),
                h = Math.min(height, image.height - j);
            canvas.width = w;
            canvas.height = h;
            core.drawImage(ctx, image, i, j, w, h, 0, 0, w, h);
            var img = new Image();
            img.src = canvas.toDataURL('image/png');
            ans.push(img);
        }
    }
    return ans;
};

////// 格式化时间为字符串 //////
utils.prototype.formatDate = function (date) {
    if (!date) date = new Date();
    return (
        '' +
        date.getFullYear() +
        '-' +
        core.setTwoDigits(date.getMonth() + 1) +
        '-' +
        core.setTwoDigits(date.getDate()) +
        ' ' +
        core.setTwoDigits(date.getHours()) +
        ':' +
        core.setTwoDigits(date.getMinutes()) +
        ':' +
        core.setTwoDigits(date.getSeconds())
    );
};

////// 格式化时间为最简字符串 //////
utils.prototype.formatDate2 = function (date) {
    if (!date) date = new Date();
    return (
        '' +
        date.getFullYear() +
        core.setTwoDigits(date.getMonth() + 1) +
        core.setTwoDigits(date.getDate()) +
        core.setTwoDigits(date.getHours()) +
        core.setTwoDigits(date.getMinutes()) +
        core.setTwoDigits(date.getSeconds())
    );
};

utils.prototype.formatTime = function (time) {
    return (
        core.setTwoDigits(Math.floor(time / 3600000)) +
        ':' +
        core.setTwoDigits(Math.floor(time / 60000) % 60) +
        ':' +
        core.setTwoDigits(Math.floor(time / 1000) % 60)
    );
};

////// 两位数显示 //////
utils.prototype.setTwoDigits = function (x) {
    const num = Number(x);
    return num < 10 && num >= 0 ? '0' + x : x;
};

utils.prototype.formatSize = function (size) {
    if (size < 1024) return size + 'B';
    else if (size < 1024 * 1024) return (size / 1024).toFixed(2) + 'KB';
    else return (size / 1024 / 1024).toFixed(2) + 'MB';
};

utils.prototype.formatBigNumber = function (x, digits) {
    if (typeof x === 'string') return x;
    if (digits === true) digits = 4; // 兼容旧版onMap参数
    digits ??= 6;
    x = Math.trunc(parseFloat(x)); // 尝试识别为小数，然后向0取整
    if (x == null || !Number.isFinite(x)) return '???'; // 无法识别的数或正负无穷大，显示'???'
    var units = [
        // 单位及其后缀字母，可自定义，如改成千进制下的K、M、G、T、P
        { val: 1e4, suffix: 'w' },
        { val: 1e8, suffix: 'e' },
        { val: 1e12, suffix: 'z' },
        { val: 1e16, suffix: 'j' },
        { val: 1e20, suffix: 'g' }
    ];
    if (Math.abs(x) > 1e20 * Math.pow(10, digits - 2))
        return x.toExponential(0); // 绝对值过大以致于失去精度的数，直接使用科学记数法，系数只保留整数
    var sign = x < 0 ? '-' : '';
    x = Math.abs(x);

    if (x < Math.pow(10, digits)) return sign + x;

    for (var i = 0; i < units.length; ++i) {
        var each = units[i];
        var u = (x / each.val)
            .toFixed(digits)
            .slice(0, sign === '-' ? digits : digits + 1);
        if (u.indexOf('.') < 0) continue;
        u = u.substring(
            0,
            u[u.length - 2] == '.' ? u.length - 2 : u.length - 1
        );
        return sign + u + each.suffix;
    }
    return sign + x.toExponential(0);
};

////// 变速移动 //////
utils.prototype.applyEasing = function (name) {
    var list = {
        easeIn: function (t) {
            return Math.pow(t, 3);
        },
        easeOut: function (t) {
            return 1 - Math.pow(1 - t, 3);
        },
        easeInOut: function (t) {
            // easeInOut试了一下感觉二次方效果明显点
            if (t < 0.5) return Math.pow(t, 2) * 2;
            else return 1 - Math.pow(1 - t, 2) * 2;
        },
        linear: function (t) {
            return t;
        }
    };
    if (name == 'random') {
        var keys = Object.keys(list);
        name = keys[Math.floor(Math.random() * keys.length)];
    }
    return list[name] || list.linear;
};

////// 数组转RGB //////
utils.prototype.arrayToRGB = function (color) {
    if (!(color instanceof Array)) return color;
    var nowR = this.clamp(parseInt(color[0]), 0, 255),
        nowG = this.clamp(parseInt(color[1]), 0, 255),
        nowB = this.clamp(parseInt(color[2]), 0, 255);
    return (
        '#' + ((nowR << 16) + (nowG << 8) + nowB).toString(16).padStart(6, '0')
    );
};

utils.prototype.arrayToRGBA = function (color) {
    if (!(color instanceof Array)) return color;
    if (color[3] == null) color[3] = 1;
    var nowR = this.clamp(parseInt(color[0]), 0, 255),
        nowG = this.clamp(parseInt(color[1]), 0, 255),
        nowB = this.clamp(parseInt(color[2]), 0, 255),
        nowA = this.clamp(parseFloat(color[3]), 0, 1);
    return 'rgba(' + nowR + ',' + nowG + ',' + nowB + ',' + nowA + ')';
};

////// 加密路线 //////
utils.prototype.encodeRoute = function (route, compress = true) {
    var ans = '',
        lastMove = '',
        cnt = 0;

    route.forEach(function (t) {
        if (t === 'up' || t === 'down' || t === 'left' || t === 'right') {
            if (t !== lastMove && cnt > 0) {
                const char = lastMove[0];
                if (char) {
                    ans += char.toUpperCase();
                }
                if (cnt > 1) ans += cnt;
                cnt = 0;
            }
            lastMove = t;
            cnt++;
        } else {
            if (cnt > 0) {
                const char = lastMove[0];
                if (char) {
                    ans += char.toUpperCase();
                }
                if (cnt > 1) ans += cnt;
                cnt = 0;
            }
            ans += core.utils._encodeRoute_encodeOne(t);
        }
    });
    if (cnt > 0) {
        ans += lastMove[0].toUpperCase();
        if (cnt > 1) ans += cnt;
    }
    if (!compress) {
        return '!!' + ans;
    } else {
        return LZString.compressToBase64(ans);
    }
};

utils.prototype._encodeRoute_id2number = function (id) {
    var number = core.maps.getNumberById(id);
    return number === 0 ? id : number;
};

utils.prototype._encodeRoute_encodeOne = function (t) {
    if (t.startsWith('item:'))
        return 'I' + this._encodeRoute_id2number(t.slice(5)) + ':';
    else if (t.startsWith('unEquip:')) return 'u' + t.slice(8);
    else if (t.startsWith('saveEquip:')) return 's' + t.slice(10);
    else if (t.startsWith('loadEquip:')) return 'l' + t.slice(10);
    else if (t.startsWith('fly:')) return 'F' + t.slice(4) + ':';
    else if (t === 'choices:none') return 'c';
    else if (t.startsWith('choices:')) return 'C' + t.slice(8);
    else if (t.startsWith('shop:')) return 'S' + t.slice(5) + ':';
    else if (t === 'turn') return 'T';
    else if (t.startsWith('turn:')) return 't' + t[5].toUpperCase() + ':';
    else if (t === 'getNext') return 'G';
    else if (t === 'input:none') return 'p';
    else if (t.startsWith('input:')) return 'P' + t.slice(6);
    else if (t.startsWith('input2:')) return 'Q' + t.slice(7) + ':';
    else if (t === 'no') return 'N';
    else if (t.startsWith('move:')) return 'M' + t.slice(5);
    else if (t.startsWith('key:')) return 'K' + t.slice(4);
    else if (t.startsWith('click:')) return 'k' + t.slice(6);
    else if (t.startsWith('random:')) return 'X' + t.slice(7);
    return '(' + t + ')';
};

////// 解密路线 //////
utils.prototype.decodeRoute = function (route) {
    if (!route) return route;

    var decodeObj = { index: 0, ans: [] };
    if (route.startsWith('!!')) decodeObj.index = 2;
    else {
        var v = LZString.decompressFromBase64(route);
        if (v != null && /^[-_a-zA-Z0-9+\/=:()]*$/.test(v)) {
            if (v != '' || route.length < 8) route = v;
        }
    }
    decodeObj.route = route;

    while (decodeObj.index < decodeObj.route.length) {
        this._decodeRoute_decodeOne(
            decodeObj,
            decodeObj.route.charAt(decodeObj.index++)
        );
    }

    return decodeObj.ans;
};

utils.prototype._decodeRoute_getNumber = function (decodeObj, noparse) {
    var num = '';
    var first = true;
    while (true) {
        var ch = decodeObj.route.charAt(decodeObj.index);
        if (ch >= '0' && ch <= '9') num += ch;
        else if (ch == '-' && first) num += ch;
        else break;
        first = false;
        decodeObj.index++;
    }
    if (num.length == 0) num = '1';
    return noparse ? num : parseInt(num);
};

utils.prototype._decodeRoute_getString = function (decodeObj) {
    var str = '';
    while (
        decodeObj.index < decodeObj.route.length &&
        decodeObj.route.charAt(decodeObj.index) != ':'
    ) {
        str += decodeObj.route.charAt(decodeObj.index++);
    }
    decodeObj.index++;
    return str;
};

utils.prototype._decodeRoute_number2id = function (number) {
    if (/^\d+$/.test(number)) {
        var info = core.maps.blocksInfo[number];
        if (info) return info.id;
    }
    return number;
};

utils.prototype._decodeRoute_decodeOne = function (decodeObj, c) {
    // --- 特殊处理自定义项
    if (c == '(') {
        var idx = decodeObj.route.indexOf(')', decodeObj.index);
        if (idx >= 0) {
            decodeObj.ans.push(decodeObj.route.substring(decodeObj.index, idx));
            decodeObj.index = idx + 1;
            return;
        }
    }
    var nxt =
        c == 'I' || c == 'e' || c == 'F' || c == 'S' || c == 'Q' || c == 't'
            ? this._decodeRoute_getString(decodeObj)
            : this._decodeRoute_getNumber(decodeObj);

    var mp = { U: 'up', D: 'down', L: 'left', R: 'right' };

    switch (c) {
        case 'U':
        case 'D':
        case 'L':
        case 'R':
            for (var i = 0; i < nxt; i++) decodeObj.ans.push(mp[c]);
            break;
        case 'I':
            decodeObj.ans.push('item:' + this._decodeRoute_number2id(nxt));
            break;
        case 'u':
            decodeObj.ans.push('unEquip:' + nxt);
            break;
        case 'e':
            decodeObj.ans.push('equip:' + this._decodeRoute_number2id(nxt));
            break;
        case 's':
            decodeObj.ans.push('saveEquip:' + nxt);
            break;
        case 'l':
            decodeObj.ans.push('loadEquip:' + nxt);
            break;
        case 'F':
            decodeObj.ans.push('fly:' + nxt);
            break;
        case 'c':
            decodeObj.ans.push('choices:none');
            break;
        case 'C':
            decodeObj.ans.push('choices:' + nxt);
            break;
        case 'S':
            decodeObj.ans.push('shop:' + nxt);
            break;
        case 'T':
            decodeObj.ans.push('turn');
            break;
        case 't':
            decodeObj.ans.push('turn:' + mp[nxt]);
            break;
        case 'G':
            decodeObj.ans.push('getNext');
            break;
        case 'p':
            decodeObj.ans.push('input:none');
            break;
        case 'P':
            decodeObj.ans.push('input:' + nxt);
            break;
        case 'Q':
            decodeObj.ans.push('input2:' + nxt);
            break;
        case 'N':
            decodeObj.ans.push('no');
            break;
        case 'M':
            ++decodeObj.index;
            decodeObj.ans.push(
                'move:' + nxt + ':' + this._decodeRoute_getNumber(decodeObj)
            );
            break;
        case 'K':
            decodeObj.ans.push('key:' + nxt);
            break;
        case 'k':
            ++decodeObj.index;
            var px = this._decodeRoute_getNumber(decodeObj);
            ++decodeObj.index;
            var py = this._decodeRoute_getNumber(decodeObj);
            decodeObj.ans.push('click:' + nxt + ':' + px + ':' + py);
            break;
        case 'X':
            decodeObj.ans.push('random:' + nxt);
            break;
    }
};

////// 判断某对象是否不为null也不为NaN //////
utils.prototype.isset = function (val) {
    return val != null && !(typeof val == 'number' && isNaN(val));
};

////// 获得子数组 //////
utils.prototype.subarray = function (a, b) {
    if (!(a instanceof Array) || !(b instanceof Array) || a.length < b.length)
        return null;
    for (var i = 0; i < b.length; ++i) {
        if (a[i] != b[i]) return null;
    }
    return a.slice(b.length);
};

utils.prototype.inArray = function (array, element) {
    return array instanceof Array && array.includes(element);
};

utils.prototype.clamp = function (x, a, b) {
    var min = Math.min(a, b),
        max = Math.max(a, b);
    return Math.min(Math.max(x || 0, min), max);
};

utils.prototype.getCookie = function (name) {
    var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
    return match ? match[2] : null;
};

////// 设置statusBar的innerHTML，会自动斜体和放缩，也可以增加自定义css //////
utils.prototype.setStatusBarInnerHTML = function (name, value, css) {};

utils.prototype.strlen = function (str) {
    var count = 0;
    for (var i = 0, len = str.length; i < len; i++) {
        count += str.charCodeAt(i) < 256 ? 1 : 2;
    }
    return count;
};

utils.prototype.turnDirection = function (turn, direction) {
    direction = direction || core.getHeroLoc('direction');
    var directionList = [
        'left',
        'leftup',
        'up',
        'rightup',
        'right',
        'rightdown',
        'down',
        'leftdown'
    ];
    if (directionList.indexOf(turn) >= 0) return turn;
    if (turn == ':hero') return core.getHeroLoc('direction');
    if (turn == ':backhero')
        return this.turnDirection(':back', core.getHeroLoc('direction'));
    if (typeof turn === 'number' && turn % 45 == 0) turn /= 45;
    else {
        switch (turn) {
            case ':left':
                turn = 6;
                break; // turn left
            case ':right':
                turn = 2;
                break; // turn right
            case ':back':
                turn = 4;
                break; // turn back
            default:
                turn = 0;
                break;
        }
    }
    var index = directionList.indexOf(direction);
    if (index < 0) return direction;
    return directionList[(index + (turn || 0)) % directionList.length];
};

utils.prototype.matchWildcard = function (pattern, string) {
    try {
        return new RegExp(
            '^' +
                pattern
                    .split(/\*+/)
                    .map(function (s) {
                        return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
                    })
                    .join('.*') +
                '$'
        ).test(string);
    } catch (e) {
        return false;
    }
};

utils.prototype.matchRegex = function (pattern, string) {
    try {
        if (pattern.startsWith('^')) pattern = pattern.substring(1);
        if (pattern.endsWith('$'))
            pattern = pattern.substring(0, pattern.length - 1);
        return new RegExp('^' + pattern + '$').test(string);
    } catch (e) {
        return false;
    }
};

////// Base64加密 //////
utils.prototype.encodeBase64 = function (str) {
    return btoa(
        encodeURIComponent(str).replace(
            /%([0-9A-F]{2})/g,
            function (match, p1) {
                return String.fromCharCode(parseInt(p1, 16));
            }
        )
    );
};

////// Base64解密 //////
utils.prototype.decodeBase64 = function (str) {
    return decodeURIComponent(
        atob(str)
            .split('')
            .map(function (c) {
                return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
            })
            .join('')
    );
};

utils.prototype.rand = function (num) {
    var rand = core.getFlag('__rand__');
    rand = this.__next_rand(rand);
    core.setFlag('__rand__', rand);
    var ans = rand / 2147483647;
    if (num && num > 0) return Math.floor(ans * num);
    return ans;
};

////// 生成随机数（录像方法） //////
utils.prototype.rand2 = function (num) {
    num = num || 2147483648;
    num = Math.abs(num);

    var value;
    if (core.isReplaying()) {
        var action = core.status.replay.toReplay.shift();
        if (action.indexOf('random:') == 0) {
            value = parseInt(action.substring(7));
            if (isNaN(value) || value >= num || value < 0) {
                console.warn('错误！当前random:项超过范围。将重新随机生成！');
                value = Math.floor(Math.random() * num);
            }
        } else {
            console.warn('错误！当前需要一个random:项。将重新随机生成！');
            value = Math.floor(Math.random() * num);
        }
    } else {
        value = Math.floor(Math.random() * num);
    }
    core.status.route.push('random:' + value);
    return value;
};

utils.prototype.__init_seed = function () {
    var rand = (new Date().getTime() % 34834795) + 3534;
    rand = this.__next_rand(rand);
    rand = this.__next_rand(rand);
    rand = this.__next_rand(rand);
    core.setFlag('__seed__', rand);
    core.setFlag('__rand__', rand);
};

utils.prototype.__next_rand = function (_rand) {
    _rand = (_rand % 127773) * 16807 - ~~(_rand / 127773) * 2836;
    _rand += _rand < 0 ? 2147483647 : 0;
    return _rand;
};

////// 读取一个本地文件内容 //////
utils.prototype.readFile = function (success, error, accept, readType) {
    core.platform.successCallback = success;
    core.platform.errorCallback = error;

    if (window.jsinterface) {
        window.jsinterface.readFile();
        return;
    }

    // step 0: 不为http/https，直接不支持
    if (!core.platform.isOnline) {
        alert('离线状态下不支持文件读取！');
        if (error) error();
        return;
    }

    // Step 1: 如果不支持FileReader，直接不支持
    if (core.platform.fileReader == null) {
        alert('当前浏览器不支持FileReader！');
        if (error) error();
        return;
    }

    if (core.platform.fileInput == null) {
        core.platform.fileInput = document.createElement('input');
        core.platform.fileInput.style.opacity = 0;
        core.platform.fileInput.type = 'file';
        core.platform.fileInput.onchange = function () {
            var files = core.platform.fileInput.files;
            if (files.length == 0) {
                if (core.platform.errorCallback) core.platform.errorCallback();
                return;
            }
            if (!readType)
                core.platform.fileReader.readAsText(
                    core.platform.fileInput.files[0]
                );
            else
                core.platform.fileReader.readAsDataURL(
                    core.platform.fileInput.files[0]
                );
            core.platform.fileInput.value = '';
        };
    }
    core.platform.fileInput.value = '';
    if (accept) core.platform.fileInput.accept = accept;

    core.platform.fileInput.click();
};

////// 读取文件完毕 //////
utils.prototype.readFileContent = function (content) {
    var obj = null;
    if (content.slice(0, 4) === 'data') {
        if (core.platform.successCallback)
            core.platform.successCallback(content);
        return;
    }
    // 检查base64
    try {
        obj = JSON.parse(LZString.decompressFromBase64(content));
    } catch (e) {}
    if (!obj) {
        try {
            obj = JSON.parse(content);
        } catch (e) {
            console.error(e);
        }
    }

    if (obj) {
        if (core.platform.successCallback) core.platform.successCallback(obj);
        return;
    }

    if (core.platform.errorCallback) core.platform.errorCallback();
};

////// 下载文件到本地 //////
utils.prototype.download = function (filename, content) {
    if (window.jsinterface) {
        window.jsinterface.download(filename, content);
        return;
    }

    // Step 0: 不为http/https，直接不支持
    if (!core.platform.isOnline) {
        alert('离线状态下不支持下载操作！');
        return;
    }

    // Step 1: 如果是iOS平台，直接不支持
    if (core.platform.isIOS) {
        if (core.copy(content)) {
            alert(
                'iOS平台下不支持直接下载文件！\n所有应下载内容已经复制到您的剪切板，请自行创建空白文件并粘贴。'
            );
        } else {
            alert('iOS平台下不支持下载操作！');
        }
        return;
    }

    // Step 2: 如果不是PC平台（Android），则只支持chrome
    if (!core.platform.isPC) {
        if (
            !core.platform.isChrome ||
            core.platform.isQQ ||
            core.platform.isWeChat
        ) {
            // 检测chrome
            if (core.copy(content)) {
                alert(
                    '移动端只有Chrome浏览器支持直接下载文件！\n所有应下载内容已经复制到您的剪切板，请自行创建空白文件并粘贴。'
                );
            } else {
                alert('该平台或浏览器暂不支持下载操作！');
            }
            return;
        }
    }

    // Step 3: 如果是Safari浏览器，则提示并打开新窗口
    if (core.platform.isSafari) {
        alert(
            '你当前使用的是Safari浏览器，不支持直接下载文件。\n即将打开一个新窗口为应下载内容，请自行全选复制然后创建空白文件并粘贴。'
        );
        var blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
        var href = window.URL.createObjectURL(blob);
        var opened = window.open(href, '_blank');
        window.URL.revokeObjectURL(href);
        return;
    }

    // Step 4: 下载
    var blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
    if (window.navigator.msSaveOrOpenBlob) {
        window.navigator.msSaveBlob(blob, filename);
    } else {
        var href = window.URL.createObjectURL(blob);
        var elem = window.document.createElement('a');
        elem.href = href;
        elem.download = filename;
        document.body.appendChild(elem);
        elem.click();
        document.body.removeChild(elem);
        window.URL.revokeObjectURL(href);
    }
};

////// 复制一段内容到剪切板 //////
utils.prototype.copy = function (data) {
    if (window.jsinterface) {
        window.jsinterface.copy(data);
        return true;
    }

    if (!core.platform.supportCopy) return false;

    var textArea = document.createElement('textarea');
    textArea.style.position = 'fixed';
    textArea.style.top = 0;
    textArea.style.left = 0;
    textArea.style.width = '2em';
    textArea.style.height = '2em';
    textArea.style.padding = 0;
    textArea.style.border = 'none';
    textArea.style.outline = 'none';
    textArea.style.boxShadow = 'none';
    textArea.style.background = 'transparent';
    textArea.value = data;
    document.body.appendChild(textArea);
    textArea.focus();
    textArea.setSelectionRange(0, textArea.value.length);
    var successful = false;
    try {
        successful = document.execCommand('copy');
    } catch (err) {
        successful = false;
    }

    document.body.removeChild(textArea);
    return successful;
};

////// 显示一段confirm //////
utils.prototype.myconfirm = function (hint, yesCallback, noCallback) {
    main.dom.inputDiv.style.display = 'block';
    main.dom.inputMessage.innerHTML = hint.replace(/\n/g, '<br/>');
    main.dom.inputBox.style.display = 'none';
    main.dom.inputYes.blur();
    main.dom.inputNo.blur();
    core.status.holdingKeys = [];

    core.platform.successCallback = yesCallback;
    core.platform.errorCallback = noCallback;
};

////// 让用户输入一段文字 //////
utils.prototype.myprompt = function (hint, value, callback) {
    main.dom.inputDiv.style.display = 'block';
    main.dom.inputMessage.innerHTML = hint.replace(/\n/g, '<br/>');
    main.dom.inputBox.style.display = 'block';
    main.dom.inputBox.value = value == null ? '' : value;
    main.dom.inputYes.blur();
    main.dom.inputNo.blur();
    setTimeout(function () {
        main.dom.inputBox.focus();
    });
    core.status.holdingKeys = [];

    core.platform.successCallback = core.platform.errorCallback = callback;
};

////// 动画显示某对象 //////
utils.prototype.showWithAnimate = function (obj, speed, callback) {
    obj.style.display = 'block';
    if (!speed || main.mode != 'play') {
        obj.style.opacity = 1;
        if (callback) callback();
        return;
    }
    obj.style.opacity = 0;
    var opacityVal = 0;
    var showAnimate = window.setInterval(function () {
        opacityVal += 0.03;
        obj.style.opacity = opacityVal;
        if (opacityVal > 1) {
            clearInterval(showAnimate);
            if (callback) callback();
        }
    }, speed);
};

////// 动画使某对象消失 //////
utils.prototype.hideWithAnimate = function (obj, speed, callback) {
    if (!speed || main.mode != 'play') {
        obj.style.display = 'none';
        if (callback) callback();
        return;
    }
    obj.style.opacity = 1;
    var opacityVal = 1;
    var hideAnimate = window.setInterval(function () {
        opacityVal -= 0.03;
        obj.style.opacity = opacityVal;
        if (opacityVal < 0) {
            obj.style.display = 'none';
            clearInterval(hideAnimate);
            if (callback) callback();
        }
    }, speed);
};

////// 生成浏览器唯一的 guid //////
utils.prototype.getGuid = function () {
    var guid = localStorage.getItem('guid');
    if (guid != null) return guid;
    guid = 'xxxxxxxx_xxxx_4xxx_yxxx_xxxxxxxxxxxx'.replace(
        /[xy]/g,
        function (c) {
            var r = (Math.random() * 16) | 0,
                v = c == 'x' ? r : (r & 0x3) | 0x8;
            return v.toString(16);
        }
    );
    localStorage.setItem('guid', guid);
    return guid;
};

utils.prototype.hashCode = function (obj) {
    if (typeof obj == 'string') {
        var hash = 0,
            i,
            chr;
        if (obj.length === 0) return hash;
        for (i = 0; i < obj.length; i++) {
            chr = obj.charCodeAt(i);
            hash = (hash << 5) - hash + chr;
            hash |= 0;
        }
        return hash;
    }
    return this.hashCode(JSON.stringify(obj).split('').sort().join(''));
};

utils.prototype.same = function (a, b) {
    if (a === b) return true;
    if (a == null || b == null) return false;
    if (a instanceof Array && b instanceof Array) {
        if (a.length !== b.length) return false;
        for (var i = 0; i < a.length; i++) {
            if (!this.same(a[i], b[i])) return false;
        }
        return true;
    }
    if (a instanceof Object && b instanceof Object) {
        const toCompare = new Set([...Object.keys(a), ...Object.keys(b)]);
        for (const key of toCompare) {
            if (!this.same(a[key], b[key])) return false;
        }
        return true;
    }
    return false;
};

utils.prototype.unzip = function (
    blobOrUrl,
    success,
    error,
    convertToText,
    onprogress
) {
    var _error = function (msg) {
        console.error(msg);
        if (error) error(msg);
    };

    if (!window.zip) {
        return _error('zip.js not exists!');
    }

    if (typeof blobOrUrl == 'string') {
        return core.http(
            'GET',
            blobOrUrl,
            null,
            function (data) {
                core.unzip(data, success, error, convertToText);
            },
            _error,
            null,
            'blob',
            onprogress
        );
    }

    if (!(blobOrUrl instanceof Blob)) {
        return _error('Should use Blob or URL as input');
    }

    zip.createReader(
        new zip.BlobReader(blobOrUrl),
        function (reader) {
            reader.getEntries(function (entries) {
                core.utils._unzip_readEntries(
                    entries,
                    function (data) {
                        reader.close(function () {
                            if (success) success(data);
                        });
                    },
                    convertToText
                );
            });
        },
        _error
    );
};

utils.prototype._unzip_readEntries = function (
    entries,
    success,
    convertToText
) {
    var results = {};
    if (entries == null || entries.length == 0) {
        return success(results);
    }
    var length = entries.length;
    entries.forEach(function (entry) {
        entry.getData(
            convertToText ? new zip.TextWriter('utf8') : new zip.BlobWriter(),
            function (data) {
                results[entry.filename] = data;
                length--;
                if (length == 0) {
                    success(results);
                }
            }
        );
    });
};

utils.prototype.http = function (
    type,
    url,
    formData,
    success,
    error,
    mimeType,
    responseType,
    onprogress
) {
    var xhr = new XMLHttpRequest();
    xhr.open(type, url, true);
    if (mimeType) xhr.overrideMimeType(mimeType);
    if (responseType) xhr.responseType = responseType;
    xhr.onload = function (e) {
        if (xhr.status == 200) {
            if (success) success(xhr.response);
        } else {
            if (error) error('HTTP ' + xhr.status);
        }
    };
    xhr.onprogress = function (e) {
        if (e.lengthComputable) {
            if (onprogress) onprogress(e.loaded, e.total);
        }
    };
    xhr.onabort = function () {
        if (error) error('Abort');
    };
    xhr.ontimeout = function () {
        if (error) error('Timeout');
    };
    xhr.onerror = function () {
        if (error) error('Error on Connection');
    };
    if (formData) xhr.send(formData);
    else xhr.send();
};

// LZW-compress
// https://gist.github.com/revolunet/843889
function lzw_encode(s) {
    var dict = {};
    var data = (s + '').split('');
    var out = [];
    var currChar;
    var phrase = data[0];
    var code = 256;
    for (var i = 1; i < data.length; i++) {
        currChar = data[i];
        if (dict[phrase + currChar] != null) {
            phrase += currChar;
        } else {
            out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
            dict[phrase + currChar] = code;
            code++;
            phrase = currChar;
        }
    }
    out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
    for (var i = 0; i < out.length; i++) {
        out[i] = String.fromCharCode(out[i]);
    }
    return out.join('');
}

// Decompress an LZW-encoded string
function lzw_decode(s) {
    var dict = {};
    var data = (s + '').split('');
    var currChar = data[0];
    var oldPhrase = currChar;
    var out = [currChar];
    var code = 256;
    var phrase;
    for (var i = 1; i < data.length; i++) {
        var currCode = data[i].charCodeAt(0);
        if (currCode < 256) {
            phrase = data[i];
        } else {
            phrase = dict[currCode] ? dict[currCode] : oldPhrase + currChar;
        }
        out.push(phrase);
        currChar = phrase.charAt(0);
        dict[code] = oldPhrase + currChar;
        code++;
        oldPhrase = phrase;
    }
    return out.join('');
}
