<!-- @author ruiwang -->
<!-- @email ruiwang@yangqianguan.com -->
<!-- @date 2019-11-18 20:05:04 -->
<!-- @desc index.vue -->

<script>
import elementResizeDetectorMaker from 'element-resize-detector';
import {mapGetters} from 'vuex';

import DefType from '../../constant/def-type';
import getFormat from '../../util/format-map';
import {setStorage, getStorage} from '../../util/local-storage';
import {evalProp, setValue, pickValue, spreadProps, appendClass, spreadObjectKeys, merge} from '../../util/object';
import {isComponent, isStorageValuesEqual} from '../util';
import FieldChildForm from './component/field-child-form';
import FieldDate from './component/field-date';
import FieldTableForm from './component/field-table-form';
import FieldMap, {defaultPropsMap} from './constant/field-map';

const DefaultLabelCol = {span: 4};

export default {
    name: 'YqgSimpleForm',

    inject: {
        timezoneKey: {default: ''},
        isValidDef: {default: () => x => x},
        formErd: {default: () => () => null}
    },

    provide() {
        return {
            formErd: () => this.erd,
            setCustomInitialValue: (initialValue, forceUpdate = false) => {
                this.customInitialValues = {...this.customInitialValues, ...initialValue};
                if (forceUpdate && this.autoSearch) {
                    this.onReset();
                }
            }
        };
    },

    props: {
        ctx: {
            type: Object,
            default: null
        },
        title: {
            type: String,
            default: ''
        },
        confirmLabel: {
            type: String,
            default: 'common.confirm'
        },
        confirmProps: {
            type: Object,
            default: null
        },
        cancelLabel: {
            type: String,
            default: 'common.cancel'
        },
        resetLabel: {
            type: String,
            default: 'common.reset'
        },
        defaultValues: {
            type: Object,
            default: () => ({})
        },
        values: {
            type: Object,
            default: () => ({})
        },
        options: {
            type: Object,
            required: true
        },
        autoSearch: {
            type: Boolean,
            default: false
        },
        enterSubmit: {
            type: Boolean,
            default: true
        },
        autocomplete: {
            type: String,
            default: 'off'
        },
        layout: {
            type: String,
            default: ''
        },
        autoSaveKey: {
            type: String,
            default: ''
        },
        idx: {
            type: Number,
            default: null
        }
    },

    data() {
        return {
            form: this.$form.createForm(this),
            btnItemLabel: ' ',
            erd: null,
            customInitialValues: null,
            interval: null
        };
    },

    computed: {
        ...mapGetters(['timezone']),

        formLayout() {
            return this.layout || this.options.layout || 'inline';
        },

        colObj() {
            const {formLayout, options: {labelCol = DefaultLabelCol, wrapperCol}} = this;

            return formLayout === 'horizontal' ? {
                labelCol,
                wrapperCol: wrapperCol || {span: 24 - labelCol.span}
            } : {};
        },

        formOptions() {
            const {
                formLayout: layout,
                options: {
                    fieldDefs,
                    row = {type: 'flex', gutter: 10, justify: 'space-between'},
                    column = {span: 24},
                    commonItemProps: defItemProps = {},
                    ...rest
                },
                colObj
            } = this;
            const commonItemProps = {colon: false, ...defItemProps};
            const isValidDef = this.$app?.isValidDef || this.isValidDef;

            return {
                fieldDefs: fieldDefs.filter(def => def && isValidDef(def)),
                commonItemProps,
                row,
                column,
                formProps: {
                    layout,
                    ...rest,
                    ...colObj
                }
            };
        },

        initialValues() {
            const {
                values,
                defaultValues,
                customInitialValues,
                options: {defaultValues: optionDefaultValues}
            } = this;

            return {
                ...optionDefaultValues,
                ...defaultValues,
                ...spreadObjectKeys(customInitialValues),
                ...values
            };
        }
    },

    watch: {
        '$i18n.locale': function watchLocale() {
            if (this.erd) this.$nextTick(this.adjustBtnItemLabel);
        }
    },

    beforeMount() {
        const formErd = this.formErd();
        this.erd = formErd ? formErd : elementResizeDetectorMaker({strategy: 'scroll'});
        if (this.formLayout === 'inline') {
            this.$once('hook:mounted', () => {
                this.erd.listenTo(this.$el, this.adjustBtnItemLabel);
            });
            this.$once('hook:beforeDestroy', () => {
                if (this.erd) this.erd.uninstall(this.$el);
            });
        }
    },

    mounted() {
        const {autoSearch, autoSaveKey, $t, form, setStorageInteval} = this;
        if (autoSearch) this.onReset();

        if (autoSaveKey) {
            const storageVO = JSON.parse(getStorage(autoSaveKey));
            const {record} = this.getFormCtx();

            if (storageVO && !isStorageValuesEqual(storageVO, record)) {
                this.$confirm({
                    title: $t('common.saveTips'),
                    onOk() {
                        const realTimeVO = {...record, ...storageVO};

                        form.setFieldsValue(realTimeVO);

                        setStorageInteval();
                    },
                    onCancel() {
                        setStorageInteval();
                    }
                });
            } else {
                setStorageInteval();
            }
        }
    },

    deactivated() {
        if (this.interval) {
            clearInterval(this.interval);
        }
    },

    methods: {
        adjustBtnItemLabel() {
            const {$refs: {btnItem}} = this;
            let label = ' ';
            if (btnItem) {
                const el = btnItem.$el;
                const parentEl = el.parentElement;
                if (el.getBoundingClientRect().x === parentEl.getBoundingClientRect().x) {
                    label = null;
                }
            }

            this.btnItemLabel = label;
        },

        getFormCtx() {
            const {ctx, form, initialValues, idx} = this;

            const formValues = form.isFieldsTouched() ? this.formatValues(form.getFieldsValue()) : initialValues;

            return {
                ctx: ctx || this.$parent,
                ysf: this,
                form,
                values: formValues,
                initialValues,
                record: {
                    ...initialValues,
                    ...formValues
                },
                idx
            };
        },

        onChange(event, def) {
            const {field, onChange} = def;
            if (!onChange && !this.$listeners.change) return;

            this.$nextTick(() => {
                const {values, ...rest} = this.getFormCtx();
                const params = {
                    ...rest,
                    values,
                    value: pickValue(values, field),
                    def
                };
                this.$emit('change', params);

                if (onChange) {
                    onChange(params);
                }
            });
        },

        is({type, component, enumType, formOptions, tableOptions}) {
            if (!type && enumType) {
                type = 'select';
            }

            if (component) {
                return component;
            }

            if (formOptions) {
                return FieldChildForm;
            }

            if (tableOptions) {
                return FieldTableForm;
            }

            const formCtx = this.getFormCtx();
            type = evalProp(type, formCtx);

            const {date, dateTime, dateRange, time, month} = DefType;
            if ([date, dateTime, dateRange, time, month].includes(type)) {
                return FieldDate;
            }

            return FieldMap[type] || FieldMap.text;
        },

        getDecorator(def) {
            if (!def) return [];

            const {field, required = false, rules: originRules = [], format} = def;
            const formCtx = this.getFormCtx();

            const formattor = getFormat(format);
            let initialValue = pickValue(formCtx.initialValues, field);
            const valuePropName = def.type === DefType.switch ? 'checked' : 'value';

            if (formattor) {
                initialValue = formattor.format(initialValue);
            }

            const rules = [
                {required: evalProp(required, formCtx), message: this.$t('rule.required')},
                ...originRules.map(rule => {
                    const {validator, message} = rule;
                    if (validator) {
                        return {
                            ...rule,
                            validator: (curRule, value, callback) => {
                                const msgKey = validator({rule: curRule, value, ...formCtx});
                                if (msgKey) {
                                    callback(this.$t(msgKey));

                                    return;
                                }

                                callback();
                            }
                        };
                    }

                    if (message) {
                        return {
                            ...rule,
                            message: message.constructor === Function ? message(rule, this.$t) : this.$t(message)
                        };
                    }

                    return rule;
                })
            ];

            return [field, {initialValue, rules, valuePropName}];
        },

        getProps(def) {
            const {enumType, disabled, formOptions, tableOptions, field, optionDisabled, useYsfAsCtx} = def;
            const formCtx = this.getFormCtx();
            formCtx.value = pickValue(formCtx.values, field);
            let {type, props = {}} = def;
            if (!type) {
                type = enumType ? DefType.select : DefType.text;
            }

            props = {
                ...defaultPropsMap[type],
                ...evalProp(props, formCtx),
                disabled: evalProp(disabled, formCtx),
                optionDisabled: optionDisabled?.bind(null, formCtx),
                ctx: formCtx
            };
            if (enumType) {
                props.enumType = evalProp(enumType, formCtx);
            }

            if (formOptions || tableOptions) {
                props.ctx = useYsfAsCtx ? this : formCtx.ctx;
            }

            const {placeholder} = props;
            if (placeholder) {
                props.placeholder = typeof placeholder === 'string' ? this.$t(evalProp(placeholder, formCtx)) : evalProp(placeholder, formCtx);
            }

            const textKeys = ['addonBefore', 'addonAfter'];
            textKeys.forEach(key => {
                if (props[key]) {
                    props[key] = this.$t(evalProp(props[key], formCtx));
                }
            });

            if (type === DefType.file) {
                const {data, previewValue} = props;
                if (data) props.data = file => evalProp(data, {...formCtx, file});

                if (previewValue) {
                    props.previewValue = evalProp(previewValue, {...formCtx, def});
                }
            }

            return {
                ...props,
                def: {
                    ...def,
                    props
                }
            };
        },

        getListeners(def) {
            const {field, enumType, type, formOptions, component} = def;
            const {text, textarea, select, virtualSelect, number} = DefType;
            const listeners = {};
            if ((!type && !enumType && !formOptions && !component) || [text, textarea, number].includes(type)) {
                listeners.pressEnter = () => {
                    this.$emit(`${field}PressEnter`, this.getFormCtx());
                    if (this.enterSubmit && type !== textarea && this.$listeners.confirm) { // input 回车 提交
                        this.onConfirm();
                    }
                };

                listeners.blur = val => {
                    this.$emit(`${field}Blur`, val);
                };
            }

            if ((!type && enumType) || select === type || virtualSelect === type) {
                listeners.search = val => {
                    this.$emit(`${field}Search`, val);
                };
            }

            return listeners;
        },

        onReset() {
            this.form.resetFields();
            const formCtx = this.getFormCtx();
            this.$emit('reset', formCtx);
            this.$emit('change', formCtx);
        },

        onCancel() {
            this.$emit('cancel', {form: this.form});
        },

        formatValues(values, needTrim) {
            const {fieldDefs} = this.formOptions;
            fieldDefs.forEach(({field, format, notrim, enumType}) => {
                const formattor = getFormat(format);
                let value = pickValue(values, field);
                let handled = false;
                if (
                    needTrim
                    && !notrim
                    && !enumType
                    && value
                    && typeof value === 'string'
                ) {
                    value = value.trim();
                    handled = true;
                }

                if (formattor) {
                    value = formattor.unformat(value);
                    handled = true;
                }

                if (handled) values = setValue(values, field, value);
            });

            return values;
        },

        // 有 onConfirm 的都认为是子表单
        // field-child-form field-group-form field-table-form
        getChildForms() {
            const {$refs} = this;

            return Object.values($refs).filter(ref => ref.onConfirm);
        },

        getFormValues() {
            const {options: {mergeRecord = false}} = this;
            const ctx = this.getFormCtx();
            const {initialValues} = ctx;
            let {values} = ctx;

            let record = !mergeRecord
                ? {...initialValues, ...values}
                : merge(initialValues, values);

            // 子表单内可能没有触发过 $emit('change')
            // 数据不能通过父表单 form.getFieldsValue 得到
            // 需要把子表单的数据添加到 record values 上
            // TODO: field-table-form 还没有实现 getFormValues
            this.getChildForms()
                .filter(childForm => childForm.getFormValues)
                .map(childForm => childForm.getFormValues())
                .filter(({def}) => def)
                .forEach(({def: {field}, record: childFormRecord, values: childFormValues}) => {
                    values = setValue(values, field, childFormValues);
                    record = setValue(record, field, childFormRecord);
                });

            return {values, record};
        },

        async onConfirm() {
            const refValidPromises = Object.values(this.$refs).filter(ref => ref && ref.onConfirm)
                .map(ref => ref.onConfirm());
            let results = await Promise.all(refValidPromises);
            const {options: {mergeRecord = false}} = this;
            const selfValidPromise = new Promise(resolve => {
                this.form.validateFields((err, values) => {
                    // 不管校验有没问题，还是要format下，以免把没format的值吐出去
                    values = this.formatValues(values, true);
                    resolve({err, values, record: !mergeRecord ? {...this.initialValues, ...values} : merge(this.initialValues, values)});
                });
            });
            results = [await selfValidPromise].concat(results);
            const err = results.reduce((acc, cur) => acc || cur.err, null);
            let [{values, record}] = results;
            if (!err) {
                results.forEach(({def, record: refRecord, values: refValues}) => {
                    if (def) {
                        values = setValue(values, def.field, refValues);
                        record = setValue(record, def.field, refRecord);
                    }
                });

                if (this.autoSaveKey) {
                    setStorage(this.autoSaveKey, JSON.stringify(null));
                }

                this.$emit('confirm', {
                    ysf: this,
                    form: this.form,
                    values,
                    record
                });
            } else {
                this.$emit('error', {err, values, record});
            }

            return {err, values, record};
        },

        setStorageInteval() {
            const {autoSaveKey, interval} = this;
            if (interval) return;

            this.interval = setInterval(() => {
                const storageVO = JSON.parse(getStorage(autoSaveKey));
                const {record} = this.getFormCtx();

                if (!isStorageValuesEqual(storageVO, record)) {
                    setStorage(autoSaveKey, JSON.stringify(record));
                }
            }, 1e4);
        },

        renderSlot(name, props, fallback) {
            return this.$scopedSlots[name]?.(props) || fallback;
        },

        hasSlot(name) {
            return !!this.$scopedSlots[name];
        },

        renderFormBtns(formCtx) {
            const {
                formLayout: layout,
                btnItemLabel,
                resetLabel,
                confirmLabel,
                confirmProps,
                cancelLabel,
                $listeners,
                onReset,
                onConfirm,
                onCancel
            } = this;
            const fallbackBtns = [$listeners.reset && resetLabel && (
                <a-button
                    vOn:click={onReset}
                >
                    {this.$t(resetLabel)}
                </a-button>
            ), $listeners.cancel && cancelLabel && (
                <a-button
                    vOn:click={onCancel}
                >
                    {this.$t(cancelLabel)}
                </a-button>
            ), $listeners.confirm && confirmLabel && (
                <a-button
                    {...spreadProps({type: 'primary', ...confirmProps})}
                    vOn:click={onConfirm}
                >
                    {this.$t(confirmLabel)}
                </a-button>
            )].filter(item => item);
            if (!fallbackBtns.length && !this.hasSlot('extraBtns') && !this.hasSlot('btns')) return null;

            let Comp = 'div';
            let ref = '';
            const props = {};
            if (layout === 'inline') {
                Comp = 'a-form-item';
                ref = 'btnItem';
                Object.assign(props, {
                    colon: false,
                    label: btnItemLabel
                });
            }

            return (
                <Comp class="yqg-form-btn-item" {...{props, ref}}>
                    {this.renderSlot('btns', formCtx, fallbackBtns)}
                    {this.renderSlot('extraBtns', formCtx)}
                </Comp>
            );
        },

        renderFormItem(def, formCtx) {
            const {
                timezone,
                formOptions: {commonItemProps}
            } = this;

            const labelIsComp = isComponent(def.labelComp);
            const itemLabel = labelIsComp ? def.labelComp : this.$t(evalProp(def.label, formCtx), def.labelParam || []);

            const itemProps = {
                key: def.field,
                ...commonItemProps,
                ...def.itemProps
            };

            const {extra} = itemProps;
            if (extra) itemProps.extra = this.$t(evalProp(extra, formCtx));
            itemProps.class = appendClass(itemProps.class, `yqg-form-item-${def.type ?? 'anonymous'}`);
            const timezoneKey = this.$app?.timezoneKey || this.timezoneKey;
            const timezoneVisible = timezoneKey && def.label && ['date', 'dateTime'].includes(def.type);
            const labelSlotName = `${def.field}.label`;
            const hasLabelSlot = this.hasSlot(labelSlotName);
            const showLabelAsSlot = timezoneVisible || hasLabelSlot || labelIsComp;
            if (!showLabelAsSlot) {
                itemProps.label = itemLabel;
            }

            const Comp = this.is(def);
            const scopedSlots = [
                'head', 'foot', 'prefix', 'suffix', 'addonAfter', 'addonBefore', 'childFormExtraBtn', 'optionLabel'
            ].reduce((acc, name) => {
                const hasSlot = this.hasSlot(`${def.field}.${name}`);
                if (hasSlot) {
                    acc[name] = args => {
                        if (!Object.keys(args).length) return this.renderSlot(`${def.field}.${name}`, formCtx);

                        return this.renderSlot(`${def.field}.${name}`, {parent: formCtx, child: args});
                    };
                }

                return acc;
            }, {});

            const fallbackEl = (
                <Comp
                    {...{
                        directives: [{
                            name: 'decorator',
                            value: this.getDecorator(def)
                        }],
                        ...spreadProps(this.getProps(def)),
                        scopedSlots,
                        on: {
                            ...this.getListeners(def),
                            change: $event => this.onChange($event, def)
                        }
                    }}
                >
                </Comp>
            );
            const itemSuffixEl = this.renderSlot(`${def.field}.itemSuffix`, formCtx);
            if (itemSuffixEl) {
                itemProps.class = appendClass(itemProps.class, 'yqg-form-item-has-suffix');
            }

            return (
                <a-form-item {...spreadProps(itemProps)}>
                    {showLabelAsSlot && (
                        <span slot="label">
                            {
                                hasLabelSlot
                                    ? this.renderSlot(labelSlotName, {...formCtx, def})
                                    : labelIsComp
                                        ? <itemLabel {...spreadProps({...formCtx, def})} />
                                        : itemLabel
                            }
                            {timezoneVisible && (
                                <span class="yqg-text-danger">
                                    {this.$t(timezoneKey)[timezone]}
                                </span>
                            )}
                        </span>
                    )}
                    {this.renderSlot(def.field, formCtx, fallbackEl)}
                    {itemSuffixEl}
                </a-form-item>
            );
        },

        renderColItem(def, formCtx) {
            const {
                formOptions: {column}
            } = this;
            const colProps = {
                key: def.field,
                ...column,
                ...(def && evalProp(def.col, formCtx))
            };

            return (
                <a-col {...spreadProps(colProps)}>
                    {this.renderFormItem(def, formCtx)}
                </a-col>
            );
        }
    },

    render() {
        const {
            form,
            title,
            formLayout: layout,
            autocomplete,
            formOptions: {formProps, fieldDefs, row}
        } = this;
        const formCtx = this.getFormCtx();
        const items = fieldDefs
            .filter(def => !evalProp(def.hide, formCtx))
            .map(def => {
                if (layout === 'vertical') {
                    return this.renderColItem(def, formCtx);
                }

                return this.renderFormItem(def, formCtx);
            });

        return (
            <div class="yqg-simple-form">
                {this.renderSlot('title', formCtx, title && (
                    <div class="yqg-form-title">
                        {this.$t(title)}
                    </div>
                ))}
                {this.renderSlot('topBtns', formCtx)}
                <a-form {...{props: {...formProps, form}, attrs: {autocomplete}}}>
                    {layout !== 'vertical' ? items : (
                        <a-row {...spreadProps(row)}>
                            {items}
                        </a-row>
                    )}
                    {this.renderSlot('default', formCtx)}
                    {this.renderFormBtns(formCtx)}
                </a-form>
            </div>
        );
    }
};
</script>

<style lang="scss" src="./index.scss"></style>
