<template>
  <div class="easy-form">
    <el-form
      ref="form"
      :model="formData"
      :label-position="_formConfig.labelPosition"
      :label-width="_formConfig.labelWidth || (['left','right'].includes(_formConfig.labelPosition) ? '100px' : undefined)"
      :disabled="getPropValue(_formConfig.readOnly)"
      :validate-on-rule-change="false"
      :rules="formRules"
      :class="{
        'read-only': getPropValue(_formConfig.readOnly),
        'is-view': getPropValue(_formConfig.isView)
      }"
    >
      <slot
        v-if="customMode"
        :easyFormItemBindData="easyFormItemBindData"
        v-bind="componentScope"
      />
      <DefaultLayout
        v-else
        ref="DefaultLayout"
        :fields="fields"
        :component-scope="componentScope"
        :form-config="_formConfig"
        :item-controller="itemController"
        :easy-form-item-bind-data="easyFormItemBindData"
        :form-data="formData"
      >
        <template v-for="(item) in allSlotName" v-slot:[item]="scope">
          <slot
            :name="item"
            v-bind="scope"
          />
        </template>
      </DefaultLayout>
    </el-form>
    <div
      v-if="!customMode && (_formConfig.showSubmit || _formConfig.showCancel || _buttons.filter((button) => {
        return getPropValue(button.show) !== false
      }).length > 0)"
      class="btns"
      :class="{
        'btn-position-center': _formConfig.buttonPosition === 'center',
        'btn-position-left': _formConfig.buttonPosition === 'left',
        'btn-position-right': _formConfig.buttonPosition === 'right',
      }"
    >
      <el-button v-if="_formConfig.showSubmit" type="primary" @click="submitForm">提交</el-button>
      <el-button v-if="_formConfig.showCancel" @click="$emit('cancel')">取消</el-button>
      <el-button
        v-for="button in _buttons.filter((button) => { return getPropValue(button.show) !== false })"
        :key="getPropValue(button.label)"
        :loading="getPropValue(button.loading)"
        :type="getPropValue(button.type)"
        :size="getPropValue(button.size)"
        :disabled="getPropValue(button.disabled)"
        :icon="getPropValue(button.leftIcon) || getPropValue(button['icon'])"
        v-bind="getPropValue(button.buttonProps)"
        @click="callButtonHandler(button)"
      >
        {{ getPropValue(button.label) }}
        <i v-if="getPropValue(button.rightIcon)" class="el-icon--right" :class="getPropValue(button.rightIcon)" />
      </el-button>
    </div>
    <template v-for="(field) in hiddenFields">
      <EasyFormItem
        v-show="false"
        :key="field.prop"
        :field="field"
        v-bind="easyFormItemBindData"
        @update:rules="(value)=>{ updateRules(field.prop, value) }"
        @created="(instance) => { registerFormItem(field.prop, instance) }"
        @beforeDestroy="(instance) => { unRegisterFormItem(field.prop, instance) }"
        @change="(scope, value, isFromUser)=>{
          clearValidate(field.prop)
          callComponentOnChange(scope, field, value, isFromUser)
        }"
      >
        <template v-slot:[getPropValue(field.slotName)]="scope">
          <slot
            :name="getPropValue(field.slotName)"
            :getField="getField"
            :formConfig="formConfig"
            :easyFormItemBindData="easyFormItemBindData"
            v-bind="scope"
          />
        </template>
      </EasyFormItem>
    </template>
  </div>

</template>

<script>
/**
 * 请不要在这个文件添加/添加任何非公共代码，所有的修改都应该在组件的传入参数处修改。
 */
