'use client';

import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { mergeStyle } from '../../helpers/mergeStyle';
import { useExternRef } from '../../hooks/useExternRef';
import { useMergeProps } from '../../hooks/useMergeProps';
import { minOr } from '../../lib/comparing';
import { defineComponentDisplayNames } from '../../lib/react/defineComponentDisplayNames';
import { getFetchPriorityProp } from '../../lib/utils';
import { warnOnce } from '../../lib/warnOnce';
import type {
  AnchorHTMLAttributesOnly,
  HasDataAttribute,
  HasRootRef,
  LiteralUnion,
} from '../../types';
import { Clickable } from '../Clickable/Clickable';
import { ImageBaseBadge, type ImageBaseBadgeProps } from './ImageBaseBadge/ImageBaseBadge';
import {
  type FloatElementIndentation,
  type FloatElementPlacement,
  ImageBaseFloatElement,
  type ImageBaseFloatElementProps,
} from './ImageBaseFloatElement/ImageBaseFloatElement';
import { ImageBaseOverlay, type ImageBaseOverlayProps } from './ImageBaseOverlay/ImageBaseOverlay';
import { ImageBaseContext } from './context';
import type { ImageBaseContextProps, ImageBaseExpectedIconProps, ImageBaseSize } from './types';
import { validateFallbackIcon, validateSize } from './validators';
import styles from './ImageBase.module.css';

export type {
  ImageBaseSize,
  ImageBaseExpectedIconProps,
  ImageBaseBadgeProps,
  ImageBaseOverlayProps,
  ImageBaseContextProps,
  ImageBaseFloatElementProps,
  FloatElementPlacement,
  FloatElementIndentation,
};

export {
  getBadgeIconSizeByImageBaseSize,
  getFallbackIconSizeByImageBaseSize,
  getOverlayIconSizeByImageBaseSize,
} from './helpers';

export { ImageBaseContext };

const warn = warnOnce('ImageBase');

/**
 * Размер по умолчанию.
 */
const defaultSize = 24;

export interface ImageBaseProps
  extends React.ImgHTMLAttributes<HTMLElement>,
    AnchorHTMLAttributesOnly,
    HasRootRef<HTMLDivElement> {
  /**
   * Задаёт размер картинки.
   *
   * Используйте размеры заданные дизайн-системой `16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 56 | 64 | 72 | 80 | 88 | 96`.
   *
   * > ⚠️ Использование кастомного размера – это пограничный кейс.
   */
  size?: LiteralUnion<ImageBaseSize, number> | undefined;
  /**
   * Ширина изображения.
   */
  widthSize?: number | string | undefined;
  /**
   * Высота изображения.
   */
  heightSize?: number | string | undefined;
  /**
   * Отключает обводку.
   */
  noBorder?: boolean | undefined;
  /**
   * Фолбек на случай, если картинка не прогрузилась.
   *
   * > 📝 Нужный для `<ImageBase size={...} />` размер можно узнать из функции `getFallbackIconSizeByImageBaseSize()`.
   *
   * > Предпочтительней использовать иконки из `@vkontakte/icons`.
   *
   * > 📊️ Если вы хотите передать кастомную иконку, то следует именовать её по шаблону `Icon<size><name>`. Или же
   * > чтобы в неё был передан параметр `width`. Тогда мы сможем выводить в консоль подсказку правильного ли размера вы
   * > использовали иконку.
   *
   * > ⚠️ Может перекрывать `children`.
   */
  fallbackIcon?: React.ReactElement<ImageBaseExpectedIconProps> | undefined;
  /**
   * Отключает фон, заданный по умолчанию. Полезен для отображения картинок с прозрачностью.
   * @since 5.10.0
   */
  withTransparentBackground?: boolean | undefined;
  /**
   * Пользовательское значения стиля object-fit
   * Подробнее можно почитать в [документации](https://developer.mozilla.org/ru/docs/Web/CSS/object-fit).
   */
  objectFit?: React.CSSProperties['objectFit'] | undefined;
  /**
   * Пользовательское значения стиля object-position
   * Подробнее можно почитать в [документации](https://developer.mozilla.org/ru/docs/Web/CSS/object-position).
   */
  objectPosition?: React.CSSProperties['objectPosition'] | undefined;
  /**
   * Флаг для сохранения пропорций картинки.
   * Для корректной работы необходимо задать размеры хотя бы одной стороны картинки.
   */
  keepAspectRatio?: boolean | undefined;
  /**
   * Смотри https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/elementtiming.
   */
  elementTiming?: string | undefined;
  /**
   * Пользовательское значения стиля filter.
   * Подробнее можно почитать в [документации](https://developer.mozilla.org/ru/docs/Web/CSS/filter).
   *
   * При передаче этого свойства `<img />` будет обёрнут в дополнительный контейнер.
   */
  filter?: React.CSSProperties['filter'] | undefined;
  /**
   * Свойства, которые можно прокинуть внутрь компонента:
   * - `img`: свойства для прокидывания в тег `<img>`;.
   */
  slotProps?:
    | {
        img?:
          | (React.ComponentProps<'img'> & HasRootRef<HTMLImageElement> & HasDataAttribute)
          | undefined;
      }
    | undefined;
  /**
   * @deprecated Since 7.9.0. Будет удалено в v9. Используйте `slotProps={ img: { getRootRef: ... } }`.
   */
  getRef?: React.Ref<HTMLImageElement> | undefined; // TODO [>=9] Удалить свойство
}

