import { Component, createRef } from 'react';
import styles from '@patternfly/react-styles/css/components/ExpandableSection/expandable-section';
import { css } from '@patternfly/react-styles';
import lineClamp from '@patternfly/react-tokens/dist/esm/c_expandable_section_m_truncate__content_LineClamp';
import RhMicronsCaretDownIcon from '@patternfly/react-icons/dist/esm/icons/rh-microns-caret-down-icon';
import { PickOptional } from '../../helpers/typeUtils';
import { debounce } from '../../helpers/util';
import { getResizeObserver } from '../../helpers/resizeObserver';
import { GenerateId } from '../../helpers';
import { Button } from '../Button';

export enum ExpandableSectionVariant {
  default = 'default',
  truncate = 'truncate'
}

/** The main expandable section component. */

export interface ExpandableSectionProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onToggle'> {
  /** Content rendered inside the expandable section. */
  children?: React.ReactNode;
  /** Additional classes added to the expandable section. */
  className?: string;
  /** Id of the content of the expandable section. When passing in the isDetached property, this
   * property's value should match the contentId property of the expandable section toggle sub-component.
   */
  contentId?: string;
  /** Id of the toggle of the expandable section, which provides an accessible name to the
   * expandable section content via the aria-labelledby attribute. When the isDetached property
   * is also passed in, the value of this property must match the toggleId property of the
   * expandable section toggle sub-component.
   */
  toggleId?: string;
  /** Display size variant. Set to "lg" for disclosure styling. */
  displaySize?: 'default' | 'lg';
  /** Flag indicating that the expandable section and expandable toggle are detached from one another. */
  isDetached?: boolean;
  /** Flag to indicate if the content is expanded. */
  isExpanded?: boolean;
  /** Flag to indicate if the content is indented. */
  isIndented?: boolean;
  /** Flag to indicate the width of the component is limited. Set to "true" for disclosure styling. */
  isWidthLimited?: boolean;
  /** Callback function to toggle the expandable section. Detached expandable sections should
   * use the onToggle property of the expandable section toggle sub-component.
   */
  onToggle?: (event: React.MouseEvent, isExpanded: boolean) => void;
  /** React node that appears in the attached toggle in place of the toggleText property.
   * Can also be a function that receives the expanded state and returns a React node.
   */
  toggleContent?: React.ReactNode | ((isExpanded: boolean) => React.ReactNode);
  /** Text that appears in the attached toggle. */
  toggleText?: string;
  /** Text that appears in the attached toggle when collapsed (will override toggleText if
   * both are specified; used for uncontrolled expandable with dynamic toggle text).
   */
  toggleTextCollapsed?: string;
  /** Text that appears in the attached toggle when expanded (will override toggleText if
   * both are specified; used for uncontrolled expandable with dynamic toggle text).
   */
  toggleTextExpanded?: string;
  /** Accessible name via human readable string for the expandable section toggle. */
  toggleAriaLabel?: string;
  /** Accessible name via space delimtted list of IDs for the expandable section toggle. */
  toggleAriaLabelledBy?: string;
  /** Icon shown in toggle when variant is not truncated. */
  toggleIcon?: React.ReactNode;
  /** Whether to show a toggle icon when variant is not truncated. If omitted, it is important to ensure the current state of the ExpandableSection is conveyed, most likely by having dynamic toggle text. */
  hasToggleIcon?: boolean;
  /** Truncates the expandable content to the specified number of lines when using the
   * "truncate" variant.
   */
  truncateMaxLines?: number;
  /** Determines the variant of the expandable section. When passing in "truncate" as the
   * variant, the expandable content will be truncated after 3 lines by default.
   */
  variant?: 'default' | 'truncate';
  /** Sets the direction of the expandable animation when isDetached is true. If this prop is not passed,
   * animation will not occur.
   */
  direction?: 'up' | 'down';
  /** The HTML element to use for the toggle wrapper. Can be 'div' (default) or any heading level.
   * When using heading elements, the button will be rendered inside the heading for proper semantics.
   * This is useful when the toggle text should function as a heading in the document structure.
   */
  toggleWrapper?: 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
}

interface ExpandableSectionState {
  isExpanded: boolean;
  hasToggle: boolean;
  previousWidth: number;
}

const directionClassMap = {
  up: styles.modifiers.expandTop,
  down: styles.modifiers.expandBottom
};

const setLineClamp = (lines: number, element: HTMLDivElement) => {
  if (!element || lines < 1) {
    return;
  }

  element.style.setProperty(lineClamp.name, lines.toString());
};

class ExpandableSection extends Component<ExpandableSectionProps, ExpandableSectionState> {
  static displayName = 'ExpandableSection';

  constructor(props: ExpandableSectionProps) {
    super(props);

    this.state = {
      isExpanded: props.isExpanded,
      hasToggle: true,
      previousWidth: undefined
    };
  }

  expandableContentRef = createRef<HTMLDivElement>();
  observer: any = () => {};

  static defaultProps: PickOptional<ExpandableSectionProps> = {
    className: '',
    toggleText: '',
    toggleTextExpanded: '',
    toggleTextCollapsed: '',
    toggleAriaLabel: undefined,
    toggleAriaLabelledBy: undefined,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    onToggle: (event, isExpanded): void => undefined,
    isDetached: false,
    displaySize: 'default',
    isWidthLimited: false,
    isIndented: false,
    variant: 'default'
  };

  private calculateToggleText(
    toggleText: string,
    toggleTextExpanded: string,
    toggleTextCollapsed: string,
    propOrStateIsExpanded: boolean
  ) {
    if (propOrStateIsExpanded && toggleTextExpanded !== '') {
      return toggleTextExpanded;
    }
    if (!propOrStateIsExpanded && toggleTextCollapsed !== '') {
      return toggleTextCollapsed;
    }
    return toggleText;
  }

