/**
 * ------------------------------------
 * File: useVuex.ts
 * Project: ~/vueProject
 * Created Date: 2021-07-16  15:14:15
 * Author: LiuQixuan(liuqixuan@hotmail.com)
 * -----
 * Last Modified:  2021-09-29  1:26:27
 * Modified By: LiuQixuan
 * -----
 * Copyright 2021 - 2021 AIUSoft by LiuQixuan
 * ------------------------------------
 */


import { computed, inject, InjectionKey } from 'vue';
import { Store, StoreOptions, GetterTree } from 'vuex'

export interface DispatchOptions {
    root?: boolean;
}

export interface CommitOptions {
    silent?: boolean;
    root?: boolean;
}

type mapDict = Record<string, string> | Array<string>

export interface Payload {
    type: string;
}

export interface Dispatch {
    (type: string, payload?: any, options?: DispatchOptions): Promise<any>;
    <P extends Payload>(payloadWithType: P, options?: DispatchOptions): Promise<any>;
}

export interface Commit {
    (type: string, payload?: any, options?: CommitOptions): void;
    <P extends Payload>(payloadWithType: P, options?: CommitOptions): void;
}

type Computed = () => any;
type InlineComputed<T extends Function> = T extends (...args: any[]) => infer R ? () => R : never
type MutationMethod = (...args: any[]) => void;
type ActionMethod = (...args: any[]) => Promise<any>;
type InlineMethod<T extends (fn: any, ...args: any[]) => any> = T extends (fn: any, ...args: infer Args) => infer R ? (...args: Args) => R : never

interface Mapper<R> {
    <Key extends string>(map: Key[]): { [K in Key]: R };
    <Map extends Record<string, string>>(map: Map): { [K in keyof Map]: R };
}

interface MapperWithNamespace<R> {
    <Key extends string>(namespace: string, map: Key[]): { [K in Key]: R };
    <Map extends Record<string, string>>(namespace: string, map: Map): { [K in keyof Map]: R };
}

interface MapperForState {
    <S, Map extends Record<string, (store: StoreOptions<S>, state: S, getters: any) => any> = {}>(
        map: Map
    ): { [K in keyof Map]: InlineComputed<Map[K]> };
}

interface MapperForStateWithNamespace {
    <S, Map extends Record<string, (store: StoreOptions<S>, state: S, getters: any) => any> = {}>(
        namespace: string,
        map: Map
    ): { [K in keyof Map]: InlineComputed<Map[K]> };
}

interface MapperForAction {
    <S, Map extends Record<string, (store: StoreOptions<S>, dispatch: Dispatch, ...args: any[]) => any>>(
        map: Map
    ): { [K in keyof Map]: InlineMethod<Map[K]> };
}

interface MapperForActionWithNamespace {
    <S, Map extends Record<string, (store: StoreOptions<S>, dispatch: Dispatch, ...args: any[]) => any>>(
        namespace: string,
        map: Map
    ): { [K in keyof Map]: InlineMethod<Map[K]> };
}

interface MapperForMutation {
    <S, Map extends Record<string, (store: StoreOptions<S>, commit: Commit, ...args: any[]) => any>>(
        map: Map
    ): { [K in keyof Map]: InlineMethod<Map[K]> };
}

interface MapperForMutationWithNamespace {
    <S, Map extends Record<string, (store: StoreOptions<S>, commit: Commit, ...args: any[]) => any>>(
        namespace: string,
        map: Map
    ): { [K in keyof Map]: InlineMethod<Map[K]> };
}


interface UseStoreHelpers {
    useState: UseState
    useGetters: UseGetters
    useMutations: UseMutations
    useActions: UseActions
}

type UseState = Mapper<Computed>
    | MapperWithNamespace<Computed>
    | MapperForState
    | MapperForStateWithNamespace;

type UseMutations = Mapper<MutationMethod>
    | MapperWithNamespace<MutationMethod>
    | MapperForMutation
    | MapperForMutationWithNamespace;
    
