import React, {
  forwardRef,
  useRef,
  useCallback,
  useLayoutEffect,
  useState,
  CSSProperties,
  ForwardRefExoticComponent,
  TextareaHTMLAttributes,
  ChangeEvent,
} from "react";
import classNames from "classnames";
import { Input, InputBaseProps } from "../Input";
import { bemHOF } from "../../utilities/bem";

const cn = bemHOF("TextInput");
const DEFAULT_MAX_ROWS = 8;

const parseStyleValue = (value: string) => {
  return parseInt(value, 10) || 0;
};

type TextareaInputProps = InputBaseProps &
  TextareaHTMLAttributes<HTMLTextAreaElement>;

interface TextareaBase extends TextareaInputProps {
  rows?: number;
}

interface TextareaAutoSizing extends TextareaBase {
  resize?: "none";
  maxRows?: number;
}

interface TextareaManualSizing extends TextareaBase {
  resize: "horizontal" | "vertical" | "both";
}

export type TextareaProps = TextareaAutoSizing | TextareaManualSizing;

const TextareaInput = Input as ForwardRefExoticComponent<TextareaInputProps>;

export const Textarea: ForwardRefExoticComponent<TextareaProps> = forwardRef<
  HTMLTextAreaElement,
  TextareaProps
>(
  (
    { className, resize = "none", style, rows = 3, onChange, ...props },
    ref,
  ) => {
    const isAutoSizing = resize === "none";
    const textAreaRef = useRef<HTMLTextAreaElement>(null);
    const shadowTextAreaRef = useRef<HTMLTextAreaElement>(null);
    const [state, setState] = useState<{
      isOverflowing: boolean;
      height: number;
      minHeight: number;
    }>({
      isOverflowing: false,
      height: 0,
      minHeight: 0,
    });

    let rest = props;
    let maxRows = DEFAULT_MAX_ROWS;
    if ("maxRows" in rest) {
      const { maxRows: maxRowsProp, ...propsMinusMaxRows } = rest;
      maxRows = maxRowsProp || DEFAULT_MAX_ROWS;
      rest = propsMinusMaxRows;
    }

    const classes = classNames(
      cn({ m: "multiline" }),
      !isAutoSizing && cn({ m: "multiline-resizable" }),
      className,
    );

    if (ref) {
      if (typeof ref === "function") {
        ref(textAreaRef.current);
      } else {
        // eslint-disable-next-line no-param-reassign
        ref.current = textAreaRef.current;
      }
    }

    const calculateHeight = useCallback(() => {
      if (!isAutoSizing) {
        return;
      }

      const textarea = textAreaRef.current;
      const shadowTextarea = shadowTextAreaRef.current;
      if (!textarea || !shadowTextarea) {
        return;
      }

      const computedStyle = window.getComputedStyle(textarea);

      shadowTextarea.style.width = computedStyle.width;
      shadowTextarea.value = textarea.value || textarea.placeholder || " ";

      const { boxSizing } = computedStyle;
      const padding =
        parseStyleValue(computedStyle.paddingBottom) +
        parseStyleValue(computedStyle.paddingTop);
      const border =
        parseStyleValue(computedStyle.borderTopWidth) +
        parseStyleValue(computedStyle.borderBottomWidth);

      const innerHeight = shadowTextarea.scrollHeight - padding;

      shadowTextarea.value = "x";
      const singleRowHeight = shadowTextarea.scrollHeight - padding;

      const minHeightNoPadding = rows * singleRowHeight;
      const minHeight = minHeightNoPadding + padding + border;
      const minOrInnerHeight = Math.max(minHeightNoPadding, innerHeight);
      const outerHeight = Math.min(maxRows * singleRowHeight, minOrInnerHeight);

      const updatedHeight =
        outerHeight + (boxSizing === "border-box" ? padding + border : 0);
      const isOverflowing = innerHeight > outerHeight;

      setState((prevState) => {
        if (
          (updatedHeight > 0 &&
            Math.abs((prevState.height || 0) - updatedHeight) > 1) ||
          prevState.isOverflowing !== isOverflowing ||
          prevState.minHeight !== minHeight
        ) {
          return {
            isOverflowing,
            height: updatedHeight,
            minHeight,
          };
        }

        return prevState;
      });
    }, [isAutoSizing, maxRows, rows]);

    useLayoutEffect(() => {
      calculateHeight();
    });

    const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
      calculateHeight();
      if (onChange) {
        onChange(e);
      }
    };

    const computedStyle: CSSProperties = {};

    computedStyle.height = state.height;
    computedStyle.overflow = state.isOverflowing ? undefined : "hidden";
    computedStyle.minHeight =
      state.minHeight > 0 ? `${state.minHeight}px` : undefined;

    return (
      <>
        <TextareaInput
          as="textarea"
          ref={textAreaRef}
          className={classes}
          style={{
            ...computedStyle,
            ...style,
            resize,
          }}
          rows={rows}
          {...rest}
          onChange={handleChange}
        />
        {isAutoSizing && (
          <TextareaInput
            aria-hidden
            as="textarea"
            ref={shadowTextAreaRef}
            className={classes}
            style={{
              ...style,
              resize: "none",
              visibility: "hidden",
              position: "absolute",
              overflow: "hidden",
              height: 0,
              top: 0,
              left: 0,
              transform: "translateZ(0)",
            }}
            rows={1}
            tabIndex={-1}
          />
        )}
      </>
    );
  },
);

Textarea.defaultProps = {
  maxRows: DEFAULT_MAX_ROWS,
};
