import React, { useRef, useCallback, useState, ReactNode } from 'react';
import {
  View,
  Image,
  ImageBackground,
  Animated,
  StyleSheet,
  ImageProps,
  ViewStyle,
  StyleProp,
  ImageSourcePropType,
  NativeSyntheticEvent,
  ImageErrorEventData,
} from 'react-native';
import { useDeepCompareEffectNoCheck } from 'use-deep-compare-effect';

export interface BetterImageProps extends ImageProps {
  viewStyle?: StyleProp<ViewStyle>;
  thumbnailFadeDuration?: number;
  imageFadeDuration?: number;
  thumbnailSource?: ImageSourcePropType;
  thumbnailBlurRadius?: number;
  fallbackSource?: ImageSourcePropType;
  children?: ReactNode;
}

const { Value, createAnimatedComponent, timing } = Animated;

const AnimatedImage = createAnimatedComponent(Image);
const AnimatedImageBackground = createAnimatedComponent(ImageBackground);

const BetterImage = ({
  viewStyle,
  thumbnailFadeDuration = 250,
  imageFadeDuration = 250,
  thumbnailSource,
  source,
  onLoadEnd,
  resizeMethod,
  resizeMode,
  thumbnailBlurRadius = 1,
  style,
  fallbackSource = { uri: '' },
  onError,
  children,
  ...otherProps
}: BetterImageProps) => {
  const imageOpacity = useRef(new Value(0)).current;
  const thumbnailOpacity = useRef(new Value(0)).current;
  const thumbnailAnimationProgress = useRef<
    Animated.CompositeAnimation | undefined
  >();
  const [hasError, setHasError] = useState(false);
  const [hasLoaded, setHasLoaded] = useState(false);

  const onImageLoad = () => {
    setHasLoaded(true);

    timing(imageOpacity, {
      toValue: 1,
      duration: imageFadeDuration,
      useNativeDriver: true,
    }).start(() => {
      thumbnailAnimationProgress.current?.stop();
      timing(thumbnailOpacity, {
        toValue: 0,
        duration: thumbnailFadeDuration,
        useNativeDriver: true,
      }).start();
    });

    onLoadEnd && onLoadEnd();
  };

  const onThumbnailLoad = () => {
    if (!hasLoaded) {
      const progress = timing(thumbnailOpacity, {
        toValue: 1,
        duration: thumbnailFadeDuration,
        useNativeDriver: true,
      });
      thumbnailAnimationProgress.current = progress;
      thumbnailAnimationProgress.current.start();
    }
  };

  const onImageLoadError = (
    event: NativeSyntheticEvent<ImageErrorEventData>
  ) => {
    setHasError(true);
    onError && onError(event);
  };

  useDeepCompareEffectNoCheck(
    useCallback(() => {
      imageOpacity.setValue(0);
      thumbnailOpacity.setValue(0);
      setHasError(false);
      setHasLoaded(false);
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []),
    [source, thumbnailSource]
  );

  const ImageComponent = children ? AnimatedImageBackground : AnimatedImage;

  return (
    <View style={[styles.imageContainerStyle, viewStyle]}>
      {thumbnailSource ? (
        <ImageComponent
          children={children}
          onLoadEnd={onThumbnailLoad}
          style={[
            styles.thumbnailImageStyle,
            { opacity: thumbnailOpacity },
            style,
          ]}
          source={thumbnailSource}
          blurRadius={thumbnailBlurRadius}
          resizeMethod={resizeMethod}
          resizeMode={resizeMode}
        />
      ) : null}
      <ImageComponent
        children={children}
        resizeMethod={resizeMethod}
        resizeMode={resizeMode}
        onLoadEnd={onImageLoad}
        onError={hasError ? () => null : onImageLoadError}
        source={hasError ? fallbackSource : source}
        style={[styles.imageStyle, { opacity: imageOpacity }, style]}
        {...otherProps}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  imageContainerStyle: {
    overflow: 'hidden',
  },
  thumbnailImageStyle: {
    ...StyleSheet.absoluteFillObject,
  },
  imageStyle: {
    ...StyleSheet.absoluteFillObject,
  },
});

export default BetterImage;