import UserScriptEngine from "../../enum/UserScriptEngine";
import Device from "../../enum/Device";
import LOADING_IMG_HTML from "../../../static/html/loading_img.html";
import Timer from "./Timer";
import FetchUtils from "./FetchUtils";
import TornStyleSwitch from "./TornStyleSwitch";
import { MenuItemConfig } from "../ZhongIcon";
import TRAVEL_STATE from "../../enum/TravelState";
import InventoryItemInfo from "../../interface/responseType/InventoryItemInfo";
import { Injectable } from "../../container/Injectable";
import ClassName from "../../container/ClassName";
import LocalConfigWrapper from "../LocalConfigWrapper";
import Logger from "../Logger";
import Global from "../Global";
import { Container } from "../../container/Container";
import TornPDAUtils from "./TornPDAUtils";
import { InjectionKey } from "vue";

@Injectable()
@ClassName('CommonUtils')
export default class CommonUtils {

    constructor(
        private readonly localConfigWrapper: LocalConfigWrapper,
        private readonly fetchUtils: FetchUtils,
        private readonly logger: Logger,
        private readonly global: Global,
        private readonly tornPDAUtils: TornPDAUtils,
    ) {
    }

    /**
     * @deprecated
     */
    static getScriptEngine() {
        let glob = Container.factory(Global);
        let tornPDAUtils = Container.factory(TornPDAUtils);
        return glob.GM_xmlhttpRequest ? UserScriptEngine.GM : tornPDAUtils.isPDA()
            ? UserScriptEngine.PDA : UserScriptEngine.RAW;
    }

    static COFetch(url: URL | string, method: 'get' | 'post' | string = 'get', body: any = null): Promise<string> {
        let logger = Container.factory(Logger);
        let start = new Timer();
        const engine = this.getScriptEngine();
        logger.info('跨域获取数据开始, 脚本引擎: ' + engine);
        return new Promise<string>((resolve, reject) => {
            switch (engine) {
                case UserScriptEngine.RAW: {
                    logger.error(`跨域请求错误：${ UserScriptEngine.RAW }环境下无法进行跨域请求`);
                    reject(`错误：${ UserScriptEngine.RAW }环境下无法进行跨域请求`);
                    break;
                }
                case UserScriptEngine.PDA: {
                    const { PDA_httpGet, PDA_httpPost } = window;
                    // get
                    if (method === 'get') {
                        if (typeof PDA_httpGet !== 'function') {
                            logger.error('COFetch网络错误：PDA版本不支持');
                            reject('COFetch网络错误：PDA版本不支持');
                        }
                        PDA_httpGet(url)
                            .then(res => {
                                logger.info('跨域获取数据成功, 耗时' + start.getTimeMs());
                                resolve(res.responseText);
                            })
                            .catch(e => {
                                logger.error('COFetch网络错误', e);
                                reject(`COFetch网络错误 ${ e }`);
                            })
                    }
                    // post
                    else {
                        if (typeof PDA_httpPost !== 'function') {
                            logger.error('COFetch网络错误：PDA版本不支持');
                            reject('COFetch网络错误：PDA版本不支持');
                        }
                        PDA_httpPost(url, { 'content-type': 'application/json' }, body)
                            .then(res => resolve(res.responseText))
                            .catch(e => {
                                logger.error('COFetch网络错误', e);
                                reject(`COFetch网络错误 ${ e }`);
                            });
                    }
                    break;
                }
                case UserScriptEngine.GM: {
                    let { GM_xmlhttpRequest } = Container.factory(Global);
                    if (typeof GM_xmlhttpRequest !== 'function') {
                        logger.error('COFetch网络错误：用户脚本扩展API错误');
                        reject('错误：用户脚本扩展API错误');
                    }
                    GM_xmlhttpRequest({
                        method: method,
                        url: url,
                        data: method === 'get' ? null : body,
                        headers: method === 'get' ? null : { 'content-type': 'application/json' },
                        onload: res => {
                            logger.info('跨域获取数据成功，耗时' + start.getTimeMs());
                            resolve(res.response);
                        },
                        onerror: res => reject(`连接错误 ${ JSON.stringify(res) }`),
                        ontimeout: res => reject(`连接超时 ${ JSON.stringify(res) }`),
                    });
                }
            }
        });
    }

