import * as React from 'react';
import { delegateTriggerEvent } from '../event';

type ValueType = string | number | readonly string[];

interface HTMLElementWithValue extends HTMLElement {
  value: ValueType;
}

export interface UseProxyInputOptions<
  ForwardedRef extends HTMLElementWithValue,
  Value = ValueType,
> {
  forwardedRef: React.Ref<ForwardedRef>;
  valueSetterFn: (value: Value) => void;
  transformChangeHandlerArgs?: (value: Value) => ValueType;
}

export const useProxyInput = <
  TheValueType = ValueType,
  ForwardedRefType extends HTMLElementWithValue = HTMLElementWithValue,
  DefaultRefType extends HTMLElementWithValue = ForwardedRefType,
>(options: UseProxyInputOptions<ForwardedRefType, TheValueType>) => {
  const {
    forwardedRef,
    valueSetterFn,
    transformChangeHandlerArgs = (e) => e as ValueType,
  } = options;
  const defaultRef = React.useRef<DefaultRefType>(null);

  React.useEffect(() => {
    const { current: currentRaw } = defaultRef;
    const current = currentRaw as unknown as ForwardedRefType;
    const forwardedRefProxy = new Proxy(current, {
      get(target, prop, receiver) {
        return Reflect.get(target, prop, receiver) as unknown;
      },
      set(target, prop, newValue, receiver) {
        if (prop === 'value') {
          const thisNewValue = newValue as TheValueType;
          const actualNewValue = (
            Array.isArray(thisNewValue) ? thisNewValue.join('') : thisNewValue?.toString() ?? ''
          ) as TheValueType;
          valueSetterFn(actualNewValue);
          current.value = actualNewValue as string;
          return true;
        }
        return Reflect.set(target, prop, newValue, receiver);
      },
    }) as unknown as ForwardedRefType;

    if (typeof forwardedRef === 'function') {
      forwardedRef(forwardedRefProxy);
      return;
    }

    if (typeof forwardedRef === 'object' && forwardedRef) {
      const mutableForwardedRef = forwardedRef as React.MutableRefObject<ForwardedRefType>;
      mutableForwardedRef.current = forwardedRefProxy;
    }
  });

  // todo Handle input event

  // do we need to expose this? or is defaultref enough?
  //   maybe we could attach the event listener to the defaultRef instead of exposing a handler (as
  //   long as target element exposes a ref)

  // why do we have to do all this? because we want to proxy event handlers to fallback elements.
  //   fallback elements ensure server-side compatibility--with this, we can serialize every value
  //   that can be inputted in every custom control. (see graceful degradation)
  const handleChange = React.useCallback((e: TheValueType) => {
    if (!(typeof defaultRef === 'object' && defaultRef)) {
      return;
    }
    const { current: input } = defaultRef;
    if (!input) {
      return;
    }
    const transformedValue = transformChangeHandlerArgs(e);
    valueSetterFn(e);
    setTimeout(() => {
      delegateTriggerEvent('change', input, transformedValue);
    });
  }, [valueSetterFn, transformChangeHandlerArgs, defaultRef]);

  return React.useMemo(() => ({
    defaultRef,
    handleChange,
  }), [
    defaultRef,
    handleChange,
  ]);
};
