/**
 * Spatial utility functions for geographic calculations.
 */

/**
 * Ray-casting algorithm to determine if a point is inside a polygon.
 * @param lat Point latitude
 * @param lng Point longitude
 * @param polygonCoords Array of [lng, lat] coordinate pairs (GeoJSON order)
 */
export function isPointInPolygon(lat: number, lng: number, polygonCoords: number[][]): boolean {
  let inside = false;
  for (let i = 0, j = polygonCoords.length - 1; i < polygonCoords.length; j = i++) {
    const xi = polygonCoords[i]![1]!; // lat
    const yi = polygonCoords[i]![0]!; // lng
    const xj = polygonCoords[j]![1]!;
    const yj = polygonCoords[j]![0]!;

    const intersect = ((yi > lng) !== (yj > lng)) &&
      (lat < (xj - xi) * (lng - yi) / (yj - yi) + xi);
    if (intersect) inside = !inside;
  }
  return inside;
}

/**
 * Parse GeoJSON string and extract coordinate arrays for all polygons.
 * Supports Polygon and MultiPolygon types.
 * Returns array of coordinate rings (outer rings only).
 */
export function parseGeoJsonPolygon(geojsonString: string): number[][][] {
  const geojson = JSON.parse(geojsonString) as {
    type: string;
    coordinates: number[][][] | number[][][][];
  };

  if (geojson.type === 'Polygon') {
    // Polygon coordinates: [ring][point][lng,lat]
    return [geojson.coordinates[0] as number[][]];
  }

  if (geojson.type === 'MultiPolygon') {
    // MultiPolygon coordinates: [polygon][ring][point][lng,lat]
    return (geojson.coordinates as number[][][][]).map((poly) => poly[0]!);
  }

  throw new Error(`Unsupported GeoJSON type: ${geojson.type}`);
}

/**
 * Calculate bounding box from an array of [lng, lat] coordinate pairs.
 * Returns { minLat, maxLat, minLng, maxLng }.
 */
export function calculateBounds(coordinates: number[][]): {
  minLat: number;
  maxLat: number;
  minLng: number;
  maxLng: number;
} {
  let minLat = Infinity;
  let maxLat = -Infinity;
  let minLng = Infinity;
  let maxLng = -Infinity;

  for (const coord of coordinates) {
    const lng = coord[0]!;
    const lat = coord[1]!;
    if (lat < minLat) minLat = lat;
    if (lat > maxLat) maxLat = lat;
    if (lng < minLng) minLng = lng;
    if (lng > maxLng) maxLng = lng;
  }

  return { minLat, maxLat, minLng, maxLng };
}

/**
 * Haversine distance between two lat/lng points in meters.
 */
export function haversineDistance(
  lat1: number, lng1: number,
  lat2: number, lng2: number,
): number {
  const R = 6371000; // Earth radius in meters
  const toRad = (deg: number) => (deg * Math.PI) / 180;

  const dLat = toRad(lat2 - lat1);
  const dLng = toRad(lng2 - lng1);
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
    Math.sin(dLng / 2) * Math.sin(dLng / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  return R * c;
}

/**
 * Calculate the centroid of an array of [lng, lat] coordinate pairs.
 * Returns { lat, lng }.
 */
/**
 * Calculate a bounding box from a map center point and zoom level.
 * Uses Web Mercator tile math to derive the geographic extent visible
 * on a map of the given pixel dimensions at the given zoom.
 * Default viewport: 1024x768 pixels.
 */
export function boundsFromCenterZoom(
  lat: number,
  lng: number,
  zoom: number,
  widthPx: number = 1024,
  heightPx: number = 768,
): { minLat: number; maxLat: number; minLng: number; maxLng: number } {
  // Meters per pixel at a given zoom level at the equator
  const metersPerPixelAtEquator = 156543.03392;
  const metersPerPixel = metersPerPixelAtEquator * Math.cos((lat * Math.PI) / 180) / Math.pow(2, zoom);

  const halfWidthMeters = (widthPx / 2) * metersPerPixel;
  const halfHeightMeters = (heightPx / 2) * metersPerPixel;

  // Convert meters offset to degrees
  const latDegPerMeter = 1 / 111320;
  const lngDegPerMeter = 1 / (111320 * Math.cos((lat * Math.PI) / 180));

  const latOffset = halfHeightMeters * latDegPerMeter;
  const lngOffset = halfWidthMeters * lngDegPerMeter;

  return {
    minLat: lat - latOffset,
    maxLat: lat + latOffset,
    minLng: lng - lngOffset,
    maxLng: lng + lngOffset,
  };
}

export function calculateCentroid(coordinates: number[][]): { lat: number; lng: number } {
  let sumLat = 0;
  let sumLng = 0;
  for (const coord of coordinates) {
    sumLng += coord[0]!;
    sumLat += coord[1]!;
  }
  return {
    lat: sumLat / coordinates.length,
    lng: sumLng / coordinates.length,
  };
}
