import React, { Component, ReactChild, ReactElement } from "react";
import { Animated, StyleSheet, View, Easing } from "react-native";
import {
  PanGestureHandler,
  NativeViewGestureHandler,
  State,
  TapGestureHandler,
} from "react-native-gesture-handler";

import { SnapPoints } from "./SnapPoints";

const USE_NATIVE_DRIVER = true;
const HEADER_HEIGHT = 44;

const ANIMATION_DURATION = 400;
const ANIMATION_EASING = Easing.in(Easing.bezier(0.25, 0.1, 0.25, 1.0));
const ANIMATION_DELAY = 0;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {},
});

type Props = {
  children: ReactChild;
  header: (boolean) => ReactElement;
  dismiss: () => void;
  visible: boolean;
  sheetHeight: number;
  width: number;
  height: number;
  hasHeader: boolean;
  maxWidth?: number;
};

type ComponentState = {
  lastSnap: number;
  dragging: boolean;
};

export class DraggableBottomSheet extends Component<Props, ComponentState> {
  masterdrawer = React.createRef();
  drawer = React.createRef();
  drawerheader = React.createRef();
  scroll = React.createRef();

  _isMounted = false;
  _animatedIn = false;
  _animating = false;
  _lastScrollYValue: number;
  _lastScrollY: Animated.Value;
  _dragY: Animated.Value;
  _reverseLastScrollY: Animated.AnimatedInterpolation<number>;
  _translateYOffset: Animated.Value;
  _translateY: Animated.AnimatedInterpolation<number>;
  _snapPoints: SnapPoints;
  _prevContentHeight = 0;

  _callWhenMounted: () => void | null;

  _onRegisterLastScroll: (...args: any[]) => void;
  _onGestureEvent: (...args: any[]) => void;

  constructor(props) {
    super(props);

    this._lastScrollYValue = 0;
    this._lastScrollY = new Animated.Value(0);

    this._onRegisterLastScroll = Animated.event(
      [{ nativeEvent: { contentOffset: { y: this._lastScrollY } } }],
      { useNativeDriver: USE_NATIVE_DRIVER }
    );

    this._lastScrollY.addListener(({ value }) => {
      this._lastScrollYValue = value;
    });

    this._dragY = new Animated.Value(0);

    this._onGestureEvent = Animated.event(
      [{ nativeEvent: { translationY: this._dragY } }],
      { useNativeDriver: USE_NATIVE_DRIVER }
    );

    this._reverseLastScrollY = Animated.multiply(
      new Animated.Value(-1),
      this._lastScrollY
    );

    const initialSnapPoints = new SnapPoints(props.height, props.sheetHeight);

    this._translateYOffset = new Animated.Value(
      props.visible ? initialSnapPoints.top : initialSnapPoints.bottom
    );

    this._prevContentHeight = initialSnapPoints.contentHeight;

    this.state = {
      lastSnap: props.height,
      dragging: false,
    };

    this.updateSnapPoints(props.height, props.sheetHeight);
  }

