import React, { createRef, useRef, useState } from 'react';
import { Svg, G } from 'react-native-svg';
import {
  Dimensions,
  GestureResponderEvent,
  PanResponder,
  StyleSheet,
} from 'react-native';
import { Nullable } from 'tsdef';
import { Line } from './line';
import { YAxis } from './y-axis';
import { XAxis } from './x-axis';
import { SBox } from '../../common/s-components/layout/s-box';
import { SRow } from '../../common/s-components/layout/s-row';
import { TDualGraphInner, TLine } from '../types';
import { fp as _ } from '../../common/utils/fp';
import { XAxisLabels } from './x-axis-labels';
import { YAxisLabels } from './y-axis-labels';
import { theme } from '../../app/theme';
import { SH6 } from '../../common/s-components/typography/s-h6';
import { SText } from '../../common/s-components/typography/s-text';
import { SDot } from '../s-components/s-dot';
import { formatStream, getWeekday } from '../../common/transducers';
import {
  buildPath,
  formatDate,
  buildScalesDynamic,
  getLinePrimaryColorByVariant,
  getGraphPercentage,
} from '../transducers';

import { SLineDot } from '../s-components/s-line-dot';
import { SHoverLine } from '../s-components/s-hover-line';
import { GRAPH_PADDING_TOP_RATIO } from '../constants';
import { TCoordinate, TCoordinates } from '../../common/types';
import { SLegendContainer } from '../s-components/s-legend-container';
import { SPointDate } from '../s-components/s-point-date';
import { STotal } from '../s-components/s-total';
import { SLegendText } from '../s-components/s-legend-text';

type TProps = TDualGraphInner & {
  hideTotal?: boolean;
  hidePercentage?: boolean;
  titleY?: string;
};

type TState = {
  width: number;
  height: number;
  pointDate1: Nullable<string>;
  pointDate2: Nullable<string>;
  values1: Record<string, null>;
  percents1: Record<string, string>;
  values2: Record<string, null>;
  percents2: Record<string, string>;
  extraTexts: Record<string, string>;
};

type TVirtualState = {
  values1?: Record<string, null>;
  percents1?: Record<string, string>;
  values2?: Record<string, null>;
  percents2?: Record<string, string>;
  pointDate1?: Nullable<string>;
  pointDate2?: Nullable<string>;
  extraTexts?: Nullable<string>;
};

