<script setup>
// A date box that reads in the order the user chose.
//
// `<input type="date">` renders in the *browser's* locale and nothing on the
// page can move it: a Settings › Date format of DD-MM-YYYY still met the owner
// with 08/22/2026 in every dialog, disagreeing with the DD-MM-YYYY the tables
// beside it printed. So the typing half is ours — a masked text box whose
// segment order comes from prefs.dateFormat, the same setting lib/format.js
// reads — and the picking half stays the browser's, behind the calendar button.
//
// The value in and out is always ISO ("2026-08-22", or "2026-08" at month
// precision), which is what the API stores and what every caller already had.
import { computed, onMounted, ref, watch } from "vue";
import { prefs } from "../prefs.js";
import { t } from "../i18n";

const props = defineProps({
  modelValue: { type: String, default: "" },
  // "day" is a full date; "month" drops the day segment, for the fields that
  // only ever knew a month (a car's build date).
  precision: { type: String, default: "day" },
  required: { type: Boolean, default: false },
});
const emit = defineEmits(["update:modelValue"]);

// The four display formats collapse to three typing orders: a month spelled out
// ("22 Aug 2026") is for reading, not for entry, so DMY and DMY_NUM share one
// numeric layout. The separator follows each format's own convention.
const LAYOUTS = {
  YMD: { order: ["y", "m", "d"], sep: "-" },
  DMY_NUM: { order: ["d", "m", "y"], sep: "-" },
  DMY: { order: ["d", "m", "y"], sep: "-" },
  MDY: { order: ["m", "d", "y"], sep: "/" },
};
const WIDTH = { y: 4, m: 2, d: 2 };

const layout = computed(() => {
  const spec = LAYOUTS[prefs.dateFormat] || LAYOUTS.YMD;
  const order = props.precision === "month" ? spec.order.filter((part) => part !== "d") : spec.order;
  return { order, sep: spec.sep };
});

const placeholder = computed(() =>
  layout.value.order.map((part) => t(`common.dateUnits.${part}`)).join(layout.value.sep)
);
const digitsWanted = computed(() => layout.value.order.reduce((sum, part) => sum + WIDTH[part], 0));

// ISO → what the box shows.
function toText(value) {
  const parsed = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/.exec(String(value || "").trim());
  if (!parsed) return "";
  const parts = { y: parsed[1], m: parsed[2], d: parsed[3] };
  if (layout.value.order.some((part) => !parts[part])) return "";
  return layout.value.order.map((part) => parts[part]).join(layout.value.sep);
}

// What the box shows → ISO, or "" while it is still half typed. A date is only
// accepted once every segment is full and the day actually exists in that month
// — 31-02 round-trips through Date as 03-03, and storing March for a February
// somebody typed is worse than holding the field invalid.
function toIso(text) {
  const digits = String(text).replace(/\D/g, "");
  if (digits.length !== digitsWanted.value) return "";
  const parts = {};
  let at = 0;
  for (const part of layout.value.order) {
    parts[part] = digits.slice(at, at + WIDTH[part]);
    at += WIDTH[part];
  }
  const year = Number(parts.y);
  const month = Number(parts.m);
  const day = parts.d ? Number(parts.d) : 1;
  if (month < 1 || month > 12 || day < 1) return "";
  const probe = new Date(Date.UTC(year, month - 1, day));
  if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return "";
  return parts.d ? `${parts.y}-${parts.m}-${parts.d}` : `${parts.y}-${parts.m}`;
}

// Digits regrouped into segments. No trailing separator: one appears as soon as
// the next digit is typed, and adding it early only gives backspace something
// to fight with.
function mask(text) {
  const digits = String(text).replace(/\D/g, "").slice(0, digitsWanted.value);
  const groups = [];
  let at = 0;
  for (const part of layout.value.order) {
    if (at >= digits.length) break;
    groups.push(digits.slice(at, at + WIDTH[part]));
    at += WIDTH[part];
  }
  return groups.join(layout.value.sep);
}

const text = ref(toText(props.modelValue));
const textEl = ref(null);
const pickerEl = ref(null);

