(() => {
  /**
   * Runtime hooks for Directional / Planner settings / I/O map zones (MiR §4.2.6.5, .10, .11).
   */
  const TYPES = {
    directional: "directional",
    directional_line: "directional_line",
    planner: "planner",
    io: "io",
  };

  const ADVANCED_TYPES = new Set([
    TYPES.directional,
    TYPES.directional_line,
    TYPES.planner,
    TYPES.io,
  ]);

  const DIRECTION_DEGREES = [0, 45, 90, 135, 180, 225, 270, 315];

  const DEFAULT_PLANNER = {
    no_localization: false,
    look_ahead: false,
    path_deviation: 0.5,
    path_timeout: 30,
    ignore_obstacles: false,
  };

  const DEFAULT_IO = {
    io_module: "",
    plc_register: null,
    plc_value: null,
    plc_mode: "set",
  };

  function pointInPolygon(px, py, points) {
    return window.MapPlannerZones?.pointInPolygon(px, py, points) || false;
  }

  function normalizeDirectionDeg(value) {
    const n = Number(value);
    if (!Number.isFinite(n)) return 0;
    let deg = ((Math.round(n / 45) * 45) % 360 + 360) % 360;
    if (!DIRECTION_DEGREES.includes(deg)) {
      deg = DIRECTION_DEGREES.reduce((best, d) =>
        Math.abs(d - deg) < Math.abs(best - deg) ? d : best,
      );
    }
    return deg;
  }

  function clampPathDeviation(value) {
    const n = Number(value);
    if (!Number.isFinite(n)) return DEFAULT_PLANNER.path_deviation;
    return Math.min(3, Math.max(0, n));
  }

  function clampPathTimeout(value) {
    const n = Number(value);
    if (!Number.isFinite(n) || n < 0) return DEFAULT_PLANNER.path_timeout;
    return n;
  }

  function normalizePlannerSettings(raw = {}) {
    return {
      no_localization: !!raw.no_localization,
      look_ahead: !!raw.look_ahead,
      path_deviation: clampPathDeviation(raw.path_deviation),
      path_timeout: clampPathTimeout(raw.path_timeout),
      ignore_obstacles: !!raw.ignore_obstacles,
    };
  }

  function normalizeIoSettings(raw = {}) {
    const mode = raw.plc_mode;
    return {
      io_module: typeof raw.io_module === "string" ? raw.io_module : "",
      plc_register:
        raw.plc_register == null || raw.plc_register === ""
          ? null
          : Number(raw.plc_register),
      plc_value:
        raw.plc_value == null || raw.plc_value === "" ? null : Number(raw.plc_value),
      plc_mode: mode === "add" || mode === "subtract" ? mode : "set",
    };
  }

  function isAdvancedZoneType(type) {
    return ADVANCED_TYPES.has(type);
  }

  function isDirectionalType(type) {
    return type === TYPES.directional || type === TYPES.directional_line;
  }

  function filterAdvancedZones(zones) {
    return (Array.isArray(zones) ? zones : []).filter((z) => isAdvancedZoneType(z?.type));
  }

  function pointNearPolyline(px, py, points, halfWidth = 8) {
    if (!points?.length || points.length < 2) return false;
    for (let i = 0; i < points.length - 1; i++) {
      const p1 = points[i];
      const p2 = points[i + 1];
      const dx = p2.x - p1.x;
      const dy = p2.y - p1.y;
      const lenSq = dx * dx + dy * dy;
      let dist;
      if (lenSq === 0) dist = Math.hypot(px - p1.x, py - p1.y);
      else {
        let t = ((px - p1.x) * dx + (py - p1.y) * dy) / lenSq;
        t = Math.max(0, Math.min(1, t));
        dist = Math.hypot(px - (p1.x + t * dx), py - (p1.y + t * dy));
      }
      if (dist <= halfWidth) return true;
    }
    return false;
  }

  function zonesAtPoint(zones, px, py, mapMeta = null) {
    const list = Array.isArray(zones) ? zones : [];
    const hits = [];
    for (let i = list.length - 1; i >= 0; i--) {
      const z = list[i];
      if (!isAdvancedZoneType(z?.type) || !z.points?.length) continue;
      if (z.type === TYPES.directional_line) {
        const width = window.MapObjects?.zoneLineWidthPx(z, mapMeta) ?? 8;
        if (pointNearPolyline(px, py, z.points, width / 2 + 4)) hits.push(z);
        continue;
      }
      if (pointInPolygon(px, py, z.points)) hits.push(z);
    }
    return hits;
  }

  function lineDirectionDeg(zone) {
    if (!zone?.points?.length || zone.points.length < 2) return 0;
    const p0 = zone.points[0];
    const p1 = zone.points[zone.points.length - 1];
    let deg = (Math.atan2(-(p1.y - p0.y), p1.x - p0.x) * 180) / Math.PI;
    if (zone.reversed) deg = (deg + 180) % 360;
    return normalizeDirectionDeg(deg);
  }

  function zoneDirectionDeg(zone) {
    if (!zone) return 0;
    if (zone.type === TYPES.directional_line) return lineDirectionDeg(zone);
    if (zone.type === TYPES.directional) return normalizeDirectionDeg(zone.direction_deg);
    return 0;
  }

  /** Heading radians (map image coords) allowed if dot >= 0 with zone arrow. */
  function isHeadingAllowed(zone, headingRad) {
    const arrowRad = (-zoneDirectionDeg(zone) * Math.PI) / 180;
    const hx = Math.cos(headingRad);
    const hy = Math.sin(headingRad);
    const ax = Math.cos(arrowRad);
    const ay = Math.sin(arrowRad);
    return hx * ax + hy * ay >= 0;
  }

  function getDirectionalConstraint(zones, px, py, headingRad) {
    const hits = zonesAtPoint(zones, px, py).filter(isDirectionalType);
    const zone = hits[0];
    if (!zone) return null;
    return {
      zone,
      direction_deg: zoneDirectionDeg(zone),
      allowed: isHeadingAllowed(zone, headingRad),
    };
  }

  function getPlannerSettings(zones, px, py) {
    const zone = zonesAtPoint(zones, px, py).find((z) => z.type === TYPES.planner);
    if (!zone) return null;
    return {
      zone,
      settings: normalizePlannerSettings(zone),
    };
  }

  function getIoActivation(zones, px, py) {
    const zone = zonesAtPoint(zones, px, py).find((z) => z.type === TYPES.io);
    if (!zone) return null;
    return {
      zone,
      settings: normalizeIoSettings(zone),
    };
  }

  function classifyPoint(zones, px, py, headingRad = null) {
    const hits = zonesAtPoint(zones, px, py);
    const directional = hits.find(isDirectionalType);
    const planner = hits.find((z) => z.type === TYPES.planner);
    const io = hits.find((z) => z.type === TYPES.io);
    const out = {
      directional: directional || null,
      direction_deg: directional ? zoneDirectionDeg(directional) : null,
      planner: planner || null,
      planner_settings: planner ? normalizePlannerSettings(planner) : null,
      io: io || null,
      io_settings: io ? normalizeIoSettings(io) : null,
      zones: hits,
    };
    if (directional && headingRad != null) {
      out.heading_allowed = isHeadingAllowed(directional, headingRad);
    }
    return out;
  }

  window.MapAdvancedZones = {
    TYPES,
    ADVANCED_TYPES,
    DIRECTION_DEGREES,
    DEFAULT_PLANNER,
    DEFAULT_IO,
    normalizeDirectionDeg,
    normalizePlannerSettings,
    normalizeIoSettings,
    clampPathDeviation,
    clampPathTimeout,
    isAdvancedZoneType,
    isDirectionalType,
    filterAdvancedZones,
    zonesAtPoint,
    zoneDirectionDeg,
    isHeadingAllowed,
    getDirectionalConstraint,
    getPlannerSettings,
    getIoActivation,
    classifyPoint,
  };
})();
