import { h, type FunctionComponent } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import cn from 'classnames'
import escapeRegex from '../utils/escape_regex.ts'

type Styles<C extends string, D extends string, S extends string> = {
  base?: string
  touched?: string
  element?: string
  label?: string
  statusIcon?: string
  noMargin?: string
  icon?: string
  labelIcon?: string
  center?: string
} & Record<C, string> &
  Record<D, string> &
  Record<S, string>

type Props<C extends string, D extends string, S extends string> = {
  autoComplete?: string
  autoFocus?: boolean
  center?: boolean
  className?: string
  classNames?: Partial<Styles<C, D, S>>
  color: C
  defaultValue?: string
  design: D
  disabled?: boolean
  icon: string
  label: string
  name: string
  noMargin?: boolean
  pattern?: string
  placeholder?: string
  required?: boolean
  sameAs?: string
  showValidity?: boolean
  size?: S
  type?: string
  value?: any
}

export default function inputFactory<C extends string = never, D extends string = never, S extends string = never>(
  styles: Styles<C, D, S>,
) {
  const Input: FunctionComponent<Props<C, D, S>> = ({
    autoComplete,
    autoFocus,
    center,
    className,
    classNames,
    color,
    defaultValue,
    design,
    disabled,
    icon,
    label,
    name,
    noMargin,
    pattern,
    placeholder,
    required,
    sameAs,
    size,
    type = 'text',
    value,
    showValidity = true,
  }) => {
    const [touched, setTouched] = useState(false)
    const inputRef = useRef<HTMLInputElement>(null)

    const onBlur = useCallback(() => setTouched(true), [])

    useEffect(() => {
      function onInput(e: Event) {
        inputRef.current!.pattern = escapeRegex((e.target! as HTMLInputElement).value)
      }

      const form = inputRef.current!.form!
      const resetTouched = setTouched.bind(null, false)

      form.addEventListener('reset', resetTouched)

      let sameAsInput: HTMLInputElement
      if (sameAs) {
        sameAsInput = form[sameAs]
        sameAsInput.addEventListener('input', onInput)
      }

      return () => {
        form.removeEventListener('reset', resetTouched)
        if (sameAsInput) sameAsInput.removeEventListener('input', onInput)
      }
    }, [])

    useEffect(() => {
      if (autoFocus) inputRef.current!.focus()
    }, [autoFocus])

    const id = styles.element + '_' + name

    return (
      <div
        className={cn(
          styles.base,
          design && cn(styles[design], classNames?.[design]),
          color && cn(styles[color], classNames?.[color]),
          size && cn(styles[size], classNames?.[size]),
          noMargin && cn(styles.noMargin, classNames?.noMargin),
          touched && cn(styles.touched, classNames?.touched),
          center && cn(styles.center, classNames?.center),
          className,
        )}
      >
        {icon && (styles.icon || classNames?.icon) && (
          <div className={cn(styles.icon, classNames?.icon)} style={{ maskImage: icon }} />
        )}

        <input
          autoComplete={autoComplete}
          autoFocus={autoFocus}
          className={cn(styles.element, classNames?.element)}
          defaultValue={
            defaultValue && type === 'datetime-local' ? new Date(defaultValue).toLocaleString('sv-SE') : defaultValue
          }
          disabled={disabled}
          id={id}
          name={name}
          onBlur={onBlur}
          pattern={pattern}
          placeholder={placeholder ?? label}
          ref={inputRef}
          required={required}
          type={type}
          value={value}
        />

        {showValidity && <i className={cn(styles.statusIcon, classNames?.statusIcon)} />}

        {/**
         * <label> comes after <input> to be able to use next sibling CSS selector
         * (see public input.module.scss). Use `flexbox` and `order` if <label> needs to be
         * displayed before <input> (see admin input.module.scss)
         */}
        {label && (
          <label className={cn(styles.label, classNames?.label)} htmlFor={id}>
            {icon && (styles.labelIcon || classNames?.labelIcon) && (
              <div className={cn(styles.labelIcon, classNames?.labelIcon)} style={{ maskImage: icon }} />
            )}
            {label}
          </label>
        )}
      </div>
    )
  }

  return Input
}
