/**
 * Copyright (c) JOB TODAY S.A. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */
// Original code copied and simplified from the link below as the codebase is currently not maintained:
// https://github.com/jobtoday/react-native-image-viewing
import {useCallback, useEffect, useMemo, useState} from 'react'
import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native'
import {SystemBars} from 'react-native-edge-to-edge'
import {Gesture} from 'react-native-gesture-handler'
import PagerView from 'react-native-pager-view'
import Animated, {
  type AnimatableValue,
  type AnimatedRef,
  cancelAnimation,
  interpolate,
  measure,
  type MeasuredDimensions,
  ReduceMotion,
  runOnJS,
  runOnUI,
  type SharedValue,
  useAnimatedReaction,
  useAnimatedRef,
  useAnimatedStyle,
  useDerivedValue,
  useSharedValue,
  withDecay,
  withSpring,
  type WithSpringConfig,
} from 'react-native-reanimated'
import {Image} from 'expo-image'
import * as ScreenOrientation from 'expo-screen-orientation'

import {type Dimensions} from '#/lib/media/types'
import {useTheme} from '#/alf'
import {setSystemUITheme} from '#/alf/util/systemUI'
import {type Lightbox} from '#/components/Lightbox/state'
import {useAnalytics} from '#/analytics'
import {IS_IOS} from '#/env'
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
import {Footer} from '../chrome/Footer'
import {Header} from '../chrome/Header'
import {
  type ImageSource,
  type LightboxTransforms,
  type Transform,
} from '../types'
import ImageItem from './ImageItem/ImageItem'

type Rect = {x: number; y: number; width: number; height: number}

const PORTRAIT_UP = ScreenOrientation.OrientationLock.PORTRAIT_UP
const PIXEL_RATIO = PixelRatio.get()

const SLOW_SPRING: WithSpringConfig = {
  mass: IS_IOS ? 1.25 : 0.75,
  damping: 300,
  stiffness: 800,
  restDisplacementThreshold: 0.001,
}
const FAST_SPRING: WithSpringConfig = {
  mass: IS_IOS ? 1.25 : 0.75,
  damping: 150,
  stiffness: 900,
  restDisplacementThreshold: 0.001,
}

function canAnimate(lightbox: Lightbox): boolean {
  if (PlatformInfo.getIsReducedMotionEnabled()) {
    return false
  }
  const img = lightbox.images[lightbox.index]
  return !!img.thumbRect && !!(img.dimensions || img.thumbDimensions)
}

