<!--suppress VueUnrecognizedSlot -->
<template>
  <Card class="mx-auto max-w-prose">
    <template #content>
      <div class="flex flex-col gap-2">
        <Message
          v-if="isPresent(formErrors)"
          severity="error"
          pt:root="border border-transparent"
        >
          {{ _join(formErrors, " ") }}
        </Message>

        <div
          v-for="({ type }, field) in props.fields"
          :key="field"
          class="group flex flex-col"
        >
          <Swap
            :class="props.action !== 'view' && 'cursor-pointer'"
            :disabled="props.action === 'view'"
            @active="focusFormControl(field, type)"
            @inactive="validate(field)"
          >
            <template #inactive>
              <div class="ml-1 block text-sm text-primary dark:text-primary">
                {{ _startCase(field) }}
              </div>

              <div
                v-if="type === 'richText'"
                class="trix-content border border-transparent px-3 py-2"
                :class="props.action !== 'view' && 'group-hover:bg-primary/15'"
                :tabindex="0"
                v-html="model?.[field] || '&nbsp;'"
              />

              <div
                v-else
                class="border border-transparent px-3 py-2"
                :class="props.action !== 'view' && 'group-hover:bg-primary/15'"
                :tabindex="0"
              >
                {{ model?.[field] || "&nbsp;" }}
              </div>
            </template>

            <template #active="{ deactivate }">
              <div class="ml-1 block text-sm text-primary dark:text-primary">
                {{ _startCase(field) }}
              </div>

              <TrixEditor
                v-if="type === 'richText'"
                v-model="model[field]"
                :id="field"
                :tabindex="0"
                @blur="waitForAnimation(deactivate)"
                :ref="(trixEditor) => (trixEditors[field] = trixEditor)"
              />

              <Select
                v-else-if="type === 'select'"
                v-model="model[field]"
                :tabindex="0"
                :labelId="field"
                fluid
                :options="options?.[field]"
                @blur="waitForAnimation(deactivate)"
                :ref="(select) => (selects[field] = select)"
              />

              <ComboBox
                v-else-if="type === 'autocomplete'"
                v-model="model[field]"
                :tabindex="0"
                :inputId="field"
                :suggestions="suggestions?.[field]"
                @blur="waitForAnimation(deactivate)"
              />

              <InputText
                v-else
                v-model="model[field]"
                :id="field"
                :tabindex="0"
                fluid
                @blur="waitForAnimation(deactivate)"
              />
            </template>
          </Swap>

          <Message
            v-if="fieldErrors?.[field]"
            severity="error"
            size="small"
            variant="simple"
            class="ml-1"
          >
            {{ _join(fieldErrors?.[field], " ") }}
          </Message>
        </div>
      </div>
    </template>

    <template #footer>
      <div class="mt-4 flex flex-row justify-end gap-3">
        <template v-if="isEdited">
          <Button
            severity="warn"
            @click="confirmReset"
          >
            Reset
          </Button>

          <Button
            :disabled="!valid"
            @click="save"
          >
            Save
          </Button>
        </template>

        <template v-else>
          <Button
            v-if="props.action === 'update'"
            severity="danger"
            @click="confirmDelete"
          >
            Delete
          </Button>

          <Button>
            <!--suppress JSValidateTypes -->
            <NuxtLink :to="props.redirectBack">Back</NuxtLink>
          </Button>
        </template>
      </div>
    </template>
  </Card>
</template>

<script setup>
const selects = ref({})
const trixEditors = ref({})

