<template>
  <component
    :is="formType"
    :class="cssClasses"
    @submit.prevent="handleSubmit"
    @focusout="handleFocusOut"
    @input.capture="handleInputCapture"
    @keyup.capture="handleInputCapture"
  >
    <slot name="errors" :errors="errors">
      <ErrorList v-if="errors" :errors="errors?.__all__" />
    </slot>
    <div v-if="defaultGroup" class="inside">
      <template
        v-for="field of defaultGroup.fields"
        :key="field.name"
        :class="{ [field.type || 'text']: true }"
      >
        <slot
          :name="field.type"
          :field="field"
          :object="props.modelValue"
          :defaults="defaults"
          :updateField="(data: any) => update(field, data)"
          :updateModel="emitUpdate"
        >
          <AutoField
            @update:modelValue="updateProxy($event)"
            :form-component="formComponent"
            :field="field"
            :errors="mergedErrors"
            :defaults="props.defaults"
            :modelValue="props.modelValue"
          />
        </slot>
      </template>
    </div>
    <Accordion>
      <AccordionTab
        v-for="group of grouped.filter((g) => g.name !== 'default')"
        :header="group.name"
      >
        <div class="inside">
          <template
            v-for="field of group.fields"
            :key="field.name"
            :class="{ [field.type || 'text']: true }"
          >
            <slot
              :name="field.type"
              :field="field"
              :object="props.modelValue"
              :defaults="defaults"
              :updateField="(data: any) => update(field, data)"
              :updateModel="emitUpdate"
            >
              <AutoField
                @update:modelValue="update($event, field)"
                :form-component="formComponent"
                :field="field"
                :errors="mergedErrors"
                :defaults="props.defaults"
                :modelValue="props.modelValue"
              />
            </slot>
            <div v-if="field.help" class="help">{{ field.help }}</div>
          </template>
        </div>
      </AccordionTab>
    </Accordion>
    <template v-if="!props.sub" class="row buttons">
      <slot>
        <div class="button-pre-cell" />
        <div class="button-cell">
          <Button type="submit" class="btn btn-primary">
            {{ actionButtonText }}
          </Button>
        </div>
      </slot>
    </template>
  </component>
</template>

<script setup lang="ts">
import Accordion from "primevue/accordion";
import AccordionTab from "primevue/accordiontab";
import Button from "primevue/button";
import type { Component } from "vue";
import { computed, nextTick, onMounted, ref, watch } from "vue";
import copy from "../../functions/copy";
import { dottedGet, dottedSet } from "../../functions/dotted_get.ts";
import AutoField from "./AutoField.vue";
import ErrorList from "./ErrorList.vue";
import {
  ErrorDict,
  FieldDef,
  flattenValidatedFields,
  mergeErrorDicts,
  updateModelValue,
  validateSchema,
} from "./defs";

const props = withDefaults(
  defineProps<{
    fields: FieldDef[];
    modelValue: Record<string, any>;
    errors?: Record<string, any>;
    defaults?: Record<string, any>;
    formComponent?: Component;
    sub?: boolean;
    actionButtonText?: string;
    focusOnError?: boolean;
  }>(),
  { actionButtonText: "Submit", focusOnError: true },
);

const emits = defineEmits<{
  submit: [];
  "update:modelValue": [any];
  "update:errors": [ErrorDict];
  error: [FieldDef, HTMLInputElement, string | undefined];
  errors: [{ field: FieldDef; el: HTMLInputElement }[]];
}>();

const localErrors = ref<ErrorDict>({});
const mergedErrors = computed(() =>
  mergeErrorDicts(props.errors, localErrors.value),
);

const defaults = computed(() => props.defaults ?? {});
const formType = computed(() => (props.sub ? "div" : "form"));
const grouped = computed(() => {
  const groups: { name: string; fields: FieldDef[] }[] = [];
  for (const field of props.fields) {
    const name = field.group || "default";
    let group = groups.find((g) => g.name === name);
    if (!group) {
      group = { name, fields: [] };
      groups.push(group);
    }
    group.fields.push(field);
  }
  return groups;
});
const defaultGroup = computed(() => {
  return grouped.value.find((g) => g.name === "default");
});
const cssClasses = computed(() => {
  const retval = {
    "auto-form": true,
    "sub-form": props.sub,
  };
  return retval;
});
const validatedFields = computed(() => flattenValidatedFields(props.fields));