export default function ImageViewRoot({
  lightbox: nextLightbox,
  onRequestClose,
  onPressSave,
  onPressShare,
}: {
  lightbox: Lightbox | null
  onRequestClose: () => void
  onPressSave: (uri: string) => void
  onPressShare: (uri: string) => void
}) {
  'use no memo'
  const ref = useAnimatedRef<View>()
  const [activeLightbox, setActiveLightbox] = useState(nextLightbox)
  const [orientation, setOrientation] = useState<'portrait' | 'landscape'>(
    'portrait',
  )
  const openProgress = useSharedValue(0)
  const thumbRects = useSharedValue<Record<number, MeasuredDimensions | null>>(
    {},
  )

  if (!activeLightbox && nextLightbox) {
    setActiveLightbox(nextLightbox)
  }

  useEffect(() => {
    if (!nextLightbox) {
      return
    }

    const initial: Record<number, MeasuredDimensions | null> = {}
    nextLightbox.images.forEach((img, i) => {
      initial[i] = img.thumbRect ?? null
    })
    thumbRects.set(initial)

    const isAnimated = canAnimate(nextLightbox)

    // https://github.com/software-mansion/react-native-reanimated/issues/6677
    rAF_FIXED(() => {
      openProgress.set(() =>
        isAnimated ? withClampedSpring(1, SLOW_SPRING) : 1,
      )
    })
    return () => {
      // https://github.com/software-mansion/react-native-reanimated/issues/6677
      rAF_FIXED(() => {
        openProgress.set(() =>
          isAnimated ? withClampedSpring(0, SLOW_SPRING) : 0,
        )
      })
    }
  }, [nextLightbox, openProgress, thumbRects])

  const onFullyClosed = useCallback(() => {
    setActiveLightbox(null)
    runOnUI(() => {
      'worklet'
      thumbRects.set({})
    })()
    requestIdleCallback(() => {
      void Image.clearMemoryCache()
    })
  }, [thumbRects])

  useAnimatedReaction(
    () => openProgress.get() === 0,
    (isGone, wasGone) => {
      if (isGone && !wasGone) {
        runOnJS(onFullyClosed)()
      }
    },
  )

  // Delay the unlock until after we've finished the scale up animation.
  // It's complicated to do the same for locking it back so we don't attempt that.
  useAnimatedReaction(
    () => openProgress.get() === 1,
    (isOpen, wasOpen) => {
      if (isOpen && !wasOpen) {
        runOnJS(ScreenOrientation.unlockAsync)()
      } else if (!isOpen && wasOpen) {
        // default is PORTRAIT_UP - set via config plugin in app.config.js -sfn
        runOnJS(ScreenOrientation.lockAsync)(PORTRAIT_UP)
      }
    },
  )

  const onFlyAway = useCallback(() => {
    'worklet'
    openProgress.set(0)
    runOnJS(onRequestClose)()
  }, [onRequestClose, openProgress])

  return (
    // Keep it always mounted to avoid flicker on the first frame.
    <View
      style={[styles.screen, !activeLightbox && styles.screenHidden]}
      aria-modal
      accessibilityViewIsModal
      aria-hidden={!activeLightbox}>
      <Animated.View
        ref={ref}
        style={{flex: 1}}
        collapsable={false}
        onLayout={e => {
          const layout = e.nativeEvent.layout
          setOrientation(
            layout.height > layout.width ? 'portrait' : 'landscape',
          )
        }}>
        {activeLightbox && (
          <ImageView
            key={activeLightbox.id + '-' + orientation}
            lightbox={activeLightbox}
            orientation={orientation}
            onRequestClose={onRequestClose}
            onPressSave={onPressSave}
            onPressShare={onPressShare}
            onFlyAway={onFlyAway}
            safeAreaRef={ref}
            openProgress={openProgress}
            thumbRects={thumbRects}
          />
        )}
      </Animated.View>
    </View>
  )
}

