import React, {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import type { ForwardedRef } from 'react';
import { withFastCompare } from '../../utils';
import type { RouteInfo } from '../utils/types';
import { AnimatedPolyline, Polyline } from './polyline.component';
import type {
  AnimatedPolylineRef,
  AnimatedPolylineAnimationProp,
} from './polyline.component';
import type { Point } from '../../bare';

type RouteProps = {
  route: RouteInfo;
};

export const Route = withFastCompare((props: RouteProps) => {
  return (
    <React.Fragment>
      {props.route.sections.map((section, i) => (
        <Polyline
          points={section.points}
          key={section.routeIndex + ' ' + i}
          strokeColor={'black'}
          strokeWidth={3}
        />
      ))}
    </React.Fragment>
  );
});

export type AnimatedRouteRef = {
  setRoute: (route: RouteInfo) => void;
  animate: (
    section: number,
    point: number,
    duration?: number,
    onNextAnimationTick?: (point: Point, duration: number) => Promise<void>,
    abortRef?: React.RefObject<boolean>
  ) => void;
  reset: () => void;
};

export const AnimatedRoute = withFastCompare(
  forwardRef(
    (
      props: RouteProps & {
        animation?: AnimatedPolylineAnimationProp;
        commonColor?: string;
        commonWidth?: number;
      },
      ref: ForwardedRef<AnimatedRouteRef>
    ) => {
      const [route, setRoute] = useState(props.route);
      const polylines = useRef<Array<AnimatedPolylineRef | null>>([]);
      const _currentSection = useRef(0);
      const _currentPoint = useRef(0);

      useEffect(() => {
        setRoute(props.route);
        _currentPoint.current = 0;
        _currentSection.current = 0;
      }, [props.route]);

      const reset = useCallback(() => {
        polylines.current.forEach((l) => l?.reset());
        _currentPoint.current = 0;
        _currentSection.current = 0;
      }, []);

      const animate = useCallback(
        async (
          section: number,
          point: number,
          _duration?: number,
          onNextAnimationTick?: (
            point: Point,
            duration: number
          ) => Promise<void>,
          abortRef?: React.RefObject<boolean>
        ) => {
          //console.log(section, point, duration);
          const duration = _duration ?? 100;

          // Check if animatable
          if (
            route.sections.length - 1 < section ||
            route.sections[section]!.points.length - 1 < point ||
            _currentSection.current > section ||
            (_currentPoint.current > point &&
              _currentSection.current === section)
          ) {
            return;
          }

          // Get sections that we need to manipulate
          const sectionRange = [
            ...Array(section - _currentSection.current + 1).keys(),
          ].map((s) => s + _currentSection.current);

          let steps = 0;

          // If animation in same section
          if (_currentSection.current === section) {
            steps = point - _currentPoint.current;
          } else {
            // current section points left
            steps +=
              route.sections[_currentSection.current]!.points.length -
              1 -
              _currentPoint.current;

            // cause section switch
            steps += section - _currentSection.current + 1;

            // for all sections between start and end
            for (let i = _currentSection.current + 1; i < section; ++i) {
              steps += route.sections[i]!.points.length;
            }

            steps += point;
          }

          // If same point
          if (!steps) {
            return;
          }

          const FRAME_TIME = 16;

          const pointDuration = (duration - FRAME_TIME * steps) / steps;

          // If we have less than 16 ms (1 frame time) for 1 step, we
          // need to optimize animation to remove glitches
          if (pointDuration < 0) {
            steps = sectionRange.length;
            const optimizedPointDuration =
              (duration - FRAME_TIME * steps) / steps;

            for await (const sectionId of sectionRange) {
              if (abortRef?.current) {
                return;
              }

              _currentSection.current = sectionId;
              _currentPoint.current =
                route.sections[sectionId]!.points.length - 1;

              onNextAnimationTick?.(
                route.sections[sectionId]!.points[_currentPoint.current]!,
                optimizedPointDuration
              );

              await polylines.current[sectionId]?.animate(
                _currentPoint.current,
                optimizedPointDuration,
                true
              );
            }
          } else {
            for await (const sectionId of sectionRange) {
              if (abortRef?.current) {
                return;
              }

              _currentSection.current = sectionId;
              for await (const point of route.sections[sectionId]!.points) {
                if (abortRef?.current) {
                  return;
                }

                onNextAnimationTick?.(point, pointDuration);

                const pointId =
                  route.sections[sectionId]!.points.indexOf(point);

                _currentPoint.current = pointId;
                await polylines.current[sectionId]?.animate(
                  pointId,
                  pointDuration
                );
              }
            }
          }
        },
        [route]
      );

      useImperativeHandle(ref, () => ({
        setRoute,
        reset,
        animate,
      }));

      return (
        <React.Fragment>
          {route.sections.map((section: any, i: number) => (
            <AnimatedPolyline
              ref={(ref: AnimatedPolylineRef) => (polylines.current[i] = ref)}
              sectionId={i}
              points={section.points}
              key={section.routeIndex + '' + i}
              strokeColor={props.commonColor ?? section.strokeColor ?? 'black'}
              strokeWidth={props.commonWidth ?? section.strokeWidth ?? 3}
              animation={props.animation}
            />
          ))}
        </React.Fragment>
      );
    }
  )
);
