import "./style.pcss";
import { getDebouncedFn } from "@web3r/flowerkit/fn";
import type {
  FC,
  ReactElement,
  ChangeEvent,
  ClipboardEvent,
  FocusEvent,
} from "react";
import {
  useImperativeHandle,
  useState,
  useEffect,
  useRef,
  useMemo, useCallback,
} from "react";
import { IMaskInput } from "react-imask";
import type { ReactMaskProps } from "react-imask";
import type {
  IMaskedInput, TMaskedCallbackArg, TInstance, TMaskedInputProps, TMaskedRef, TMaskOptions,
} from "./config/types";
import { getCurrentMask, getCurrentValue } from "./lib";
import { constants } from "../../constants";
import type { IComponentBaseProps } from "../../types";
import { useCN } from "../../utils/react/useCN";
import { useFormValidation } from "../../utils/react/useFormValidation";
import { useSSGRender } from "../../utils/react/useSSGRender";
import { Input } from "../input";


const MaskedControl: FC<TInstance & IMaskedInput & ReactMaskProps<HTMLInputElement> & {
  onAccept: unknown;
  onComplete: unknown;
  onPaste: unknown;
  onKeyDown: unknown;
  defaultValue: IMaskedInput["value"];
}> = (props = {}): ReactElement => {

  const { getCN } = useCN("formInput");

  const computedProps = useMemo<TMaskedInputProps>(() => {

    const extraClasses: IComponentBaseProps["extraClasses"] = {};

    const currentProps: TMaskedInputProps = {
      ...props,
      ...((props?.mask ?? {}) as object),
    } as TMaskedInputProps;

    // readonly prop
    if (currentProps.isReadOnly) {
      currentProps.readOnly = true;
      extraClasses.isReadOnly = true;
    }
    delete currentProps.isReadOnly;

    // disabled prop
    if (currentProps.isDisabled) {
      currentProps.disabled = true;
      extraClasses.isDisabled = true;
    }
    delete currentProps.isDisabled;

    // inputMode prop
    if (!currentProps.inputMode) {
      delete currentProps.inputMode;
    }

    // form prop
    if (!currentProps.form) {
      delete currentProps.form;
    }

    // name prop
    if (!currentProps.name) {
      delete currentProps.name;
    }

    // id prop
    if (!currentProps.id) {
      delete currentProps.id;
    }

    // classname
    if (currentProps.inputExtraClasses) {
      currentProps.className = getCN("", {
        ...extraClasses,
        ...(props?.inputExtraClasses ?? {}),
      });
      delete currentProps.inputExtraClasses;
    }

    // extra attrs
    if (currentProps.inputExtraAttrs) {
      Object.entries(currentProps.inputExtraAttrs)
        .forEach(([ key, value ]: [ string, unknown ]) => {
          currentProps[key as keyof TMaskedInputProps] = value;
        });
      delete currentProps.inputExtraAttrs;
    }

    return currentProps;
  }, [ getCN, props ]);

  return <IMaskInput {...computedProps} />;

};


/**
 * Компонент маски
 * @returns {ReactElement}
 */