export const DualGraphAnimated: React.FC<TProps> = props => {
  const [state, setState] = useState<TState>({
    width: Dimensions.get('window').width,
    height: Dimensions.get('window').height,
    pointDate1: null,
    pointDate2: null,
    values1: {},
    percents1: {},
    values2: {},
    percents2: {},
    extraTexts: {},
  });

  const virtualState = useRef<TVirtualState>({
    values1: undefined,
    percents1: undefined,
    values2: undefined,
    percents2: undefined,
    extraTexts: undefined,
    pointDate1: null,
    pointDate2: null,
  });

  const cursors1 = useRef<Record<string, React.RefObject<any>>>({});

  const cursors2 = useRef<Record<string, React.RefObject<any>>>({});

  const line1 = useRef<React.RefObject<any>>();

  const line2 = useRef<React.RefObject<any>>();

  const bgLayer = useRef();

  const getSizes = (state: TState) => {
    const { width: windowWidth, height: windowHeight } = state;
    const padding = 56;
    const paddingTop = 27;
    const paddingBottom = 123;
    const svgHeight = windowHeight - 38;
    const svgWidth = windowWidth - padding * 2 - 171;
    const innerHeight = svgHeight - paddingBottom;

    return {
      width: windowWidth,
      padding,
      paddingTop,
      innerHeight,
      svgHeight,
      svgWidth,
      xAxisOffset: padding + 61,
      xAxisOffsetRight: svgWidth - 60,
      offsetTop: 15,
    };
  };

  const getScales1 = (props: TProps, state: TState) => {
    const {
      maxes1: { xMin, xMax, yMax },
      lines1,
    } = props;

    return buildScalesDynamic({
      ...getSizes(state),
      xMin,
      xMax,
      yMax: yMax || 1,
      lines: lines1,
    });
  };

  const getScales2 = (props: TProps, state: TState) => {
    const {
      maxes2: { xMin, xMax, yMax },
      lines2,
    } = props;

    return buildScalesDynamic({
      ...getSizes(state),
      xMin,
      xMax,
      yMax: yMax || 1,
      lines: lines2,
    });
  };

  const updateStateFromVirtual = () => {
    setState(prevState => {
      const newState = { ...prevState, ...virtualState.current };
      virtualState.current = {};
      return newState as TState;
    });
  };

  const setStateVirtual = (nextState: Partial<TVirtualState>): void => {
    virtualState.current = _.updateI(
      [_.$merge(nextState)],
      virtualState.current
    );
  };

  const init1 = () => {
    const { lines1 } = props;
    let values1: Record<string, null> = {};
    _.forEachIndexed((o, index) => {
      values1[index] = null;
    }, lines1);

    const params = { values1, pointDate1: null };

    setState(params as any);
    setTimeout(() => {
      virtualState.current = params;
    }, 0);
  };

  const init2 = () => {
    const { lines2 } = props;
    let values2: Record<string, null> = {};
    _.forEachIndexed((o, index) => {
      values2[index] = null;
    }, lines2);

    const params = { values2, pointDate2: null };

    setState(params as any);
    setTimeout(() => {
      virtualState.current = params;
    }, 0);
  };

  const setZeroOpacity1 = (ref: React.RefObject<any>) => {
    ref.current.setNativeProps({
      style: {
        opacity: 0,
      },
    });
  };

  const setZeroOpacity2 = (ref: React.RefObject<any>) => {
    ref.current.setNativeProps({
      style: {
        opacity: 0,
      },
    });
  };

  const removeZeroOpacity1 = (ref: React.RefObject<any>) => {
    ref.current.setNativeProps({
      style: {
        opacity: 1,
      },
    });
  };

  const removeZeroOpacity2 = (ref: React.RefObject<any>) => {
    ref.current.setNativeProps({
      style: {
        opacity: 1,
      },
    });
  };

  const setZeroOpacityAll1 = () => {
    _.forEach(setZeroOpacity1 as any, cursors1);
    setZeroOpacity1(line1);
    setZeroOpacity1(bgLayer);
    init1();
  };

  const setZeroOpacityAll2 = () => {
    _.forEach(setZeroOpacity2 as any, cursors2);
    setZeroOpacity2(line2);
    setZeroOpacity2(bgLayer);
    init2();
  };

  const setLabel1 = (pageX: number, index: number) => {
    const { scalesQuant } = getScales1(props, state);
    const scale = scalesQuant[index];
    const {
      values1 = {},
      percents1 = {},
      extraTexts = {},
    } = virtualState.current;
    const valueRaw = (scale(pageX) || {}).y;
    const extraTextRaw = (scale(pageX) || {}).extraText;
    const value = formatStream(valueRaw);
    const oldValue = _.path([index], values1);

    if (oldValue !== value) {
      const { total1 } = props;
      const percent = getGraphPercentage(total1, valueRaw);
      setStateVirtual({
        values1: _.updateI(
          [index, _.isNotNil(valueRaw) ? _.$set(value) : _.$none],
          values1
        ),
        percents1: _.updateI(
          [index, _.isNotNil(valueRaw) ? _.$set(percent) : _.$none],
          percents1
        ),
        extraTexts: _.updateI(
          [index, _.isNotNil(extraTextRaw) ? _.$set(extraTextRaw) : _.$none],
          extraTexts
        ),
      });
    }
  };

  const setLabel2 = (pageX: number, index: number) => {
    const { scalesQuant } = getScales2(props, state);
    const scale = scalesQuant[index];
    const {
      values2 = {},
      percents2 = {},
      extraTexts = {},
    } = virtualState.current;
    const valueRaw = (scale(pageX) || {}).y;
    const extraTextRaw = (scale(pageX) || {}).extraText;
    const value = formatStream(valueRaw);
    const oldValue = _.path([index], values2);

    if (oldValue !== value) {
      const { total2 } = props;
      const percent = getGraphPercentage(total2, valueRaw);
      setStateVirtual({
        values2: _.updateI(
          [index, _.isNotNil(valueRaw) ? _.$set(value) : _.$none],
          values2
        ),
        percents2: _.updateI(
          [index, _.isNotNil(valueRaw) ? _.$set(percent) : _.$none],
          percents2
        ),
        extraTexts: _.updateI(
          [index, _.isNotNil(extraTextRaw) ? _.$set(extraTextRaw) : _.$none],
          extraTexts
        ),
      });
    }
  };

  const setLabelDate1 = (pageX: number) => {
    const { scalesQuant } = getScales1(props, state);
    const { lines1 } = props;
    const { pointDate1 } = virtualState.current;
    let match = null;

    for (let i = 0, { length } = lines1; i < length; i++) {
      const scale = scalesQuant[i];
      const target = scale(pageX);

      if (target) {
        match = formatDate(_.path('x', target));
      }

      if (match) {
        break;
      }
    }

    if (pointDate1 !== match) {
      setStateVirtual({ pointDate1: match });
    }
  };

  const setLabelDate2 = (pageX: number) => {
    const { scalesQuant } = getScales1(props, state);
    const { lines2 } = props;
    const { pointDate2 } = virtualState.current;
    let match = null;

    for (let i = 0, { length } = lines2; i < length; i++) {
      const scale = scalesQuant[i];
      const target = scale(pageX);

      if (target) {
        match = formatDate(_.path('x', target));
      }

      if (match) {
        break;
      }
    }

    if (pointDate2 !== match) {
      setStateVirtual({ pointDate2: match });
    }
  };

  const setDotPosition1 = (ref: React.RefObject<any>, target: TCoordinate) => {
    const { xAxisOffset, offsetTop } = getSizes(state);

    ref.current.setNativeProps({
      style: {
        transform: [
          { translateX: target.x - 12 / 2 - xAxisOffset },
          { translateY: target.y! - 12 / 2 + offsetTop + 1 },
        ],
        opacity: 1,
      },
    });
  };

  const setDotPosition2 = (ref: React.RefObject<any>, target: TCoordinate) => {
    const { xAxisOffset, offsetTop } = getSizes(state);

    ref.current.setNativeProps({
      style: {
        transform: [
          { translateX: target.x - 12 / 2 - xAxisOffset },
          { translateY: target.y! - 12 / 2 + offsetTop + 1 },
        ],
        opacity: 1,
      },
    });
  };

  const setLinePosition1 = (anyTarget: TCoordinate) => {
    const { xAxisOffset, offsetTop } = getSizes(state);

    if (anyTarget) {
      const targetX = anyTarget.x;
      //@ts-ignore
      line1.current.setNativeProps({
        style: {
          transform: [
            { translateX: targetX - 1 - xAxisOffset },
            { translateY: offsetTop - 24 },
          ],
          opacity: 1,
        },
      });
    }
  };

  const setLinePosition2 = (anyTarget: TCoordinate) => {
    const { xAxisOffset, offsetTop } = getSizes(state);

    if (anyTarget) {
      const targetX = anyTarget.x;
      //@ts-ignore
      line2.current.setNativeProps({
        style: {
          transform: [
            { translateX: targetX - 1 - xAxisOffset },
            { translateY: offsetTop - 24 },
          ],
          opacity: 1,
        },
      });
    }
  };

  const updateEntityIfExist1 = (
    entity: TCoordinates,
    target: TCoordinate,
    pageX: number,
    index: number
  ) => {
    if (entity) {
      if (target) {
        //@ts-ignore
        setDotPosition1(cursors1[index], target);
        setLabel1(pageX, index);
      } else {
        //@ts-ignore
        setZeroOpacity1(cursors1[index]);
        // @ts-ignore
        setLabel1('N/A', index);
      }
    }
  };

  const updateEntityIfExist2 = (
    entity: TCoordinates,
    target: TCoordinate,
    pageX: number,
    index: number
  ) => {
    if (entity) {
      if (target) {
        //@ts-ignore
        setDotPosition2(cursors2[index], target);
        setLabel2(pageX, index);
      } else {
        //@ts-ignore
        setZeroOpacity2(cursors2[index]);
        // @ts-ignore
        setLabel2('N/A', index);
      }
    }
  };

  const getPath1 = (data: TCoordinates): string => {
    const { xScaleMinus1, yScale } = getScales1(props, state);
    return buildPath({ xScale: xScaleMinus1, yScale, data });
  };

  const getPath2 = (data: TCoordinates): string => {
    const { xScaleMinus1, yScale } = getScales2(props, state);
    return buildPath({ xScale: xScaleMinus1, yScale, data });
  };

  const onInteraction1 = (evt: GestureResponderEvent) => {
    const { pageX } = evt.nativeEvent;
    const { scalesQuantRaw } = getScales1(props, state);
    const { lines1 } = props;
    const targets: TCoordinates = [];

    _.forEachIndexed<any, any>((line: TLine, index: number) => {
      const target = scalesQuantRaw[index](pageX);
      updateEntityIfExist1(line.dotted, target, pageX, index);
      targets.push(target);
    }, lines1);

    if (_.any(_.isNotNil, targets)) {
      setLinePosition1(_.find(_.isNotNil, targets)!);
      setLabelDate1(pageX);
      removeZeroOpacity1(bgLayer);
    } else if (!_.isNotNil(targets)) {
      setZeroOpacityAll1();
    }

    updateStateFromVirtual();
  };

  const onInteraction2 = (evt: GestureResponderEvent) => {
    const { pageX } = evt.nativeEvent;
    const { scalesQuantRaw } = getScales2(props, state);
    const { lines2 } = props;
    const targets: TCoordinates = [];

    _.forEachIndexed<any, any>((line: TLine, index: number) => {
      const target = scalesQuantRaw[index](pageX);
      updateEntityIfExist2(line.dotted, target, pageX, index);
      targets.push(target);
    }, lines2);

    if (_.any(_.isNotNil, targets)) {
      setLinePosition2(_.find(_.isNotNil, targets)!);
      setLabelDate2(pageX);
      removeZeroOpacity2(bgLayer);
    } else if (!_.isNotNil(targets)) {
      setZeroOpacityAll2();
    }

    updateStateFromVirtual();
  };

  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: _.T,
    onStartShouldSetPanResponderCapture: _.T,
    onMoveShouldSetPanResponder: _.T,
    onMoveShouldSetPanResponderCapture: _.T,
    onPanResponderGrant: e => {
      onInteraction1(e);
      onInteraction2(e);
    },
    onPanResponderMove: e => {
      onInteraction1(e);
      onInteraction2(e);
    },
    onPanResponderTerminationRequest: _.T,
    // onPanResponderRelease: () => {
    //   this.setZeroOpacityAll();
    // },
    onShouldBlockNativeResponder: _.T,
  });

  const {
    width,
    svgWidth,
    svgHeight,
    innerHeight,
    padding,
    xAxisOffset,
    offsetTop,
  } = getSizes(state);
  const {
    values1,
    values2,
    pointDate1,
    pointDate2,
    percents1,
    percents2,
  } = state;

  const {
    maxes1: { yMax: yMax1 },
    ticks1,
    lines1,
    maxes2: { yMax: yMax2 },
    ticks2,
    lines2,
    total1,
    total2,
  } = props;
  // lines1
  //@ts-ignore
  const title1 = _.path('title', lines1[0]);
  const { xScale: xScale1, yScale: yScale1 } = getScales1(props, state);
  const yTicks1 = [0, yMax1 / 2, yMax1];
  // const hideGraph1 = _.any(_.isNil)([yMax1, xMax1, xMin1]);
  const graphMax1 = yMax1 / GRAPH_PADDING_TOP_RATIO;
  // lines2
  //@ts-ignore
  const title2 = _.path('title', lines2[0]);
  const { xScale: xScale2, yScale: yScale2 } = getScales2(props, state);
  const yTicks2 = [0, yMax2 / 2, yMax2];
  // const hideGraph2 = _.any(_.isNil)([yMax2, xMax2, xMin2]);
  const graphMax2 = yMax2 / GRAPH_PADDING_TOP_RATIO;

  return (
    <SBox bg="fiord" position="relative">
      <SRow
        position="absolute"
        left={56}
        top={2}
        width={svgWidth - padding}
        justifyContent="space-between"
      >
        <SH6 color={theme.colors.periwinkleGray} lineHeight="15px">
          {title1}
        </SH6>
        <SH6 color={theme.colors.periwinkleGray} lineHeight="15px">
          {title2}
        </SH6>
      </SRow>

      {/*legend box*/}
      <SLegendContainer>
        {/*legend bg on tap*/}
        <SBox
          style={StyleSheet.absoluteFill}
          opacity={0}
          bg="ebonyClay"
          ref={bgLayer}
        />
        {/*legend date*/}
        <SBox height={24}>
          <SPointDate>
            {pointDate1 || pointDate2
              ? `${getWeekday(pointDate1! || pointDate2!)}, ${pointDate1 ||
                  pointDate2}:`
              : null}
          </SPointDate>
        </SBox>
        <SBox>
          {lines1.map((line, i) => {
            const { variant, sourceName, title } = line;
            const label = sourceName || title;
            const color = getLinePrimaryColorByVariant(variant);

            return label ? (
              <SBox key={variant}>
                <SRow justifyContent="space-between" mb={8}>
                  <SRow alignItems="center">
                    <SDot size={10} bg={color} mr={10} />
                    <SLegendText>{label}</SLegendText>
                  </SRow>
                  <SText testID="multiline-graph-value">
                    <STotal>{_.path(i, values1)}</STotal>
                    <SLegendText> {_.path(i, percents1)}</SLegendText>
                  </SText>
                </SRow>
                <SRow justifyContent="space-between" pl={20}>
                  <SLegendText color="periwinkleGray">
                    Total In Period
                  </SLegendText>
                  <STotal>{formatStream(total1)}</STotal>
                </SRow>
              </SBox>
            ) : null;
          })}
        </SBox>

        <SBox mt={25}>
          {lines2.map((line, i) => {
            const { variant, sourceName, title } = line;
            const label = sourceName || title;
            const color = getLinePrimaryColorByVariant(variant);

            return label ? (
              <SBox key={variant}>
                <SRow justifyContent="space-between" mb={9}>
                  <SRow alignItems="center">
                    <SDot size={10} bg={color} mr={10} />
                    <SLegendText>{label}</SLegendText>
                  </SRow>
                  <SText testID="multiline-graph-value">
                    <STotal>{_.path(i, values2)}</STotal>
                    <SLegendText> {_.path(i, percents2)}</SLegendText>
                  </SText>
                </SRow>
                <SRow justifyContent="space-between" pl={20}>
                  <SLegendText color="periwinkleGray">
                    Total In Period
                  </SLegendText>
                  <STotal>{formatStream(total2)}</STotal>
                </SRow>
              </SBox>
            ) : null;
          })}
        </SBox>
      </SLegendContainer>

      <XAxisLabels
        ticks={ticks1 || ticks2}
        scale={xScale1 || xScale2}
        yOffset={innerHeight + offsetTop}
        alignRight
        variant="large"
      />
      <YAxisLabels
        ticks={yTicks1}
        scale={yScale1}
        offsetLeft={padding}
        offsetTop={1}
        variant="big"
      />
      <YAxisLabels
        ticks={yTicks2}
        scale={yScale2}
        offsetLeft={svgWidth}
        alignRight
        offsetTop={1}
        variant="big"
      />
      {/*graph*/}
      <SBox pt={offsetTop}>
        <Svg height={svgHeight} width={svgWidth}>
          <>
            <XAxis
              ticks={ticks1 || ticks2}
              scale={xScale1}
              yOffset={innerHeight}
            />
            <YAxis
              ticks={yMax1 === 0 ? yTicks2 : yTicks1}
              scale={yMax1 === 0 ? yScale2 : yScale1}
              xOffsetRight={width - padding}
              xOffset={padding}
            />

            {lines1.map(line => {
              const { dotted, variant } = line;

              return dotted ? (
                <G transform="translate(0, 0)" key={variant}>
                  <Line
                    // testID="multiline-graph-line"
                    graphMax={graphMax1}
                    variant={variant}
                    data={dotted}
                    path={getPath1(dotted)}
                  />
                </G>
              ) : null;
            }, lines1)}

            {lines2.map(line => {
              const { dotted, variant } = line;

              return dotted ? (
                <G transform="translate(0, 0)" key={variant}>
                  <Line
                    // testID="multiline-graph-line"
                    graphMax={graphMax2}
                    variant={variant}
                    data={dotted}
                    path={getPath2(dotted)}
                  />
                </G>
              ) : null;
            }, lines2)}
          </>
        </Svg>
      </SBox>

      <SBox
        style={StyleSheet.absoluteFill}
        ml={xAxisOffset - 1}
        mr={padding - 1}
        {...panResponder.panHandlers}
        // Pan responder doesn't work with views without color on android
        // https://stackoverflow.com/a/51833007/6190198
        bg="transparent"
      >
        {lines1.map((value, index) => {
          const { variant } = lines1[index];
          //@ts-ignore
          if (!cursors1[index]) {
            //@ts-ignore
            cursors1[index] = createRef();
          }

          const color = getLinePrimaryColorByVariant(variant);

          return (
            <SLineDot
              testID="multiline-graph-line-dots"
              //@ts-ignore
              ref={cursors1[index]}
              bg={color}
              key={variant}
            />
          );
        }, lines1)}

        {lines2.map((value, index) => {
          const { variant } = lines2[index];

          //@ts-ignore
          if (!cursors2[index]) {
            //@ts-ignore
            cursors2[index] = createRef();
          }

          const color = getLinePrimaryColorByVariant(variant);

          return (
            <SLineDot
              testID="multiline-graph-line-dots"
              //@ts-ignore
              ref={cursors2[index]}
              bg={color}
              key={variant}
            />
          );
        }, lines2)}

        <SBox pt={0} mt={2}>
          <SHoverLine ref={line1} height={innerHeight - 27} />
          <SHoverLine ref={line2} height={innerHeight - 27} />
        </SBox>
      </SBox>
    </SBox>
  );
};
