import clsx from 'clsx';
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import ContentLoader from 'react-content-loader';
import { noop } from '../../../helpers/utils';
import { ImgElement } from '../../VisualElements/ImgElement';
import { SquareOutlineSkeleton } from '../skeletons';
import classes from './ImageLoader.scss';

export interface ImageLoaderProps {
  /** Image's url */
  imgSrc?: string;
  /** Image's alt attribute */
  alt?: string;
  /** Image loader height (default: 50) */
  height?: number;
  /** Image loader width (default: 50) */
  width?: number;
  /** Image height (default: undefined) */
  imgHeight?: string | number;
  /** Image width (default: undefined) */
  imgWidth?: string | number;
  /** SVGRectElement used as the loading animation. If set, will replace the default SVGRectElement */
  loadingSkeleton?: ReactNode;
  /** Animation speed (default: 2) */
  speed?: number;
  /** Loader's background color (default: '#CCCCCC') */
  backgroundColor?: string;
  /** Loader's foreground color (default: '#EEECEC') */
  foregroundColor?: string;
  /** SVG's viewbox (defaults to a view box matching the given height and width), adjust this change the skeleton's rendering*/
  viewBox?: string;
  /** Fallback image url to load in case of load failure. If not set, a fallback background color will be used */
  fallbackSrc?: string;
  /** Specify the amount of time the component should wait for the image source to arrive before rendering the fallback, in milliseconds (default: 2000). If the image manages to load at a later time, the image will be rendered */
  imageTimeout?: number;
  /** Callback emitted after the image has successfully loaded. Img src is sent back */
  onLoad?: (src?: string) => void;
  /** Callback emitted after the image has failed to load. Img src is sent back */
  onError?: (src?: string) => void;
  /** Callback emitted when the image element is clicked */
  onImageClick?: (
    event: React.MouseEvent<HTMLImageElement, MouseEvent>,
  ) => void;

  /** CSS Class name for additional img element styles */
  imageClassName?: string;
}

enum ImageLoaderState {
  Loading = 'loading',
  Loaded = 'loaded',
  Failed = 'failed',
}

/**
 * Renders a loading animation while an image is being fetched
 * @example
 * <ImageLoader imgSrc="/location/thumb.png" />
 */
export const ImageLoader: React.FC<ImageLoaderProps> = ({
  imgSrc,
  alt,
  height = 50,
  width = 50,
  imgHeight,
  imgWidth,
  loadingSkeleton = SquareOutlineSkeleton,
  speed = 2,
  backgroundColor = '#CCCCCC',
  foregroundColor = '#EEECEC',
  viewBox,
  fallbackSrc,
  imageTimeout = 2000,
  onLoad = noop,
  onError = noop,
  onImageClick = noop,
  children,
  imageClassName = '',
}) => {
  const actualViewBox = viewBox || `0 0 ${width} ${height}`;

  const [state, setState] = useState<ImageLoaderState>(
    ImageLoaderState.Loading,
  );

  // Fallback if the image source is undefined
  // We won't even try to load the image in that case
  useEffect(() => {
    if (imgSrc === undefined) {
      setState(ImageLoaderState.Failed);
    } else if (state !== ImageLoaderState.Loading) {
      setState(ImageLoaderState.Loading);
    }
    // else: already loading, don't reset
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [imgSrc]);

  // Fallback if the image source fails to load in time
  useEffect(() => {
    if (state === ImageLoaderState.Loading) {
      const timer = setTimeout(() => {
        if (state === ImageLoaderState.Loading) {
          setState(ImageLoaderState.Failed);
          onError(imgSrc);
        }
      }, imageTimeout);
      return () => {
        clearTimeout(timer);
      };
    }
  }, [imageTimeout, imgSrc, onError, state]);

  const customStyles = {
    height: `${height}px`,
    width: `${width}px`,
  } as React.CSSProperties;

  const onLoadHandler = useCallback(() => {
    setState(ImageLoaderState.Loaded);
    onLoad(imgSrc);
  }, [imgSrc, onLoad]);

  const onErrorHandler = useCallback(() => {
    setState(ImageLoaderState.Failed);
    onError(imgSrc);
  }, [imgSrc, onError]);

  return (
    <div className={classes.container}>
      {state === ImageLoaderState.Loading && (
        <div className={classes.loader} style={customStyles}>
          <ContentLoader
            speed={speed}
            backgroundColor={backgroundColor}
            foregroundColor={foregroundColor}
            viewBox={actualViewBox}
          >
            {loadingSkeleton}
          </ContentLoader>
        </div>
      )}
      <div className={classes.imageContainer}>
        {state !== ImageLoaderState.Failed && imgSrc !== undefined && (
          <ImgElement
            src={imgSrc}
            height={imgHeight}
            width={imgWidth}
            style={{
              display: state === ImageLoaderState.Loading ? 'none' : 'unset',
              objectFit: 'contain',
              maxWidth: '100%',
            }}
            alt={alt ? alt : 'Loaded content image'}
            onLoad={onLoadHandler}
            onError={onErrorHandler}
            onClick={onImageClick}
            data-test-id="image-loader-img"
            className={imageClassName}
          />
        )}
        {state === ImageLoaderState.Failed &&
          (fallbackSrc ? (
            <ImgElement
              className={classes.fallBackImage}
              src={String(fallbackSrc)}
              height={imgHeight}
              width={imgWidth}
              alt="Image unavailable"
            />
          ) : (
            <div
              className={clsx(classes.container, classes.fallback)}
              style={customStyles}
            ></div>
          ))}
        {state === ImageLoaderState.Loaded && children}
      </div>
    </div>
  );
};
