import {
    useImperativeHandle,
    type ComponentRef,
    forwardRef,
    useEffect,
    useState,
    useRef
} from "react";
import {
    type LayoutChangeEvent,
    PanResponder,
    ScrollView,
    Animated,
    Easing,
    View
} from "react-native";
import type IBottomSheetProps from "./type";
import type {
    IBottomSheetRef
} from "./type";
import stylesheet from "./stylesheet";
import {
    NCoreUIKitTheme
} from "../../core/hooks";
import {
    useSafeAreaInsets
} from "react-native-safe-area-context";
import type {
    RefForwardingComponent
} from "../../types";
import type {
    IModalRef
} from "../modal/type";
import {
    windowHeight,
    uuid
} from "../../utils";
import Modal from "../modal";

const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> = ({
    renderHeader: RenderHeaderComponent,
    renderBottom: RenderBottomComponent,
    isForceFullScreenOnSwipe = false,
    isCanFullScreenOnSwipe = false,
    handleContainerBackgroundColor,
    handleHeight: handleHeightProp,
    isWrapSafeAreaContext = true,
    backgroundColor = "default",
    isWorkAsFullScreen = false,
    scrollEndThreshold = 0.85,
    isWorkWithPortal = true,
    isCloseOnOverlay = true,
    handleContainerSpacing,
    isActive: isActiveProp,
    handleBackgroundColor,
    isAutoHeight = false,
    isSwipeClose = true,
    isShowHandle = true,
    isCanSwipe = true,
    onOverlayPressed,
    scrollViewProps,
    scrollViewStyle,
    onScrollEnd,
    customTheme,
    portalName,
    modalProps,
    snapPoint,
    customKey,
    id: outID,
    children,
    onClosed,
    onOpened,
    onClose,
    onOpen,
    style,
    ...props
}, ref) => {
    const {
        colors,
        spaces
    } = NCoreUIKitTheme.useContext(customTheme);

    const {
        bottom,
        top
    } = useSafeAreaInsets();

    const [
        isMeasured,
        setIsMeasured
    ] = useState(false);

    const [
        isActive,
        setIsActive
    ] = useState(isActiveProp === undefined ? false : isActiveProp);

    const id = useRef(outID ? outID : uuid());

    const bottomSheetKey = useRef(customKey ? customKey : uuid());

    let bottomSafeArea = isWrapSafeAreaContext ? bottom : 0;
    let topSafeArea = isWrapSafeAreaContext ? top : 0;

    if(isForceFullScreenOnSwipe) {
        topSafeArea = 0;
    }

    if(!isWorkWithPortal) {
        bottomSafeArea = 0;
        topSafeArea = 0;
    }

    const scrollViewRef = useRef<ComponentRef<ScrollView>>(null);
    const modalRef = useRef<IModalRef>(null);

    const containerHeightRef = useRef(windowHeight);

    const animatedTranslateY = useRef(new Animated.Value(snapPoint && !isWorkAsFullScreen ? snapPoint : containerHeightRef.current)).current;
    const animatedHeight = useRef(new Animated.Value(
        isAutoHeight ? 0 : snapPoint ?? 0
    )).current;

    const TOP_GRAB_AREA = 140;

    const maxHeight = useRef(isWorkAsFullScreen ? containerHeightRef.current - (isWrapSafeAreaContext ? topSafeArea : 0) : containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0));
    const heightValue = useRef(isWorkAsFullScreen ? containerHeightRef.current : snapPoint ?? 0);
    const initialTranslateY = useRef(0);
    const translateYValue = useRef(0);
    const contentHeight = useRef(-1);
    const initialHeight = useRef(0);

    const scrollViewContentHeight = useRef(-1);
    const scrollViewLayoutHeight = useRef(-1);
    const initialScrollOffset = useRef(0);
    const scrollOffset = useRef(0);

    const gestureStartY = useRef(0);

    const isCanFullScreenOnSwipeRef = useRef(isCanFullScreenOnSwipe);
    const isWorkAsFullScreenRef = useRef(isWorkAsFullScreen);
    const isSwipeCloseRef = useRef(isSwipeClose);
    const isCanSwipeRef = useRef(isCanSwipe);

    if(!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
        maxHeight.current = snapPoint;
    }

    useImperativeHandle(
        ref,
        () => ({
            close: (onClosed) => {
                closeAnimation(undefined, onClosed);
            },
            open: () => {
                setIsActive(true);
            }
        }),
        []
    );

    useEffect(() => {
        isCanSwipeRef.current = isCanSwipe;
    }, [isCanSwipe]);

    useEffect(() => {
        isSwipeCloseRef.current = isSwipeClose;
    }, [isSwipeClose]);

    useEffect(() => {
        isCanFullScreenOnSwipeRef.current = isCanFullScreenOnSwipe;
    }, [isCanFullScreenOnSwipe]);

    useEffect(() => {
        isWorkAsFullScreenRef.current = isWorkAsFullScreen;
    }, [isWorkAsFullScreen]);

    useEffect(() => {
        if(isMeasured && contentHeight.current !== -1) {
            if(isCanFullScreenOnSwipe && !isWorkAsFullScreen) {
                maxHeight.current = containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : topSafeArea);
            } else if(isAutoHeight || !snapPoint) {
                maxHeight.current = contentHeight.current;
            }
        }
    }, [isMeasured]);

    useEffect(() => {
        if(isActive && isMeasured) {
            if(!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
                maxHeight.current = snapPoint;
            } else if(isWorkAsFullScreen) {
                maxHeight.current = containerHeightRef.current - (isWrapSafeAreaContext ? topSafeArea : 0);
            } else {
                maxHeight.current = containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0);
            }

            openAnimation();
        }
    }, [
        isActive,
        isMeasured
    ]);

    if(isActiveProp !== undefined) {
        useEffect(() => {
            setIsActive(isActiveProp);
        }, [isActiveProp]);
    }

    useEffect(() => {
        if (!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
            maxHeight.current = snapPoint;
        } else if (!isWorkAsFullScreen) {
            maxHeight.current = containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0);
        } else {
            maxHeight.current = containerHeightRef.current - (isWrapSafeAreaContext ? topSafeArea : 0);
        }
    }, [
        isForceFullScreenOnSwipe,
        isCanFullScreenOnSwipe,
        isWrapSafeAreaContext,
        isWorkAsFullScreen,
        topSafeArea,
        snapPoint
    ]);

    useEffect(() => {
        if (!isActive) {
            const newSnapValue = isWorkAsFullScreen
                ? containerHeightRef.current
                : (snapPoint ?? contentHeight.current);

            animatedHeight.setValue(isAutoHeight ? 0 : newSnapValue);
            animatedTranslateY.setValue(newSnapValue);

            heightValue.current = newSnapValue;
            translateYValue.current = newSnapValue;
            initialHeight.current = newSnapValue;
        }

        if (!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
            maxHeight.current = snapPoint;
        } else if (!isWorkAsFullScreen) {
            maxHeight.current = containerHeightRef.current - (
                isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0
            );
        } else {
            maxHeight.current = containerHeightRef.current - (
                isWrapSafeAreaContext ? topSafeArea : 0
            );
        }
    }, [
        snapPoint,
        isWorkAsFullScreen
    ]);

    useEffect(() => {
        const listenerAHeightId = animatedHeight.addListener(({
            value
        }) => {
            heightValue.current = value;
        });

        const listenerTYId = animatedTranslateY.addListener(({
            value
        }) => {
            translateYValue.current = value;
        });

        return () => {
            animatedHeight.removeListener(listenerAHeightId);
            animatedTranslateY.removeListener(listenerTYId);
        };
    }, []);

    const checkScrollThreshold = () => {
        if (!onScrollEnd) return;

        const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current);

        if (maxS <= 0) return;

        const ratio = scrollOffset.current / maxS;

        if (ratio >= scrollEndThreshold) {
            onScrollEnd();
        }
    };

    const openAnimation = () => {
        resetState();

        if(onOpen) onOpen();

        Animated.timing(animatedTranslateY, {
            useNativeDriver: false,
            duration: 300,
            toValue: 0
        }).start(({
            finished
        }) => {
            if(finished) {
                if(onOpened) onOpened();
            }
        });
    };

    const closeAnimation = (toValue?: number, _onClosed?: (props: {
        id: string;
    }) => void) => {
        if(onClose) onClose({
            id: id.current
        });

        const currentSnapPoint = isWorkAsFullScreen
            ? containerHeightRef.current
            : (isAutoHeight || !snapPoint ? contentHeight.current : snapPoint);

        Animated.timing(animatedTranslateY, {
            toValue: toValue ?? currentSnapPoint,
            useNativeDriver: false,
            duration: 300
        }).start(({
            finished
        }) => {
            if(finished) {
                resetState();

                setIsActive(false);

                if(onClosed) onClosed({
                    id: id.current
                });

                if(_onClosed) _onClosed({
                    id: id.current
                });
            }
        });
    };

    const onLayout = (event: LayoutChangeEvent) => {
        if (isMeasured) return;

        const {
            height
        } = event.nativeEvent.layout;

        animatedHeight.setValue(height);

        contentHeight.current = height;

        setIsMeasured(true);
    };

    const resetState = () => {
        animatedHeight.setOffset(0);
        animatedTranslateY.setOffset(0);

        scrollOffset.current = 0;
        initialScrollOffset.current = 0;

        const pivot = isWorkAsFullScreen
            ? containerHeightRef.current
            : (snapPoint ?? contentHeight.current);

        initialHeight.current = pivot;
        initialTranslateY.current = 0;

        heightValue.current = pivot;
        translateYValue.current = 0;
    };

    const panResponder = useRef(
        PanResponder.create({
            onStartShouldSetPanResponder: () => false,
            onMoveShouldSetPanResponderCapture: (_, gestureState) => {
                const {
                    dy
                } = gestureState;

                if(!isCanSwipeRef.current) {
                    return false;
                }

                if(Math.abs(dy) < 20) {
                    return false;
                }

                return true;
            },
            onPanResponderGrant: (evt) => {
                if(!isCanSwipeRef.current) return;

                gestureStartY.current = evt.nativeEvent.pageY;

                animatedTranslateY.stopAnimation((currentY) => {
                    translateYValue.current = currentY;
                });

                animatedHeight.stopAnimation((currentH) => {
                    heightValue.current = currentH;
                });

                animatedTranslateY.flattenOffset();
                animatedHeight.flattenOffset();

                initialTranslateY.current = translateYValue.current;
                initialScrollOffset.current = scrollOffset.current;
                initialHeight.current = heightValue.current;

                animatedHeight.setOffset(heightValue.current);
                animatedHeight.setValue(0);

                animatedTranslateY.setOffset(translateYValue.current);
                animatedTranslateY.setValue(0);
            },
            onPanResponderMove: (_, gestureState) => {
                if(!isCanSwipeRef.current) return;

                const {
                    dy
                } = gestureState;

                const isAtTop = scrollOffset.current <= 0;
                const isAtTavan = heightValue.current >= maxHeight.current - 1;
                const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current;

                const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA;

                if (dy > 0 && isAtTavan && hasScroll && !isAtTop && !isFromTopArea) {
                    const currentDelta = -dy;

                    const initialS = initialScrollOffset.current;

                    const usedForS = Math.max(currentDelta, -initialS);

                    scrollOffset.current = initialS + usedForS;

                    scrollViewRef.current?.scrollTo({
                        y: scrollOffset.current,
                        animated: false
                    });

                    return;
                }

                const delta = -dy;

                const pivot = snapPoint ?? contentHeight.current;

                const initialH = initialHeight.current;
                const initialS = initialScrollOffset.current;
                const initialT = initialTranslateY.current;

                const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current);

                if (dy < 0) {
                    let currentDelta = delta;

                    const effectiveInitialT = Math.max(0, initialT);
                    const usedForT = Math.min(currentDelta, effectiveInitialT);

                    animatedTranslateY.setValue(-usedForT);
                    currentDelta -= usedForT;

                    if (currentDelta > 0) {
                        if (initialH < pivot) {
                            const spaceToPivot = pivot - initialH;
                            const usedForH = Math.min(currentDelta, spaceToPivot);

                            animatedHeight.setValue(usedForH);

                            currentDelta -= usedForH;
                        }

                        const isRestoringHeight = initialH < pivot;
                        const canScroll = isCanFullScreenOnSwipeRef.current || isWorkAsFullScreenRef.current || !isRestoringHeight;

                        if (currentDelta > 0 && canScroll) {
                            const remainingScroll = maxS - initialS;
                            if (remainingScroll > 0) {
                                const usedForS = Math.min(currentDelta, remainingScroll);
                                scrollOffset.current = initialS + usedForS;

                                scrollViewRef.current?.scrollTo({
                                    y: scrollOffset.current,
                                    animated: false
                                });

                                currentDelta -= usedForS;

                                animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0);
                            }
                        }

                        if (currentDelta > 0) {
                            if (isCanFullScreenOnSwipeRef.current) {
                                const totalUsedBefore = (initialH < pivot ? (pivot - initialH) : 0);

                                animatedHeight.setValue(totalUsedBefore + currentDelta);
                            } else {
                                animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0);
                            }
                        }
                    }
                } else {
                    let currentDelta = delta;

                    if (initialH > pivot || initialS > 0) {
                        if (initialS > 0) {
                            const usedForS = Math.max(currentDelta, -initialS);
                            scrollOffset.current = initialS + usedForS;

                            scrollViewRef.current?.scrollTo({
                                y: scrollOffset.current,
                                animated: false
                            });

                            currentDelta -= usedForS;

                            animatedHeight.setValue(initialH > pivot ? 0 : pivot - initialH);
                        }

                        if (currentDelta < 0 && initialH > pivot) {
                            const distanceToPivot = pivot - initialH;
                            const usedForH = Math.max(currentDelta, distanceToPivot);

                            animatedHeight.setValue(usedForH);

                            currentDelta -= usedForH;
                        }
                    }

                    if (currentDelta < 0) {
                        animatedHeight.setValue(initialH > pivot ? (pivot - initialH) : 0);
                        animatedTranslateY.setValue(-currentDelta);

                        scrollOffset.current = 0;

                        scrollViewRef.current?.scrollTo({
                            y: 0,
                            animated: false
                        });
                    }
                }
            },
            onPanResponderEnd: (_, gestureState) => {
                if(!isCanSwipeRef.current) return;

                const isAtTop = scrollOffset.current <= 1;
                const isAtTavan = heightValue.current >= maxHeight.current - 1;
                const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current;

                const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA;

                if (isAtTavan && hasScroll && !isAtTop && !isFromTopArea) {
                    const velocity = gestureState.vy;

                    if (Math.abs(velocity) > 0.1) {
                        const momentum = velocity * -350;

                        const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current);

                        const targetOffset = Math.max(0, Math.min(scrollOffset.current + momentum, maxS));

                        const scrollAnim = new Animated.Value(scrollOffset.current);

                        scrollAnim.addListener(({
                            value
                        }) => {
                            scrollViewRef.current?.scrollTo({
                                y: value,
                                animated: false
                            });
                            scrollOffset.current = value;
                        });

                        Animated.timing(scrollAnim, {
                            duration: Math.min(Math.abs(momentum) * 2, 600),
                            easing: Easing.out(Easing.quad),
                            useNativeDriver: false,
                            toValue: targetOffset
                        }).start(() => {
                            scrollAnim.removeAllListeners();

                            checkScrollThreshold();
                        });
                    } else {
                        checkScrollThreshold();
                    }

                    return;
                }

                const currentH = (animatedHeight as Animated.Value & {
                    __getValue(): number;
                }).__getValue();

                const currentT = (animatedTranslateY as Animated.Value & {
                    __getValue(): number;
                }).__getValue();

                animatedHeight.flattenOffset();
                animatedTranslateY.flattenOffset();

                heightValue.current = currentH;
                translateYValue.current = currentT;

                const pivot = snapPoint ?? contentHeight.current;
                const tavan = maxHeight.current;

                const isFastSwipeDown = gestureState.vy > 0.5;
                const isFastSwipeUp = gestureState.vy < -0.5;

                if (currentT <= Math.max(pivot * 0.2, 60)) {
                    let toValue = pivot;

                    if (isFastSwipeUp && isCanFullScreenOnSwipeRef.current) {
                        toValue = tavan;
                    } else if (isFastSwipeDown) {
                        toValue = pivot;
                    } else {
                        if (!isCanFullScreenOnSwipeRef.current) {
                            toValue = pivot;
                        } else {
                            const totalRange = tavan - pivot;
                            const distFromPivot = currentH - pivot;

                            if (distFromPivot < totalRange * 0.33) {
                                toValue = pivot;
                            } else if (distFromPivot > totalRange * 0.66) {
                                toValue = tavan;
                            } else {
                                toValue = (tavan - currentH) < (currentH - pivot) ? tavan : pivot;
                            }
                        }
                    }

                    Animated.spring(animatedHeight, {
                        useNativeDriver: false,
                        toValue: toValue,
                        friction: 10,
                        tension: 40
                    }).start();

                    Animated.spring(animatedTranslateY, {
                        useNativeDriver: false,
                        toValue: 0,
                        friction: 10,
                        tension: 40
                    }).start();
                } else {
                    let toValueT = 0;
                    let isClosing = false;

                    if (!isSwipeCloseRef.current) {
                        Animated.spring(animatedHeight, {
                            useNativeDriver: false,
                            toValue: pivot,
                            friction: 10,
                            tension: 40
                        }).start();

                        Animated.timing(animatedTranslateY, {
                            useNativeDriver: false,
                            duration: 300,
                            toValue: 0
                        }).start();
                    } else {
                        if (isFastSwipeDown) {
                            toValueT = pivot + 100;

                            isClosing = true;
                        } else {
                            if (currentT > pivot * 0.5) {
                                toValueT = pivot + 100;

                                isClosing = true;
                            } else {
                                toValueT = 0;

                                isClosing = false;
                            }
                        }

                        if(onClose) onClose({
                            id: id.current
                        });

                        Animated.timing(animatedTranslateY, {
                            useNativeDriver: false,
                            toValue: toValueT,
                            duration: 300
                        }).start(({
                            finished
                        }) => {
                            if (finished && isClosing) {
                                setIsActive(false);

                                if (onClosed) onClosed({
                                    id: id.current
                                });
                            }
                        });
                    }
                }
            },
            onPanResponderTerminationRequest: () => false,
            onShouldBlockNativeResponder: () => true
        })
    ).current;

    const renderView = () => {
        return <Animated.View
            {...props}
            {...panResponder.panHandlers}
            onLayout={onLayout}
            style={[
                style,
                {
                    height: (isAutoHeight || !snapPoint) && !isMeasured ? "auto" : animatedHeight,
                    backgroundColor: colors.content.container[backgroundColor],
                    paddingBottom: bottomSafeArea + spaces.spacingMd,
                    opacity: isMeasured || !isAutoHeight ? 1 : 0,
                    paddingRight: spaces.spacingMd,
                    paddingLeft: spaces.spacingMd,
                    paddingTop: spaces.spacingMd,
                    maxHeight: maxHeight.current,
                    transform: [{
                        translateY: animatedTranslateY
                    }]
                },
                stylesheet.container
            ]}
        >
            {renderHeader()}
            <ScrollView
                {...scrollViewProps}
                onContentSizeChange={(_, h) => {
                    scrollViewContentHeight.current = h;
                }}
                onLayout={(e) => {
                    scrollViewLayoutHeight.current = e.nativeEvent.layout.height;
                }}
                onStartShouldSetResponderCapture={() => false}
                onMoveShouldSetResponderCapture={() => false}
                showsHorizontalScrollIndicator={false}
                showsVerticalScrollIndicator={false}
                scrollEnabled={!isCanSwipe}
                scrollEventThrottle={1}
                overScrollMode="never"
                ref={scrollViewRef}
                bounces={false}
                style={[
                    scrollViewStyle
                ]}
            >
                {children}
            </ScrollView>
            {renderBottom()}
            {renderHandle()}
        </Animated.View>;
    };

    const renderHeader = () => {
        if(!RenderHeaderComponent) {
            return null;
        }

        return RenderHeaderComponent();
    };

    const renderBottom = () => {
        if(!RenderBottomComponent) {
            return null;
        }

        return <RenderBottomComponent/>;
    };

    const renderHandle = () => {
        if(!isShowHandle || isWorkAsFullScreen) {
            return null;
        }

        const handleHeight = handleHeightProp ? handleHeightProp : 8;
        const handleContainerHeight = handleHeight + ((handleContainerSpacing ? spaces[handleContainerSpacing] : spaces.spacingSm) * 2);
        const handleBorderRadius = Math.ceil(handleHeight / 2);

        return <View
            {...panResponder.panHandlers}
            onStartShouldSetResponderCapture={() => false}
            onMoveShouldSetResponderCapture={() => false}
            style={[
                stylesheet.handleContainer,
                {
                    backgroundColor: handleContainerBackgroundColor
                        ? colors.content.container[handleContainerBackgroundColor]
                        : "transparent",
                    height: handleContainerHeight,
                    transform: [{
                        translateY: -handleContainerHeight
                    }]
                }
            ]}
        >
            <View
                style={[
                    stylesheet.handle,
                    {
                        backgroundColor: handleBackgroundColor
                            ? colors.content.container[handleBackgroundColor]
                            : colors.content.container.default,
                        borderRadius: handleBorderRadius,
                        height: handleHeight
                    }
                ]}
            />
        </View>;
    };

    return <Modal
        {...modalProps}
        portalName={portalName ? portalName : "bottomSheet-system"}
        isWorkWithPortal={isWorkWithPortal}
        key={`${bottomSheetKey}-modal`}
        isContentRequired={false}
        isActive={isActive}
        alignContent="free"
        isAnimated={false}
        id={id.current}
        ref={modalRef}
        onContainerLayout={(e) => {
            const containerLayoutHeight = e.nativeEvent.layout.height;

            if (containerLayoutHeight && containerLayoutHeight !== containerHeightRef.current) {
                containerHeightRef.current = containerLayoutHeight;

                if (!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
                    maxHeight.current = snapPoint;
                } else if (!isWorkAsFullScreen) {
                    maxHeight.current = containerLayoutHeight - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0);
                } else {
                    maxHeight.current = containerLayoutHeight;
                }
            }
        }}
        overlayProps={{
            onStartShouldSetResponderCapture: () => false,
            onMoveShouldSetResponderCapture: () => false
        }}
        onOverlayPress={() => {
            if(onOverlayPressed) onOverlayPressed();

            if(isCloseOnOverlay) {
                closeAnimation();
            }
        }}
        style={[
            {
                paddingTop: topSafeArea
            }
        ]}
    >
        {renderView()}
    </Modal>;
};
export default forwardRef(BottomSheet);