const MaskedInput: FC<IMaskedInput & IComponentBaseProps> = (props): ReactElement => {

  const {
    baseClass = "maskedInput",
    extraAttrs = {},
    extraClasses = {},
    children = null,
    isDisabled = false,
    isReadOnly = false,
    isRequired = false,
    value = "",
    mask,
    name = "",
    onBlur = null,
    onChange = null,
    onFocus = null,
    onPaste = null,
    onInput = null,
    onKeyDown = null,
    placeholder = "",
    title = "",
    form = "",
    id = "",
    inputMode,
    onMount = null,
    onUnmount = null,
    inputExtraAttrs = {},
    inputExtraClasses = {},
    ref,
  } = props || {};

  // Classnames
  const { getCN } = useCN(baseClass);

  // Ref
  const containerRef = useRef<HTMLDivElement>(null);
  const instanceRef = useRef<TInstance>(null);
  const inputRef = useRef(null);

  // States
  const [ inputValue, setInputValue ] = useState<IMaskedInput["value"]>(() => getCurrentValue(value));
  const [ isInputDisabled, setIsInputDisabled ] = useState<IMaskedInput["isDisabled"]>(isDisabled);
  const [ isInputReadOnly, setIsInputReadOnly ] = useState<IMaskedInput["isReadOnly"]>(isReadOnly);
  const [ isInputRequired, setIsInputRequired ] = useState<IMaskedInput["isRequired"]>(isRequired);
  const [ currentMask, setCurrentMask ] = useState<IMaskedInput["mask"]>(() => getCurrentMask(mask));

  // Prev states
  const prevMask = useRef<IMaskedInput["mask"]>(currentMask);
  const prevValue = useRef<IMaskedInput["value"]>(inputValue);

  // State actions
  const stateActions = useMemo(() => ({
    setInputValue,
    setIsInputDisabled,
    setIsInputReadOnly,
    setIsInputRequired,
    setCurrentMask,
  }), []);

  // Updater
  const syncInstance = getDebouncedFn(() => {
    const instance = instanceRef?.current?.maskRef;
    if (instance) {
      instance.updateValue();
      instance.updateControl();
    }
  }, 300);

  // Callback arg
  const getCbArg = useCallback<((value: IMaskedInput["value"], event?: ChangeEvent | FocusEvent | ClipboardEvent) => TMaskedCallbackArg)>((value, event) => {
    return {
      el: containerRef.current,
      ref: instanceRef,
      utils: null,
      event: event || null,
      getData: () => ({
        value,
        prevValue: prevValue.current,
        syncInstance,
      }),
      stateActions,
    };
  }, [ stateActions, syncInstance ]);

  const emitChange = useMemo(() => getDebouncedFn((nextValue: string, event?: ChangeEvent) => {
    if (typeof onChange === "function") {
      const arg = getCbArg(nextValue, event);
      onChange(arg);
    }
  }, 300), [ getCbArg, onChange ]);

  const emitInput = useMemo(() => getDebouncedFn((nextValue: string, event?: ChangeEvent) => {
    if (typeof onInput === "function") {
      const arg = getCbArg(nextValue, event);
      onInput(arg);
    }
  }, 300), [ getCbArg, onInput ]);

  // onChange
  const handleChange = useCallback((value: string, mask: TInstance, event?: ChangeEvent) => {
    const nextValue = value || "";
    setInputValue((prev) => (prev !== nextValue ? nextValue : prev));
    emitChange(nextValue, event);
  }, [ emitChange ]);

  const handleInput = useCallback((value: string, mask: TInstance, event?: ChangeEvent) => {
    const nextValue = value || "";
    setInputValue((prev) => (prev !== nextValue ? nextValue : prev));
    emitInput(nextValue, event);
  }, [ emitInput ]);

  // onBlur
  const handleBlur = useCallback((event: FocusEvent) => {
    if (typeof onBlur === "function") {
      const arg = getCbArg((event.target as HTMLInputElement).value, event);
      onBlur(arg);
    }
  }, [ getCbArg, onBlur ]);

  // onFocus
  const handleFocus = useCallback((event: FocusEvent) => {
    if (typeof onFocus === "function") {
      const arg = getCbArg((event.target as HTMLInputElement).value, event);
      onFocus(arg);
    }
  }, [ getCbArg, onFocus ]);

  // onPaste
  const handlePaste = useCallback((event: ClipboardEvent) => {
    if (typeof onPaste === "function") {
      const arg = getCbArg((event.target as HTMLInputElement).value, event);
      onPaste(arg);
    }
  }, [ getCbArg, onPaste ]);

  // onKeyDown
  const handleKeyDown = useCallback((event: ClipboardEvent) => {
    if (typeof onKeyDown === "function") {
      const arg = getCbArg((event.target as HTMLInputElement).value, event);
      onKeyDown(arg);
    }
  }, [ getCbArg, onKeyDown ]);

  // Validation
  useFormValidation(inputRef, (e) => {
    containerRef?.current?.classList.toggle(constants.stateClasses.invalid, !e.detail.isValid);
  });

  // Ref
  useImperativeHandle(ref, () => {
    return getCbArg(inputValue) as TMaskedRef;
  }, [ getCbArg, inputValue ]);

  useEffect(() => {
    if (typeof onMount === "function") {
      onMount(instanceRef);
    }
    return () => {
      if (typeof onUnmount === "function") {
        onUnmount(instanceRef);
      }
    };

  }, []);

  // Change `mask` or `value`
  useEffect(() => {
    // value
    const newValue = getCurrentValue(value);
    if (newValue !== prevValue.current) {
      prevValue.current = newValue;
      setInputValue(newValue);
    }
    // mask
    const newMask = getCurrentMask(mask);
    if (newMask !== prevMask.current) {
      prevMask.current = newMask;
      setCurrentMask(newMask);
    }
  }, [ mask, value ]);

  // Sync
  useEffect(() => {
    if (inputValue !== prevValue.current) {
      prevValue.current = inputValue;
      syncInstance();
    }

  }, [ inputValue ]);

  // Classnames
  const classMods = useMemo(() => ({
    isDisabled: !!isInputDisabled,
    isReadOnly: !!isInputReadOnly,
    isRequired: !!isInputRequired,
    ...extraClasses,
  }), [ extraClasses, isInputDisabled, isInputReadOnly, isInputRequired ]);

  const SSGComponent = useSSGRender("data-js-masked-input", props);

  if (SSGComponent) {
    return SSGComponent;
  }

  return (
    <div ref={containerRef} className={getCN("", classMods)} {...extraAttrs}>
      <Input extraAttrs={{
        "data-js-input-validation-bubble": "true",
        "data-js-component-input": "maskedInput",
      }} isRequired={isInputRequired} ref={inputRef} type={"hidden"} name={name} value={inputValue} />
      <MaskedControl
        value={inputValue}
        ref={instanceRef}
        mask={currentMask}
        isReadOnly={isReadOnly}
        isDisabled={isInputDisabled}
        placeholder={placeholder}
        title={title}
        inputMode={inputMode}
        defaultValue={value}
        onAccept={handleInput}
        onPaste={handlePaste}
        onComplete={handleChange}
        onKeyDown={handleKeyDown}
        onBlur={handleBlur}
        onFocus={handleFocus}
        form={form}
        id={id}
        inputExtraAttrs={inputExtraAttrs}
        inputExtraClasses={inputExtraClasses}
      />
      {children}
    </div>
  );

};

export type {
  IMaskedInput, TMaskedCallbackArg, TMaskedRef, TMaskOptions,
};

export {
  MaskedInput,
};
