<template>
  <form
    class="ds-form"
    novalidate="true"
    autocomplete="off"
    @submit.prevent="submit">
    <slot
      :errors="errors"
      :reset="reset"/>
  </form>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

import Schema from 'async-validator'
import cloneDeep from 'clone-deep'
import { setProperty } from 'dot-prop'
// Disable warnings to console
Schema.warning = function() {}

/**
 * Used for handling complex user input.
 * @version 1.0.0
 */
export default defineComponent({
  name: 'DsForm',

  provide() {
    return {
      $parentForm: this
    }
  },

  props: {
    /**
     * The value of the input. Can be passed via v-model.
     */
    modelValue: {
      type: Object,
      required: true
    },
    /**
     * The async-validator schema used for the form data.
     */
    schema: {
      type: Object,
      default: () => ({})
    }
  },
  emits: ['submit', 'update:modelValue', 'input-valid', 'reset'],

  data() {
    return {
      newData: null,
      subscriber: [],
      errors: null
    }
  },

  watch: {
    modelValue: {
      handler(value) {
        this.newData = cloneDeep(value)
        this.notify(value, this.errors)
      },
      deep: true
    }
  },

  created() {
    this.newData = cloneDeep(this.modelValue)
  },

  methods: {
    submit() {
      this.validate(() => {
        /**
         * Fires on form submit.
         * Receives the current form data.
         *
         * @event submit
         */
        this.$emit('submit', this.newData)
      })
    },
    validate(cb) {
      const validator = new Schema(this.schema)
      validator.validate(this.newData, errors => {
        if (errors) {
          this.errors = errors.reduce((errorObj, error) => {
            const result = { ...errorObj }
            result[error.field] = error.message
            return result
          }, {})
        } else {
          this.errors = null
        }
        this.notify(this.newData, this.errors)
        if (!errors && cb && typeof cb === 'function') {
          cb()
        }
      })
    },
    subscribe(cb) {
      if (cb && typeof cb === 'function') {
        cb(cloneDeep(this.newData))
        this.subscriber.push(cb)
      }
    },
    unsubscribe(cb) {
      const index = this.subscriber.findIndex(cb)
      if (index > -1) {
        this.subscriber.splice(index, 1)
      }
    },
    notify(data, errors) {
      this.subscriber.forEach(cb => {
        cb(cloneDeep(data), errors)
      })
    },
    async update(model, value) {
      setProperty(this.newData, model, value)
      /**
       * Fires after user input.
       * Receives the current form data.
       * The form data is not validated and can be invalid.
       * This event is fired before the input-valid event.
       *
       * @event input
       */
      await this.$emit('update:modelValue', cloneDeep(this.newData))
      this.validate(() => {
        /**
         * Fires after user input.
         * Receives the current form data.
         * This is only called if the form data is successfully validated.
         *
         * @event input-valid
         */
        this.$emit('input-valid', cloneDeep(this.newData))
      })
    },
    reset() {
      /**
       * Fires after reset() was called.
       * Receives the current form data.
       * Reset has to be handled manually.
       *
       * @event reset
       */
      this.$emit('reset', cloneDeep(this.modelValue))
    }
  },
});
</script>

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