/*
* Enums
*/
const STAGES = Object.freeze({
    DEV: "development",
    QA: "quality_assurance",
    RELEASE: "release",
});

const TYPES = Object.freeze({
    VERBOSE: "(V)",
    INFO: "(I)",
    DEBUG: "(D)",
    WARN: "(W)",
    ERROR: "(E)",
});

class LogUtil {
    /**
      * @param {enum} stageEnum - 项目状态: DEV, QA, RELEASE
      * @param {number} maxLogQueueSize - 日志队列最大限制
      * @param {number} clearSizePerTime - 日志队列一次清除的长度
      */
    constructor(stageEnum = STAGES.DEV, maxLogQueueSize = 1000, clearSizePerTime = 200) {
        this._verifyValidQueueSize(maxLogQueueSize);
        this._maxLogQueueSize = maxLogQueueSize;

        this._verifyValidClearSize(clearSizePerTime);
        this._clearSizePerTime = clearSizePerTime;

        this._verifyValidStageEnum(stageEnum);
        this._currentState = stageEnum;

        this._issuePipes = {};
        this._logQueue = [];

        this._shouldDropingAnyLogs = false;
    }

    /*
     * Public methods
    */

    /**
     * 输出详细日志
     * @param {string} format -  格式化字符串, 仅支援 %s 格式字元符
     * @param {object[]|string[]} args - 相应于格式字元符的变量
     */
    v(format, ...args) {
        this._issueLogWithType(TYPES.VERBOSE, format, ...args);
    }

    /**
     * 输出提示性消息日志
     * @param {string} format -  格式化字符串, 仅支援 %s 格式字元符
     * @param {object[]|string[]} args - 相应于格式字元符的变量
     */
    i(format, ...args) {
        this._issueLogWithType(TYPES.INFO, format, ...args);
    }

    /**
     * 输出除错日志
     * @param {string} format -  格式化字符串, 仅支援 %s 格式字元符
     * @param {object[]|string[]} args - 相应于格式字元符的变量
     */
    d(format, ...args) {
        this._issueLogWithType(TYPES.DEBUG, format, ...args);
    }

    /**
     * 输出警告日志
     * @param {string} format -  格式化字符串, 仅支援 %s 格式字元符
     * @param {object[]|string[]} args - 相应于格式字元符的变量
     */
    w(format, ...args) {
        this._issueLogWithType(TYPES.WARN, format, ...args);
    }

    /**
     * 输出错误日志
     * @param {string} format -  格式化字符串, 仅支援 %s 格式字元符
     * @param {object[]|string[]} args - 相应于格式字元符的变量
     */
    e(format, ...args) {
        this._issueLogWithType(TYPES.ERROR, format, ...args);
    }

    /**
     * 新增 Log 管道, Log 在打印的同时也会流向这些管道
     * @param {handler} pipeHandler - 管道处理器
     * @returns {string} - 管道编号, 可以用来移除管道
     */
    addPipe(pipeHandler) {
        const uniqKey = `${new Date().getTime().toString()}${Object.keys(this._issuePipes).length}`;
        this._issuePipes[uniqKey] = pipeHandler;
        return uniqKey;
    }

    /**
     * 移除管道
     * @param {string} pipeId - 管道编号
     */
    removePipe(pipeId) {
        delete this._issuePipes[pipeId];
    }

    /**
     * 移除全部管道
     */
    removeAllPipe() {
        this._issuePipes = {};
    }

    /**
     * 抽出移除最古老的一笔 Log
     * @returns {string} - 最古老的一笔 Log
     */
    popOldestLog() {
        return this._logQueue.shift();
    }

    /**
     * 读出最古老的一笔 Log
     * @returns {string} - 最古老的一笔 Log
     */
    getOldestLog() {
        return this._logQueue[0];
    }

    /**
     * 移除最古老的一笔 Log
     */
    removeOldestLog() {
        this.popOldestLog();
    }

    dropAnyLogs(shouldDroping) {
        this._shouldDropingAnyLogs = shouldDroping;
    }

    /**
     * Private methods
     */

    _issueLogWithType(type, format, ...args) {
        if (!this._decideIssuable(type)) return;
        const logString = this._generateLogString(type, format, ...args);
        this._pushToLogQueue(logString);
        this._issueToConsole(logString);
        this._issueToPipes(logString, format, ...args);
    }

    _generateLogString(type, format, ...args) {
        const dateString = this._generateDateString();
        const messageString = typeof format === "object"
            ? format
            : this._generateMessageString(format, ...args);
        return `${dateString}: ${type}/${messageString}`;
    }

    _generateDateString() {
        const d = new Date();
        const month = Number.parseInt(d.getMonth().toString().padStart(2, "0"), 10) + 1;
        const day = d.getDate().toString().padStart(2, "0");
        const hours = d.getHours().toString().padStart(2, "0");
        const minutes = d.getMinutes().toString().padStart(2, "0");
        const seconds = d.getSeconds().toString().padStart(2, "0");
        const milliSeconds = d.getMilliseconds().toString().padStart(3, "0");
        return `[${month}-${day} ${hours}:${minutes}:${seconds}.${milliSeconds}]`;
    }

    _generateMessageString(format, ...args) {
        let i = 0;
        let formatStr = format ? format : `${format}`;
        return formatStr.replace(/%s/g, () => {
            let arg = args[i++];
            if (typeof arg === "object") return arg;
            return `${arg}`;
        });
    }

    _decideIssuable(type) {
        this._verifyValidTypeEnum(type);
        this._verifyValidStage();

        switch (this._currentState) {
            case STAGES.DEV:
                return !this._shouldDropingAnyLogs

            case STAGES.QA:
                return !this._shouldDropingAnyLogs

            case STAGES.RELEASE:
                return TYPES.ERROR === type
                        || TYPES.WARN === type
                        || TYPES.INFO === type;

            default:
                return false;
        }
    }

    _pushToLogQueue(logString) {
        this._logQueue.push(logString);
        this._clearLogQueueIfNeeded();
    }

    _issueToConsole(logString) {
        window.console.log(`[CONSOLE] ${logString}`);
    }

    _issueToPipes(logString, format, ...args) {
        Object.values(this._issuePipes).forEach(cb => cb(logString, format, ...args));
    }

    _clearLogQueueIfNeeded() {
        if (this._logQueue.length >= this._maxLogQueueSize) {
            this._logQueue.splice(0, this._clearSizePerTime);
        }
    }

    _verifyValidQueueSize(maxLogQueueSize) {
        if (maxLogQueueSize < 1) throw new Error("LogUtil: maxLogQueueSize must be greater than zero.");
        this._verifyValidStageEnum(this._currentState);
    }

    _verifyValidClearSize(clearSizePerTime) {
        if (clearSizePerTime > this._maxLogQueueSize) throw new Error("LogUtil: clearSizePerTime must less than or equal to maxLogQueueSize.");
    }

    _verifyValidStage() {
        if (!this._currentState) throw new Error("LogUtil: this._currentState = null. Please initWithStage first.");
        this._verifyValidStageEnum(this._currentState);
    }

    _verifyValidStageEnum(stageEnum) {
        if (!STAGES.valueOf(stageEnum)) throw new Error(`LogUtil: unknown stageEnum: ${stageEnum}`);
    }

    _verifyValidTypeEnum(typeEnum) {
        if (!TYPES.valueOf(typeEnum)) throw new Error(`LogUtil: unknown typeEnum: ${typeEnum}`);
    }
}

export { LogUtil, STAGES, TYPES };