// Only re-render the box when the value it is showing is genuinely a different
// date. Half-typed input emits "" (there is no date yet), and reacting to that
// would wipe the very digits being typed.
watch(
  () => props.modelValue,
  (value) => {
    if (toIso(text.value) === (value || "")) return;
    text.value = toText(value);
  }
);

// Switching the setting in another tab — or in Settings with a dialog open —
// re-lays out what is already there rather than leaving one box in the old order.
watch(layout, () => {
  text.value = toText(props.modelValue);
});

// Native validation still runs the submit: `required` catches an empty box, and
// this catches a full one that is not a date, so a half-typed "22-08-20" cannot
// slip through as no date at all.
function refreshValidity() {
  const el = textEl.value;
  if (!el) return;
  el.setCustomValidity(text.value && !toIso(text.value) ? t("forms.common.dateInvalid") : "");
}
watch(text, refreshValidity);
onMounted(refreshValidity);

// The caret is put back by counting digits rather than characters, so editing
// the middle of a date does not throw the cursor to the end on every keystroke.
function caretAfterDigits(masked, count) {
  if (count <= 0) return 0;
  let seen = 0;
  for (let i = 0; i < masked.length; i++) {
    if (/\d/.test(masked[i]) && ++seen === count) return i + 1;
  }
  return masked.length;
}

function applyEdit(el) {
  const raw = el.value;
  const before = raw.slice(0, el.selectionStart ?? raw.length).replace(/\D/g, "").length;
  const masked = mask(raw);
  text.value = masked;
  // Written straight to the DOM: Vue skips the patch when the bound value is
  // unchanged from last render, which would leave the stray separator the user
  // just typed sitting in the box.
  el.value = masked;
  const caret = caretAfterDigits(masked, before);
  el.setSelectionRange(caret, caret);
  // Set here as well as from the watcher: a form submitted in the same tick as
  // the last keystroke must see the validity the box has now, not the one it
  // had before the digit landed.
  refreshValidity();
  emit("update:modelValue", toIso(masked));
}

function onInput(event) {
  applyEdit(event.target);
}

// Backspace onto a separator takes the digit in front of it too. The separators
// are ours, not the user's — deleting one alone leaves the same digits, which
// the mask puts straight back, and the key reads as broken.
function onBeforeInput(event) {
  if (event.inputType !== "deleteContentBackward") return;
  const el = event.target;
  const at = el.selectionStart ?? 0;
  if (at < 2 || at !== el.selectionEnd || /\d/.test(el.value[at - 1])) return;
  event.preventDefault();
  el.value = el.value.slice(0, at - 2) + el.value.slice(at);
  el.setSelectionRange(at - 2, at - 2);
  applyEdit(el);
}

function openPicker() {
  const el = pickerEl.value;
  if (!el) return;
  if (typeof el.showPicker === "function") {
    try {
      el.showPicker();
      return;
    } catch {
      // Not allowed here (no user activation, or an older engine) — fall through.
    }
  }
  el.focus();
  el.click();
}

function onPick(event) {
  const value = event.target.value;
  text.value = toText(value);
  emit("update:modelValue", value);
}
</script>

<template>
  <div class="relative">
    <input
      ref="textEl"
      type="text"
      inputmode="numeric"
      autocomplete="off"
      class="dh-input data pr-9"
      :value="text"
      :placeholder="placeholder"
      :required="required"
      @beforeinput="onBeforeInput"
      @input="onInput"
    />
    <!-- The browser's own picker, kept out of sight: it is opened by the button
         and only ever hands back an ISO value. -->
    <input
      ref="pickerEl"
      :type="precision === 'month' ? 'month' : 'date'"
      class="dv-date-picker"
      tabindex="-1"
      aria-hidden="true"
      :value="modelValue"
      @input="onPick"
    />
    <button
      type="button"
      class="dv-date-open"
      :aria-label="t('forms.common.pickDate')"
      :title="t('forms.common.pickDate')"
      @click="openPicker"
    >
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" class="h-4 w-4">
        <path stroke-linecap="round" stroke-linejoin="round" d="M8 3v3m8-3v3M4 9h16M5 6h14a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1Z" />
      </svg>
    </button>
  </div>
</template>
