const FEET_PER_METER = 3.280839895;
const METERS_PER_FOOT = 1 / FEET_PER_METER;
const EARTH_METERS_PER_DEGREE_LAT = 111320;

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

function rotateLocalToTrueNorthEast(northMeters, eastMeters, assumedNorthClockwiseDegrees = 0) {
  const clockwiseRadians = toRadians(assumedNorthClockwiseDegrees);
  const trueNorthMeters =
    northMeters * Math.cos(clockwiseRadians) - eastMeters * Math.sin(clockwiseRadians);
  const trueEastMeters =
    northMeters * Math.sin(clockwiseRadians) + eastMeters * Math.cos(clockwiseRadians);

  return {
    northMeters: trueNorthMeters,
    eastMeters: trueEastMeters,
  };
}

export function point(lat, lon) {
  return { lat, lon };
}

export function lngLat(lon, lat) {
  return point(lat, lon);
}

export function anchor(lon, lat, options = {}) {
  return {
    ...lngLat(lon, lat),
    ...options,
  };
}

export function pointsFromLngLat(coordinates) {
  return coordinates.map(([lon, lat]) => lngLat(lon, lat));
}

export function ft(value) {
  return value * METERS_PER_FOOT;
}

function isPoint(value) {
  return Boolean(value) && typeof value.lat === "number" && typeof value.lon === "number";
}

function isMove(value) {
  return (
    Boolean(value) &&
    typeof value.northMeters === "number" &&
    typeof value.eastMeters === "number"
  );
}

function isOffsetAnchor(value) {
  return Boolean(value) && value.kind === "offset-anchor" && isPoint(value.origin);
}

function translate(origin, northMeters, eastMeters) {
  const latDelta = northMeters / EARTH_METERS_PER_DEGREE_LAT;
  const lonDelta = eastMeters / (EARTH_METERS_PER_DEGREE_LAT * Math.cos(toRadians(origin.lat)));

  return point(origin.lat + latDelta, origin.lon + lonDelta);
}

export function offset(origin, ...moves) {
  if (moves.length === 0) {
    return { kind: "offset-anchor", origin };
  }

  const net = moves.reduce(
    (accumulator, move) => ({
      northMeters: accumulator.northMeters + move.northMeters,
      eastMeters: accumulator.eastMeters + move.eastMeters,
    }),
    { northMeters: 0, eastMeters: 0 },
  );

  const adjusted = rotateLocalToTrueNorthEast(
    net.northMeters,
    net.eastMeters,
    origin.assumedNorthClockwiseDegrees ?? 0,
  );

  return translate(origin, adjusted.northMeters, adjusted.eastMeters);
}

export function north(distanceMeters) {
  return { northMeters: distanceMeters, eastMeters: 0 };
}

export function south(distanceMeters) {
  return { northMeters: -distanceMeters, eastMeters: 0 };
}

export function east(distanceMeters) {
  return { northMeters: 0, eastMeters: distanceMeters };
}

export function west(distanceMeters) {
  return { northMeters: 0, eastMeters: -distanceMeters };
}

function buildFeature(type, geometryType, style = {}) {
  const coordinates = [];

  return {
    add(...values) {
      coordinates.push(resolveCoordinate(values));
      return this;
    },
    toGeoJSON() {
      if (geometryType === "Point") {
        const [first] = coordinates;
        return {
          type: "Feature",
          properties: { type, style },
          geometry: {
            type: "Point",
            coordinates: [first.lon, first.lat],
          },
        };
      }

      const ring = coordinates.map((coordinate) => [coordinate.lon, coordinate.lat]);
      const geometry =
        geometryType === "LineString"
          ? { type: "LineString", coordinates: ring }
          : { type: "Polygon", coordinates: [closeRing(ring)] };

      return {
        type: "Feature",
        properties: { type, style },
        geometry,
      };
    },
  };
}

function resolveCoordinate(values) {
  if (values.length === 1 && isPoint(values[0])) {
    return values[0];
  }

  const [first, ...rest] = values;
  if (isOffsetAnchor(first) && rest.every(isMove)) {
    return offset(first.origin, ...rest);
  }

  throw new Error("add(...) expects a point, or offset(origin) followed by directional moves");
}

function closeRing(coordinates) {
  if (coordinates.length === 0) {
    return coordinates;
  }

  const [firstLon, firstLat] = coordinates[0];
  const [lastLon, lastLat] = coordinates[coordinates.length - 1];
  if (firstLon === lastLon && firstLat === lastLat) {
    return coordinates;
  }

  return [...coordinates, coordinates[0]];
}

export function polygon(type, style) {
  return buildFeature(type, "Polygon", style);
}

export function path(type, style) {
  return buildFeature(type, "LineString", style);
}

export function marker(type, style) {
  return buildFeature(type, "Point", style);
}

export function sprinkler(name) {
  return marker(name, {
    color: "#2d7dd2",
    radius: 5,
    fillColor: "#88bdf2",
    fillOpacity: 0.95,
  });
}

export function tree(name) {
  return marker(name, {
    color: "#2f6b3d",
    radius: 9,
    fillColor: "#5c9e64",
    fillOpacity: 0.85,
  });
}

export function collectGeoJSON(...collections) {
  return {
    type: "FeatureCollection",
    features: collections.flat().map((entry) => entry.toGeoJSON()),
  };
}