<script setup>
// A time box that reads on the clock the user chose.
//
// The same problem DateField solves for dates, and the same shape of answer.
// `<input type="time">` renders in the *browser's* locale and nothing on the
// page moves it — `lang` included, which was measured rather than assumed: the
// control comes out identically wide whatever it is set to, because it follows
// navigator.languages and not the document. So a Settings → Time format of
// 24-hour still met this account with "12:00 AM" in every schedule field,
// disagreeing with the 00:00 the card beside it printed, and the browser's am/pm
// did not fit the box either.
//
// So the typing half is ours: four digits, masked into the clock in force, with
// the meridiem as its own control rather than something to be spelled. There is
// no picker half — a calendar earns its button, four digits do not, and the
// browser's time popup would have brought the same 12-hour reading back with it.
//
// The value in and out is always 24-hour "HH:MM", which is what the charger's
// schedule commands take and what every caller already had.
import { computed, ref, watch } from "vue";
import { clockIsTwelveHour } from "../lib/format.js";

const props = defineProps({
  modelValue: { type: String, default: "" },
  disabled: { type: Boolean, default: false },
  ariaLabel: { type: String, default: "" },
});
const emit = defineEmits(["update:modelValue"]);

const twelve = computed(() => clockIsTwelveHour());
const pad = (n) => String(n).padStart(2, "0");

// "HH:MM" → its two numbers, or null for anything that is not a time of day.
function parse(value) {
  const m = /^(\d{1,2}):(\d{2})$/.exec(String(value || "").trim());
  if (!m) return null;
  const h = Number(m[1]);
  const min = Number(m[2]);
  return h > 23 || min > 59 ? null : { h, min };
}

// The digits the box shows: the hour as this clock writes it, and the minute.
function toText(value) {
  const p = parse(value);
  if (!p) return "";
  return `${pad(twelve.value ? p.h % 12 || 12 : p.h)}:${pad(p.min)}`;
}

// Whether the value sits in the afternoon. Only consulted on a 12-hour clock,
// where the box cannot say it and the select has to.
function toPm(value) {
  const p = parse(value);
  return !!p && p.h >= 12;
}

// What the box and the select hold → "HH:MM", or "" while it is still half
// typed. A half-typed time is not a time, and emitting the part of it that
// parses would set the charger's schedule to whatever was passed through on the
// way to the value somebody meant.
function toValue(text, pm) {
  const digits = String(text).replace(/\D/g, "");
  if (digits.length !== 4) return "";
  let h = Number(digits.slice(0, 2));
  const min = Number(digits.slice(2));
  if (min > 59) return "";
  if (twelve.value) {
    if (h < 1 || h > 12) return "";
    h = (h % 12) + (pm ? 12 : 0);
  } else if (h > 23) {
    return "";
  }
  return `${pad(h)}:${pad(min)}`;
}

// Digits regrouped as hh:mm. No trailing colon: it appears with the next digit,
// and adding it early only gives backspace something to fight with.
function mask(text) {
  const digits = String(text).replace(/\D/g, "").slice(0, 4);
  return digits.length > 2 ? `${digits.slice(0, 2)}:${digits.slice(2)}` : digits;
}

const text = ref(toText(props.modelValue));
const pm = ref(toPm(props.modelValue));

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

// Switching the setting in another tab re-lays out what is already in the box,
// rather than leaving one field on the old clock.
watch(twelve, () => {
  text.value = toText(props.modelValue);
  pm.value = toPm(props.modelValue);
});

function onInput(event) {
  const el = event.target;
  const masked = mask(el.value);
  text.value = masked;
  // Written straight to the DOM: Vue skips the patch when the bound value is
  // unchanged from the last render, which would leave a stray character the
  // user just typed sitting in the box.
  el.value = masked;
  emit("update:modelValue", toValue(masked, pm.value));
}

function onMeridiem(event) {
  pm.value = event.target.value === "pm";
  emit("update:modelValue", toValue(text.value, pm.value));
}
</script>

<template>
  <span class="flex items-center gap-1">
    <input
      type="text"
      inputmode="numeric"
      autocomplete="off"
      class="dh-input data w-20 shrink-0 text-center"
      :value="text"
      :disabled="disabled"
      :aria-label="ariaLabel"
      placeholder="--:--"
      maxlength="5"
      @input="onInput"
    />
    <select
      v-if="twelve"
      class="dh-input w-16 shrink-0"
      :value="pm ? 'pm' : 'am'"
      :disabled="disabled"
      :aria-label="ariaLabel"
      @change="onMeridiem"
    >
      <option value="am">am</option>
      <option value="pm">pm</option>
    </select>
  </span>
</template>
