import {reactive} from 'vue'
import {createGlobalState, hasOwn} from "@vueuse/core";
import {empty, func} from "@/uni_modules/uv-ui-tools/libs/function/test";
import {sleep} from "@/uni_modules/uv-ui-tools/libs/function";
import {getSizeOfObject} from "@/utils";
import _ from 'lodash';

function CacheResult(data, options) {
    if (data && hasOwn(data, 'dataSize')) {
        // 如果对象内部有循环钩子，getSizeOfObject无法计算，则预先缓存大小
        this.data = data.data
        this.dataSize = data.dataSize
    } else {
        this.data = data
    }
    this.expires = options?.timeout ? (options.timeout + Date.now()) : null
    this.obsoleted = () => {
        return this.expires && this.expires < Date.now()
    }
}

export const useCacheStore = createGlobalState(() => {
    // data
    const container = reactive(new Map())
    const defaultOptions = {
        timeout: 2 * 3600 * 1000, // default cache 2 hours
        emptyIgnore: false // default cache empty values
    }

    // hooks

    // functions
    const checkActionDefine = (action) => {
        if (!action) throw new Error('action is required')
        if (func(action)) {
            return {
                type: action.toString(),
                handler: async (...args) => await action(...args)
            }
        } else {
            if (!action.type) throw new Error('action.type is required')
            if (!action.handler) throw new Error('action.handler is required')
            if (!func(action.handler)) throw new Error('action.handler must be a function')
        }
        return action
    }
    const generateCacheKey = function (payload) {
        return JSON.stringify({payload})
    }
    const executeAction = async (action, payload) => {
        let result = action.handler(payload)
        if (result instanceof Promise) result = await result
        return result
    }

    /*
    * 获取缓存结果。
    * 虽然改进后缓存动作传入function也可以，但用defineCacheAction的方式调试时更直观。
    * @param {Object|Function} action 缓存动作。action是Function时尤其要注意用payload区隔结果！
    * @param {Object} payload 缓存参数，区隔缓存结果的关键！
    * @param {Object} options 缓存选项
    * @returns {Object} 缓存结果
    * */
    const dispatchCache = async (action, payload = undefined, options = undefined) => {
        // TODO:并发锁, 并发机会不高且没有致命性影响，以后再优化
        const formatAction = checkActionDefine(action)
        let typeNode = container.get(formatAction.type)
        if (!typeNode) {
            typeNode = new Map()
            container.set(formatAction.type, typeNode)
        }
        const key = generateCacheKey(payload)
        let cacheData = typeNode.get(key)

        // fire cached result
        if (cacheData instanceof CacheResult && !cacheData.obsoleted()) {
            // call `sleep` to simulate api reaction.
            // NOTE: 这个模拟对于mx-condition.config.isImmediate有关键作用，否则需要对缓存和API实时做不同配置
            await sleep(0)
            return cacheData.data
        }
        // cache miss, execute action and cache result
        const result = await executeAction(formatAction, payload)
        const cacheOptions = _.merge(defaultOptions, options)
        cacheData = new CacheResult(result, cacheOptions)
        if (!(empty(result) && cacheOptions.emptyIgnore)) {
            typeNode.set(key, cacheData)
        }

        return cacheData.data
    }

    /*
    * 是否存在缓存
    * @param {Object} action 缓存动作
    * @param {Object} payload 缓存参数
    * @returns {Boolean} 是否存在缓存
    * */
    const hasCache = (action, payload) => {
        const formatAction = checkActionDefine(action)
        const typeNode = container.get(formatAction.type)
        if (!typeNode) return false
        const key = generateCacheKey(payload)
        return typeNode.has(key)
    }

    /*
    * 删除缓存
    * @param {Object} action 缓存动作
    * @param {Object} payload 缓存参数
    * */
    const removeCache = (action, payload = null) => {
        if (payload === null || payload === undefined) {
            clearCache(action)
            return;
        }

        const formatAction = checkActionDefine(action)
        const typeNode = container.get(formatAction.type)
        if (!typeNode) return
        const key = generateCacheKey(payload)
        typeNode.delete(key)
        if (!typeNode.size) container.delete(formatAction.type)
    }

    /*
    * 清理缓存
    * @param {Object} action 缓存动作，action = null时清空所有缓存
    * */
    const clearCache = (action = null) => {
        if (!action) {
            container.clear()
            return
        }
        const formatAction = checkActionDefine(action)
        container.delete(formatAction.type)
    }

    /*
    * 清理过期缓存
    * */
    const gcCache = () => {
        if (!container.size) return
        console.log('run useCacheStore: gc')
        for (const [type, typeNode] of container.entries()) {
            for (const [key, cacheData] of typeNode.entries()) {
                if (cacheData.obsoleted()) {
                    typeNode.delete(key)
                    console.log('run useCacheStore: gc: remove cache', type, key)
                }
            }
            if (!typeNode.size) {
                container.delete(type)
                console.log('run useCacheStore: gc: remove type', type)
            }
        }
    }

    const getSize = function () {
        // then calculate
        let total = 0
        container.forEach(typeNode => {
            typeNode.forEach(i => {
                if (hasOwn(i, 'dataSize')) {
                    total += i.dataSize
                } else {
                    total += getSizeOfObject(i)
                }
            })
        })
        return total
    }

    // window.cacheContainer = container
    return {
        container, // open for test
        dispatchCache,
        hasCache,
        removeCache,
        clearCache,
        gcCache,
        getSize
    }
})