const getObjectFitClassName = (objectFit: React.CSSProperties['objectFit']) => {
  switch (objectFit) {
    case 'contain':
      return styles.imgObjectFitContain;
    case 'cover':
      return styles.imgObjectFitCover;
    case 'none':
      return styles.imgObjectFitNone;
    case 'scale-down':
      return styles.imgObjectFitScaleDown;
  }
  return undefined;
};

const parsePx = (value: string): number | undefined => {
  if (value.endsWith('px')) {
    return parseInt(value);
  }
  return undefined;
};

const sizeToNumber = (size: number | string | undefined): number | undefined => {
  if (typeof size === 'string') {
    return parsePx(size);
  }
  return size;
};

/**
 * @see https://vkui.io/components/image-base
 */
export const ImageBase: React.FC<ImageBaseProps> & {
  Badge: typeof ImageBaseBadge;
  Overlay: typeof ImageBaseOverlay;
  FloatElement: typeof ImageBaseFloatElement;
} = ({
  alt,
  crossOrigin,
  decoding,
  loading,
  referrerPolicy,
  sizes,
  src,
  srcSet,
  useMap,
  fetchPriority: fetchPriorityProp,
  getRef,
  size: sizeProp,
  width: widthImg,
  height: heightImg,
  widthSize,
  heightSize,
  noBorder = false,
  fallbackIcon: fallbackIconProp,
  children,
  onLoad,
  onError,
  withTransparentBackground,
  objectFit = 'cover',
  objectPosition,
  filter,
  keepAspectRatio = false,
  getRootRef,
  elementTiming,
  slotProps,
  ...restProps
}: ImageBaseProps) => {
  if (process.env.NODE_ENV === 'development') {
    /* istanbul ignore if: не проверяем в тестах */
    if (getRef) {
      warn(
        'Свойство `getRef` устаревшее и будет удалено в VKUI v9. Используйте `slotProps={ img: { getRootRef: ... } }`',
      );
    }
  }

  const size = sizeProp ?? minOr([sizeToNumber(widthSize), sizeToNumber(heightSize)], defaultSize);
  const wrapperRef = useExternRef(getRootRef);

  const width = widthSize ?? (keepAspectRatio ? undefined : size);
  const height = heightSize ?? (keepAspectRatio ? undefined : size);

  const [loaded, setLoaded] = React.useState(false);
  const [failed, setFailed] = React.useState(false);

  const [mouseOverHandlers] = React.useState<VoidFunction[]>([]);
  const [mouseOutHandlers] = React.useState<VoidFunction[]>([]);

  const hasSrc = src || srcSet;
  const fallbackIcon = failed || !hasSrc ? fallbackIconProp : null;

  if (process.env.NODE_ENV === 'development') {
    validateSize(size);
    if (React.isValidElement(fallbackIcon)) {
      validateFallbackIcon(size, { name: 'fallbackIcon', value: fallbackIcon });
    }
  }

  const handleImageLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
    if (loaded) {
      return;
    }

    setLoaded(true);
    setFailed(false);
    onLoad?.(event);
  };

  const handleImageError = (event: React.SyntheticEvent<HTMLImageElement>) => {
    setLoaded(false);
    setFailed(true);
    onError?.(event);
  };

  const {
    getRootRef: getImgRef,
    fetchPriority,
    ...imgRest
  } = useMergeProps<React.ComponentProps<'img'> & HasRootRef<HTMLImageElement> & HasDataAttribute>(
    hasSrc
      ? {
          // safari и firefox нужно чтобы атрибут `loading` был до `src`
          //
          // https://mihaly4.ru/image-loading-lazy-bug
          loading,
          getRootRef: getRef,
          alt,
          className: classNames(
            styles.img,
            getObjectFitClassName(objectFit),
            objectPosition && styles.withObjectPosition,
            filter && styles.withFilter,
            keepAspectRatio && styles.imgKeepRatio,
            failed && styles.imgHiddenAlt,
          ),
          crossOrigin,
          decoding,
          referrerPolicy,
          style: mergeStyle(
            keepAspectRatio
              ? {
                  width: widthImg || width,
                  height: heightImg || height,
                }
              : undefined,
            objectPosition || filter
              ? {
                  '--vkui_internal--ImageBase_object_position': objectPosition,
                  '--vkui_internal--ImageBase_object_filter': filter,
                }
              : undefined,
          ),
          sizes,
          src,
          srcSet,
          useMap,
          width,
          height,
          onLoad: handleImageLoad,
          onError: handleImageError,
          fetchPriority: fetchPriorityProp,
          // @ts-expect-error: TS2322 отсутствует в @types/react
          elementtiming: elementTiming, // eslint-disable-line react/no-unknown-property
        }
      : {},
    hasSrc ? slotProps?.img : undefined,
  );

  const imgRef = useExternRef(getImgRef);
  const isOnLoadStatusCheckedRef = React.useRef(false);
  React.useEffect(
    function dispatchLoadEventForAlreadyLoadedResourceIfReactInitializedLater() {
      if (isOnLoadStatusCheckedRef.current) {
        return;
      }
      isOnLoadStatusCheckedRef.current = true;

      if (imgRef.current && imgRef.current.complete && !loaded) {
        const event = new Event('load');
        imgRef.current.dispatchEvent(event);
      }
    },
    [imgRef, loaded],
  );

  const onMouseOver = () => {
    mouseOverHandlers.forEach((fn) => fn());
  };

  const onMouseOut = () => {
    mouseOutHandlers.forEach((fn) => fn());
  };

  const contextValue = React.useMemo(
    () => ({
      size,
      onMouseOverHandlers: mouseOverHandlers,
      onMouseOutHandlers: mouseOutHandlers,
    }),
    [mouseOutHandlers, mouseOverHandlers, size],
  );

  const imgSlot = hasSrc && (
    <img ref={imgRef} {...imgRest} {...getFetchPriorityProp(fetchPriority)} />
  );

  return (
    <ImageBaseContext.Provider value={contextValue}>
      <Clickable
        baseStyle={{ width, height }}
        baseClassName={classNames(
          styles.host,
          hasSrc && loaded && styles.loaded,
          withTransparentBackground && styles.transparentBackground,
        )}
        getRootRef={wrapperRef}
        onMouseOver={onMouseOver}
        onMouseOut={onMouseOut}
        {...restProps}
      >
        {filter ? <div className={styles.imgWithFilterContainer}>{imgSlot}</div> : imgSlot}
        {fallbackIcon && <div className={styles.fallback}>{fallbackIcon}</div>}
        {children && <div className={styles.children}>{children}</div>}
        {!noBorder && <div aria-hidden className={styles.border} />}
      </Clickable>
    </ImageBaseContext.Provider>
  );
};
ImageBase.Badge = ImageBaseBadge;
ImageBase.Overlay = ImageBaseOverlay;
ImageBase.FloatElement = ImageBaseFloatElement;

if (process.env.NODE_ENV !== 'production') {
  defineComponentDisplayNames(ImageBase.Badge, 'ImageBase.Badge');
  defineComponentDisplayNames(ImageBase.Overlay, 'ImageBase.Overlay');
  defineComponentDisplayNames(ImageBase.FloatElement, 'ImageBase.FloatElement');
}
