/**
 * @file Container组件
 * @author dongtiancheng
 */
import * as React from 'react';
import { isEmpty, isObjectLike, clone, isNil, isObject, find, each, isPlainObject } from 'lodash';
import { BasicContainer, ContainerProps, getRuntimeContext } from './types';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { actionCreators, IContainerAction } from './action';
import { RootState } from '../../data/reducers';
import { DataProvider } from '../DataProvider/Controller';
import { ContainerPropsInterface } from '../../../components/Container/Container';
import { compileExpressionString, isExpression, parseExpressionString } from '../../util/vm';
import { DataCustomer } from '../DataCustomer/Controller';
import { createChild } from '../../util/createChild';

// First Init Life Circle:
// ComponentWillMount -> Render -> ComponentDidMount

// Component Update Life Circle:
// componentWillReceiveProps -> shouldComponentUpdate -> ComponentWillUpdate -> Render -> ComponentDidMount
export class Container extends BasicContainer<ContainerProps, {}> {
    static WrappedComponent: string;
    static displayName: string;

    private dataProvider: DataProvider;
    private dataCustomer: DataCustomer;

    constructor(props: ContainerProps) {
        super(props);
        this.dataProvider = new DataProvider();
        this.dataCustomer = new DataCustomer();
    }

    async componentWillMount() {
        let info = this.getPropsInfo(this.props.info, this.props, ['children', 'data', 'dataCustomer', 'dataProvider']);
        if (info.model) {
            if (isEmpty(info.data)) {
                info.data = {
                    $loading: false
                };
            }

            const defaultCustomer = {
                mode: 'pass',
                name: '$SELF_PASS_CUSTOMER',
                config: {
                    model: this.props.info.model,
                    assign: {}
                }
            };

            if (info.dataCustomer) {
                info.dataCustomer.customers.unshift(defaultCustomer);
            } else {
                info.dataCustomer = {
                    customers: [defaultCustomer],
                    groups: []
                };
            }
            this.dataCustomer.initCustomerConfig(info.dataCustomer);

            // to keep it safe, this.props.info should be readonly
            Object.freeze(this.props.info);

            const providerActions = {
                setDataList: this.props.setDataList,
                asyncLoadDataProgress: this.props.asyncLoadDataProgress,
                asyncLoadDataSuccess: this.props.asyncLoadDataSuccess,
                asyncLoadDataFail: this.props.asyncLoadDataFail,
                syncLoadDataSuccess: this.props.syncLoadDataSuccess,
                syncLoadDataFail: this.props.syncLoadDataFail
            };

            // 用于初始化的内置Provider
            const initProvider = {
                mode: 'init',
                config: info.data,
                __previousConfig: null
            };

            this.dataProvider.requestForData([initProvider], providerActions, info, this.props, this.context);
        } else {
            console.error('Container Component Should have model property');
        }
    }

    componentWillReceiveProps(nextProps: ContainerProps) {
        let info = this.getPropsInfo(this.props.info, this.props, ['children', 'data', 'dataCustomer', 'dataProvider']);
        const providerActions = {
            setDataList: this.props.setDataList,
            asyncLoadDataProgress: this.props.asyncLoadDataProgress,
            asyncLoadDataSuccess: this.props.asyncLoadDataSuccess,
            asyncLoadDataFail: this.props.asyncLoadDataFail,
            syncLoadDataSuccess: this.props.syncLoadDataSuccess,
            syncLoadDataFail: this.props.syncLoadDataFail
        };

        let dataProvider = this.props.info.dataProvider;
        if (dataProvider) {
            this.dataProvider.requestForData(dataProvider, providerActions, info, nextProps, this.context);
        }
    }

    componentWillUnmount() {
        let info = this.getPropsInfo(this.props.info, this.props, ['children', 'data', 'dataCustomer', 'dataProvider']);
        if (info.model) {
            this.props.clearData({
                model: info.model
            });
        }
    }

    // shouldComponentUpdate(nextProps: ContainerProps) {
    //     // 判断Container数据是否有更新.
    //     return isEqual(this.props.$data, nextProps.$data);
    // }

