import React, { useState, useEffect, memo, useMemo } from 'react';
import { Animated, StyleSheet, View, LayoutChangeEvent, Text, TextInput, ViewStyle, TextStyle } from 'react-native';
import { gestureHandlerRootHOC, GestureHandlerRootView, PanGestureHandler, PanGestureHandlerGestureEvent, State } from 'react-native-gesture-handler';
import KnobBubble from './components/KnobBubble';
import useUtils, { osRtl } from './components/utils';

const SMALL_SIZE = 24;
const MEDIUM_SIZE = 34;
const LARGE_SIZE = 44;
const fontSize = 15;

interface SliderProps {
  min: number,
  max: number,
  fromValueOnChange: (value: number) => void,
  toValueOnChange: (value: number) => void,
  step?: number,
  styleSize?: 'small' | 'medium' | 'large' | number,
  fromKnobColor?: string,
  toKnobColor?: string,
  inRangeBarColor?: string,
  outOfRangeBarColor?: string,
  knobBubbleTextStyle?: TextStyle,
  valueLabelsBackgroundColor?: string,
  rangeLabelsTextColor?: string,
  showRangeLabels?: boolean,
  showValueLabels?: boolean,
  initialFromValue?: number,
  initialToValue?: number,
  knobSize?: number,
  containerStyle?: ViewStyle,
  barHeight?: number,
  labelFormatter?: (value: number) => string,
}