import { formConfig } from '../globalConfig'
import { createPathObject, deepClone, getValueByLevel } from '../utils'
import EasyFormItem from '../EasyFormItem'
import DefaultLayout from './defaultLayout'
export default {
  name: 'EasyForm',
  components: {
    EasyFormItem,
    DefaultLayout
  },
  props: {
    /**
     * 定义字段及相关属性，详细参考文档
     */
    fields: {
      type: Array,
      default: () => {
        return []
      }
    },
    /**
     * 双向绑定的表单对象，表单项的输入会反映到该对象同名的字段上
     */
    value: {
      type: Object,
      default: () => {
        return {}
      }
    },
    /**
     * 表单样式等配置项，详细参考下表，若要全局修改则写在src/plugins/EasyComponent/globalConfig.js
     */
    formConfig: {
      type: Object,
      default: () => {
        return {}
      }
    },
    /**
     * 定义表单下方的按钮，详细参考文档
     */
    buttons: {
      type: Array,
      default: () => {
        return []
      }
    },
    /**
     * 一些自定义的原信息，会在使用闭包或调用按钮时在作用域中返回。
     * 一般用于多个Easy组件公用方法时区别不同的调用来源
     */
    meta: {
      type: Object,
      default: () => {
        return {}
      }
    },
    /**
     * 不提供布局，需要在插槽中手动注册EasyFormItem实例
     */
    customMode: {
      type: Boolean,
      default: () => {
        return false
      }
    }
  },
  data() {
    return {
      dontValidateRequired: false,
      useSubmitValidate: false,
      itemController: {},
      formRules: {},
      formItemRefs: {}
    }
  },
  computed: {
    hiddenFields() {
      return this.fields.map((item, index) => {
        return {
          key: item.prop || index,
          ...item,
          slotName: this.getPropValue(item.slotName),
          keepOnForm: this.getPropValue(item.keepOnForm),
          showInForm: this.getPropValue(item.showInForm),
          formSort: this.getPropValue(item.formSort) || Infinity,
          type: this.getPropValue(item.type),
          span: this.getPropValue(item.span)
        }
      }).sort((a, b) => {
        return a.formSort - b.formSort
      }).filter((field) => {
        return field.showInForm === false && field.keepOnForm
      })
    },
    allSlotName() {
      return Object.keys({
        ...this.$slots,
        ...this.$scopedSlots
      })
    },
    componentScope() {
      return {
        formData: this.formData,
        formConfig: this._formConfig,
        formInstance: this,
        getOptionValue: this.getOptionValue,
        setOptionLoading: this.setOptionLoading,
        getPropValue: this.getPropValue,
        formRules: this.formRules,
        meta: this.meta,
        updateRules: this.updateRules,
        registerFormItem: this.registerFormItem,
        unRegisterFormItem: this.unRegisterFormItem,
        label2propMap: this.label2propMap
      }
    },
    label2propMap() {
      return this.fields.reduce((a, b) => {
        a[b.label] = b.prop
        return a
      }, {})
    },
    formData() {
      return createPathObject(this.value)
    },
    _formConfig() {
      return {
        ...formConfig,
        ...this.formConfig
      }
    },
    _buttons() {
      for (const item of this.buttons) {
        this.$set(item, 'loading', item.loading === undefined ? false : item.loading)
      }
      return this.buttons
    },
    easyFormItemBindData() {
      return {
        componentScope: this.componentScope,
        configReadOnly: this.getPropValue(this._formConfig.readOnly),
        configIsView: this.getPropValue(this._formConfig.isView),
        configTrim: this.getPropValue(this._formConfig.trim),
        configDisableValidates: this.getPropValue(this._formConfig.disableValidates),
        formData: this.formData,
        dontValidateRequired: this.dontValidateRequired,
        useSubmitValidate: this.useSubmitValidate
      }
    }
  },
  watch: {
    value(val) {
      for (const instance of Object.values(this.formItemRefs)) {
        instance.callOnChange(val[instance.field.prop], false)
      }
    }
  },
  created() {
    this.$emit('created', this)
  },
  beforeDestroy() {
    this.$emit('beforeDestroy', this)
  },
  methods: {
    /**
     * private
     */
    getField(propName) {
      return this.fields.find((item) => {
        return item.prop === propName
      })
    },
    getOptionValue(prop, options) {
      if (this.formItemRefs[prop]) {
        return this.formItemRefs[prop].getOptionValue(prop, options)
      }
    },
    setOptionLoading(prop, promiseInstane) {
      if (promiseInstane instanceof Promise) {
        this.setItemControllerValue(prop, 'userOptionsLoading', true)
        promiseInstane.finally(() => {
          this.setItemControllerValue(prop, 'userOptionsLoading', false)
        })
      }
    },
    getPropValue(prop, ...args) {
      return typeof prop === 'function' ? prop(this.componentScope, ...args) : prop
    },
    registerFormItem(prop, instance) {
      if (instance.isEasyFormItem) {
        this.formItemRefs[prop] = instance
      } else {
        console.error(instance)
        throw new Error('registerFormItem 方法注册的实例并非EasyFormItem实例')
      }
    },
    unRegisterFormItem(prop, instance) {
      if (this.formItemRefs[prop] === instance) {
        delete this.formItemRefs[prop]
      }
    },
    updateRules(prop, rules) {
      this.$set(this.formRules, prop, rules)
    },
    callButtonHandler(button) {
      this.$set(button, 'loading', true)
      try {
        const result = button.handler(this.componentScope, button)
        if (result instanceof Promise) {
          result.finally(() => {
            this.$set(button, 'loading', false)
          })
        } else {
          this.$set(button, 'loading', false)
        }
      } catch (e) {
        console.error(e)
        this.$set(button, 'loading', false)
      }
    },
    setItemControllerValue(prop, key, value) {
      if (!this.itemController[prop]) {
        this.$set(this.itemController, prop, {})
      }
      this.$set(this.itemController[prop], key, value)
    },
    setAllItemControllerValue(key, value) {
      for (const prop in this.itemController) {
        this.setItemControllerValue(prop, key, value)
      }
    },
    getItemControllerValue(prop, key) {
      if (!this.itemController[prop]) {
        this.$set(this.itemController, prop, {})
      }
      return this.itemController[prop][key]
    },
    callComponentOnChange(scope, layoutColItem, value, isFromUser) {
      if (typeof this._formConfig.onChange === 'function') {
        this._formConfig.onChange({
          scope,
          field: layoutColItem,
          value,
          isFromUser
        })
      }
    },
    /**
     * public
     * @options-params dontValidateRequired 不校验必填项是否未填
     * @options-params fieldOnly 仅返回field定义的字段
     * @options-params cleanDisabledOnValidError 校验失败时解除该字段的表单禁用 默认为true
     * @options-params showValidMessage 是否显示校验异常信息
     */
    submitForm(options = {}) {
      /**
       * 检验成功时会返回经过valueFormat字段处理过的parseFormData
       */
      return new Promise((resolve, reject) => {
        const dontValidateRequiredCache = this.dontValidateRequired
        this.dontValidateRequired = options.dontValidateRequired
        this.useSubmitValidate = true
        this.$nextTick(() => {
          (this.$refs.form.validateScroll || this.$refs.form.validate)((valid, rules) => {
            if (valid) {
              const parseFormData = this.getParseFormData(options)
              this.$emit('submit', parseFormData)
              resolve(parseFormData)
            } else {
              this.$emit('valid', rules)
              if (options.cleanDisabledOnValidError !== false) {
                for (const key in rules) {
                  this.setItemControllerValue(key, 'disabled', false)
                }
              }
              if (getValueByLevel(options.showValidMessage, this._formConfig.showValidMessage) !== false) {
                this.$message({
                  type: 'warning',
                  message: rules[Object.keys(rules)[0]][0].message,
                  duration: 5000
                })
              }
              reject(rules)
            }
            this.dontValidateRequired = dontValidateRequiredCache
            this.useSubmitValidate = false
          })
        })
      })
    },
    /**
     * public
     * @options-params fieldOnly 仅返回field定义的字段
     */
    getParseFormData(options = {}) {
      const parseFormData = createPathObject(deepClone(options.fieldOnly ? {} : this.formData))
      const lockProp = []
      for (const field of this.fields) {
        try {
          const formDataProp = this.getPropValue(field.formDataProp) || field.prop
          if (this.getPropValue(field.showInForm) === false && !this.getPropValue(field.keepOnForm) && !lockProp.includes(field.prop)) {
            delete parseFormData[field.prop]
          } else if (this.formItemRefs[field.prop]) {
            lockProp.push(field.prop)
            const { value, type } = this.formItemRefs[field.prop].parseData()
            if (type === 'delete') {
              delete parseFormData[field.prop]
            } else if (type === 'value') {
              parseFormData[formDataProp] = value
            }
          }
        } catch (e) {
          console.error(e)
        }
      }
      return parseFormData
    },
    /**
     * 设置整个表单的值
     */
    resetFormData(formData = {}) {
      return this.updateValue(formData).then(() => {
        this.$nextTick(() => {
          if (this.$refs.form) {
            this.$refs.form.clearValidate()
          }
        })
      })
    },
    /**
     * 逐个设置整个表单的值
     * @options-params setDisabled 是否禁止修改设置的值(仅值不为undefined|null|''时有效)
     * @options-params setDisabledForce 是否禁止修改设置的值(字段存在则生效)
     * @options-params restDisabledMap 是否重置禁用列表, 不传入该值时setDisabled或setDisabledForce为真时自动为真
     * @options-params disabledExclude 排除设置禁用的项
     * @options-params fieldExclude 排除设置的项
     * @options-params copyAllData 设置单项值前先拷贝整个表单对象
     * @options-params dontUpdate 表单中原本有值(不为undefined|null|'')的字段不进行更新，且当传入的值与表单值不一致时不会禁止该字段编辑
     */
    async setFieldValue(fieldValueMap = {}, options = {}) {
      options = {
        setDisabled: false,
        setDisabledForce: false,
        dontUpdate: false,
        ...options
      }
      options = {
        ...options,
        disabledExclude: (Array.isArray(options.disabledExclude) ? options.disabledExclude : [options.disabledExclude]).filter((item) => { return !!item }),
        fieldExclude: (Array.isArray(options.fieldExclude) ? options.fieldExclude : [options.fieldExclude]).filter((item) => { return !!item }),
        restDisabledMap: options.restDisabledMap === undefined ? (options.setDisabled || options.setDisabledForce) : options.restDisabledMap,
        copyAllData: typeof options.copyAllData === 'boolean' ? options.copyAllData : false
      }
      const pathObject = createPathObject(fieldValueMap)
      const formData = this.getParseFormData()
      const fieldPropsSet = new Set()
      if (options.restDisabledMap) {
        this.setAllItemControllerValue('disabled', false)
      }
      for (const field of this.fields) {
        if (field.prop) {
          fieldPropsSet.add(field.prop)
        }
        if (field.prop in pathObject && !options.fieldExclude.includes(field.prop)) {
          if (options.setDisabledForce || (options.setDisabled && !['', undefined, null].includes(pathObject[field.prop]))) {
            if (!options.disabledExclude.includes(field.prop)) {
              if (options.dontUpdate && formData[field.prop] !== pathObject[field.prop]) {
                this.setItemControllerValue(field.prop, 'disabled', false)
              } else {
                this.setItemControllerValue(field.prop, 'disabled', true)
              }
            }
          }
          if (options.dontUpdate) {
            if ([null, undefined, ''].includes(this.formData[field.prop])) {
              this.formData[field.prop] = pathObject[field.prop]
            }
            if (this.formItemRefs[field.prop] && this.formItemRefs[field.prop].isMultipleValue && this.formData[field.prop].length === 0) {
              this.formData[field.prop] = pathObject[field.prop]
            }
          } else {
            this.formData[field.prop] = pathObject[field.prop]
          }
        }
      }
      if (options.copyAllData) {
        for (const key of pathObject._pathKeys) {
          if (!fieldPropsSet.has(key) && !options.fieldExclude.includes(key)) {
            this.formData[key] = pathObject[key]
          }
        }
      }
      return this.updateValue(this.formData).then(() => {
        this.$nextTick(() => {
          if (this.$refs.form) {
            this.$refs.form.clearValidate()
          }
        })
      })
    },
    clearValidate(prop) {
      if (this.$refs.form) {
        this.$refs.form.clearValidate(prop)
      }
    },
    validateField(prop) {
      if (this.$refs.form) {
        this.$refs.form.validateField(prop)
      }
    },
    updateValue(value) {
      let target = this
      let $parent = this.$parent
      let targetPropKey = 'value'
      while ($parent) {
        for (const key in $parent.$props) {
          if ($parent.$props[key] === this.value) {
            target = $parent
            targetPropKey = key
            break
          }
        }
        $parent = $parent.$parent
      }
      const targetListener = [`update:${targetPropKey}`]
      if (targetPropKey === 'value') {
        targetListener.push('input')
      }
      if (!Object.keys(target.$listeners).some((key) => {
        return targetListener.includes(key)
      })) {
        console.warn(`[EasyForm] 祖辈组件未声明[${targetPropKey}]监听器，表单值可能更新失效`)
      }
      for (const eventName of targetListener) {
        target.$emit(eventName, value)
      }
      return new Promise((resolve) => {
        this.$nextTick(() => {
          resolve()
        })
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.easy-form{
  // 样式修改请在src/styles下新建样式文件
  .btns{
    margin-top: 20px;
  }
  .btn-position-center{
    text-align: center;
  }
  .btn-position-left{
    text-align: left;
  }
  .btn-position-right{
    text-align: right;
  }
}

</style>