    getScriptEngine() {
        // let glob = Container.factory(Global);
        // let tornPDAUtils = Container.factory(TornPDAUtils);
        return this.global.GM_xmlhttpRequest ? UserScriptEngine.GM : this.tornPDAUtils.isPDA()
            ? UserScriptEngine.PDA : UserScriptEngine.RAW;
    }

    // /**
    //  * 返回玩家信息的对象 { playername: string, userID: number }
    //  * @return {PlayerInfo} rs
    //  */
    // static getPlayerInfo(): PlayerInfo {
    //     const node = document.querySelector('script[uid]');
    //     if (node) {
    //         return {
    //             playername: node.getAttribute('name'),
    //             userID: node.getAttribute('uid') as unknown as number,
    //         }
    //     } else {
    //         new Alert('严重错误：芜湖助手无法获取用户数据，已退出');
    //         throw '芜湖助手无法获取用户数据';
    //     }
    // }

    // 用户设备类型 对应PC MOBILE TABLET
    public static getDeviceType(): Device {
        return window.innerWidth >= 1000
            ? Device.PC : window.innerWidth <= 600 ? Device.MOBILE : Device.TABLET;
    }

    public static getYaoCD(): string {
        if (document.querySelector("#icon49-sidebar")) { // 0-10min
            return '<10分'
        } else if (document.querySelector("#icon50-sidebar")) { // 10min-1h
            return '<1时'
        } else if (document.querySelector("#icon51-sidebar")) { // 1h-2h
            return '1~2时'
        } else if (document.querySelector("#icon52-sidebar")) { // 2h-5h
            return '2~5时'
        } else if (document.querySelector("#icon53-sidebar")) { // 5h+
            return '>5时'
        } else {
            return '无效'
        }
    }

    // public static ajaxFetch(opt: AjaxFetchOption) {
    //     let { url, referrer = '/', method, body = null } = opt;
    //     let req_params: RequestInit = {
    //         headers: { 'X-Requested-With': 'XMLHttpRequest' },
    //         referrer,
    //         method,
    //     };
    //     if (method === 'POST') {
    //         req_params.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
    //         req_params.body = body;
    //     }
    //     return window.fetch(url, req_params);
    // }

    /**
     * 通过 mutation.observe 方法异步返回元素
     * @param {String} selectors - CSS规则的HTML元素选择器
     * @param {Document} content - 上下文
     * @param {number} timeout - 超时毫秒数
     * @returns {Promise<HTMLElement|null>}
     */
    public static elementReady(selectors: string, content: Document = document, timeout: number = 30000): Promise<HTMLElement> {
        const logger = Container.factory(Logger);
        logger.info('等待元素：' + selectors);
        let timer = new Timer();
        return new Promise((resolve, reject) => {
            let el = content.querySelector(selectors) as HTMLElement;
            if (el) {
                logger.info('已获取元素, 耗时' + timer.getTimeMs(), el);
                resolve(el);
                return;
            }
            let observer = new MutationObserver((_, observer) => {
                content.querySelectorAll(selectors).forEach((element) => {
                    logger.info({ innerHTML: element.innerHTML, element });
                    observer.disconnect();
                    logger.info('已获取元素, 耗时' + timer.getTimeMs(), element);
                    resolve(element as HTMLElement);
                });
            });
            window.setTimeout(() => {
                observer.disconnect();
                logger.error(`等待元素超时! [${ selectors }]\n${ content.documentElement.tagName }, 耗时` + timer.getTimeMs());
                reject(`等待元素超时! [${ selectors }]\n${ content.documentElement.tagName }, 耗时` + timer.getTimeMs());
            }, timeout);
            observer.observe(content.documentElement, { childList: true, subtree: true });
        });
    }

