<template>
  <div
    ref="field"
    :class="{
      'mm-form-field--error': validities.length > 0,
      'mm-form-field--required': isRequired,
      'mm-form-field--with-help': validities.length > 0,
      [`has-${validateStatus}`]: validateStatus,
    }"
    class="mm-form-field">
    <div
      v-if="label || $slots.label"
      :style="labelStyle"
      class="mm-form-field-label">
      <slot name="label">
        <span>{{ label }}{{ labelSuffix }}</span>
      </slot>
    </div>
    <div
      :style="controlStyle"
      class="mm-form-field-control">
      <span class="mm-form-field-children">
        <slot></slot>
      </span>
      <template v-if="realHelp || $slots.help">
        <div
          class="mm-form-field-explain">
          <slot name="help">
            <help-message :content="realHelp"></help-message>
          </slot>
        </div>
      </template>

      <div
        v-if="extra || $slots.extra"
        class="mm-form-field-extra">
        <slot name="extra">
          {{ extra }}
        </slot>
      </div>
    </div>
  </div>
</template>
<script>
import Schema from "async-validator"
import toPath from "lodash/toPath"
import get from "lodash/get"
import cloneDeep from "lodash/cloneDeep"
import isFunction from "lodash/isFunction"

export const VALIDATE_STATUS_SUCCESS = "success"
export const VALIDATE_STATUS_WARNING = "warning"
export const VALIDATE_STATUS_ERROR = "error"
export const VALIDATE_STATUS_VALIDATING = "validating"

// override warning (async-validator 1.10.1)
Schema.warning = function() {}

const getValidate = (...args) => {
  const validator = new Schema(...args)

  return (...validateArgs) =>
    new Promise((resolve, reject) => {
      validator.validate(...validateArgs, (errors, fields) => {
        const result = {
          fields,
          errors,
        }

        if (errors) {
          reject(result)
        } else {
          resolve(result)
        }
      })
    })
}

export default {
  name: "mm-form-field",
  uiTypes: ["field"],
  getValidate,
  components: {
    HelpMessage: {
      functional: true,
      props: {
        content: null,
      },
      render(h, context) {
        return [context.props.content]
      },
    },
  },
  provide() {
    return {
      "mm-form-field": this,
    }
  },
  props: {
    label: String,
    name: String, // 在rules对应的字段
    field: String, // 在data中对应的字段
    rules: {
      type: Array,
      default() {
        return []
      },
    },
    disabled: Boolean,
    readonly: Boolean,
    hasFeedback: Boolean,
    help: {
      type: String,
    },
    required: {
      type: Boolean,
      default: false,
    },
    validateStatus: {
      type: String,
      validate(val) {
        return ["success", "warning", "error", "validating"].includes(val)
      },
    },
    extra: {
      type: String,
    },
  },
  inject: {
    form: {
      from: "mm-form",
      default: () => ({}),
    },
  },

  data() {
    return {
      validities: [],
      initialValue: null,
      validateState: false,
      inputInstance: null,
      instance: {
        field: this,
        errors: [],
      },
    }
  },
  computed: {
    realRules() {
      return this.rules
        .concat(get(this.form.realRules, this.ruleField, []))
        .map(({ triggers = "submit", ...rule }) => ({
          ...rule,
          triggers: Array.isArray(triggers) ? triggers : triggers.split(",").map(t => t.trim()),
        }))
    },
    fieldValue() {
      return get(this.form.data, this.dataField)
    },
    resetValue() {
      return get(this.form.resetData, this.dataField)
    },
    isRequired() {
      return this.required || this.realRules.some(perRule => perRule.required)
    },
    realHelp() {
      return this.help || get(this.validities, "[0].message")
    },
    controlStyle() {
      const style = {}
      if (this.labelWidth && this.form.layout === "horizontal") {
        style.marginLeft = this.labelWidth
      }
      return style
    },
    labelStyle() {
      const style = {}
      if (this.labelWidth && this.form.layout !== "vertical") {
        style.width = this.labelWidth
      }
      return style
    },
    labelWidth() {
      return typeof this.form.labelWidth === "number" ? `${this.form.labelWidth}px` : this.form.labelWidth
    },
    labelSuffix() {
      return this.form.labelSuffix
    },
    ruleField() {
      return this.name || this.field
    },
    dataField() {
      return this.field
    },
  },
  created() {
    if (!this.form || !this.ruleField) {
      return
    }

    this.form.fieldList.push(this.instance)

    this.$on("interact", this.handleInteract)
    this.initialValue = cloneDeep(this.fieldValue)
  },
  beforeDestroy() {
    this.$off("interact", this.handleInteract)
    const index = this.form.fieldList.indexOf(this.instance)
    if (index > -1) {
      this.form.fieldList.splice(index, 1)
    }
  },
  methods: {
    register(inputInstance) {
      this.inputInstance = inputInstance
    },
    unregister(inputInstance) {
      this.inputInstance = inputInstance
    },
    validate(o) {
      const options = {
        detail: false,
        trigger: "",
        complete: () => {},
        validatorOptions: {},
        validateOptions: {},
        rules: null,
      }

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

      const cb = options.complete
      const rules = options.rules || this.getRules(options.trigger)

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

      return new Promise((resolve, reject) => {
        this.validateState = true
        const validator = new Schema(
          {
            [this.dataField]: rules,
          },
          options.validatorOptions
        )
        validator.validate({ [this.dataField]: this.fieldValue }, {}, (errors, fields) => {
          this.validateState = false
          this.validities = errors || []

          const result = options.detail
            ? {
              element: this.$refs.field,
              name: this.ruleField,
              field: this.dataField,
              value: this.fieldValue,
              rules: rules,
              errors: errors,
              fields: fields,
              instance: this.inputInstance,
            }
            : errors

          if (errors) {
            cb(result)
            reject(result)
          } else {
            cb(result)
            resolve(result)
          }
        })
      })
    },

    getRules(trigger) {
      return this.realRules.filter(rule => {
        if (rule.triggers.length === 0 || trigger === "") {
          return true
        }
        return rule.triggers.includes(trigger)
      })
    },

    reset(targetValue) {
      this.validities = []

      if (!this.ruleField) {
        return
      }

      let path = toPath(this.ruleField)

      if (path.length >= 2 && this.form.data.hasOwnProperty(this.ruleField)) {
        path = [this.ruleField]
      }
      const name = path.pop()
      const value = [targetValue, this.resetValue, this.initialValue].find(v => v !== undefined)
      const parentValue = path.length > 0 ? get(this.form.data, path) : this.form.data

      this.$set(parentValue, name, cloneDeep(value))
    },

    handleInteract(eventName) {
      const rules = this.getRules(eventName)

      if (rules.length > 0) {
        this.validate({
          detail: true,
          trigger: eventName,
          rules: rules,
        }).catch(errors => {
          this.instance.errors = errors
        })
      }
    },
  },
}
</script>