  componentDidMount() {
    if (this.props.variant === ExpandableSectionVariant.truncate) {
      const expandableContent = this.expandableContentRef.current;
      this.setState({ previousWidth: expandableContent.offsetWidth });
      this.observer = getResizeObserver(expandableContent, this.handleResize, false);

      if (this.props.truncateMaxLines) {
        setLineClamp(this.props.truncateMaxLines, expandableContent);
      }

      this.checkToggleVisibility();
    }
  }

  componentDidUpdate(prevProps: ExpandableSectionProps) {
    if (
      this.props.variant === ExpandableSectionVariant.truncate &&
      (prevProps.truncateMaxLines !== this.props.truncateMaxLines || prevProps.children !== this.props.children)
    ) {
      const expandableContent = this.expandableContentRef.current;
      setLineClamp(this.props.truncateMaxLines, expandableContent);
      this.checkToggleVisibility();
    }
  }

  componentWillUnmount() {
    if (this.props.variant === ExpandableSectionVariant.truncate) {
      this.observer();
    }
  }

  checkToggleVisibility = () => {
    if (this.expandableContentRef?.current) {
      const maxLines = this.props.truncateMaxLines || parseInt(lineClamp.value);
      const totalLines =
        this.expandableContentRef.current.scrollHeight /
        parseInt(getComputedStyle(this.expandableContentRef.current).lineHeight);

      this.setState({
        hasToggle: totalLines > maxLines
      });
    }
  };

  resize = () => {
    if (this.expandableContentRef.current) {
      const { offsetWidth } = this.expandableContentRef.current;
      if (this.state.previousWidth !== offsetWidth) {
        this.setState({ previousWidth: offsetWidth });
        this.checkToggleVisibility();
      }
    }
  };
  handleResize = debounce(this.resize, 250);

  render() {
    const {
      onToggle: onToggleProp,
      className,
      toggleText,
      toggleTextExpanded,
      toggleTextCollapsed,
      toggleContent,
      toggleAriaLabel,
      toggleAriaLabelledBy,
      toggleIcon = <RhMicronsCaretDownIcon />,
      hasToggleIcon = true,
      children,
      isExpanded,
      isDetached,
      displaySize,
      isWidthLimited,
      isIndented,
      contentId,
      toggleId,
      variant,
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      truncateMaxLines,
      direction,
      toggleWrapper = 'div',
      ...props
    } = this.props;

    if (isDetached && !toggleId) {
      /* eslint-disable no-console */
      console.warn(
        'ExpandableSection: The toggleId value must be passed in and must match the toggleId of the ExpandableSectionToggle.'
      );
    }

    let onToggle = onToggleProp;
    let propOrStateIsExpanded = isExpanded;

    // uncontrolled
    if (isExpanded === undefined) {
      propOrStateIsExpanded = this.state.isExpanded;
      onToggle = (event, isOpen) => {
        this.setState({ isExpanded: isOpen }, () => onToggleProp(event, this.state.isExpanded));
      };
    }

    const computedToggleText = this.calculateToggleText(
      toggleText,
      toggleTextExpanded,
      toggleTextCollapsed,
      propOrStateIsExpanded
    );

    const computedToggleContent =
      typeof toggleContent === 'function' ? toggleContent(propOrStateIsExpanded) : toggleContent;
    const ToggleWrapper = toggleWrapper as any;

    return (
      <GenerateId prefix="expandable-section-content-">
        {(genContentId) => (
          <GenerateId prefix="expandable-section-toggle-">
            {(genToggleId) => {
              const uniqueContentId = contentId || genContentId;
              const uniqueToggleId = toggleId || genToggleId;

              const expandableToggle = !isDetached && (
                <ToggleWrapper className={`${styles.expandableSection}__toggle`}>
                  <Button
                    variant="link"
                    {...(variant === ExpandableSectionVariant.truncate && { isInline: true })}
                    aria-expanded={propOrStateIsExpanded}
                    aria-controls={uniqueContentId}
                    id={uniqueToggleId}
                    onClick={(event) => onToggle(event, !propOrStateIsExpanded)}
                    {...(variant !== ExpandableSectionVariant.truncate &&
                      hasToggleIcon && {
                        icon: <span className={css(styles.expandableSectionToggleIcon)}>{toggleIcon}</span>
                      })}
                    aria-label={toggleAriaLabel}
                    aria-labelledby={toggleAriaLabelledBy}
                  >
                    {computedToggleContent || computedToggleText}
                  </Button>
                </ToggleWrapper>
              );

              return (
                <div
                  className={css(
                    styles.expandableSection,
                    propOrStateIsExpanded && styles.modifiers.expanded,
                    displaySize === 'lg' && styles.modifiers.displayLg,
                    isWidthLimited && styles.modifiers.limitWidth,
                    isIndented && styles.modifiers.indented,
                    isDetached && direction && directionClassMap[direction],
                    isDetached && direction && 'pf-m-detached',
                    variant === ExpandableSectionVariant.truncate && styles.modifiers.truncate,
                    className
                  )}
                  {...props}
                >
                  {variant === ExpandableSectionVariant.default && expandableToggle}
                  <div
                    ref={this.expandableContentRef}
                    className={css(styles.expandableSectionContent)}
                    hidden={variant !== ExpandableSectionVariant.truncate && !propOrStateIsExpanded}
                    id={uniqueContentId}
                    aria-labelledby={uniqueToggleId}
                    role="region"
                  >
                    {children}
                  </div>
                  {variant === ExpandableSectionVariant.truncate && this.state.hasToggle && expandableToggle}
                </div>
              );
            }}
          </GenerateId>
        )}
      </GenerateId>
    );
  }
}

export { ExpandableSection };
