import {
  ChangeEventHandler,
  MouseEventHandler,
  ReactNode,
  forwardRef,
  useCallback,
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
} from 'react';

import { useTestIdAttribute } from '../../hooks/useTestIdAttribute';
import { assertEmptyObject } from '../../utils/assertEmptyObject';
import { makeTestId } from '../../utils/makeTestId';
import { setForwarderRef } from '../../utils/setForwarderRef';
import { SupportedInputProps } from '../types';

import {
  StyledBorderContainer,
  StyledTextArea,
  StyledTextAreaContainer,
  StyledTextAreaMirror,
  StyledTextAreaRowContainer,
} from './styled';

/**
 * Props for {@link TextArea}
 *
 * Use the slot props to add supporting UI elements to the textarea, such as buttons, toolbars, or character counters.
 */
export interface TextAreaProps extends SupportedInputProps {
  /** Maximum number of rows to display. */
  minRows?: number;
  /** Minimum number of rows to display. */
  maxRows?: number;

  /** Element that will be appended before the input field */
  topSlot?: ReactNode;
  /** Element that will be appended after the input field */
  bottomSlot?: ReactNode;

  /** Element that will be appended to the left of the input field */
  leftSlot?: ReactNode;
  /** Element that will be appended to the right of the input field */
  rightSlot?: ReactNode;
}

export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function TextArea(
  props,
  forwardedRef,
) {
  const {
    className,
    onChange,
    onBlur,
    minRows = 3,
    maxRows,
    value,
    required,
    disabled,
    id,
    placeholder,
    topSlot,
    bottomSlot,
    leftSlot,
    rightSlot,
    ariaLabel,
    ariaInvalid,
    ariaDescribedBy,
    ariaErrorMessage,
    ariaRequired,
    testId,
    ...rest
  } = props;
  assertEmptyObject(rest);

  const testIdAttribute = useTestIdAttribute();
  const [height, setHeight] = useState<number>();

  const inputRef = useRef<HTMLTextAreaElement | null>(null);
  const mirrorRef = useRef<HTMLTextAreaElement | null>(null);

  const setRef = useCallback(
    (ref: HTMLTextAreaElement) => {
      setForwarderRef(forwardedRef, ref);
      setForwarderRef(inputRef, ref);
    },
    [forwardedRef, inputRef],
  );

  const syncHeight = useCallback(() => {
    const mirror = mirrorRef.current!;

    mirror.value = value ?? '';
    let outerHeight = mirror.scrollHeight;

    mirror.value = 'x';
    const singleRowHeight = mirror.scrollHeight;

    outerHeight = Math.max(Number(minRows) * singleRowHeight, outerHeight);

    if (maxRows) {
      outerHeight = Math.min(Number(maxRows) * singleRowHeight, outerHeight);
    }

    outerHeight = Math.max(outerHeight, singleRowHeight);

    const { borderTopWidth, borderBottomWidth, paddingTop, paddingBottom } = getComputedStyle(
      inputRef.current!,
    );

    [borderTopWidth, borderBottomWidth, paddingTop, paddingBottom].forEach((size) => {
      outerHeight += parseInt(size);
    });

    setHeight(outerHeight);
  }, [setHeight, value, maxRows, minRows]);

  useEffect(() => {
    let width = 0;
    const resizeObserver = new ResizeObserver(([entry]) => {
      if (entry.contentRect.width !== width) {
        width = entry.contentRect.width;
        syncHeight();
      }
    });
    resizeObserver.observe(mirrorRef.current!);
    return () => {
      resizeObserver.disconnect();
    };
  }, [syncHeight]);

  useLayoutEffect(syncHeight, [syncHeight]);

  const handleChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
    (event) => {
      onChange?.(event.target.value);
      syncHeight();
    },
    [onChange, syncHeight],
  );

  const handleContainerClick = useCallback<MouseEventHandler<HTMLDivElement>>(() => {
    inputRef.current?.focus();
  }, []);

  return (
    <StyledBorderContainer
      $disabled={disabled}
      $invalid={ariaInvalid}
      className={className}
      onClick={handleContainerClick}
      {...{ [testIdAttribute]: testId }}
    >
      {topSlot}
      <StyledTextAreaRowContainer>
        {leftSlot}
        <StyledTextAreaContainer>
          <StyledTextArea
            ref={setRef}
            aria-describedby={ariaDescribedBy}
            aria-errormessage={ariaErrorMessage}
            aria-invalid={ariaInvalid}
            aria-label={ariaLabel}
            aria-required={ariaRequired}
            disabled={disabled}
            id={id}
            onBlur={onBlur}
            onChange={handleChange}
            placeholder={placeholder}
            required={required}
            rows={minRows}
            style={{ height }}
            value={value}
            {...{ [testIdAttribute]: makeTestId(testId, 'textarea') }}
          />
          <StyledTextAreaMirror ref={mirrorRef} aria-hidden readOnly tabIndex={-1} />
        </StyledTextAreaContainer>
        {rightSlot}
      </StyledTextAreaRowContainer>
      {bottomSlot}
    </StyledBorderContainer>
  );
});