    render() {
        let info = this.getPropsInfo(this.props.info, this.props, ['children', 'data', 'dataCustomer', 'dataProvider']);

        if (!info.model) {
            return <div className="err-text">model should defined in container like components</div>;
        }

        if (!info.children) {
            return <div className="err-text">children props must be specific in Container Component</div>;
        }

        const setData = (name: string, value: any) => {
            if (isObjectLike(value)) {
                value = clone(value);
            }

            if (info.bind && info.bind instanceof Array) {
                let bindTarget = find(info.bind, o => o.child === name);

                if (bindTarget && this.props.$setData) {
                    this.props.$setData(bindTarget.parent, value);
                }
            }

            this.props.setData({
                name: name,
                value: value
            }, info.model);
        };

        const setMultiData = (items: { name: string, value: any }[]) => {
            items = items.map(item => {
                if (isObjectLike(item.value)) {
                    item.value = clone(item.value);
                }

                return item;
            });

            if (info.bind && info.bind instanceof Array) {
                let bindItems = items
                    .filter(i => {
                        let bindTarget = find(info.bind, f => f.child === i.name);

                        if (bindTarget) {
                            i.name = bindTarget.parent;
                        }

                        return !bindTarget;
                    });

                if (bindItems.length > 0 && this.props.$setMultiData) {
                    this.props.$setMultiData(bindItems);
                }
            }

            this.props.setMultiData(items, info.model);
        };

        const $deleteData = (name: string) => {
            if (info.bind && info.bind instanceof Array) {
                let bindTarget = find(info.bind, o => o.child === name);

                if (bindTarget && this.props.$deleteData) {
                    this.props.$deleteData(bindTarget.parent);
                }
            }

            this.props.deleteData({name}, info.model);
        };

        // Container Component no long compile expression string for child
        // instead, AbstractComponent should compile it by themSelf
        let childElements = info.children.map((child, index) => {
            child = this.getPropsInfo(child, this.props, [], false, ['show', 'hidden']);
            let childElement = createChild(child, {
                ...this.props,
                info: child,
                model: info.model,
                $data: this.props.$data,
                $parent: this.props.$parent,
                options: this.props.options,
                dataCustomer: this.dataCustomer,
                $index: this.props.$index,
                $item: this.props.$item,
                $setData: setData,
                $deleteData: $deleteData,
                $setMultiData: setMultiData,
                key: `${child.type}_${index}`
            });

            return this.renderChildren(child, childElement);
        });

        const containerStyle = {
            ...info.style,
            width: '100%'
        };

        return (
            <div className={'rcre-container ' + (info.className || '')} style={containerStyle}>
                {childElements}
            </div>
        );
    }
}

const mapStateToProps = (state: RootState, ownProps: ContainerPropsInterface) => {
    let runTime = getRuntimeContext(ownProps, {});
    // direct compile
    let info = compileExpressionString(ownProps.info, runTime,
        ['children', 'data', 'dataCustomer', 'dataProvider'], false);

    return {
        $data: state.container[info.model] || {}
    };
};

const mapDispatchToProps = (dispatch: Dispatch<IContainerAction>) => bindActionCreators({
    setData: actionCreators.setData,
    setDataList: actionCreators.setDataList,
    setMultiData: actionCreators.setMultiData,
    clearData: actionCreators.clearData,
    deleteData: actionCreators.deleteData,
    asyncLoadDataProgress: actionCreators.asyncLoadDataProgress,
    asyncLoadDataSuccess: actionCreators.asyncLoadDataSuccess,
    asyncLoadDataFail: actionCreators.asyncLoadDataFail,
    syncLoadDataSuccess: actionCreators.syncLoadDataSuccess,
    syncLoadDataFail: actionCreators.syncLoadDataFail
}, dispatch);

/**
 * 执行container组件嵌套的逻辑
 *
 * @param stateProps 当前container组件的mapStateToProps返回的对象
 * @param dispatchProps 当前container组件的mapDispatchToProps返回的对象
 * @param parentProps connect组件接收到的props
 * @return 当前container组件的props
 */
export const mergeProps =
    (stateProps: {
        $data: Object
    }, dispatchProps: ContainerProps, ownProps: ContainerPropsInterface): ContainerProps => {
        // 启用兼容模式， 父级优先
        if (ownProps.options && ownProps.options.oldNestContainerCompatible) {
            let parentProps = ownProps.$data || {};
            let stateData = stateProps.$data;
            if (isObject(ownProps.info.parentMapping)) {
                let runTime = getRuntimeContext(ownProps, {});
                let parentMappingRet = compileExpressionString(ownProps.info.parentMapping, {
                    ...runTime,
                    $parent: parentProps,
                    $data: stateProps.$data
                });
                if (parentMappingRet) {
                    let originalData = stateProps.$data;
                    Object.assign(stateData, originalData);
                }
            } else {
                // 屏蔽$loading属性
                delete parentProps['$loading'];
                Object.assign(stateData, parentProps);
            }
            return Object.assign({}, ownProps, stateProps, dispatchProps, {
                $data: stateData
            });
        }

        // 子级优先
        let parentData = ownProps.$data;

        if (ownProps.$parent && parentData) {
            parentData['$parent'] = ownProps.$parent;
        }

        if (parentData && !isEmpty(parentData) && ownProps.info.props) {
            let runTime = getRuntimeContext(ownProps, {});

            each(ownProps.info.props, (val: any, name) => {
                // priority == parent
                if (typeof val === 'string' && isExpression(val)) {
                    let result = parseExpressionString(val, {
                        ...runTime,
                        $parent: parentData
                    });

                    if (isNil(result)) {
                        return;
                    }

                    stateProps.$data[name] = result;
                    return;
                }

                if (isPlainObject(val)) {
                    let prop = val.prop;
                    let priority = val.priority;

                    // priority === child and nothing
                    if (typeof prop !== 'string' || !isExpression(prop)) {
                        return;
                    }

                    let result = parseExpressionString(prop, {
                        ...runTime,
                        $parent: parentData
                    });

                    if (isNil(result)) {
                        return;
                    }

                    // 字级优先，而字级又没有值的时候，就给个初始值
                    if (priority === 'child') {
                        if (!(name in stateProps.$data)) {
                            stateProps.$data[name] = result;
                        }

                        return;
                    }

                    stateProps.$data[name] = result;
                }
            });
        }

        return Object.assign({}, ownProps, stateProps, dispatchProps, {
            $parent: parentData
        });
    };

export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Container);