let timeout = 0;
watch(
  () => props.fields,
  async () => {
    let local = 0;
    await new Promise((resolve) => {
      clearTimeout(timeout);
      timeout = window.setTimeout(resolve, 100);
      local = timeout;
    });
    if (local === timeout) {
      applyDefaults();
    }
  },
  { deep: true },
);

onMounted(async () => {});

function updateProxy(value: any) {
  emitUpdate(value);
}

function update(field: FieldDef, value: any, key?: string) {
  if (field.name) {
    const current = dottedGet(value, field.name);

    if (value === "") {
      value = undefined;
    }

    if (key || current != value) {
      emitUpdate(updateModelValue(props.modelValue, field, value, key));
    }
  }
}

function applyDefaults() {
  const data = copy(props.modelValue);
  let usedDefault = false;
  for (const field of props.fields.filter((f) => f.name && f.default)) {
    if (!dottedGet(data, field.name!)) {
      dottedSet(data, field.name!, field.default);
      usedDefault = true;
    }
  }
  if (usedDefault) {
    emitUpdate(data);
  }
}

function emitUpdate(data: any) {
  emits("update:modelValue", data);
}

async function checkFieldValidity(field: FieldDef, value: unknown) {
  const error = await validateSchema(field.validation, value);
  console.log("check", field.name, value, "~", error);
  dottedSet(localErrors.value, field.name!, error ? [error] : undefined);
  emits("update:errors", localErrors.value);
  const el: HTMLInputElement | null = document.querySelector(
    `[name="${field.name}"]`,
  );
  if (el) {
    emits("error", field, el, error);
  }
  return error;
}

function handleFocusOut(event: FocusEvent) {
  const target = event.target as HTMLInputElement | null;
  const field = validatedFields.value.find((f) => f.name === target?.name);
  if (field) {
    checkFieldValidity(field, dottedGet(props.modelValue, field.name!));
  }
}

function handleInputCapture(event: Event) {
  const target = event.target as HTMLInputElement | null;
  if (
    target?.name &&
    validatedFields.value.some((f) => f.name === target.name)
  ) {
    dottedSet(localErrors.value, target.name, undefined);
    emits("update:errors", localErrors.value);
  }
}

async function validateAllFields(): Promise<boolean> {
  let firstInvalid: HTMLInputElement | null = null;
  const errors: { field: FieldDef; el: HTMLInputElement }[] = [];
  let hasError = false;
  for (const field of validatedFields.value) {
    const error = await checkFieldValidity(
      field,
      dottedGet(props.modelValue, field.name!),
    );
    if (error) {
      hasError = true;
      const el: HTMLInputElement | null = document.querySelector(
        `[name="${field.name}"]`,
      );
      if (el) {
        errors.push({ field, el });
        firstInvalid ??= el;
      }
    }
  }
  if (hasError) {
    emits("errors", errors);
    if (props.focusOnError && firstInvalid) {
      firstInvalid.scrollIntoView({ behavior: "smooth", block: "center" });
    }
    return false;
  }
  return true;
}

async function handleSubmit($event: SubmitEvent) {
  $event.stopImmediatePropagation();
  if (!(await validateAllFields())) {
    return;
  }
  emits("submit");
}

function isArray(object: any) {
  return object instanceof Array;
}
</script>

<style scoped>
.auto-form > .error {
  text-align: center;
  max-width: 600px;
  margin: auto;
}
.auto-form .inside {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 5px;
  padding: 5px;
  max-width: 600px;
  margin: auto;
}
.auto-form label,
.auto-form :deep(label) {
  text-align: right;
  padding-top: 10px;
  white-space: wrap;
  max-width: 30vw;
}
:deep(.p-inputtext, .p-password) {
  width: 100%;
  min-width: 1px;
}
.error {
  grid-column: span 1;
  padding: 3px;
}
.component {
  vertical-align: top;
  align-items: flex-start;
}
</style>
