import { clsx } from 'clsx';
import { createContext, useContext, useMemo, useRef, useState } from 'react';

import { useResizeObserver } from '../common/hooks/useResizeObserver';
import { cssValueWithUnit } from '../utilities/cssValueWithUnit';
import {
  FieldLabelContextProvider,
  InputDescribedByProvider,
  InputIdContextProvider,
  InputInvalidProvider,
} from './contexts';

type InputPaddingContextType = [
  number | string | undefined,
  React.Dispatch<React.SetStateAction<number | string | undefined>>,
];

const InputPaddingStartContext = createContext<InputPaddingContextType>([undefined, () => {}]);

const InputPaddingEndContext = createContext<InputPaddingContextType>([undefined, () => {}]);

export function useInputPaddings() {
  const [paddingStart] = useContext(InputPaddingStartContext);
  const [paddingEnd] = useContext(InputPaddingEndContext);

  return {
    paddingInlineStart: paddingStart,
    paddingInlineEnd: paddingEnd,
  } satisfies React.CSSProperties;
}

interface InputGroupAddon {
  content: React.ReactNode;
  initialContentWidth?: number | string;
  interactive?: boolean;
  padding?: 'none' | 'sm' | 'md';
}

export interface InputGroupProps {
  addonStart?: InputGroupAddon;
  addonEnd?: InputGroupAddon;
  disabled?: boolean;
  className?: string;
  children?: React.ReactNode;
}

function inputPaddingInitialState({
  initialContentWidth,
  padding = inputAddonDefaultPadding,
}: Pick<
  InputGroupAddon,
  'initialContentWidth' | 'padding'
> = {}): () => InputPaddingContextType[0] {
  return () =>
    initialContentWidth != null
      ? `calc(${cssValueWithUnit(initialContentWidth)} + ${cssValueWithUnit(
          inputAddonContentWidthAddendByPadding[padding],
        )})`
      : undefined;
}

export function InputGroup({
  addonStart,
  addonEnd,
  disabled,
  className,
  children,
}: InputGroupProps) {
  const [paddingStart, setPaddingStart] = useState(inputPaddingInitialState(addonStart));
  const [paddingEnd, setPaddingEnd] = useState(inputPaddingInitialState(addonEnd));

  return (
    <InputPaddingStartContext.Provider
      value={useMemo(() => [paddingStart, setPaddingStart], [paddingStart])}
    >
      <InputPaddingEndContext.Provider
        value={useMemo(() => [paddingEnd, setPaddingEnd], [paddingEnd])}
      >
        <fieldset disabled={disabled} className={clsx(className, 'np-input-group')}>
          {addonStart != null ? <InputAddon placement="start" {...addonStart} /> : null}
          {children}
          {addonEnd != null ? <InputAddon placement="end" {...addonEnd} /> : null}
        </fieldset>
      </InputPaddingEndContext.Provider>
    </InputPaddingStartContext.Provider>
  );
}

interface InputAddonProps extends Omit<InputGroupAddon, 'initialContentWidth'> {
  placement: 'start' | 'end';
}

const inputAddonContentWidthAddendByPadding = {
  none: 0,
  sm: '1rem',
  md: '1.5rem',
} satisfies {
  [key in NonNullable<InputAddonProps['padding']>]: InputPaddingContextType[0];
};

const inputAddonDefaultPadding = 'md' satisfies InputAddonProps['padding'];

function InputAddon({
  placement,
  content,
  interactive,
  padding = inputAddonDefaultPadding,
}: InputAddonProps) {
  const [, setInputPadding] = useContext(
    placement === 'start' ? InputPaddingStartContext : InputPaddingEndContext,
  );

  const ref = useRef<HTMLSpanElement>(null);
  useResizeObserver(ref, (entry) => {
    // TODO: Remove fallback once most browsers support `borderBoxSize`
    const inlineSize = entry.borderBoxSize?.[0]?.inlineSize;
    if (inlineSize != null) {
      setInputPadding(inlineSize);
    } else {
      const targetStyle = getComputedStyle(entry.target);
      setInputPadding(
        entry.contentRect.width +
          Number.parseFloat(targetStyle.paddingInlineStart) +
          Number.parseFloat(targetStyle.paddingInlineEnd),
      );
    }
  });

  const isAvatarView = (node: unknown): boolean => {
    if (!node || typeof node !== 'object') return false;
    const { type } = node as { type?: { displayName?: string; name?: string } };
    if (!type || (typeof type !== 'function' && typeof type !== 'object')) return false;
    return type.displayName === 'AvatarView' || type.name === 'AvatarView';
  };

  const hasAvatarView = Array.isArray(content) ? content.some(isAvatarView) : isAvatarView(content);

  return (
    /* Prevent nested controls from being labeled redundantly */
    <FieldLabelContextProvider value={undefined}>
      <InputIdContextProvider value={undefined}>
        <InputDescribedByProvider value={undefined}>
          <InputInvalidProvider value={undefined}>
            <span
              ref={ref}
              className={clsx(
                'np-input-addon',
                {
                  'np-input-addon--placement-start': placement === 'start',
                  'np-input-addon--placement-end': placement === 'end',
                },
                interactive && 'np-input-addon--interactive',
                {
                  'np-input-addon--padding-sm': padding === 'sm',
                  'np-input-addon--padding-md': padding === 'md',
                },
                hasAvatarView && 'np-input-addon--avatar',
              )}
            >
              {content}
            </span>
          </InputInvalidProvider>
        </InputDescribedByProvider>
      </InputIdContextProvider>
    </FieldLabelContextProvider>
  );
}
