import { pick, debounce } from "../helpers/util";
import { queryToState, stateToQuery, IQueryParser, IQueryStringify } from "../helpers/convert";
import { filterQuery } from "../helpers/url";
import { isArray, isObject } from "../helpers/type";
import { deepEqual } from "../helpers/deepEqual";

export function syncQueryCb(callbackDeps?:string[]) {
    return function (target: any, propertyKey: string) {
        target.callbackName = propertyKey;
        if (isArray(callbackDeps) && callbackDeps.length > 0) {
            target.callbackDeps = callbackDeps;
        }
    };
}

const __SYNC_QUERY_DIFF_IGNORE__ = '__SYNC_QUERY_DIFF_IGNORE__';
const __SYNC_QUERY_CALLBACK_IGNORE__ = '__SYNC_QUERY_CALLBACK_IGNORE__';

export function SyncQueryFactory(stateList: string[], callbackName?:string, config?:SyncQueryConfig) {
    return function(WrappedComponent) {
        return syncQueryHOC(WrappedComponent, stateList, callbackName, config);
    }
}

type SyncQueryConfig = {
    wait?: number,                          // 函数防抖的等待时间， 单位 ms
    callbackDeps?: string[],                // callbackDeps 存放 state key 的数组，监听到 state 中对应key 的 value 变化时，会调用 callback（网络请求等）
                                            // PS: callbackDeps 没有传入时，默认监听的内容等于 stateList
    parser?: IQueryParser,                  // 解析器：用于将路由参数 query 解析到 state，默认是 JSON.parse
    stringify?: IQueryStringify,            // 生成器：用于生成 state 对应的 query 字符串，默认是 JSON.stringify
    disableAutoSync?: boolean,              // 是否关闭自动同步
}

export interface SyncQueryHost {
    triggerSync
}

/**
 * syncQueryHOC
 * @param WrappedComponent 
 * @param stateList states are observed
 * @param callbackName callbackName would be called when state difference is detected
 * @param config SyncQueryConfig
 */
export function syncQueryHOC(WrappedComponent, stateList: string[], callbackName?:string, config?:SyncQueryConfig) : any{    
    if (!isObject(config)) {
        config = {
            wait: 300,
        }
    } else {
        config = {
            wait: 300,
            ...config,
        }
    }
    return class Enhancer extends WrappedComponent {
        private prevStateCache = {};
        constructor(param) {
            super(param);
            this.state = {
                ...this.state,
                ...this.getStateFromURL(stateList),
            }
            this.reBindCallback(false, true);
            this.prevStateCache = this.state;
            if (config.disableAutoSync === true) {
                this.triggerSync = () => {
                    this.stateDiffEffect(this.state);
                }
            }
            this.stateDiffEffect = debounce(this.stateDiffEffect, config.wait).bind(this);
        }
        private getStateFromURL(stateList:string[]) {
            const query = location.href.split('?')[1];
            if (query == null) {
                return;
            }
            return queryToState(query, stateList, config.parser);
        }
        private syncStateToURL(state:Object) {
            const [locationAddress, oldQuery] = location.href.split('?');
            const restQuery = filterQuery(oldQuery, (key, value) => (stateList.indexOf(key) === -1))
            const query =  stateToQuery(state, config.stringify);
            const href = `${locationAddress}?${query}&${restQuery}`;
            location.href = href;
        }
        private reBindCallback(diffIgnore:boolean = true, callBackIgnore?:boolean) {
            this.callbackName = this.callbackName || callbackName;            
            if (this.callbackName == null) {
                return;
            }
            if (typeof super[this.callbackName] !== 'function') {
                console.error('sync-query: callback must be react component method name!!! Tips:  SyncQueryFactory and syncQueryHOC must be closest with Component Class');
                return;
            }
            const clone = Object.create(this);
            clone.setState = (updater, callback) => {
                this.setState(updater, callback, diffIgnore, callBackIgnore);
            }
            // super[callbackName] is super.prototype[callbackName], so it is not bound with _super.
            this[this.callbackName] = super[this.callbackName].bind(clone);
            this.callbackDeps = this.callbackDeps || config.callbackDeps;
        }
        componentDidMount() {
            const result = super.componentDidMount && super.componentDidMount();
            this.reBindCallback();
            return result;
        }
        componentDidUpdate(prevProps, prevState) {
            if (this.state[__SYNC_QUERY_DIFF_IGNORE__] === true || config.disableAutoSync === true) {
                return (
                    super.componentDidUpdate &&
                    super.componentDidUpdate(prevProps, prevState)
                );
            }
            this.stateDiffEffect(this.state);
            return (
                super.componentDidUpdate &&
                super.componentDidUpdate(prevProps, prevState)
            );
        }
        private stateDiffEffect(state) {
            const prevState = this.prevStateCache;
            if (prevState == null && state == null) {
                console.error('sync-query: stateDiffEffect could not be null');
                return;
            }
            // stateList diff
            const pickedPrevState = pick(prevState, stateList);
            const pickedState = pick(state, stateList);

            const isDiff = !deepEqual(pickedPrevState, pickedState);
            if (isDiff) {
                this.syncStateToURL(pickedState);
            }

            if (this.state[__SYNC_QUERY_CALLBACK_IGNORE__] === true) {
                console.warn('Ingore: sync-query: auto trigger callback，only after ComponentDidMount');
                return;
            }
            // callbackDeps diff
            const callbackDeps = this.callbackDeps;
            if (callbackDeps == null) {
                isDiff && this[this.callbackName] && typeof this[this.callbackName] === 'function' && this[this.callbackName]();
            } else {
                const pickedPrevState = pick(prevState, callbackDeps);
                const pickedState = pick(state, callbackDeps);
                const isDiff = !deepEqual(pickedPrevState, pickedState);
                isDiff && this[this.callbackName] && typeof this[this.callbackName] === 'function' && this[this.callbackName]();
            }
            this.prevStateCache = state;
        }
        setState(updater, callback?:() => void, diffIgnore?:boolean, callBackIgnore?:boolean) {
            // Ref: https://zh-hans.reactjs.org/docs/react-component.html#setstate
            if (typeof updater === 'object') {
                return super.setState(
                    {
                        ...updater,
                        [__SYNC_QUERY_DIFF_IGNORE__]: diffIgnore,
                        [__SYNC_QUERY_CALLBACK_IGNORE__]: callBackIgnore,
                    },
                    callback,
                );
            }
            if (typeof updater === 'function') {
                return super.setState(
                    function (state, props) {
                        return {
                            ...updater(state, props),
                            [__SYNC_QUERY_DIFF_IGNORE__]: diffIgnore,
                            [__SYNC_QUERY_CALLBACK_IGNORE__]: callBackIgnore,
                        }
                    },
                    callback,
                );
            }
            return super.setState(updater, callback);
        }
    }
}
