import type { FormEvent } from "react";

export type InputElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;

export function ignoreTarget(target: InputElement | Element | null, form?: HTMLFormElement) {
   const tagName = target?.tagName.toLowerCase() ?? "";
   const tagNames = ["input", "select", "textarea"];

   return (
      !target ||
      !form?.contains(target) ||
      !tagNames.includes(tagName) ||
      !("name" in target) ||
      target.hasAttribute("data-ignore") ||
      target.closest("[data-ignore]")
   );
}

export function getFormTarget(e: FormEvent<HTMLFormElement>): InputElement | null {
   const form = e.currentTarget;
   const target = e.target as InputElement | null;

   return ignoreTarget(target, form) ? null : target;
}

export function getTargetsByName(form: HTMLFormElement, name: string): InputElement[] {
   const query = form.querySelectorAll(`[name="${name}"]`);
   return Array.from(query).filter((e) => ignoreTarget(e)) as InputElement[];
}

export function coerce(target: InputElement | null, value?: any) {
   if (!target) return value;
   const required = target.required;
   if (!value && !required) return undefined;

   if (target.type === "number") {
      const num = Number(value);
      if (Number.isNaN(num) && !required) return undefined;

      const min = "min" in target && target.min.length > 0 ? Number(target.min) : undefined;
      const max = "max" in target && target.max.length > 0 ? Number(target.max) : undefined;
      const step = "step" in target && target.step.length > 0 ? Number(target.step) : undefined;

      if (min && num < min) return min;
      if (max && num > max) return max;
      if (step && step !== 1) return Math.round(num / step) * step;

      return num;
   } else if (target.type === "text") {
      const maxLength =
         "maxLength" in target && target.maxLength > -1 ? Number(target.maxLength) : undefined;
      const pattern = "pattern" in target ? new RegExp(target.pattern) : undefined;

      if (maxLength && value.length > maxLength) return value.slice(0, maxLength);
      if (pattern && !pattern.test(value)) return "";

      return value;
   } else if (target.type === "checkbox") {
      if ("checked" in target) return !!target.checked;
      return ["on", "1", "true", 1, true].includes(value);
   } else {
      return value;
   }
}

export type CleanOptions = {
   empty?: any[];
   emptyInArray?: any[];
   keepEmptyArray?: boolean;
};
export function cleanObject<Obj extends { [key: string]: any }>(
   obj: Obj,
   _opts?: CleanOptions,
): Obj {
   if (!obj) return obj;
   const _empty = [null, undefined, ""];
   const opts = {
      empty: _opts?.empty ?? _empty,
      emptyInArray: _opts?.emptyInArray ?? _empty,
      keepEmptyArray: _opts?.keepEmptyArray ?? false,
   };

   return Object.entries(obj).reduce((acc, [key, value]) => {
      if (value && Array.isArray(value) && value.some((v) => typeof v === "object")) {
         const nested = value.map((o) => cleanObject(o, opts));
         if (nested.length > 0 || opts?.keepEmptyArray) {
            acc[key] = nested;
         }
      } else if (value && typeof value === "object" && !Array.isArray(value)) {
         const nested = cleanObject(value, opts);
         if (Object.keys(nested).length > 0) {
            acc[key] = nested;
         }
      } else if (Array.isArray(value)) {
         const nested = value.filter((v) => !opts.emptyInArray.includes(v));
         if (nested.length > 0 || opts?.keepEmptyArray) {
            acc[key] = nested;
         }
      } else if (!opts.empty.includes(value)) {
         acc[key] = value;
      }
      return acc;
   }, {} as any);
}

export function setPath(object, _path, value) {
   let path = _path;

   // Optional string-path support.
   // You can remove this `if` block if you don't need it.
   if (typeof path === "string") {
      const isQuoted = (str) => str[0] === '"' && str.at(-1) === '"';
      path = path
         .split(/[.\[\]]+/)
         .filter((x) => x)
         .map((x) => (!Number.isNaN(Number(x)) ? Number(x) : x))
         .map((x) => (typeof x === "string" && isQuoted(x) ? x.slice(1, -1) : x));
   }

   if (path.length === 0) {
      throw new Error("The path must have at least one entry in it");
   }

   const [head, ...tail] = path;

   if (tail.length === 0) {
      object[head] = value;
      return object;
   }

   if (!(head in object)) {
      object[head] = typeof tail[0] === "number" ? [] : {};
   }

   setPath(object[head], tail, value);
   return object;
}
