import { useCallback } from 'react';
import classnames from 'classnames';
import css from 'styled-jsx/css';

import type { WithSpacingProps } from '~/src/lib/hocs/withSpacing';
import type {
  CSSProperties,
  KeyboardEventHandler,
  MouseEvent,
  ReactNode,
  RefObject,
} from 'react';
import type { LinkClickHandler, LinkProps } from '../Link2';

import withSpacing from '~/src/lib/hocs/withSpacing';
import { stripNonDomProps } from '~/src/lib/utils/stripNonDomProps';
import Link from '../Link2';

type ClickableElement =
  | HTMLDivElement
  | HTMLAnchorElement
  | HTMLButtonElement
  | HTMLLabelElement;

export type ClickableOnClickParams<TData = undefined> = {
  data: TData;
  event: MouseEvent<ClickableElement>;
  href?: string;
};

export type ClickableOnClick<TData = undefined> = (
  params: ClickableOnClickParams<TData>
) => void | Promise<void>;

export interface ClickableProps<TData = unknown>
  extends Omit<LinkProps, 'href' | 'onClick' | 'children'>,
    WithSpacingProps {
  // TODO: find a way to union type these
  // https://stackoverflow.com/questions/61328676/react-typescript-interfaces-errors-when-get-props-from-union-types
  href?: string;
  id?: string;
  onClick?: ClickableOnClick<TData>;
  onKeyUp?: KeyboardEventHandler;
  isDisabled?: boolean;
  style?: CSSProperties;
  title?: string;
  isInline?: boolean;
  className?: string;
  tabIndex?: number;
  isSubmit?: boolean;
  withActiveStyle?: boolean;
  withFocusStyle?: boolean;
  withDisabledStyle?: boolean;
  withHoverOpacityFrom?: number;
  children?: ReactNode;
  nodeRef?: RefObject<
    HTMLAnchorElement | HTMLButtonElement | HTMLElement | null
  >;

  /**
   * Set the html tag/element to use. This is ignored if using
   * `href` as <Link>/<a> must be used in that case.
   */
  tag?: 'div' | 'label';

  testId?: string;
  data?: TData;
  autoFocus?: boolean;
}

const root = css.resolve`
  * {
    display: block;
    font: inherit;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    outline: 0;
    cursor: pointer;
    color: inherit;
    user-select: none;
  }

  * :global(*) {
    cursor: pointer !important;
  }

  .withHoverOpacityFrom,
  .withActiveStyle {
    transition: opacity 500ms 100ms;
  }

  .withActiveStyle:active {
    opacity: 0.6 !important;
    transition-duration: 0s !important;
    transition-delay: 0s !important;
  }

  .withHoverOpacityFrom:hover {
    opacity: 1 !important;
    transition-delay: 0s;
  }

  .withActiveStyle:active {
    opacity: 0.6 !important;
    transition-duration: 0s !important;
    transition-delay: 0s !important;
  }

  /* subtle glow when focused via keyboard */
  .withFocusStyle:focus-visible {
    filter: drop-shadow(0px 1px 8px white);
  }

  .isUnderlined {
    display: inline-block;
    color: #999;
    border-bottom: dotted 0.1em #666;
  }

  .isUnderlined.withHoverStyle:hover {
    text-decoration: none;
    color: #fff;
  }
`;

export const Clickable = <TData,>({
  onClick,
  children,
  data,
  style,
  className,
  isInline,
  isUnderlined,
  href,
  routerQuery,
  title,
  testId,
  isDisabled,
  isSubmit,
  withActiveStyle = true,
  withFocusStyle = true,
  withDisabledStyle = true,
  withHoverOpacityFrom,
  tag,
  tabIndex,
  nodeRef,
  ...props
}: ClickableProps<TData>) => {
  const onLinkClickInternal = useCallback<LinkClickHandler>(
    ({ event, href }) => {
      if (onClick) {
        return onClick({
          event,
          href,
          data: data as TData,
        });
      }
    },
    [onClick, data]
  );

  const onNodeClickInternal = useCallback(
    (event: MouseEvent<ClickableElement>) => {
      event.stopPropagation();

      if (onClick) {
        return onClick({
          event,
          data: data as TData,
        });
      }
    },
    [onClick, data]
  );

  const classNameInternal = classnames(root.className, className, {
    withFocusStyle,
    withActiveStyle,
    withHoverOpacityFrom,
    isUnderlined,
  });

  const styleInternal: CSSProperties = {
    opacity: withHoverOpacityFrom,
    ...style,
  };

  if (isDisabled) {
    styleInternal.pointerEvents = 'none';

    if (withDisabledStyle) {
      styleInternal.opacity = 0.4;
    }
  }

  // when there's an href we use an `<a>`
  if (href) {
    return (
      <Link
        {...props}
        routerQuery={routerQuery}
        href={href}
        onClick={onLinkClickInternal}
        className={classNameInternal}
        title={title}
        style={styleInternal}
        testId={testId}
        tabIndex={tabIndex}
        withFocusStyle={withFocusStyle}
        noDrag
      >
        {children}
        {root.styles}
      </Link>
    );
  }

  const Tag = tag ?? 'button';

  // Using <button> instead of `<div role=button>` comes with lots of a11y
  // benefits. Keyboard interactions trigger 'click' events by default,
  // disabled attributes work as expected, element is tab-able/focusable by default.
  // However there are some default styling issues that make <button> different to <div>.
  // We may not have all these covered yet, but the a11y benefits are worth it.
  return (
    <Tag
      {...stripNonDomProps(props)}
      data-testid={testId}
      // must specify type="button" to prevent any <button> submitting parent <form>
      // https://stackoverflow.com/a/10836076/516629
      type={Tag === 'button' ? (isSubmit ? 'submit' : 'button') : undefined}
      role={Tag === 'div' ? 'button' : undefined}
      ref={nodeRef as any}
      tabIndex={
        // if tabIndex explicitly provided: use it
        typeof tabIndex === 'number'
          ? tabIndex
          : // if <div> is used we need to give an explicit tabIndex to make it keyboard focusable
            Tag === 'div'
            ? 0
            : undefined
      }
      className={classNameInternal}
      style={{
        // override default <button> centered text
        textAlign: 'start',

        // <button> isn't full-width by default, if you need to
        // an inline <Clickable> you can use the `isInline` prop
        width: !isInline ? '100%' : undefined,

        ...styleInternal,
      }}
      onClick={onNodeClickInternal}
      title={title}
      aria-label={title}
      disabled={isDisabled}
    >
      {children}
      {root.styles}
    </Tag>
  );
};

const ClickableWithSpacing = withSpacing(Clickable);

export default ClickableWithSpacing;
