<template>
  <div>
    <Toasts></Toasts>
    <div class="form-view__page">
      <div v-if="fields.length === 0" class="form-view__body">
        <div class="form-view__no-fields margin-bottom-4">
          This form doesn't have any fields. Use Baserow to add at least one
          field.
        </div>
        <FormViewPoweredBy v-if="showLogo"></FormViewPoweredBy>
      </div>
      <component
        :is="component"
        v-else
        ref="form"
        v-model="values"
        :loading="loading"
        :submitted="submitted"
        :title="title"
        :description="description"
        :cover-image="coverImage"
        :logo-image="logoImage"
        :submit-text="submitText"
        :all-fields="fields"
        :visible-fields="visibleFields"
        :is-redirect="isRedirect"
        :submit-action-redirect-url="submitActionRedirectUrl"
        :submit-action-message="submitActionMessage"
        :show-logo="showLogo"
        @submit="submit"
      ></component>
    </div>
  </div>
</template>

<script>
import { clone, isPromise } from '@baserow/modules/core/utils/object'
import { notifyIf } from '@baserow/modules/core/utils/error'
import Toasts from '@baserow/modules/core/components/toasts/Toasts'
import FormService from '@baserow/modules/database/services/view/form'
import {
  getHiddenFieldNames,
  getPrefills,
} from '@baserow/modules/database/utils/form'
import { matchSearchFilters } from '@baserow/modules/database/utils/view'
import FormViewPoweredBy from '@baserow/modules/database/components/view/form/FormViewPoweredBy'

