<script setup>
import { compile, computed, defineComponent, inject, onMounted, ref, shallowRef, watch } from "vue";
import { watchDebounced } from "@vueuse/core";
import { useRules } from "../../composables/utils/validation";
import { useDocumentTemplate } from "../../composables/document/template";
import { isObject, isArray, isString, isPlainObject, isEqual, debounce } from "lodash-es";
import FormPad from "./Pad.vue";
defineOptions({
  inheritAttrs: false
});
const props = defineProps({
  modelValue: { type: Object, required: false },
  originalData: { type: Object, required: false },
  template: { type: null, required: false },
  templateScript: { type: String, required: false },
  disabled: { type: Boolean, required: false, default: false },
  readonly: { type: Boolean, required: false, default: false },
  isolated: { type: Boolean, required: false, default: false },
  decoration: { type: Object, required: false, default: () => {
    return {};
  } },
  parentTemplates: { type: [String, Array], required: false, default: () => [] },
  dirtyClass: { type: String, required: false, default: "form-data-dirty" },
  dirtyOnCreate: { type: Boolean, required: false, default: false },
  sanitizeDelay: { type: Number, required: false, default: 5e3 }
});
const emit = defineEmits(["update:modelValue"]);
const disabled = ref(props.disabled);
const readonly = ref(props.readonly);
const decoration = ref(props.decoration);
watch(() => props.disabled, (newValue) => {
  disabled.value = newValue;
});
watch(() => props.readonly, (newValue) => {
  readonly.value = newValue;
});
watch(() => props.decoration, (newValue) => {
  decoration.value = newValue;
}, { deep: true });
const { rules } = useRules();
const trimmedTemplate = ref("");
const vueFunctions = { ref, shallowRef, computed, watch, onMounted };
const templateScriptFunction = computed(() => {
  let templateScript = props.templateScript?.trim() || "return {}";
  const pattern = /^\s*[{[].*[}\]]\s*$/;
  if (pattern.test(templateScript)) templateScript = "return {}";
  return Function("props", "ctx", ...Object.keys(vueFunctions), templateScript);
});
const formPad = ref();
const formInjectKey = /* @__PURE__ */ Symbol.for("vuetify:form");
const formInjected = ref();
const formData = ref({});
function isBlankString(v) {
  return isString(v) && v.trim().length === 0;
}
function scheduleIdle(fn) {
  if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
    window.requestIdleCallback(fn, { timeout: 2e3 });
  } else {
    setTimeout(fn, 0);
  }
}
const sanitizeBlankStrings = debounce((val, original) => {
  scheduleIdle(() => sanitizeBlankStringsSync(val, original));
}, props.sanitizeDelay);
function sanitizeBlankStringsSync(val, original) {
  if (!original && props.originalData) {
    sanitizeBlankStringsSync(val, props.originalData);
    return;
  }
  if (isArray(val)) {
    for (let i = val.length - 1; i >= 0; i--) {
      const item = val[i];
      if (isBlankString(item)) {
        val.splice(i, 1);
      } else if (isPlainObject(item) || isArray(item)) {
        sanitizeBlankStringsSync(item, {});
      } else {
        if (item && typeof item === "string") val[i] = item.replace(/[ \t]+$/, "");
      }
    }
    return;
  }
  if (isPlainObject(val)) {
    for (const key of Object.keys(val)) {
      const v = val[key];
      if (isBlankString(v)) {
        if (original && !Object.keys(original).includes(key)) delete val[key];
        else val[key] = null;
      } else if (isPlainObject(v) || isArray(v)) {
        let originalChild = original && (isPlainObject(original[key]) || isArray(original[key])) ? original[key] : {};
        sanitizeBlankStringsSync(v, originalChild);
      } else {
        if (v && typeof v === "string") val[key] = v.replace(/[ \t]+$/, "");
      }
    }
  }
}
function flushSanitize() {
  sanitizeBlankStrings.cancel();
  sanitizeBlankStringsSync(formData.value, props.originalData ?? {});
}
function autoSanitizedDisplay(item, separator = ",") {
  const isEmptyScalar = (v) => v === null || v === void 0 || typeof v === "string" && v.trim() === "" || typeof v === "number" && Number.isNaN(v);
  const toStr = (v) => {
    if (isEmptyScalar(v)) return void 0;
    if (typeof v === "string") return v.trim();
    if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
    if (typeof v === "symbol") return v.description ?? String(v);
    if (typeof v === "function") return v.name ? `[Function ${v.name}]` : "[Function]";
    return void 0;
  };
  if (isEmptyScalar(item)) return void 0;
  if (Array.isArray(item)) {
    const parts = item.map((x) => autoSanitizedDisplay(x, separator)).filter((s) => typeof s === "string" && s.trim() !== "");
    return parts.length ? parts.join(separator) : void 0;
  }
  const scalar = toStr(item);
  if (scalar !== void 0) return scalar;
  if (typeof item === "object") {
    if ("label" in item) {
      const v = autoSanitizedDisplay(item.label, separator) ?? toStr(item.label);
      if (v !== void 0) return v;
    }
    if ("value" in item) {
      const v = autoSanitizedDisplay(item.value, separator) ?? toStr(item.value);
      if (v !== void 0) return v;
    }
    const entries = Object.entries(item).map(([k, v]) => {
      const rendered = autoSanitizedDisplay(v, separator) ?? toStr(v);
      if (rendered === void 0 || rendered.trim() === "") return void 0;
      return `${k}:${rendered}`;
    }).filter((x) => typeof x === "string" && x.trim() !== "");
    return entries.length ? entries.join(separator) : void 0;
  }
  try {
    const s = String(item);
    return s.trim() ? s : void 0;
  } catch {
    return void 0;
  }
}
watch(formData, (newValue) => {
  sanitizeBlankStrings(newValue);
  emit("update:modelValue", newValue);
}, { deep: true });
watch(() => props.modelValue, (newValue) => {
  formData.value = isObject(newValue) ? newValue : {};
}, { deep: true, immediate: true });
function diffPaths(a, b, base = [], out = []) {
  const keys = /* @__PURE__ */ new Set([
    ...Object.keys(a ?? {}),
    ...Object.keys(b ?? {})
  ]);
  for (const k of keys) {
    const av = a?.[k];
    const bv = b?.[k];
    if (!av && !bv) continue;
    const path = [...base, k];
    if (isPlainObject(av) && isPlainObject(bv)) {
      diffPaths(av, bv, path, out);
      continue;
    }
    if (!isEqual(av, bv)) {
      out.push(path.join("."));
    }
  }
  return out;
}
const injectedClass = computed(() => {
  const data = formData.value ?? {};
  const result = {};
  if (!props.dirtyOnCreate && !props.originalData) return result;
  const original = props.originalData ?? {};
  const cls = props.dirtyClass || "form-data-dirty";
  const paths = diffPaths(data, original);
  for (const p of paths) result[p] = cls;
  return result;
});
const formComponent = shallowRef();
function buildFormComponent() {
  if (!trimmedTemplate.value) return;
  const originalConsoleError = console.warn;
  console.warn = (error) => {
    throw new Error(error);
  };
  try {
    const componentTemplate = '<form-pad ref="formPadTemplate" v-model="formComponentData" :originalData="originalData" :disabled="disabled" :readonly="readonly" :decoration="decoration" :isolated="isolated"><template v-slot="{ data,isDisabled,isReadonly,rules,formProvided,decoration,injectedClass }">' + trimmedTemplate.value + "</template></form-pad>";
    compile(componentTemplate);
    formComponent.value = defineComponent({
      components: { FormPad },
      props: {
        modelValue: { type: Object, default: void 0 },
        originalData: { type: Object, default: void 0 },
        disabled: { type: Boolean, default: false },
        readonly: { type: Boolean, default: false },
        decoration: { type: Object, default: () => {
          return {};
        } },
        isolated: { type: Boolean, default: false }
      },
      emits: ["update:modelValue"],
      setup(props2, ctx) {
        const formComponentData = ref({});
        const formPadTemplate = ref({});
        watch(formComponentData, (newValue) => {
          sanitizeBlankStrings(newValue);
          ctx.emit("update:modelValue", newValue);
        }, { deep: true });
        watch(() => props2.modelValue, (newValue) => {
          formComponentData.value = isObject(newValue) ? newValue : {};
        }, { deep: true, immediate: true });
        const isValid2 = computed(() => formPadTemplate.value.isValid);
        return {
          formComponentData,
          formPadTemplate,
          reset: () => formPadTemplate.value.reset(),
          validate: () => formPadTemplate.value.validate(),
          resetValidate: () => formPadTemplate.value.resetValidate(),
          isValid: isValid2,
          ...templateScriptFunction.value(props2, ctx, ...Object.values(vueFunctions)),
          autoSanitizedDisplay
        };
      },
      template: componentTemplate
    });
  } catch (e) {
    formComponent.value = null;
    console.error(e);
  }
  console.warn = originalConsoleError;
}
function reset() {
  if (!formInjected.value) formPad.value.reset();
  else formInjected.value.items.forEach((item) => item.reset());
}
function validate() {
  if (!formInjected.value) formPad.value.validate();
  else formInjected.value.items.forEach((item) => item.validate());
}
function resetValidate() {
  if (!formInjected.value) formPad.value.resetValidate();
  else formInjected.value.items.forEach((item) => item.resetValidate());
}
const isValid = computed(() => {
  validate();
  return formInjected.value ? formInjected.value.isValid || false : formPad.value.isValid || false;
});
onMounted(() => {
  if (!props.isolated) formInjected.value = inject(formInjectKey, false);
  buildFormComponent();
});
watchDebounced(() => props.template, (newValue) => {
  trimmedTemplate.value = useDocumentTemplate(newValue, props.parentTemplates).trim() || "";
  buildFormComponent();
}, { debounce: 500, maxWait: 5e3, deep: true, immediate: true });
watchDebounced(() => props.templateScript, buildFormComponent, { debounce: 500, maxWait: 5e3 });
defineExpose({
  isValid,
  disabled,
  readonly,
  reset,
  validate,
  resetValidate,
  flushSanitize
});
</script>

