/**
 * Geographic scale utilities for Web Mercator (EPSG:3857) projection.
 *
 * Provides accurate meters ↔ pixels conversions, tile bounds,
 * and lat/lon → tile pixel coordinate transforms.
 */

const EARTH_CIRCUMFERENCE_M = 40075017;

/**
 * Calculate pixels per meter at given latitude and zoom level.
 * Uses the Web Mercator distortion factor: cos(lat).
 */
export function getPixelsPerMeter(lat: number, zoom: number): number {
  const tileSize = 256;
  const numTiles = Math.pow(2, zoom);
  const worldWidthPixels = tileSize * numTiles;
  const metersPerPixelEquator = EARTH_CIRCUMFERENCE_M / worldWidthPixels;
  const latRad = (lat * Math.PI) / 180;
  const metersPerPixel = metersPerPixelEquator * Math.cos(latRad);
  return 1 / metersPerPixel;
}

/**
 * Convert geographic distance (meters) to pixel distance at given zoom/latitude.
 */
export function metersToPixels(meters: number, lat: number, zoom: number): number {
  return meters * getPixelsPerMeter(lat, zoom);
}

/**
 * Get tile geographic bounds (lat/lon) for given tile coordinates.
 * Returns [[latMin, lonMin], [latMax, lonMax]].
 */
export function getTileBounds(
  x: number,
  y: number,
  zoom: number
): [[number, number], [number, number]] {
  const n = Math.pow(2, zoom);

  const lonMin = (x / n) * 360 - 180;
  const lonMax = ((x + 1) / n) * 360 - 180;

  const latMin =
    (Math.atan(Math.sinh(Math.PI * (1 - (2 * (y + 1)) / n))) * 180) / Math.PI;
  const latMax =
    (Math.atan(Math.sinh(Math.PI * (1 - (2 * y) / n))) * 180) / Math.PI;

  return [
    [latMin, lonMin],
    [latMax, lonMax],
  ];
}

/**
 * Convert lat/lon to pixel position within a specific tile.
 * Returns [pixelX, pixelY] relative to tile's top-left corner.
 */
export function latLonToTilePixel(
  lat: number,
  lon: number,
  tileX: number,
  tileY: number,
  zoom: number
): [number, number] {
  const n = Math.pow(2, zoom);

  // Point's world-tile coordinates
  const worldX = ((lon + 180) / 360) * n;
  const latRad = (lat * Math.PI) / 180;
  const worldY =
    ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) *
    n;

  // Offset within this tile (in pixels)
  const pixelX = (worldX - tileX) * 256;
  const pixelY = (worldY - tileY) * 256;

  return [pixelX, pixelY];
}