type UseGetters = Mapper<Computed>
    | MapperWithNamespace<Computed>;

type UseActions = Mapper<ActionMethod>
    | MapperWithNamespace<ActionMethod>
    | MapperForAction
    | MapperForActionWithNamespace;


/**
 * Determine if the parameter is an object.
 * @param {*} obj 
 * @returns {Boolean}
 */
function isObject(obj: any): boolean {
    return obj !== null && typeof obj === 'object'
}
/**
 * Validate whether given map is valid or not
 * @param {*} map
 * @return {Boolean}
 */
function isValidMap(map: any): boolean {
    return Array.isArray(map) || isObject(map);
}
/**
 * Normalize the map
 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
 * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
 * @param {Array|Object} map
 * @return {Object}
 */
function normalizeMap(map: Array<any> | Record<string | number, string | ((...arg: Array<any>) => any)>): Array<{ key: string, val: string | ((...arg: Array<any>) => any) }> {
    if (!isValidMap(map)) {
        return [];
    }
    return Array.isArray(map)
        ? map.map((key: string) => ({ key, val: key }))
        : Object.keys(map).map(key => ({ key, val: map[key] }));
}
/**
 * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map.
 * @param {Function} fn
 * @return {Function}
 */
function normalizeNamespace(fn: (namespace: string | mapDict, map?: mapDict) => any): (namespace: string, map?: mapDict) => any {
    return function (namespace, map) {
        if (typeof namespace !== 'string') {
            map = namespace
            namespace = ''
        } else if (namespace.charAt(namespace.length - 1) !== '/') {
            namespace += '/'
        }
        return fn(namespace, map)
    }
}
/**
 * Search a special module from store by namespace. if module not exist, print error message.
 * @param {Object} store
 * @param {String} helper
 * @param {String} namespace
 * @return {Object}
 */