<template>
  <v-form
    v-if="!formInjected && !trimmedTemplate"
    ref="formPad"
    :disabled="disabled"
    :readonly="readonly"
    :class="$attrs.class"
    autocomplete="off"
  >
    <template #default="formProvided">
      <slot
        :data="formData"
        :form-provided="formProvided"
        :is-disabled="disabled"
        :is-readonly="readonly"
        :rules="rules"
        :decoration="decoration"
        :injectedClass="injectedClass"
      />
    </template>
  </v-form>
  <template v-if="formInjected && !trimmedTemplate">
    <slot
      :data="formData"
      :form-provided="formInjected"
      :is-disabled="disabled"
      :is-readonly="readonly"
      :rules="rules"
      :decoration="decoration"
      :injectedClass="injectedClass"
    />
  </template>
  <component
    :is="formComponent"
    v-if="trimmedTemplate"
    ref="formPad"
    v-model="formData"
    :originalData="originalData"
    :disabled="disabled"
    :readonly="readonly"
    :decoration="decoration"
    :isolated="isolated"
    :class="$attrs.class"
  />
</template>

<style>
.form-data-dirty:not(.v-input--error) :not(.v-chip):not(.v-chip *):not(.v-toolbar):not(.v-toolbar *){color:color-mix(in srgb,currentColor 70%,rgb(var(--v-theme-primary)))!important;text-shadow:0 0 .02em currentColor}
</style>