function ImageView({
  lightbox,
  orientation,
  onRequestClose,
  onPressSave,
  onPressShare,
  onFlyAway,
  safeAreaRef,
  openProgress,
  thumbRects,
}: {
  lightbox: Lightbox
  orientation: 'portrait' | 'landscape'
  onRequestClose: () => void
  onPressSave: (uri: string) => void
  onPressShare: (uri: string) => void
  onFlyAway: () => void
  safeAreaRef: AnimatedRef<View>
  openProgress: SharedValue<number>
  thumbRects: SharedValue<Record<number, MeasuredDimensions | null>>
}) {
  const {images, index: initialImageIndex, metricsContext} = lightbox
  const ax = useAnalytics()
  const isAnimated = useMemo(() => canAnimate(lightbox), [lightbox])
  const [isScaled, setIsScaled] = useState(false)
  const [isDragging, setIsDragging] = useState(false)
  const [imageIndex, setImageIndex] = useState(initialImageIndex)
  const [showControls, setShowControls] = useState(true)
  const [isAltExpanded, setIsAltExpanded] = useState(false)
  const dismissSwipeTranslateY = useSharedValue(0)
  const isFlyingAway = useSharedValue(false)

  const containerStyle = useAnimatedStyle(() => {
    if (openProgress.get() < 1) {
      return {
        pointerEvents: 'none',
        opacity: isAnimated ? 1 : 0,
      }
    }
    if (isFlyingAway.get()) {
      return {
        pointerEvents: 'none',
        opacity: 1,
      }
    }
    return {pointerEvents: 'auto', opacity: 1}
  })

  const backdropStyle = useAnimatedStyle(() => {
    const screenSize = measure(safeAreaRef)
    let opacity = 1
    const openProgressValue = openProgress.get()
    if (openProgressValue < 1) {
      opacity = Math.sqrt(openProgressValue)
    } else if (screenSize && orientation === 'portrait') {
      const dragProgress = Math.min(
        Math.abs(dismissSwipeTranslateY.get()) / (screenSize.height / 2),
        1,
      )
      opacity -= dragProgress
    }
    const factor = IS_IOS ? 100 : 50
    return {
      opacity: Math.round(opacity * factor) / factor,
    }
  })

  const animatedHeaderStyle = useAnimatedStyle(() => {
    const show = showControls && dismissSwipeTranslateY.get() === 0
    return {
      pointerEvents: show ? 'box-none' : 'none',
      opacity: withClampedSpring(
        show && openProgress.get() === 1 ? 1 : 0,
        FAST_SPRING,
      ),
      transform: [
        {
          translateY: withClampedSpring(show ? 0 : -30, FAST_SPRING),
        },
      ],
    }
  })
  const animatedFooterStyle = useAnimatedStyle(() => {
    const show = showControls && dismissSwipeTranslateY.get() === 0
    return {
      flexGrow: 1,
      pointerEvents: show ? 'box-none' : 'none',
      opacity: withClampedSpring(
        show && openProgress.get() === 1 ? 1 : 0,
        FAST_SPRING,
      ),
      transform: [
        {
          translateY: withClampedSpring(show ? 0 : 30, FAST_SPRING),
        },
      ],
    }
  })

  const handleRequestClose = useCallback(() => {
    const activeRef = images[imageIndex]?.thumbRef
    if (isAnimated && activeRef) {
      runOnUI(() => {
        'worklet'
        const rect = measure(activeRef)
        thumbRects.modify(rects => {
          'worklet'
          rects[imageIndex] = rect
          return rects
        })
        runOnJS(onRequestClose)()
      })()
    } else {
      onRequestClose()
    }
  }, [isAnimated, images, imageIndex, thumbRects, onRequestClose])

  const onTap = useCallback(() => {
    setShowControls(show => !show)
  }, [])

  const onZoom = useCallback((nextIsScaled: boolean) => {
    setIsScaled(nextIsScaled)
    if (nextIsScaled) {
      setShowControls(false)
    }
  }, [])

  useAnimatedReaction(
    () => {
      const screenSize = measure(safeAreaRef)
      return (
        !screenSize ||
        Math.abs(dismissSwipeTranslateY.get()) > screenSize.height
      )
    },
    (isOut, wasOut) => {
      if (isOut && !wasOut) {
        // Stop the animation from blocking the screen forever.
        cancelAnimation(dismissSwipeTranslateY)
        onFlyAway()
      }
    },
  )

  // style system ui on android
  const t = useTheme()
  useEffect(() => {
    setSystemUITheme('lightbox', t)
    return () => {
      setSystemUITheme('theme', t)
    }
  }, [t])

  return (
    <Animated.View style={[styles.container, containerStyle]}>
      <SystemBars
        style={{statusBar: 'light', navigationBar: 'light'}}
        hidden={{
          statusBar: isScaled || !showControls,
          navigationBar: false,
        }}
      />
      <Animated.View
        style={[styles.backdrop, backdropStyle]}
        renderToHardwareTextureAndroid
      />
      <PagerView
        scrollEnabled={!isScaled}
        initialPage={initialImageIndex}
        onPageSelected={e => {
          const next = e.nativeEvent.position
          setImageIndex(prev => {
            if (metricsContext && prev !== next) {
              ax.metric('post:photoEmbed:lightboxSwipe', {
                layout: metricsContext.layout,
                fromImage: prev + 1,
                toImage: next + 1,
                totalImages: images.length,
                postUri: metricsContext.postUri,
                postAuthorDid: metricsContext.postAuthorDid,
                feedDescriptor: metricsContext.feedDescriptor,
              })
            }
            return next
          })
          setIsScaled(false)
        }}
        onPageScrollStateChanged={e => {
          setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
        }}
        overdrag={true}
        style={styles.pager}>
        {images.map((imageSrc, i) => (
          <View key={`${i}-${imageSrc.uri}`}>
            <LightboxImage
              onTap={onTap}
              onZoom={onZoom}
              imageSrc={imageSrc}
              onRequestClose={handleRequestClose}
              isScrollViewBeingDragged={isDragging}
              showControls={showControls}
              safeAreaRef={safeAreaRef}
              isScaled={isScaled}
              isFlyingAway={isFlyingAway}
              isActive={i === imageIndex}
              dismissSwipeTranslateY={dismissSwipeTranslateY}
              openProgress={openProgress}
              thumbRects={thumbRects}
              imageIndex={i}
            />
          </View>
        ))}
      </PagerView>
      <View style={styles.controls} pointerEvents="box-none">
        <Animated.View
          style={animatedHeaderStyle}
          pointerEvents="box-none"
          renderToHardwareTextureAndroid>
          <Header
            onRequestClose={handleRequestClose}
            onPressShare={() => onPressShare(images[imageIndex].uri)}
            onPressSave={() => onPressSave(images[imageIndex].uri)}
            imageCount={images.length}
            activeIndex={imageIndex}
          />
        </Animated.View>
        <Animated.View
          style={animatedFooterStyle}
          pointerEvents="box-none"
          renderToHardwareTextureAndroid={!isAltExpanded}>
          <Footer
            altText={images[imageIndex].alt}
            isAltExpanded={isAltExpanded}
            onToggleAltExpanded={() => setIsAltExpanded(e => !e)}
          />
        </Animated.View>
      </View>
    </Animated.View>
  )
}