function getModuleByNamespace(store: any, helper: string, namespace: string) {
    const module = store._modulesNamespaceMap[namespace];
    if ((process.env.NODE_ENV !== 'production') && !module) {
        console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`);
    }
    return module;
}
/**
 * Merge arg and function return new function with args.
 * @param {Function} fn 
 * @param {*} arg 
 * @returns 
 */
function partial(fn: (...arg: Array<any>) => any, ...arg: Array<any>) {
    return function (...args: Array<any>) {
        return fn(...arg, ...args)
    }
}
/**
 * Reduce the code which written in Vue.js for getting the state.
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it.
 * @return {Object}
 */
const _useState = <S>(store: Store<S>, namespace: string, states: Record<string, any> | Array<string>) => {
    const res: Record<string, any> = {};
    if ((process.env.NODE_ENV !== 'production') && !isValidMap(states)) {
        console.error('[vuex] useState: mapper parameter must be either an Array or an Object');
    }
    normalizeMap(states).forEach(({ key, val }) => {
        res[key] = computed(function mappedState() {
            let state = <Record<string, any>>store.state;
            let getters = store.getters;
            if (namespace) {
                const module = getModuleByNamespace(store, 'useState', namespace);
                if (!module) {
                    return;
                }
                state = module.context.state;
                getters = module.context.getters;
            }
            return typeof val === 'function'
                ? val(state, getters)
                : state[val];
        });
        // mark vuex state for devtools
        res[key].vuex = true
    });
    return res;
};
/**
 * Reduce the code which written in Vue.js for getting the getters
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} getters
 * @return {Object}
 */
const _useGetters = <S>(store: Store<S>, namespace: string, getters: Record<string, string> | Array<string>) => {
    const res: Record<string, any> = {};
    if ((process.env.NODE_ENV !== 'production') && !isValidMap(getters)) {
        console.error('[vuex] useGetters: mapper parameter must be either an Array or an Object');
    }
    normalizeMap(getters).forEach(({ key, val }) => {
        // The namespace has been mutated by normalizeNamespace
        val = namespace + val;
        res[key] = computed(function mappedGetter() {
            if (namespace && !getModuleByNamespace(store, 'useGetters', namespace)) {
                return;
            }
            let getterTree = store.getters as GetterTree<Record<string, any>, Record<string, any>>
            if ((process.env.NODE_ENV !== 'production') && !((val as string) in getterTree)) {
                console.error(`[vuex] unknown getter: ${val}`);
                return;
            }
            return getterTree[(val as string)];
        });
        // mark vuex getter for devtools
        res[key].vuex = true
    });
    return res;
};
/**
 * Reduce the code which written in Vue.js for committing the mutation
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept anthor params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function.
 * @return {Object}
 */
const _useMutations = <S>(store: Store<S>, namespace: string, mutations: Record<string, any> | Array<string>) => {
    const res: Record<string, any> = {};
    if ((process.env.NODE_ENV !== 'production') && !isValidMap(mutations)) {
        console.error('[vuex] useMutations: mapper parameter must be either an Array or an Object');
    }
    normalizeMap(mutations).forEach(({ key, val }) => {
        res[key] = function mappedMutation(...args: Array<any>) {
            // Get the commit method from store
            let commit = store.commit;
            if (namespace) {
                const module = getModuleByNamespace(store, 'useMutations', namespace);
                if (!module) {
                    return;
                }
                commit = module.context.commit;
            }
            return typeof val === 'function'
                ? val.apply(this, [commit].concat(args))
                : commit.apply(store, ([val].concat(args) as Array<any>));
        };
    });
    return res;
};
/**
 * Reduce the code which written in Vue.js for dispatch the action
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function.
 * @return {Object}
 */
const _useActions = <S>(store: Store<S>, namespace: string, actions: Record<string, any> | Array<string>) => {
    const res: Record<string, any> = {};
    if ((process.env.NODE_ENV !== 'production') && !isValidMap(actions)) {
        console.error('[vuex] useActions: mapper parameter must be either an Array or an Object');
    }
    normalizeMap(actions).forEach(({ key, val }) => {
        res[key] = function mappedAction(...args: Array<any>) {
            // get dispatch function from store
            let dispatch = store.dispatch;
            if (namespace) {
                const module = getModuleByNamespace(store, 'useActions', namespace);
                if (!module) {
                    return;
                }
                dispatch = module.context.dispatch;
            }
            return typeof val === 'function'
                ? val.apply(this, [dispatch].concat(args))
                : dispatch.apply(store, ([val].concat(args) as Array<any>));
        };
    });
    return res;
};

/**
 * Get $store from current instance
 * @return {Store} vm.$store
 */

function useStore<S = any>(injectKey: InjectionKey<Store<S>> | string = "store"): Store<S> {
    return inject(injectKey) as Store<S>
};
/**
 * Use Vuex with composition api easily.
 * @param {String} namespace
 * @param {Store} store vm.$store
 */
function useNamespacedStore(namespace?: string): UseStoreHelpers {
    const store = useStore();
    let helpers: UseStoreHelpers
    // pre-specify initial arguments with store instance
    if (namespace === undefined) {
        helpers = {
            useState: normalizeNamespace(partial(_useState, store)),
            useGetters: normalizeNamespace(partial(_useGetters, store)),
            useMutations: normalizeNamespace(partial(_useMutations, store)),
            useActions: normalizeNamespace(partial(_useActions, store))
        }
    } else {
        helpers = {
            useState: partial(normalizeNamespace(partial(_useState, store)), namespace),
            useGetters: partial(normalizeNamespace(partial(_useGetters, store)), namespace),
            useMutations: partial(normalizeNamespace(partial(_useMutations, store)), namespace),
            useActions: partial(normalizeNamespace(partial(_useActions, store)), namespace)
        }
    }
    return helpers;
}

export { useStore, useNamespacedStore };