  componentDidMount() {
    this._isMounted = true;

    if (typeof this._callWhenMounted === "function") {
      this._callWhenMounted();
    }
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  componentDidUpdate(prevProps) {
    if (
      this.props.height !== prevProps.height ||
      this.props.sheetHeight !== prevProps.sheetHeight
    ) {
      const args: [number, number, (() => void)?] = [
        this.props.height,
        this.props.sheetHeight,
      ];

      if (
        this.props.visible &&
        this.props.sheetHeight > 0 &&
        !this._animatedIn
      ) {
        this._animatedIn = true;
        args.push(this.animateIn);
      }

      this.updateSnapPoints.apply(this, args);
    }

    if (this.props.visible && this.props.sheetHeight > 0 && !this._animatedIn) {
      setTimeout(() => {
        this._animatedIn = true;
        this.animateIn();
      }, 1);
    }

    if (!this.props.visible && prevProps.visible) {
      this.animateOut();
    }
  }

  updateSnapPoints(windowHeight, contentHeight, callback = () => {}) {
    const totalSheetHeight = this.props.hasHeader
      ? contentHeight + HEADER_HEIGHT
      : contentHeight;

    if (!this._snapPoints) {
      this._snapPoints = new SnapPoints(windowHeight, totalSheetHeight);
      this._prevContentHeight = totalSheetHeight;
    } else {
      this._snapPoints.windowHeight = windowHeight;
      this._snapPoints.contentHeight = totalSheetHeight;

      if (this.props.visible && this._isMounted) {
        Animated.timing(this._translateYOffset, {
          toValue: this._snapPoints.top,
          duration: ANIMATION_DURATION,
          easing: ANIMATION_EASING,
          delay: ANIMATION_DELAY,
          useNativeDriver: USE_NATIVE_DRIVER,
        }).start();
      } else {
        this._translateYOffset.setValue(this._snapPoints.top);
      }

      this._prevContentHeight = totalSheetHeight;
    }

    this._translateY = Animated.add(
      this._translateYOffset,
      Animated.add(this._dragY, this._reverseLastScrollY)
    ).interpolate({
      inputRange: [this._snapPoints.top, this._snapPoints.bottom],
      outputRange: [this._snapPoints.top, this._snapPoints.bottom],
      extrapolate: "clamp",
    });

    const updateState = () => {
      this.setState({ lastSnap: this._snapPoints.top }, callback);
    };

    if (this._isMounted) {
      updateState();
    } else {
      this._callWhenMounted = updateState;
    }
  }

  animate = (toValue) => {
    this._animating = true;

    Animated.timing(this._translateYOffset, {
      toValue,
      duration: ANIMATION_DURATION,
      easing: ANIMATION_EASING,
      delay: ANIMATION_DELAY,
      useNativeDriver: USE_NATIVE_DRIVER,
    }).start(() => {
      this._animating = false;
    });
  };

  animateIn = () => {
    this.animate(this._snapPoints.top);
  };

  animateOut = () => {
    this.animate(this._snapPoints.bottom);
  };

  _onHeaderHandlerStateChange = ({ nativeEvent }) => {
    if (this._animating) {
      return;
    }

    if (nativeEvent.oldState === State.BEGAN) {
      this._lastScrollY.setValue(0);
    }

    this._onHandlerStateChange({ nativeEvent });
  };

  _onHandlerStateChange = ({ nativeEvent }) => {
    if (this._animating) return;

    if (!this.state.dragging) this.setState({ dragging: true });

    if (nativeEvent.oldState === State.ACTIVE) {
      const { velocityY } = nativeEvent;
      let { translationY } = nativeEvent;
      translationY -= this._lastScrollYValue;
      const dragToss = 0.05;

      const endOffsetY =
        this.state.lastSnap + translationY + dragToss * velocityY;

      const destSnapPoint = this._snapPoints.top;

      if (endOffsetY > this._snapPoints.threshold) {
        this.props.dismiss();

        return;
      }

      this.setState({ lastSnap: destSnapPoint });
      this._translateYOffset.extractOffset();
      this._translateYOffset.setValue(translationY);
      this._translateYOffset.flattenOffset();
      this._dragY.setValue(0);

      Animated.spring(this._translateYOffset, {
        velocity: velocityY,
        tension: 68,
        friction: 12,
        toValue: destSnapPoint,
        useNativeDriver: USE_NATIVE_DRIVER,
      }).start();

      this.setState({ dragging: false });
    }
  };

  getDraggableAreaStyles() {
    return {
      width: this.props.maxWidth,
      top: 0,
      left: (this.props.width - this.props.maxWidth) / 2 || 0,
    };
  }

  render() {
    return (
      <TapGestureHandler
        maxDurationMs={100000}
        ref={this.masterdrawer}
        maxDeltaY={this.state.lastSnap - this._snapPoints.top}
      >
        <View style={StyleSheet.absoluteFillObject} pointerEvents="box-none">
          <Animated.View
            style={[
              StyleSheet.absoluteFillObject,
              this.getDraggableAreaStyles(),
              {
                transform: [{ translateY: this._translateY }],
              },
            ]}
          >
            <PanGestureHandler
              ref={this.drawerheader}
              simultaneousHandlers={[this.scroll, this.masterdrawer]}
              shouldCancelWhenOutside={false}
              onGestureEvent={this._onGestureEvent}
              onHandlerStateChange={this._onHeaderHandlerStateChange}
            >
              <Animated.View style={styles.header}>
                {this.props?.header(this.state.dragging)}
              </Animated.View>
            </PanGestureHandler>
            <PanGestureHandler
              ref={this.drawer}
              simultaneousHandlers={[this.scroll, this.masterdrawer]}
              shouldCancelWhenOutside={false}
              onGestureEvent={this._onGestureEvent}
              onHandlerStateChange={this._onHandlerStateChange}
            >
              <Animated.View style={styles.container}>
                <NativeViewGestureHandler
                  ref={this.scroll}
                  waitFor={this.masterdrawer}
                  simultaneousHandlers={this.drawer}
                >
                  <Animated.ScrollView
                    bounces={false}
                    onScrollBeginDrag={this._onRegisterLastScroll}
                    scrollEventThrottle={1}
                    scrollEnabled={false}
                    showsVerticalScrollIndicator={false}
                  >
                    {this.props.children}
                  </Animated.ScrollView>
                </NativeViewGestureHandler>
              </Animated.View>
            </PanGestureHandler>
          </Animated.View>
        </View>
      </TapGestureHandler>
    );
  }
}