    /**
     * 通过 mutation.observe 方法异步返回元素
     * @param selectors
     * @param content
     * @param timeout
     */
    public static querySelector(selectors: string, content: Document = document, timeout: number = 30000): Promise<HTMLElement> {
        return CommonUtils.elementReady(selectors, content, timeout);
    }

    public querySelector(selectors: string, content: Document = document, timeout: number = 30000): Promise<HTMLElement> {
        return CommonUtils.elementReady(selectors, content, timeout);
    }

    public static addStyle(rules: string): void {
        const logger = Container.factory(Logger);
        let element = document.querySelector('style#wh-trans-gStyle');
        if (element) {
            element.innerHTML += rules;
        } else {
            element = document.createElement("style");
            element.id = 'wh-trans-gStyle';
            element.innerHTML = rules;
            document.head.appendChild(element);
        }
        logger.info('CSS规则已添加', element);
    }

    public styleInject(rules: string): void {
        const element = document.createElement("style");
        element.setAttribute('type', 'text/css');
        element.innerHTML = rules;
        document.head.appendChild(element);
    }

    public static loading_gif_html(): string {
        return LOADING_IMG_HTML;
    }

    /**
     * 播放音频
     * @param {string} url 播放的音频URL
     * @returns {undefined}
     */
    public audioPlay(url: string = 'https://www.torn.com/js/chat/sounds/Warble_1.mp3'): Promise<void> {
        return new Promise((resolve, reject) => {
            const audio = new Audio(url);
            audio.addEventListener("canplaythrough", () => {
                audio.play()
                    .catch(err => {
                        this.logger.error('播放音频出错', err.message, err.stack);
                        reject();
                    })
                    .then(
                        () => resolve()
                    );
            });
        });
    }

    /**
     * 与给定日期对比，现在是否是新的一天
     * @param target
     * @param offsetHours 比对时间的偏移小时数
     */
    public isNewDay(target: number | Date, offsetHours: number = 0): boolean {
        let tar: Date = typeof target === "number" ? new Date(target) : target;
        let today = new Date();
        let utcNewDay = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
        utcNewDay.setHours(utcNewDay.getHours() + offsetHours);
        return utcNewDay > tar;
    }

    public jQueryReady(): Promise<null> {
        this.logger.info('等待jQuery加载中...');
        this.fetchUtils.fetchText('/js/script/lib/jquery-1.8.2.js?v=f9128651g')
            .then(res => window.eval(res));
        let intervalId = window.setInterval(() => {
            this.logger.info('仍在等待jQuery加载中...');
        }, 1000);
        return new Promise(async resolve => {
            while (true) {
                if (typeof window.$ === 'function') break;
                await this.sleep(100);
            }
            window.clearInterval(intervalId);
            this.logger.info('jQuery已加载');
            resolve(null);
        });
    }

    /**
     * 等待毫秒数
     * @param {Number} ms 毫秒
     * @returns {Promise<void>}
     */
    public sleep(ms: number): Promise<null> {
        let time = Math.max(ms, 10);
        return new Promise(resolve => setTimeout(() => resolve(null), time));
    }

