import classNames from "classnames";
import React, {
  ReactNode,
  forwardRef,
  ButtonHTMLAttributes,
  ForwardRefExoticComponent,
  useContext,
} from "react";
import { Icon, IconProps, ICON_TYPE, ICON_STYLE_PREFIX } from "../Icon";
import { Link } from "../Link";
import { bem } from "../../utilities/bem";
import { ButtonGroup, ButtonGroupProps } from "./ButtonGroup";
import { THEME } from "../../types";
import { ThemeContext } from "../../contexts";

const cn = "Button";

export enum BUTTON_VARIANT {
  PRIMARY = "primary",
  SECONDARY = "secondary",
  UTILITY = "utility",
  DANGER = "danger",
  MINIMAL = "minimal",
}

export enum BUTTON_SIZE {
  MEDIUM = "medium",
  LARGE = "large",
}

export enum BUTTON_ICON_POSITION {
  LEFT = "left",
  RIGHT = "right",
}

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  /**
   * The component or HTML element that should be rendered
   */
  as?: any;
  /**
   * If true, the button will expand to the full width available to it
   */
  block?: boolean;
  /**
   * If true, the button will appear in an active state
   */
  depressed?: boolean;
  /**
   * If true, disables the button and shows a loading spinner in place of the content
   */
  isLoading?: boolean;
  /**
   * The FontAwesome icon to show.  Can be used alongside button content or with no content for an "icon only" appearance
   */
  icon?: ICON_TYPE;
  /**
   * A tailwind color that will apply to the icon provided
   */
  iconColor?: string;
  /**
   * To which side of the button content the provided icon will be shown
   */
  iconPosition?: BUTTON_ICON_POSITION;
  /**
   * If true, the FontAwesome Icon provided will spin
   */
  iconSpin?: boolean;
  /**
   * Font Awesome Icon prefix to apply to icon
   */
  iconPrefix?: ICON_STYLE_PREFIX;
  /**
   * Sets the size of the button
   */
  size?: BUTTON_SIZE;
  /**
   * Sets the style of the button
   */
  variant?: BUTTON_VARIANT;
  /**
   * Sets the theme of the button
   */
  theme?: THEME;
  [key: string]: any;
}

function getButtonSizeClasses(
  size: BUTTON_SIZE,
  icon?: ICON_TYPE,
  children?: ReactNode,
): string {
  const buttonSizeClasses = {
    [BUTTON_SIZE.MEDIUM]: bem(cn, { m: "medium" }),
    [BUTTON_SIZE.LARGE]: bem(cn, { m: "large" }),
  };

  const buttonIconOnlySizeClasses = {
    [BUTTON_SIZE.MEDIUM]: bem(cn, { m: "iconOnly--medium" }),
    [BUTTON_SIZE.LARGE]: bem(cn, { m: "iconOnly--large" }),
  };

  return icon && !children
    ? buttonIconOnlySizeClasses[size]
    : buttonSizeClasses[size];
}

// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34757
export type ButtonWithButtonGroup = ForwardRefExoticComponent<ButtonProps> & {
  Group: (props: ButtonGroupProps) => JSX.Element;
};

export const Button = forwardRef<HTMLElement, ButtonProps>(
  (
    {
      as: Component = "button",
      children,
      // Display props
      className,
      block = false,
      size = BUTTON_SIZE.MEDIUM,
      variant = BUTTON_VARIANT.PRIMARY,
      depressed = false,
      isLoading = false,
      theme: themeProp,
      // Icon props
      icon,
      iconColor,
      iconSpin = false,
      iconPosition = BUTTON_ICON_POSITION.LEFT,
      iconPrefix,
      // Dom props
      disabled,
      type: typeProp,
      // Other
      ...rest
    }: ButtonProps,
    ref,
  ) => {
    const isDisabled = disabled || isLoading;
    const themeContext = useContext(ThemeContext);
    const theme = themeProp || themeContext;

    const buttonClassNames = classNames(
      bem(cn),
      bem(cn, { m: variant }),
      bem(cn, { m: theme }),
      block ? ["flex", "w-full"] : "inline-flex",
      getButtonSizeClasses(size, icon, children),
      depressed && bem(cn, { m: "depressed" }),
      isDisabled ? ["opacity-50", "pointer-events-none"] : "",
      className,
    );

    const iconClassNames = classNames(
      bem(cn, { e: "icon" }),
      children && bem(cn, { e: "icon", m: iconPosition }),
      isLoading && bem(cn, { e: "icon", m: "invisible" }),
    );

    const iconProps: IconProps | undefined = icon
      ? {
          icon,
          color: iconColor,
          spin: iconSpin,
          prefix: iconPrefix,
          className: iconClassNames,
        }
      : undefined;

    let type = typeProp;
    if (!typeProp && Component === "button") {
      type = "button";
    }
    const propsIfLink = Component === Link ? { noStyles: true } : {};

    return (
      <Component
        type={type}
        className={buttonClassNames}
        disabled={disabled}
        ref={ref}
        {...propsIfLink}
        {...rest}
      >
        {icon && iconProps && iconPosition === BUTTON_ICON_POSITION.LEFT && (
          <Icon {...iconProps} />
        )}
        <span
          className={classNames(
            bem(cn, { e: "content" }),
            isLoading && bem(cn, { e: "content", m: "invisible" }),
          )}
        >
          {children}
        </span>
        {icon && iconProps && iconPosition === BUTTON_ICON_POSITION.RIGHT && (
          <Icon {...iconProps} />
        )}
        {isLoading && (
          <Icon
            className={bem(cn, { e: "loader" })}
            icon={ICON_TYPE.CIRCLE_NOTCH}
            spin
          />
        )}
      </Component>
    );
  },
) as ButtonWithButtonGroup;

Button.Group = ButtonGroup;
