import React from 'react';
import {
  MAP_SIMULATION_REFERENCE_TYPE,
  SIMULATION_COMMAND_TYPE,
} from './CommandTypes';
import {
  GEOJSON_PATH_TYPE,
  SIMULATION_OPTIONS_TYPE,
  SIMULATION_STATUS_TYPE,
} from './JibestreamTypes';

export type PathSimulatorProps = {
  updateUserLocation: (loc: number[], heading: number) => void;
  onSimStatusChanged?: (status: SIMULATION_STATUS_TYPE) => void;
};

export type PathSimulatorState = {
  simStatus: SIMULATION_STATUS_TYPE;
  simOptions?: SIMULATION_OPTIONS_TYPE;
  path?: GEOJSON_PATH_TYPE;
  timer?: NodeJS.Timer;
};

export type PathSimulatorRef = {
  startPathSimulation: (
    path: GEOJSON_PATH_TYPE,
    options: SIMULATION_OPTIONS_TYPE
  ) => void;
  controlSimulation: (
    command: SIMULATION_COMMAND_TYPE
  ) => SIMULATION_STATUS_TYPE;
  getSimulationStatus: () => SIMULATION_STATUS_TYPE;
};

export class PathSimulator
  extends React.Component<PathSimulatorProps, PathSimulatorState>
  implements MAP_SIMULATION_REFERENCE_TYPE
{
  updateUserLocation: (loc: number[], heading: number) => void;
  onSimStatusChanged?: (status: SIMULATION_STATUS_TYPE) => void;

  public onLocationChange?: (loc: number[]) => void;
  public onStatusChange?: (status: SIMULATION_STATUS_TYPE) => void;

  constructor(props: PathSimulatorProps) {
    super(props);
    this.state = {
      simStatus: {},
      simOptions: undefined,
      path: undefined,
      timer: undefined,
    };
    this.updateUserLocation = props.updateUserLocation;
    this.onSimStatusChanged = props.onSimStatusChanged;
    this.updatePathLocation = this.updatePathLocation.bind(this);
  }

  startPathSimulation(
    path: GEOJSON_PATH_TYPE,
    options: SIMULATION_OPTIONS_TYPE
  ) {
    this.setState((prevState) => {
      if (prevState.timer) clearInterval(prevState.timer);
      return {
        ...prevState,
        simOptions: options,
        path: path,
        simStatus: {
          status: 'ACTIVE',
          details: {
            timeRemaining: path.distance / options.movementSpeed,
            currLoc: this.getPoint(path.steps[0]!),
            currRndmLoc: this.randomizeLocation(
              this.getPoint(path.steps[0]!),
              options.variance_m
            ),
            pathLeg: 0,
          },
        },
        timer: setInterval(this.updatePathLocation, options.updateRate * 1000),
      };
    });
    this.updatePathLocation();
  }
  controlSimulation(command: SIMULATION_COMMAND_TYPE): SIMULATION_STATUS_TYPE {
    switch (command) {
      case 'PLAY':
        this.setState((prevState) => {
          const updatesStatus: SIMULATION_STATUS_TYPE = {
            ...prevState.simStatus,
            status: 'ACTIVE',
          };
          this.onSimStatusChanged?.(updatesStatus);
          return {
            ...prevState,
            simStatus: updatesStatus,
          };
        });
        break;
      case 'PAUSE':
        this.setState((prevState) => {
          const updatesStatus: SIMULATION_STATUS_TYPE = {
            ...prevState.simStatus,
            status: 'PAUSED',
          };
          this.onSimStatusChanged?.(updatesStatus);
          return {
            ...prevState,
            simStatus: updatesStatus,
          };
        });
        break;
      case 'STOP':
        this.setState((prevState) => {
          const updatesStatus: SIMULATION_STATUS_TYPE = {};
          this.onSimStatusChanged?.(updatesStatus);
          return {
            ...prevState,
            simStatus: updatesStatus,
            simOptions: undefined,
            path: undefined,
          };
        });
        this.setState((prevState) => ({
          ...prevState,
          simStatus: {},
          simOptions: undefined,
          path: undefined,
        }));
        break;
    }
    return this.state.simStatus;
  }

  getSimulationStatus(): SIMULATION_STATUS_TYPE {
    return this.state.simStatus;
  }

  getPoint(p: { lat: number; lon: number }) {
    return [p.lat, p.lon];
  }

  updatePathLocation() {
    this.setState((prevState) => {
      const updatedState = {
        ...prevState,
        simStatus: {
          ...prevState.simStatus,
          details: { ...prevState.simStatus.details! },
        },
      };
      if (
        updatedState.simStatus?.status == 'ACTIVE' &&
        updatedState.simOptions &&
        updatedState.path &&
        updatedState.simStatus.details
      ) {
        const currLocation = updatedState.simStatus.details?.currLoc!;
        const currSegment = updatedState.simStatus.details?.pathLeg!;
        const speed = updatedState.simOptions.movementSpeed;
        const time = updatedState.simOptions.updateRate;
        const distance = speed * time;

        const { loc, leg } = this.moveDistanceAlongPath(
          updatedState.path,
          currLocation,
          currSegment,
          distance
        );
        updatedState.simStatus.details.currLoc = loc;
        updatedState.simStatus.details.pathLeg = leg;

        if (leg == updatedState.path.steps.length) {
          return {
            ...prevState,
            simStatus: {},
            simOptions: undefined,
            path: undefined,
          };
        }
        updatedState.simStatus.details.currRndmLoc = this.randomizeLocation(
          loc,
          updatedState.simOptions.variance_m
        );
        const bearing = this.getBearingBetweenPoints(
          prevState.simStatus.details?.currLoc!,
          updatedState.simStatus.details?.currLoc!
        );
        this.updateUserLocation(
          updatedState.simStatus.details?.currRndmLoc!,
          bearing
        );
        this.onSimStatusChanged?.({ status: 'ACTIVE' });
      } else if (updatedState.simStatus?.status == 'PAUSED') {
        this.onSimStatusChanged?.({ status: 'PAUSED' });
        return updatedState;
      } else {
        this.onSimStatusChanged?.({ status: undefined });
        return updatedState;
      }

      return updatedState;
    });
  }

  // -------------------------- UTILITY METHODS --------------------------
  moveDistanceAlongPath(
    path: GEOJSON_PATH_TYPE,
    startLocation: number[],
    startSegment: number,
    distance: number
  ): { loc: number[]; leg: number } {
    let start = startLocation;
    for (let i = startSegment; i < path.steps.length - 1; i++) {
      const end = this.getPoint(path.steps[i + 1]!);
      const { endLoc, remainingDist } = this.getPointDistanceAlongPath(
        start,
        end,
        distance
      );
      if (endLoc) {
        return { loc: endLoc, leg: i };
      } else {
        if (i === path.steps.length - 1) {
          return { loc: end, leg: i };
        }
        distance = remainingDist;
        start = end;
      }
    }
    // Should never get here, but just to make the compiler happy...
    return {
      loc: this.getPoint(path.steps[path.steps.length - 1]!),
      leg: path.steps.length,
    };
  }

  getRandomNumber(start: number, end: number) {
    return Math.random() * (end - start) + start;
  }

  // get random location within specified distance of specified location
  randomizeLocation(loc: number[], radius: number): number[] {
    const bearing = this.getRandomNumber(0, 360);
    const distance = this.getRandomNumber(0, radius);

    return this.getDestinationPoint(loc, bearing, distance);
  }

  // project at specified distance between two lat/long points
  getPointDistanceAlongPath(
    start: number[],
    end: number[],
    distance: number
  ): { endLoc?: number[]; remainingDist: number } {
    const d = this.getDistanceBetweenPoints(start, end);
    if (distance > d) {
      return { endLoc: undefined, remainingDist: distance - d };
    } else {
      const bearing = this.getBearingBetweenPoints(start, end);
      const endLoc = this.getDestinationPoint(start, bearing, distance);
      return { endLoc, remainingDist: 0 };
    }
  }

  // get lat/long point at specified distance and bearing from start point
  getDestinationPoint(start: number[], bearing: number, distance: number) {
    const lat1 = this.deg2rad(start[0]!);
    const lon1 = this.deg2rad(start[1]!);
    const R = 6371; // Radius of the earth in km
    const d = distance / 1000; // Distance in km
    const brng = this.deg2rad(bearing);
    const lat2 = Math.asin(
      Math.sin(lat1) * Math.cos(d / R) +
        Math.cos(lat1) * Math.sin(d / R) * Math.cos(brng)
    );
    const lon2 =
      lon1 +
      Math.atan2(
        Math.sin(brng) * Math.sin(d / R) * Math.cos(lat1),
        Math.cos(d / R) - Math.sin(lat1) * Math.sin(lat2)
      );

    return [this.rad2deg(lat2), this.rad2deg(lon2)];
  }

  // get bearing between two lat/long points
  getBearingBetweenPoints(start: number[], end: number[]) {
    const lat1 = this.deg2rad(start[0]!);
    const lon1 = this.deg2rad(start[1]!);
    const lat2 = this.deg2rad(end[0]!);
    const lon2 = this.deg2rad(end[1]!);
    const dLon = lon2 - lon1;
    const y = Math.sin(dLon) * Math.cos(lat2);
    const x =
      Math.cos(lat1) * Math.sin(lat2) -
      Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
    const brng = Math.atan2(y, x);
    return this.rad2deg(brng);
  }

  // get distance between two lat/long points, in meters
  getDistanceBetweenPoints(start: number[], end: number[]) {
    const lat1 = this.deg2rad(start[0]!);
    const lon1 = this.deg2rad(start[1]!);
    const lat2 = this.deg2rad(end[0]!);
    const lon2 = this.deg2rad(end[1]!);
    const R = 6371; // Radius of the earth in km
    const dLat = lat2 - lat1; // deg2rad below
    const dLon = lon2 - lon1;
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c * 1000; // Distance in meters
  }

  deg2rad(deg: number) {
    return deg * (Math.PI / 180);
  }

  rad2deg(rad: number) {
    return rad * (180 / Math.PI);
  }

  render() {
    return <></>;
  }
}