    public elemGenerator(setting: MenuItemConfig, root_node: Element): HTMLElement {
        let { tip, domType } = setting;
        let new_node = null;
        switch (domType) {
            case 'checkbox': {
                new_node = document.createElement('div');
                let { domId, dictName, domText, changeEv } = setting;

                let switcher = new TornStyleSwitch(domText);
                let _input = switcher.getInput();
                switcher.getBase().id = domId;
                (tip) && (switcher.getBase().setAttribute('title', tip));
                _input.checked = this.localConfigWrapper.config[dictName];
                _input.onchange = e => {
                    this.localConfigWrapper.config[dictName] = _input.checked;
                    if (changeEv) changeEv(e);
                };
                new_node.appendChild(switcher.getBase());
                break;
            }
            case 'button': {
                new_node = document.createElement('div');
                let { domId, domText, isTornBtn, clickFunc } = setting;
                let btn = document.createElement('button');
                (tip) && (btn.setAttribute('title', tip));
                btn.id = domId;
                btn.innerHTML = domText;
                if (isTornBtn) btn.classList.add('torn-btn');
                btn.addEventListener('click', clickFunc);
                new_node.appendChild(btn);
                break;
            }
            case 'select': {
                new_node = document.createElement('div');
                let { domSelectOpt, dictName, domId, domText } = setting;
                let label = document.createElement('label');
                (tip) && (label.setAttribute('title', tip));
                let text = document.createTextNode(domText);
                let select = document.createElement('select');
                select.id = domId;
                domSelectOpt.forEach((opt, i) => {
                    let { domVal, domText } = opt;
                    let option = document.createElement('option');
                    option.value = domVal;
                    option.innerHTML = domText;
                    option.selected = i === this.localConfigWrapper.config[dictName];
                    option.innerHTML = domText;
                    select.appendChild(option);
                });
                select.onchange = e => this.localConfigWrapper.config[dictName] = (<HTMLSelectElement>e.target).selectedIndex;
                label.appendChild(text);
                label.appendChild(select);
                new_node.appendChild(label);
                break;
            }
            case 'plain': {
                let tag = setting.tagName || 'div';
                new_node = document.createElement(tag);
                if (setting.domId) new_node.id = setting.domId;
                new_node.innerHTML += setting['domHTML'];
                break;
            }
        }
        // 移动节点
        return root_node.appendChild(new_node);
    }

    public exportTextFile(filename, content) {
        const tmp = document.createElement('a');
        tmp.href = URL.createObjectURL(new Blob(content, { type: 'text/plain', endings: 'transparent' }));
        tmp.download = filename;
        tmp.click();
        tmp.remove();
        URL.revokeObjectURL(tmp.href);
    }

    public isValidUid(id: string | number): boolean {
        if (typeof id === 'string') {
            return /^[0-9]{1,7}$/.test(id);
        } else {
            return /^[0-9]{1,7}$/.test(id.toString());
        }
    }

    public getTravelStage(): TRAVEL_STATE {
        let global = Container.factory(Global);
        if (global.bodyAttrs["data-abroad"] === 'false') {
            return TRAVEL_STATE.IN_TORN;
        }
        // 海外落地：abroad
        else if (global.bodyAttrs["data-traveling"] === 'false') {
            return TRAVEL_STATE.ABROAD;
        }
        // 飞行中：traveling+abroad
        else {
            return TRAVEL_STATE.FLYING;
        }
    }

    public matchOne(src: string, reg: string | RegExp): string {
        let regExp = typeof reg === 'string' ? new RegExp(reg) : reg,
            ret = null;
        let match = src.match(regExp);
        if (match.length > 0) ret = match[0];
        return ret;
    }

    /**
     * 把秒数字格式化为
     *
     * `日 时 分 秒`
     * @param s
     */
    public secondsFormat(s: number): string {
        let gap = '日 时 分 秒'.split(' ');
        let last = s;
        let formatDate = [];
        let days = last / 86400 | 0;
        formatDate.push(days);
        last -= days * 86400;
        let hours = last / 3600 | 0;
        formatDate.push(hours);
        last -= hours * 3600;
        let mins = last / 60 | 0;
        formatDate.push(mins);
        last -= mins * 60;
        formatDate.push(last);
        let ret = '';
        formatDate.forEach((num, i) => {
            if (num > 0) {
                let twoDig = i === 0 ? num : ('0' + num).slice(-2);
                ret += twoDig + gap[i];
            }
        });
        return ret;
    }

    /**
     * @param key id或物品名
     * @param m1 id->name map
     * @param m2 name->item map
     */
    public getItemByIdOrName(key: string, m1, m2: {
        [k: string]: Partial<InventoryItemInfo>
    }): Partial<InventoryItemInfo> {
        return m1[key] ? m2[m1[key]] : m2[key];
    }
}

export const CommonUtilsKey = Symbol('CommonUtilsKey') as InjectionKey<CommonUtils>;