function LightboxImage({
  imageSrc,
  onTap,
  onZoom,
  onRequestClose,
  isScrollViewBeingDragged,
  isScaled,
  isFlyingAway,
  isActive,
  showControls,
  safeAreaRef,
  openProgress,
  dismissSwipeTranslateY,
  thumbRects,
  imageIndex,
}: {
  imageSrc: ImageSource
  onRequestClose: () => void
  onTap: () => void
  onZoom: (scaled: boolean) => void
  isScrollViewBeingDragged: boolean
  isScaled: boolean
  isActive: boolean
  isFlyingAway: SharedValue<boolean>
  showControls: boolean
  safeAreaRef: AnimatedRef<View>
  openProgress: SharedValue<number>
  dismissSwipeTranslateY: SharedValue<number>
  thumbRects: SharedValue<Record<number, MeasuredDimensions | null>>
  imageIndex: number
}) {
  const [fetchedDims, setFetchedDims] = useState<Dimensions | null>(null)
  const dims = fetchedDims ?? imageSrc.dimensions ?? imageSrc.thumbDimensions
  let imageAspect: number | undefined
  if (dims) {
    imageAspect = dims.width / dims.height
    if (Number.isNaN(imageAspect)) {
      imageAspect = undefined
    }
  }

  const {
    width: widthDelayedForJSThreadOnly,
    height: heightDelayedForJSThreadOnly,
  } = useWindowDimensions()
  const measureSafeArea = useCallback(() => {
    'worklet'
    let safeArea: Rect | null = measure(safeAreaRef)
    if (!safeArea) {
      if (_WORKLET) {
        console.error('Expected to always be able to measure safe area.')
      }
      safeArea = {
        x: 0,
        y: 0,
        width: widthDelayedForJSThreadOnly,
        height: heightDelayedForJSThreadOnly,
      }
    }
    return safeArea
  }, [safeAreaRef, heightDelayedForJSThreadOnly, widthDelayedForJSThreadOnly])

  const {thumbRect: thumbRectJS, thumbBorderRadius} = imageSrc
  const transforms = useDerivedValue<LightboxTransforms>(() => {
    'worklet'
    const safeArea = measureSafeArea()
    const openProgressValue = openProgress.get()
    const dismissTranslateY =
      isActive && openProgressValue === 1 ? dismissSwipeTranslateY.get() : 0

    if (openProgressValue === 0 && isFlyingAway.get()) {
      return {
        isHidden: true,
        isResting: false,
        borderRadius: 0,
        scaleAndMoveTransform: [],
        cropFrameTransform: [],
        cropContentTransform: [],
      }
    }

    if (isActive && imageAspect && openProgressValue < 1) {
      let thumbRect
      if (_WORKLET) {
        thumbRect = thumbRects.get()[imageIndex]
      } else {
        thumbRect = thumbRectJS
      }
      if (thumbRect) {
        return interpolateTransform(
          openProgressValue,
          thumbRect,
          safeArea,
          imageAspect,
          thumbBorderRadius,
        )
      }
    }
    return {
      isHidden: false,
      isResting: dismissTranslateY === 0,
      borderRadius: 0,
      scaleAndMoveTransform: [{translateY: dismissTranslateY}],
      cropFrameTransform: [],
      cropContentTransform: [],
    }
  })

  const dismissSwipePan = Gesture.Pan()
    .enabled(isActive && !isScaled)
    .activeOffsetY([-10, 10])
    .failOffsetX([-10, 10])
    .maxPointers(1)
    .onUpdate(e => {
      'worklet'
      if (openProgress.get() !== 1 || isFlyingAway.get()) {
        return
      }
      dismissSwipeTranslateY.set(e.translationY)
    })
    .onEnd(e => {
      'worklet'
      if (openProgress.get() !== 1 || isFlyingAway.get()) {
        return
      }
      if (Math.abs(e.velocityY) > 200) {
        isFlyingAway.set(true)
        if (dismissSwipeTranslateY.get() === 0) {
          // HACK: If the initial value is 0, withDecay() animation doesn't start.
          // This is a bug in Reanimated, but for now we'll work around it like this.
          dismissSwipeTranslateY.set(1)
        }
        dismissSwipeTranslateY.set(() => {
          'worklet'
          return withDecay({
            velocity: e.velocityY,
            velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
            deceleration: 1, // Danger! This relies on the reaction below stopping it.
            reduceMotion: ReduceMotion.Never, // If this animation doesn't run, the image gets stuck - therefore override Reduce Motion
          })
        })
      } else {
        dismissSwipeTranslateY.set(() => {
          'worklet'
          return withSpring(0, {
            stiffness: 700,
            damping: 50,
            reduceMotion: ReduceMotion.Never,
          })
        })
      }
    })

  return (
    <ImageItem
      imageSrc={imageSrc}
      onTap={onTap}
      onZoom={onZoom}
      onRequestClose={onRequestClose}
      onLoad={setFetchedDims}
      isScrollViewBeingDragged={isScrollViewBeingDragged}
      showControls={showControls}
      measureSafeArea={measureSafeArea}
      imageAspect={imageAspect}
      imageDimensions={dims ?? undefined}
      dismissSwipePan={dismissSwipePan}
      transforms={transforms}
    />
  )
}

