<script setup lang="ts">
import { ref, watch } from "vue";

interface Props {
  modelValue?: number | string | null;
  min?: number;
  max?: number;
  precision?: 0 | 2;
  disabled?: boolean;
  readonly?: boolean;
  placeholder?: string;
}

const props = withDefaults(defineProps<Props>(), {
  modelValue: null,
  precision: 2,
  disabled: false,
  readonly: false,
  placeholder: "",
});
const emit = defineEmits<{ "update:modelValue": [value: number | null] }>();
const text = ref(formatValue(props.modelValue));
const focused = ref(false);

watch(
  () => props.modelValue,
  (value) => {
    if (!focused.value) text.value = formatValue(value);
  },
);

function formatValue(value: number | string | null | undefined) {
  if (value === null || value === undefined || value === "") return "";
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) return "";
  const factor = 10 ** props.precision;
  return String(Math.round(numeric * factor) / factor);
}

function sanitize(value: string) {
  const normalized = value.replace(/,/g, ".").replace(/[^\d.-]/g, "");
  const negative = normalized.startsWith("-");
  const unsigned = normalized.replace(/-/g, "");
  const [integer = "", ...decimalParts] = unsigned.split(".");
  const sign = negative ? "-" : "";
  if (props.precision === 0) return sign + integer;
  const hasDecimalPoint = unsigned.includes(".");
  const decimal = decimalParts.join("").slice(0, props.precision);
  return sign + integer + (hasDecimalPoint ? `.${decimal}` : "");
}

function update(value: string) {
  text.value = sanitize(value);
  if (text.value === "" || text.value === "-" || text.value === "." || text.value === "-.") {
    emit("update:modelValue", null);
    return;
  }
  const numeric = Number(text.value);
  if (Number.isFinite(numeric)) emit("update:modelValue", numeric);
}

function commit() {
  focused.value = false;
  if (text.value === "" || text.value === "-" || text.value === "." || text.value === "-.") {
    text.value = "";
    emit("update:modelValue", null);
    return;
  }
  let value = Number(text.value);
  if (!Number.isFinite(value)) {
    text.value = formatValue(props.modelValue);
    return;
  }
  if (props.min !== undefined) value = Math.max(props.min, value);
  if (props.max !== undefined) value = Math.min(props.max, value);
  const factor = 10 ** props.precision;
  value = Math.round(value * factor) / factor;
  text.value = String(value);
  emit("update:modelValue", value);
}
</script>

<template>
  <el-input
    class="admin-number-input"
    :class="{ 'is-readonly': readonly }"
    :model-value="text"
    type="text"
    :inputmode="precision === 0 ? 'numeric' : 'decimal'"
    :disabled="disabled"
    :readonly="readonly"
    :placeholder="placeholder"
    @focus="focused = true"
    @input="update"
    @blur="commit"
  />
</template>

<style scoped>
.admin-number-input {
  width: 100%;
}

.admin-number-input :deep(.el-input__wrapper) {
  min-height: 40px;
  padding: 0 12px;
  border-radius: var(--radius-sm);
  box-shadow: 0 0 0 1px var(--color-border) inset;
}

.admin-number-input :deep(.el-input__wrapper.is-focus) {
  box-shadow: 0 0 0 1px var(--color-primary) inset;
}

.admin-number-input :deep(.el-input__inner) {
  font-variant-numeric: tabular-nums;
  text-align: left;
}

.admin-number-input.is-readonly :deep(.el-input__wrapper) {
  background: var(--color-bg);
}
</style>