export default {
  components: {
    Toasts,
    FormViewPoweredBy,
  },
  async asyncData({ params, error, app, route, redirect, store }) {
    const slug = params.slug
    const publicAuthToken = await store.dispatch(
      'page/view/public/setAuthTokenFromCookiesIfNotSet',
      { slug }
    )

    let data = null
    try {
      const { data: responseData } = await FormService(
        app.$client
      ).getMetaInformation(slug, publicAuthToken)
      data = responseData
    } catch (e) {
      const statusCode = e.response?.status
      // password protect forms require authentication
      if (statusCode === 401) {
        return redirect({
          name: 'database-public-view-auth',
          query: { original: route.path },
        })
      } else {
        return error({ statusCode: 404, message: 'Form not found.' })
      }
    }

    // After the form field meta data has been fetched, we need to make the values
    // object with the empty field value as initial form value.
    const values = {}
    const prefills = getPrefills(route.query)
    const hiddenFields = getHiddenFieldNames(route.query)
    const promises = []
    data.fields.forEach((field) => {
      field._ = {
        touched: false,
        hiddenViaQueryParam: hiddenFields.includes(field.name),
      }
      const fieldType = app.$registry.get('field', field.field.type)
      const setValue = (value) => {
        values[`field_${field.field.id}`] = value
      }

      const prefill = prefills[field.name]
      values[`field_${field.field.id}`] = fieldType.getEmptyValue(field.field) // Default value
      if (
        prefill !== undefined &&
        prefill !== null &&
        fieldType.canParseQueryParameter()
      ) {
        const result = fieldType.parseQueryParameter(field, prefill, {
          slug,
          client: app.$client,
          publicAuthToken,
        })

        if (isPromise(result)) {
          result.then(setValue)
          promises.push(result)
        } else {
          setValue(result)
        }
      }
    })

    await Promise.all(promises)

    // Order the fields directly after fetching the results to make sure the form is
    // serverside rendered in the right order.
    data.fields = data.fields.sort((a, b) => {
      // First by order.
      if (a.order > b.order) {
        return 1
      } else if (a.order < b.order) {
        return -1
      }

      // Then by id.
      if (a.field.id < b.field.id) {
        return -1
      } else if (a.field.id > b.field.id) {
        return 1
      } else {
        return 0
      }
    })

    return {
      title: data.title,
      description: data.description,
      coverImage: data.cover_image,
      logoImage: data.logo_image,
      submitText: data.submit_text,
      fields: data.fields,
      mode: data.mode,
      showLogo: data.show_logo,
      values,
      publicAuthToken,
    }
  },
  data() {
    return {
      loading: false,
      submitted: false,
      submitAction: 'MESSAGE',
      submitActionMessage: '',
      submitActionRedirectUrl: '',
    }
  },
  head() {
    const head = {
      title: this.title || 'Form',
      bodyAttrs: {
        class: ['background-white'],
      },
    }
    if (!this.showLogo) {
      head.titleTemplate = '%s'
    }
    return head
  },
  computed: {
    isRedirect() {
      return (
        this.submitAction === 'REDIRECT' && this.submitActionRedirectUrl !== ''
      )
    },
    /**
     * Returns all the fields that should be visible to the visitor. They can change
     * depending on the values because some fields have conditions whether they
     * should be visible.
     */
    visibleFields() {
      return this.fields.reduce((visibleFields, field, index, tmp) => {
        // If the conditional visibility is disabled, we must always show the field.
        if (!field.show_when_matching_conditions) {
          return [...visibleFields, field]
        }

        // A condition is only valid if the filter field is before this field because
        // you can only filter fields before. Therefore, we check which fields are
        // before.
        const fieldsBefore = this.fields.slice(0, index).map((f) => f.field)
        // Find the valid filters by checking if the filter field is before this field
        // and if the filter type is compatible with the field.
        const conditions = field.conditions.filter((condition) => {
          const filterType = this.$registry.get('viewFilter', condition.type)
          const filterField = fieldsBefore.find((f) => f.id === condition.field)
          return (
            filterField !== undefined &&
            filterType.fieldIsCompatible(filterField)
          )
        })
        const conditionType = field.condition_type

        // If there aren't any conditions, we must always show the field.
        if (conditions.length === 0) {
          return [...visibleFields, field]
        }

        // We only want to work with the values of fields that are actually visible.
        // This to avoid matching the conditions on values of fields that aren't
        // visible, but were filled out in the past and are still remembered in memory.
        const visibleFieldIds = visibleFields.map((f) => f.field.id)
        const visibleValues = clone(this.values)
        this.fields
          .filter(
            (f) =>
              !visibleFieldIds.includes(f.field.id) &&
              f.field.id !== field.field.id
          )
          .forEach((f) => {
            visibleValues['field_' + f.field.id] = this.$registry
              .get('field', f.field.type)
              .getEmptyValue(f.field)
          })

        if (
          matchSearchFilters(
            this.$registry,
            conditionType,
            conditions,
            fieldsBefore,
            visibleValues
          )
        ) {
          return [...visibleFields, field]
        }

        return visibleFields
      }, [])
    },
    component() {
      return this.$registry.get('formViewMode', this.mode).getFormComponent()
    },
  },
  methods: {
    async submit() {
      if (this.loading) {
        return
      }

      this.touch()
      this.loading = true
      const values = clone(this.values)
      const submitValues = {}

      // Loop over the visible fields, because we only want to submit those values.
      for (let i = 0; i < this.visibleFields.length; i++) {
        const field = this.visibleFields[i]
        const fieldType = this.$registry.get('field', field.field.type)
        const valueName = `field_${field.field.id}`
        const value = values[valueName]
        const ref = this.$refs.form.$refs['field-' + field.field.id][0]

        // If the field required and empty or if the value has a validation error, then
        // we don't want to submit the form, focus on the field and top the loading.
        if (
          (field.required && fieldType.isEmpty(field.field, value)) ||
          fieldType.getValidationError(field.field, value) !== null ||
          // It could be that the field component is in an invalid state and hasn't
          // update the value yet. In that case, we also don't want to submit the form.
          !ref.isValid()
        ) {
          ref.focus()
          this.loading = false
          return
        }

        submitValues[valueName] = fieldType.prepareValueForUpdate(
          field.field,
          values[valueName]
        )
      }

      try {
        const slug = this.$route.params.slug
        const { data } = await FormService(this.$client).submit(
          slug,
          submitValues,
          this.publicAuthToken
        )

        this.submitted = true
        this.submitAction = data.submit_action
        this.submitActionMessage = data.submit_action_message
        this.submitActionRedirectUrl = data.submit_action_redirect_url.replace(
          `{row_id}`,
          data.row_id
        )

        // If the submit action is a redirect, then we need to redirect safely to the
        // provided URL.
        if (this.isRedirect) {
          setTimeout(() => {
            window.location.assign(this.submitActionRedirectUrl)
          }, 4000)
        }
      } catch (error) {
        notifyIf(error, 'view')
      }
      this.loading = false
    },
    /**
     * Marks all the fields are touched. This will show any validation error messages
     * if there are any.
     */
    touch() {
      this.visibleFields.forEach((field) => {
        field._.touched = true
      })
    },
  },
}
</script>