const styles = StyleSheet.create({
  screen: {
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  },
  screenHidden: {
    opacity: 0,
    pointerEvents: 'none',
  },
  container: {
    flex: 1,
  },
  backdrop: {
    backgroundColor: '#000',
    position: 'absolute',
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  },
  controls: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    gap: 20,
    zIndex: 1,
    pointerEvents: 'box-none',
  },
  pager: {
    flex: 1,
  },
})

function interpolatePx(
  px: number,
  inputRange: readonly number[],
  outputRange: readonly number[],
) {
  'worklet'
  const value = interpolate(px, inputRange, outputRange)
  return Math.round(value * PIXEL_RATIO) / PIXEL_RATIO
}

function interpolateTransform(
  progress: number,
  thumbnailDims: {
    pageX: number
    width: number
    pageY: number
    height: number
  },
  safeArea: {width: number; height: number; x: number; y: number},
  imageAspect: number,
  thumbBorderRadius?: number,
): {
  scaleAndMoveTransform: Transform
  cropFrameTransform: Transform
  cropContentTransform: Transform
  borderRadius: number
  isResting: boolean
  isHidden: boolean
} {
  'worklet'
  const thumbAspect = thumbnailDims.width / thumbnailDims.height
  let uncroppedInitialWidth
  let uncroppedInitialHeight
  if (imageAspect > thumbAspect) {
    uncroppedInitialWidth = thumbnailDims.height * imageAspect
    uncroppedInitialHeight = thumbnailDims.height
  } else {
    uncroppedInitialWidth = thumbnailDims.width
    uncroppedInitialHeight = thumbnailDims.width / imageAspect
  }
  const safeAreaAspect = safeArea.width / safeArea.height
  let finalWidth
  let finalHeight
  if (safeAreaAspect > imageAspect) {
    finalWidth = safeArea.height * imageAspect
    finalHeight = safeArea.height
  } else {
    finalWidth = safeArea.width
    finalHeight = safeArea.width / imageAspect
  }
  const initialScale = Math.min(
    uncroppedInitialWidth / finalWidth,
    uncroppedInitialHeight / finalHeight,
  )
  const croppedFinalWidth = thumbnailDims.width / initialScale
  const croppedFinalHeight = thumbnailDims.height / initialScale
  const screenCenterX = safeArea.width / 2
  const screenCenterY = safeArea.height / 2
  const thumbnailSafeAreaX = thumbnailDims.pageX - safeArea.x
  const thumbnailSafeAreaY = thumbnailDims.pageY - safeArea.y
  const thumbnailCenterX = thumbnailSafeAreaX + thumbnailDims.width / 2
  const thumbnailCenterY = thumbnailSafeAreaY + thumbnailDims.height / 2
  const initialTranslateX = thumbnailCenterX - screenCenterX
  const initialTranslateY = thumbnailCenterY - screenCenterY
  const scale = interpolate(progress, [0, 1], [initialScale, 1])
  const translateX = interpolatePx(progress, [0, 1], [initialTranslateX, 0])
  const translateY = interpolatePx(progress, [0, 1], [initialTranslateY, 0])
  const cropScaleX = interpolate(
    progress,
    [0, 1],
    [croppedFinalWidth / finalWidth, 1],
  )
  const cropScaleY = interpolate(
    progress,
    [0, 1],
    [croppedFinalHeight / finalHeight, 1],
  )
  // The border radius in the source thumbnail needs to be scaled to account
  // for the crop frame and overall scale so it visually matches at progress=0.
  const sourceBorderRadius = thumbBorderRadius ?? 0
  const initialCropScaleX = croppedFinalWidth / finalWidth
  const borderRadius = interpolate(
    progress,
    [0, 1],
    [sourceBorderRadius / (initialScale * initialCropScaleX), 0],
  )

  return {
    isHidden: false,
    isResting: progress === 1,
    scaleAndMoveTransform: [{translateX}, {translateY}, {scale}],
    cropFrameTransform: [{scaleX: cropScaleX}, {scaleY: cropScaleY}],
    cropContentTransform: [{scaleX: 1 / cropScaleX}, {scaleY: 1 / cropScaleY}],
    borderRadius,
  }
}

function withClampedSpring<T extends AnimatableValue>(
  value: T,
  config: WithSpringConfig,
): T {
  'worklet'
  return withSpring(value, {...config, overshootClamping: true})
}

// We have to do this because we can't trust RN's rAF to fire in order.
// https://github.com/facebook/react-native/issues/48005
let isFrameScheduled = false
let pendingFrameCallbacks: Array<() => void> = []
function rAF_FIXED(callback: () => void) {
  pendingFrameCallbacks.push(callback)
  if (!isFrameScheduled) {
    isFrameScheduled = true
    requestAnimationFrame(() => {
      const callbacks = pendingFrameCallbacks.slice()
      isFrameScheduled = false
      pendingFrameCallbacks = []
      let hasError = false
      let error
      for (let i = 0; i < callbacks.length; i++) {
        try {
          callbacks[i]()
        } catch (e) {
          hasError = true
          error = e
        }
      }
      if (hasError) {
        throw error
      }
    })
  }
}
