export type LngLatTuple = [number, number];

const EARTH_RADIUS_METERS = 6371008.8;
const SAME_COORDINATE_EPSILON = 0.0000001;

export function getDistanceMeters(from: LngLatTuple, to: LngLatTuple) {
  const fromLatitude = toRadians(from[1]);
  const toLatitude = toRadians(to[1]);
  const deltaLatitude = toRadians(to[1] - from[1]);
  const deltaLongitude = toRadians(to[0] - from[0]);
  const a =
    Math.sin(deltaLatitude / 2) ** 2 +
    Math.cos(fromLatitude) * Math.cos(toLatitude) * Math.sin(deltaLongitude / 2) ** 2;

  return 2 * EARTH_RADIUS_METERS * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

export function getLineDistanceMeters(coordinates: LngLatTuple[]) {
  return coordinates.reduce((total, coordinate, index) => {
    if (index === 0) {
      return total;
    }

    return total + getDistanceMeters(coordinates[index - 1], coordinate);
  }, 0);
}

export function getLastSegmentDistanceMeters(coordinates: LngLatTuple[]) {
  if (coordinates.length < 2) {
    return 0;
  }

  return getDistanceMeters(coordinates[coordinates.length - 2], coordinates[coordinates.length - 1]);
}

export function getPolygonAreaSquareMeters(coordinates: LngLatTuple[]) {
  if (coordinates.length < 3) {
    return 0;
  }

  const closedCoordinates = [...coordinates, coordinates[0]];
  const area = closedCoordinates.slice(0, -1).reduce((total, coordinate, index) => {
    const nextCoordinate = closedCoordinates[index + 1];
    return (
      total +
      toRadians(nextCoordinate[0] - coordinate[0]) *
        (2 + Math.sin(toRadians(coordinate[1])) + Math.sin(toRadians(nextCoordinate[1])))
    );
  }, 0);

  return Math.abs((area * EARTH_RADIUS_METERS * EARTH_RADIUS_METERS) / 2);
}

export function createCircleCoordinates(center: LngLatTuple, radiusMeters: number, steps = 48) {
  const angularDistance = radiusMeters / EARTH_RADIUS_METERS;
  const centerLongitude = toRadians(center[0]);
  const centerLatitude = toRadians(center[1]);
  const coordinates: LngLatTuple[] = [];

  for (let index = 0; index <= steps; index += 1) {
    const bearing = (2 * Math.PI * index) / steps;
    const latitude = Math.asin(
      Math.sin(centerLatitude) * Math.cos(angularDistance) +
        Math.cos(centerLatitude) * Math.sin(angularDistance) * Math.cos(bearing)
    );
    const longitude =
      centerLongitude +
      Math.atan2(
        Math.sin(bearing) * Math.sin(angularDistance) * Math.cos(centerLatitude),
        Math.cos(angularDistance) - Math.sin(centerLatitude) * Math.sin(latitude)
      );

    coordinates.push([toDegrees(longitude), toDegrees(latitude)]);
  }

  return coordinates;
}

export function isSameCoordinate(previous: LngLatTuple | undefined, next: LngLatTuple) {
  if (!previous) {
    return false;
  }

  return (
    Math.abs(previous[0] - next[0]) < SAME_COORDINATE_EPSILON &&
    Math.abs(previous[1] - next[1]) < SAME_COORDINATE_EPSILON
  );
}

export function toRadians(value: number) {
  return (value * Math.PI) / 180;
}

function toDegrees(value: number) {
  return (value * 180) / Math.PI;
}
