<template>
  <form
    :class="{
      [`mm-form-${layout}`]: layout,
    }"
    class="mm-form"
    @submit.prevent="handleSubmit"
    @reset.prevent="reset(undefined)">
    <slot></slot>
  </form>
</template>

<script>
import mergeWith from "lodash/mergeWith"
import zipObject from "lodash/zipObject"
import isFunction from "lodash/isFunction"
import flatten from "lodash/flatten"
import omit from "lodash/omit"
import get from "lodash/get"
import scrollIntoView from "scroll-into-view"

import config from "../../managers/config"

config.defaults({
  "Form.layout": "horizontal",
  "Form.labelWidth": 80,
  "Form.labelSuffix": function() {
    return this.layout === "vertical" ? "" : ":"
  },
})

export default {
  name: "mm-form",
  provide() {
    return {
      "mm-form": this,
    }
  },
  inject: {
    formBox: {
      from: "mm-form-box",
      default: null,
    },
  },
  props: {
    rules: {
      type: [Object, Array],
      default() {
        return {}
      },
    },
    beforeValidate: {
      type: Function,
      default() {},
    },
    afterValidate: {
      type: Function,
      default() {},
    },
    data: {
      type: [Object, Array],
      default() {
        return {}
      },
    },
    resetData: {
      type: Object,
      default() {
        return {}
      },
    },
    disabled: Boolean,
    readonly: Boolean,

    inlineMessage: {
      type: Boolean,
      default: false,
    },
    showMessage: {
      type: Boolean,
      default: true,
    },
    layout: {
      type: String,
      default: () => config.get("Form.layout"),
      validator(val) {
        return ["horizontal", "vertical", "inline"].includes(val)
      },
    },
    labelWidth: {
      type: [String, Number],
      default: () => config.get("Form.labelWidth"),
    },
    labelSuffix: {
      type: String,
      default() {
        return config.get("Form.labelSuffix").call(this)
      },
    },
    order: {
      type: Number,
      default: 0,
    },
    name: {
      type: [String, Number],
    },
    onSucceed: {
      type: Function,
      default() {},
    },
    onInvalid: {
      type: Function,
      default() {},
    },
  },
  data() {
    return {
      fieldList: [],
    }
  },
  computed: {
    realRules() {
      if (Array.isArray(this.rules)) {
        return this.rules.reduce((result, rule) => {
          const { fields, triggers, ...rest } = rule
          const rules = fields.map((field, index) => [
            {
              ...rest,
              triggers: Array.isArray(triggers) ? triggers[index] : triggers,
            },
          ])
          return mergeWith(result, zipObject(fields, rules), (t, s) => (t || []).concat(s))
        }, {})
      }
      return this.rules
    },
    fields() {
      return this.fieldList.map(f => f.field)
    },

    realFields() {
      return this.fields.filter(item => item.ruleField && item.dataField)
    },
  },
  created() {
    if (this.formBox) {
      this.formBox.register(this)
    }
  },
  beforeDestroy() {
    if (this.formBox) {
      this.formBox.unregister(this)
    }
  },
  methods: {
    submit() {
      this.handleSubmit(null)
    },

    handleSubmit(e) {
      const data = omit(this.data, this.fields.filter(f => f.realDisabled).map(f => f.dataField))
      //  1. false   2. reject()
      return Promise.resolve(this.beforeValidate(this, data))
        .catch(() => false)
        .then(res => {
          if (this.isValid(res)) {
            return this.validate()
              .then(() => {
                this.$emit("succeed", data, e)
                this.onSucceed(data, e)
              })
              .catch(res => {
                this.$emit("invalid", res)
                this.onInvalid(res)
              })
          }
          return res
        })
    },

    validate(o) {
      const options = {
        detail: false,
        scroll: false,
        names: [],
        trigger: "",
        complete: () => {},
        validatorOptions: {},
        validateOptions: {},
      }

      if (isFunction(o)) {
        options.complete = o
      } else {
        Object.assign(options, o)
      }

      if (options.scroll) {
        options.detail = true
      }

      const cb = options.complete

      const targets = this.realFields.filter(
        ({ name, realRules }) =>
          // has rules
          realRules.length &&
          // 1. names 为空  2. 包含name
          (options.names.length === 0 || options.names.includes(name)) &&
          // 1. !trigger => all  2. trigger => some
          (!options.trigger || realRules.some(({ triggers }) => triggers.includes(options.trigger)))
      )

      if (targets.length === 0) {
        cb(null)
        return Promise.resolve(null)
      }

      const validates = targets.map(target => {
        return target
          .validate({
            detail: options.detail,
            trigger: options.trigger,
            validatorOptions: options.validatorOptions,
            validateOptions: options.validateOptions,
          })
          .catch(result => result)
      })

      return Promise.all(validates).then(validities => {
        let result

        if (options.detail) {
          result = validities.filter(v => v.errors)
        } else {
          result = flatten(validities.filter(v => v))
        }

        if (result.length) {
          if (options.scroll) {
            const first = result[0]
            scrollIntoView(first.element, () => {
              if (first.instance && first.instance.focus) {
                first.instance.focus()
              }
            })
          }

          cb(result)
          return Promise.reject(result)
        } else {
          cb(null)
          return Promise.resolve(null)
        }
      })
    },

    reset(names = [], resetValues = {}) {
      const targets =
        names.length > 0
          ? this.realFields.filter(({ name }) => names.includes(name))
          : this.realFields

      targets.forEach(target => {
        const resetValue = get(resetValues, target.field)
        target.reset(resetValue)
      })
    },

    isValid(val) {
      return val === undefined || (typeof val === "boolean" && val)
    },
  },
}
</script>