const RangeSlider = memo(({
  min, max, fromValueOnChange, toValueOnChange,
  step = 1,
  styleSize = 'medium',
  fromKnobColor = '#00a2ff',
  toKnobColor = '#00a2ff',
  inRangeBarColor = 'rgb(100,100,100)',
  outOfRangeBarColor = 'rgb(200,200,200)',
  valueLabelsBackgroundColor = '#3a4766',
  rangeLabelsTextColor = 'rgb(60,60,60)',
  showRangeLabels = true,
  showValueLabels = true,
  initialFromValue,
  initialToValue,
  knobSize: _knobSize,
  knobBubbleTextStyle = {},
  containerStyle: customContainerStyle = {},
  barHeight: customBarHeight,
  labelFormatter,
}: SliderProps) => {
  
  // settings
  const [wasInitialized, setWasInitialized] = useState(false);
  const [knobSize, setknobSize] = useState(0);
  const [barHeight, setBarHeight] = useState(0);
  const [stepInPixels, setStepInPixels] = useState(0);

  // rtl settings
  const [flexDirection, setFlexDirection] = useState<"row" | "row-reverse" | "column" | "column-reverse" | undefined>('row');

  const [fromValueOffset, setFromValueOffset] = useState(0);
  const [toValueOffset, setToValueOffset] = useState(0);
  const [sliderWidth, setSliderWidth] = useState(0);
  const [fromElevation, setFromElevation] = useState(3);
  const [toElevation, setToElevation] = useState(3);

  // animation values
  const [translateXfromValue] = useState(new Animated.Value(0));
  const [translateXtoValue] = useState(new Animated.Value(0));
  const [fromValueScale] = useState(new Animated.Value(0.01));
  const [toValueScale] = useState(new Animated.Value(0.01));
  const [rightBarScaleX] = useState(new Animated.Value(0.01));
  const [leftBarScaleX] = useState(new Animated.Value(0.01));

  // refs
  const toValueTextRef = React.createRef<TextInput>();
  const fromValueTextRef = React.createRef<TextInput>();
  const opacity = React.useRef<Animated.Value>(new Animated.Value(0)).current;
  const {formatLabel, decimalRound} = useUtils({step, labelFormatter});

  // initalizing settings
  useEffect(() => {
    setFlexDirection(osRtl ? 'row-reverse' : 'row');
  }, [knobSize]);
  
  useEffect(() => {
    if (wasInitialized) {
      const stepSize = setStepSize(max, min, step);
      fromValueTextRef.current?.setNativeProps({ text: formatLabel(min) });
      toValueTextRef.current?.setNativeProps({ text: formatLabel(min) });
      if (typeof initialFromValue === 'number' && initialFromValue >= min && initialFromValue <= max) {
        const offset = ((initialFromValue - min) / step) * stepSize - (knobSize / 2);
        setFromValueStatic(offset, knobSize, stepSize);
        setValueText(offset + knobSize, true);
      }
      if (typeof initialToValue === 'number' && initialToValue >= min && initialToValue <= max && typeof initialFromValue === 'number' && initialToValue > initialFromValue) {
        const offset = ((initialToValue - min) / step) * stepSize - (knobSize / 2);
        setToValueStatic(offset, knobSize, stepSize);
        setValueText(offset, false);
      }
      Animated.timing(opacity, {
        toValue: 1,
        duration: 64,
        useNativeDriver: true
      }).start();
    }
  }, [min, max, step, initialFromValue, initialToValue, wasInitialized]);

  useEffect(() => {
    const sizeBasedOnStyleSize = typeof styleSize === 'number' ? styleSize : styleSize === 'small' ? SMALL_SIZE : styleSize === 'medium' ? MEDIUM_SIZE : LARGE_SIZE;
    const size = _knobSize ?? sizeBasedOnStyleSize;
    setknobSize(customBarHeight ? Math.max(customBarHeight, size) : size);
    setBarHeight(customBarHeight ?? sizeBasedOnStyleSize / 3)
    translateXfromValue.setValue(-size / 4);
  }, [styleSize, customBarHeight]);
  
  // initalizing settings helpers
  const setFromValueStatic = (newOffset: number, knobSize: number, stepInPixels: number) => {
    newOffset = Math.floor((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
    setFromValue(newOffset);
    setFromValueOffset(newOffset);
    const changeTo = Math.floor(((newOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
    fromValueOnChange(decimalRound(changeTo));
  }
  const setFromValue = (newOffset: number) => {
    translateXfromValue.setValue(newOffset);
    leftBarScaleX.setValue((newOffset + (knobSize / 2)) / sliderWidth + 0.01);
  }
  const setToValueStatic = (newOffset: number, knobSize: number, stepInPixels: number) => {
    newOffset = Math.ceil((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
    setToValue(newOffset);
    setToValueOffset(newOffset);
    const changeTo = Math.ceil(((newOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
    toValueOnChange(decimalRound(changeTo));
  }
  const setToValue = (newOffset: number) => {
    translateXtoValue.setValue(newOffset);
    rightBarScaleX.setValue(1.01 - ((newOffset + (knobSize / 2)) / sliderWidth));
  }
  const setStepSize = (max: number, min: number, step: number) => {
    const numberOfSteps = ((max - min) / step);
    const stepSize = sliderWidth / numberOfSteps;
    setStepInPixels(stepSize);
    return stepSize;
  }
  const setValueText = (totalOffset: number, from = true) => {
    const isFrom = from && fromValueTextRef != null;
    const isTo = !from && toValueTextRef != null;
    if (isFrom || isTo) {
      const numericValue: number = Math[isFrom ? 'floor' : 'ceil'](((totalOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
      const text = formatLabel(numericValue);
      (isFrom ? fromValueTextRef : toValueTextRef).current?.setNativeProps({ text });
    }
  }

  // from value gesture events ------------------------------------------------------------------------
  const onGestureEventFromValue = (event: PanGestureHandlerGestureEvent) => {
    let totalOffset = event.nativeEvent.translationX + fromValueOffset;
    if (totalOffset >= -knobSize / 2 && totalOffset < toValueOffset) {
      translateXfromValue.setValue(totalOffset);
      setValueText(totalOffset, true);
      leftBarScaleX.setValue((totalOffset + (knobSize / 2)) / sliderWidth + 0.01);
    }
  }
  const onHandlerStateChangeFromValue = (event: PanGestureHandlerGestureEvent) => {
    if (event.nativeEvent.state === State.BEGAN) {
      scaleTo(fromValueScale, 1);
      setElevations(6, 5);
    }
    if (event.nativeEvent.state === State.END) {
      let newOffset = event.nativeEvent.translationX + fromValueOffset;
      newOffset = Math.floor((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
      if (newOffset < -knobSize / 2) {
        newOffset = -knobSize / 2;
      } else if (newOffset >= toValueOffset) {
        newOffset = toValueOffset - stepInPixels;
      }
      setFromValueStatic(newOffset, knobSize, stepInPixels)
      scaleTo(fromValueScale, 0.01);
    }
  }
  // ------------------------------------------------------------------------------------------------

  // to value gesture events ------------------------------------------------------------------------
  const onGestureEventToValue = (event: PanGestureHandlerGestureEvent) => {
    const totalOffset = event.nativeEvent.translationX + toValueOffset;
    if (totalOffset <= sliderWidth - knobSize / 2 && totalOffset > fromValueOffset) {
      translateXtoValue.setValue(totalOffset);
      setValueText(totalOffset, false);
      rightBarScaleX.setValue(1.01 - ((totalOffset + (knobSize / 2)) / sliderWidth));
    }
  }
  const onHandlerStateChangeToValue = (event: PanGestureHandlerGestureEvent) => {
    if (event.nativeEvent.state === State.BEGAN) {
      scaleTo(toValueScale, 1);
      setElevations(5, 6);
    }
    if (event.nativeEvent.state === State.END) {
      let newOffset = event.nativeEvent.translationX + toValueOffset;
      newOffset = Math.ceil((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
      if (newOffset > sliderWidth - knobSize / 2) {
        newOffset = sliderWidth - knobSize / 2;
      } else if (newOffset <= fromValueOffset) {
        newOffset = fromValueOffset + stepInPixels;
      }
      setToValueOffset(newOffset);
      translateXtoValue.setValue(newOffset);
      rightBarScaleX.setValue(1.01 - ((newOffset + (knobSize / 2)) / sliderWidth));
      scaleTo(toValueScale, 0.01);
      const changeTo = Math.ceil(((newOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
      toValueOnChange(decimalRound(changeTo));
    }
  }
  // ------------------------------------------------------------------------------------------------

  // gesture events help functions ------------------------------------------------------------------
  const scaleTo = (param: Animated.Value, toValue: number) => Animated.timing(param, {
    toValue,
    duration: 150,
    useNativeDriver: true
  }).start();

  const setElevations = (fromValue: number, toValue: number) => {
    setFromElevation(fromValue);
    setToElevation(toValue)
  }
  // ------------------------------------------------------------------------------------------------

  // setting bar width ------------------------------------------------------------------------------
  const onLayout = (event: LayoutChangeEvent) => {
    if (wasInitialized === false) {
      const { width } = event.nativeEvent.layout;
      setSliderWidth(width);
      translateXtoValue.setValue(width - knobSize / 2);
      setToValueOffset(width - knobSize / 2);
      setWasInitialized(true);
    }
  }
  // ------------------------------------------------------------------------------------------------

  const padding = useMemo(() => styleSize === 'large' ? 17 : styleSize === 'medium' ? 24 : 31, [styleSize]);

  return (
    <GestureHandlerRootView>
      <Animated.View style={[styles.container, { opacity, padding }, customContainerStyle]}>
        {
          showValueLabels &&
          <View style={{ width: '100%', height: 1, flexDirection }}>
            <KnobBubble {...{ knobSize, valueLabelsBackgroundColor }}
              translateX={translateXfromValue}
              scale={fromValueScale}
              textInputRef={fromValueTextRef}
              textStyle={knobBubbleTextStyle}
            />
            <KnobBubble {...{ knobSize, valueLabelsBackgroundColor }}
              translateX={translateXtoValue}
              scale={toValueScale}
              textInputRef={toValueTextRef}
              textStyle={knobBubbleTextStyle}
            />
          </View>
        }
        <View style={{ width: '100%', height: knobSize, marginVertical: 4, position: 'relative', flexDirection, alignItems: 'center' }}>
          <View style={{ position: 'absolute', backgroundColor: inRangeBarColor, left: knobSize / 4, marginLeft: -knobSize / 4, right: knobSize / 4, height: barHeight }} onLayout={onLayout} />
          <Animated.View style={{ position: 'absolute', left: knobSize / 4, marginLeft: -knobSize / 4, right: knobSize / 4, height: barHeight, backgroundColor: outOfRangeBarColor, transform: [{ translateX: sliderWidth / 2 }, { scaleX: rightBarScaleX }, { translateX: -sliderWidth / 2 }] }} />
          <Animated.View style={{ position: 'absolute', left: -knobSize / 4, width: knobSize / 2, height: barHeight, borderRadius: barHeight, backgroundColor: outOfRangeBarColor }} />
          <Animated.View style={{ width: sliderWidth, height: barHeight, backgroundColor: outOfRangeBarColor, transform: [{ translateX: -sliderWidth / 2 }, { scaleX: leftBarScaleX }, { translateX: sliderWidth / 2 }] }} />
          <Animated.View style={{ position: 'absolute', left: sliderWidth - knobSize / 4, width: knobSize / 2, height: barHeight, borderRadius: barHeight, backgroundColor: outOfRangeBarColor }} />
          <PanGestureHandler onGestureEvent={onGestureEventFromValue} onHandlerStateChange={onHandlerStateChangeFromValue}>
            <Animated.View style={[styles.knob, { height: knobSize, width: knobSize, borderRadius: knobSize, backgroundColor: fromKnobColor, elevation: fromElevation, transform: [{ translateX: translateXfromValue }] }]} />
          </PanGestureHandler>
          <PanGestureHandler onGestureEvent={onGestureEventToValue} onHandlerStateChange={onHandlerStateChangeToValue}>
            <Animated.View style={[styles.knob, { height: knobSize, width: knobSize, borderRadius: knobSize, backgroundColor: toKnobColor, elevation: toElevation, transform: [{ translateX: translateXtoValue }] }]} />
          </PanGestureHandler>
        </View>
        {
          showRangeLabels &&
          <View style={{ width: '100%', flexDirection, justifyContent: 'space-between' }}>
            <Text style={{ color: rangeLabelsTextColor, fontWeight: "bold", fontSize }}>{min}</Text>
            <Text style={{ color: rangeLabelsTextColor, fontWeight: "bold", fontSize }}>{max}</Text>
          </View>
        }
      </Animated.View>
    </GestureHandlerRootView>
  );
})

const styles = StyleSheet.create({
  container: {
    height: 100,
    width: '100%'
  },
  knob: {
    position: 'absolute',
    elevation: 4
  }
});

export default gestureHandlerRootHOC(RangeSlider);