// Vendored from leaflet-polylineoffset@1.1.1 (MIT).
//
// Why this lives in-tree instead of being an npm dep:
//   • The published package would require --legacy-peer-deps because of an
//     unrelated React-17 peer-dep conflict elsewhere in the project, and we
//     don't want a renderer plugin to force a global resolver flag.
//   • It's frozen upstream (no meaningful updates since 2020), tiny, and
//     has zero runtime deps besides leaflet (already in package.json).
//
// What it does:
//   Monkey-patches L.Polyline so that any path passed with a numeric
//   `offset` in pathOptions is rendered shifted perpendicular to its
//   direction of travel by that many pixels (positive = right of travel,
//   negative = left). Used by Dispatch.js's Compare → Combined view to
//   render planned + actual as parallel rails when they share the same
//   road geometry; without this they overlap and read as one polyline.
//
// Import once for the side effect:
//   import '../../../utils/leafletPolylineOffset';
//
// Then add to any pathOptions:
//   pathOptions={{ ..., offset: 5 }}
//
// Plays nicely with both SVG and Canvas renderers.

import L from 'leaflet';

L.PolylineOffset = {
  translatePoint(pt, dist, radians) {
    return L.point(pt.x + dist * Math.cos(radians), pt.y + dist * Math.sin(radians));
  },

  offsetPointLine(points, distance) {
    const l = points.length;
    if (l < 2) {
      throw new Error('Line should be defined by at least 2 points');
    }
    let a = points[0];
    let b;
    const offsetAngle = Math.PI / 2;
    const offsetSegments = [];

    for (let i = 1; i < l; i++) {
      b = points[i];
      // Each segment's offset angle is perpendicular to its direction.
      const segAngle = Math.atan2(b.y - a.y, b.x - a.x);
      offsetSegments.push({
        offsetAngle: segAngle - offsetAngle,
        original: [a, b],
        offset: [
          this.translatePoint(a, distance, segAngle - offsetAngle),
          this.translatePoint(b, distance, segAngle - offsetAngle)
        ]
      });
      a = b;
    }

    return offsetSegments;
  },

  // Find the intersection of two segments by extending them to infinity
  // along their direction, then walking along segment 1 by parameter t.
  // Returns null when the segments are parallel (no intersection).
  intersection(l1a, l1b, l2a, l2b) {
    const line1 = this.segmentAsVector(l1a, l1b);
    const line2 = this.segmentAsVector(l2a, l2b);

    const denom = -line2.x * line1.y + line1.x * line2.y;
    if (denom === 0) return null;

    const s = (-line1.y * (l1a.x - l2a.x) + line1.x * (l1a.y - l2a.y)) / denom;
    const t = (line2.x * (l1a.y - l2a.y) - line2.y * (l1a.x - l2a.x)) / denom;

    if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
      return L.point(l1a.x + t * line1.x, l1a.y + t * line1.y);
    }
    return null;
  },

  segmentAsVector(a, b) {
    return L.point(b.x - a.x, b.y - a.y);
  },

  // Walk the offset segments and join adjacent ones at their intersection
  // points (mitered corners). When two consecutive segments don't intersect
  // within their bounds (sharp turn, or co-linear), fall back to the offset
  // endpoint so the polyline doesn't gap.
  joinLineSegments(segments) {
    const joined = [];
    let last = segments[0].offset;
    joined.push(last[0]);

    for (let i = 1; i < segments.length; i++) {
      const next = segments[i].offset;
      const inter = this.intersection(last[0], last[1], next[0], next[1]);
      if (inter) {
        joined.push(inter);
      } else {
        joined.push(last[1]);
      }
      last = next;
    }
    joined.push(last[1]);
    return joined;
  },

  offsetPoints(points, offset) {
    if (!points || points.length < 2) return points;
    const offsets = this.offsetPointLine(points, offset);
    return this.joinLineSegments(offsets);
  },

  // Operates on a ring of LatLngs by projecting → offsetting → unprojecting,
  // since leaflet polyline math is in screen pixels but our points are LatLng.
  offsetLatLngs(map, latlngs, offset) {
    const points = latlngs.map((ll) => map.latLngToLayerPoint(ll));
    const offsetPts = this.offsetPoints(points, offset);
    return offsetPts.map((p) => map.layerPointToLatLng(p));
  }
};

// Patch Polyline._projectLatlngs (used by both SVG and Canvas renderers) so
// that when an offset is set, the projected ring is offset before clipping.
// We keep the original on _projectLatlngsOriginal so we can call through.
const originalProject = L.Polyline.prototype._projectLatlngs;

L.Polyline.prototype._projectLatlngs = function patchedProject(latlngs, result, projectedBounds) {
  const offset = this.options.offset;
  if (!offset || typeof offset !== 'number') {
    return originalProject.call(this, latlngs, result, projectedBounds);
  }

  // Recurse for multi-ring polylines (shouldn't happen for simple lines,
  // but the leaflet API allows it).
  const flat = latlngs[0] instanceof L.LatLng;
  if (!flat) {
    for (let i = 0; i < latlngs.length; i++) {
      this._projectLatlngs(latlngs[i], result, projectedBounds);
    }
    return undefined;
  }

  const projected = latlngs.map((ll) => this._map.latLngToLayerPoint(ll));
  const offsetted = L.PolylineOffset.offsetPoints(projected, offset);
  // Update projectedBounds with each offset point so the renderer's
  // viewport-clipping check still works.
  for (let i = 0; i < offsetted.length; i++) {
    projectedBounds.extend(offsetted[i]);
  }
  result.push(offsetted);
  return undefined;
};

export default L;