const props = defineProps({
  name: {
    type: String,
    required: true
  },

  modelId: {
    type: [String, Number],
    required: false,
    validator: (value, props) => props.action === "create" || isPresent(value)
  },

  action: {
    type: String,
    required: false,
    default: "view",
    validator: (value) => _includes(["create", "update", "view"], value)
  },

  fields: {
    type: Object,
    required: true
  },

  initialValue: {
    type: Object,
    required: true
  },

  options: {
    type: Object,
    default: {},
    required: false
  },

  isSaved: {
    type: Boolean,
    required: true
  },

  redirectBack: {
    type: [String, Object],
    required: true
  },

  schema: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(["delete", "create", "update"])
const confirm = useConfirm()
const toast = useToast()

const { original, model, editedFields, isEdited, revert } = useRevertible(
  () => props.initialValue
)

const suggestions = ref(toValue(props.options))
const valid = ref(true)
const formErrors = ref([])
const fieldErrors = ref({})

onBeforeRouteLeave(async () => {
  if (isEdited.value && !props.isSaved && !(await confirmLeave())) {
    return false
  }
})

async function focusFormControl(field, type) {
  await nextTick()
  const control = document.getElementById(field)
  control.focus()

  switch (type) {
    case "select": {
      // noinspection JSUnresolvedReference
      selects.value[field].show()
      break
    }

    case "richText": {
      await nextTick()
      const length = control.editor.getDocument().getLength()
      control.editor.setSelectedRange(length - 1)
      break
    }

    default: {
      const length = _isNull(model.value[field]) ? 0 : model.value[field].length

      control.setSelectionRange(length, length)
    }
  }
}

async function waitForAnimation(deactivate) {
  await callAfterDelay(deactivate, 200)
}

async function reset() {
  revert()
  formErrors.value = []
  fieldErrors.value = {}
  await nextTick()

  // noinspection JSUnresolvedReference
  _forEach(_keys(_pickBy(props.fields, { type: "richText" })), (field) =>
    trixEditors.value[field].reset()
  )
}

function validate(field = null) {
  const result = props.schema.safeParse(
    props.action === "create" ? model.value : editedFields.value
  )

  if (result.success) {
    valid.value = true
    formErrors.value = []
    fieldErrors.value = {}
  } else {
    valid.value = false
    const flattened = z.flattenError(result.error)
    formErrors.value = flattened.formErrors

    if (field) {
      fieldErrors.value[field] = flattened.fieldErrors[field]
    } else {
      fieldErrors.value = flattened.fieldErrors
    }
  }

  return result
}

function save() {
  const result = validate()

  if (result.success) {
    emit(props.action, result.data)
  } else {
    console.error(
      "save() called with invalid data",
      z.flattenError(result.error)
    )
  }
}

function confirmDelete() {
  confirm.require({
    header: "Really?",
    icon: "ph:warning-bold",
    message: `Do you want to delete this ${props.name}?`,
    defaultFocus: "reject",

    acceptProps: {
      label: "Delete",
      severity: "danger"
    },

    rejectProps: { label: "Cancel" },
    accept: () => emit("delete"),

    reject: () =>
      toast.add({
        severity: "info",
        summary: "Cancelled.",
        detail: "Delete cancelled.",
        life: 3000
      })
  })
}

function confirmReset() {
  // noinspection JSUnusedGlobalSymbols
  const confirmOptions = {
    header: "Reset?",
    icon: "ph:warning-bold",
    message: `Do you want to reset this ${props.name}?`,
    defaultFocus: "reject",

    acceptProps: {
      label: "Reset",
      severity: "warn"
    },

    rejectProps: { label: "Cancel" },
    accept: async () => await reset(),

    reject: () =>
      toast.add({
        severity: "info",
        summary: "Cancelled.",
        detail: "Reset cancelled.",
        life: 3000
      })
  }

  if (props.action === "create") {
    _assign(confirmOptions, {
      header: "Revert?",
      message: "Do you want to revert your edits?",

      acceptProps: {
        label: "Revert",
        severity: "warn"
      },

      reject: () =>
        toast.add({
          severity: "info",
          summary: "Cancelled.",
          detail: "Revert cancelled.",
          life: 3000
        })
    })
  }

  confirm.require(confirmOptions)
}

async function confirmLeave() {
  const modal = new Promise((resolve) => {
    const confirmOptions = {
      header: "Leave?",
      icon: "ph:warning-bold",
      message: `Do you want to abandon this ${props.name}?`,
      defaultFocus: "reject",

      acceptProps: {
        label: "Leave",
        severity: "danger"
      },

      rejectProps: { label: "Cancel" },

      accept: () => resolve(true),
      reject: () => resolve(false)
    }

    if (props.action === "create") {
      _assign(confirmOptions, {
        message: "Do you want to abandon your edits?"
      })
    }

    confirm.require(confirmOptions)
  })

  return await modal
}
</script>

<style scoped></style>
