<template>
  <!-- map container -->
  <div
    id="map"
    style="height: 100vh; background-color: transparent; position: relative"
  ></div>

  <!-- fullscreen video shown after 10 s of inactivity -->
  <div
    id="video-overlay"
    v-show="videoOverlayVisible"
    style="
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      z-index: 10000;
      background: #000;
      pointer-events: none;
    "
  ></div>
</template>

<script>
import * as PIXI from "pixi.js";
import { Viewport } from "pixi-viewport";
import { Text, TextStyle, FillGradient, Assets } from "pixi.js";
import { API_URL, GEO_URL } from "../config";
import sightIcon from "../icons/sight.svg";
import tramRight from "../icons/tram-right.svg";
import tramLeft from "../icons/tram-left.svg";
import tramTopRight from "../icons/tram-top-right.svg";
import tramBottomRight from "../icons/tram-bottom-right.svg";
import tramTopLeft from "../icons/tram-top-left.svg";
import tramBottomLeft from "../icons/tram-bottom-left.svg";
const arrowTransforms = {
  left: [-1, 0],
  right: [1, 0],
  "bottom-left": [1, 1],
  "top-left": [1, -1],
  "bottom-right": [-1, 1],
  "top-right": [-1, -1],
};

export default {
  name: "MainMap",
  data() {
    return {
      routeId: null,
      pixiApp: null,
      viewport: null,
      routeGraphics: null,
      videoOverlayVisible: false,
      lastActivityTime: Date.now(),
      inactivityInterval: null,
      lastVideoPreviewId: null,
      stationContainer: null,
      sightsContainer: null,
      toCanvasPoint: null,
      routePollTimer: null,
      attemptCount: 0,
      routeGroup: null,
      routeLatlngs: [],
      sights: [],
      tramPollTimer: null,
      passedGraphics: null,
      remainingGraphics: null,
      tramGraphic: null,
      tramSprite: null,
      tramContainer: null,
      tramArrow: null,
      currentTramDirection: "right",
      manualTramDirection: null,
      manualDirectionUntil: 0,
      inactivityTimer: null,
      isFollowingTram: false,
      followTramTimer: null,
      chooseTramDirection(canvasPt) {
        // canvasPt: [x, y] in PIXI coords
        let bestDir = "right";
        let bestScore = Infinity;
        const candidates = arrowTransforms;

        Object.entries(candidates).forEach(([dir, vec]) => {
          const len = Math.hypot(vec[0], vec[1]);
          const unit = [vec[0] / len, vec[1] / len];
          let score = 0;
          [30, 60, 90].forEach((d) => {
            const probeX = canvasPt[0] + unit[0] * d;
            const probeY = canvasPt[1] + unit[1] * d;
            // penalize proximity to stations
            this.stationContainer.children.forEach((node) => {
              const dx = probeX - node.x;
              const dy = probeY - node.y;
              if (Math.hypot(dx, dy) < 25) score += 3;
            });
            // penalize proximity to sights
            this.sightsContainer.children.forEach((node) => {
              const dx = probeX - node.x;
              const dy = probeY - node.y;
              if (Math.hypot(dx, dy) < 25) score += 3;
            });
            // penalize proximity to route segments
            const pts = this.routeLatlngs.map((pt) => this.toCanvasPoint(pt));
            pts.forEach((rpt) => {
              const dx = probeX - rpt.x;
              const dy = probeY - rpt.y;
              if (Math.hypot(dx, dy) < 20) score += 1;
            });
          });
          if (score < bestScore) {
            bestScore = score;
            bestDir = dir;
          }
        });
        return bestDir;
      },
      routeRotate: 0,
      routeCentered: false,
      viewportAdjusted: false,
      clampApplied: false,
      stationZoomed: false,
      centerLatitude: null,
      centerLongitude: null,
    };
  },
  async mounted() {
    await this.createPixiApp();
    await this.fetchContext();
    this.startRoutePolling();
    this.startInactivityWatcher();
    this.setupActivityListeners();
    this.startInactivityCheck();
  },

  methods: {
    setupActivityListeners() {
      ["mousemove", "mousedown", "keydown", "touchstart", "scroll"].forEach(
        (evt) => window.addEventListener(evt, this.resetInactivity)
      );
    },
    resetInactivity() {
      this.lastActivityTime = Date.now();
      if (this.videoOverlayVisible) this.hideVideoOverlay();
      clearTimeout(this.inactivityTimer);
      // stop following on any user activity
      this.isFollowingTram = false;
      this.stopFollowTram();
      // restart inactivity timer on any user activity
      this.resetInactivityTimer();
    },
    startInactivityCheck() {
      if (this.inactivityInterval) return;
      this.inactivityInterval = setInterval(() => {
        if (Date.now() - this.lastActivityTime >= 120000) {
          this.showNearestSightVideo();
        }
      }, 1000);
    },
    stopInactivityCheck() {
      if (this.inactivityInterval) {
        clearInterval(this.inactivityInterval);
        this.inactivityInterval = null;
      }
    },
    async showNearestSightVideo() {
      console.log(
        "[videoOverlay] Attempting to show nearest sight video at",
        new Date().toISOString()
      );
      if (!this.sights || !this.sights.length) {
        console.log("[videoOverlay] No sights available to select video.");
        return;
      }
      // select a sight with a video_preview, fallback to first
      const sight = this.sights.find((s) => s.video_preview) || this.sights[0];
      const mediaId = sight.video_preview;
      console.log("[videoOverlay] Selected mediaId:", mediaId);
      if (!mediaId) {
        console.log("[videoOverlay] No mediaId found on selected sight.");
        return;
      }
      if (this.lastVideoPreviewId === mediaId) {
        console.log(
          "[videoOverlay] mediaId same as last. Showing existing overlay."
        );
        this.videoOverlayVisible = true;
        return;
      }
      const src = `${API_URL}/media/${mediaId}/download`;
      this.replaceVideo(src, mediaId);
    },
    replaceVideo(src, previewId) {
      console.log(
        "[videoOverlay] replaceVideo called with src:",
        src,
        "previewId:",
        previewId
      );
      const overlay = document.getElementById("video-overlay");
      if (!overlay) return;
      const vid = document.createElement("video");
      vid.src = src;
      vid.autoplay = true;
      vid.muted = true;
      vid.loop = true;
      vid.playsInline = true;
      vid.style.cssText = "width:100%;height:100%;object-fit:cover;";
      vid.addEventListener("canplay", () => {
        [...overlay.children].forEach((child) => {
          if (child !== vid) overlay.removeChild(child);
        });
        this.videoOverlayVisible = true;
        this.lastVideoPreviewId = previewId;
      });
      overlay.appendChild(vid);
    },
    hideVideoOverlay() {
      this.videoOverlayVisible = false;
      const overlay = document.getElementById("video-overlay");
      if (overlay) overlay.innerHTML = "";
      this.lastVideoPreviewId = null;
    },
    async createPixiApp() {
      const container = document.getElementById("map");
      if (!container || this.pixiApp) return;

      // PixiJS v8: create then init
      this.pixiApp = new PIXI.Application();
      await this.pixiApp.init({
        resizeTo: container,
        antialias: true,
        backgroundAlpha: 0,
      });

      // add canvas to DOM (missing after last refactor)
      container.appendChild(this.pixiApp.canvas);
      await Assets.load(sightIcon);
      await Assets.load([
        tramRight,
        tramLeft,
        tramTopRight,
        tramBottomRight,
        tramTopLeft,
        tramBottomLeft,
      ]);

      // гарантируем, что канвас всегда принимает pointer‑/touch‑события
      this.pixiApp.canvas.style.pointerEvents = "auto";
      this.pixiApp.canvas.style.touchAction = "none";
      this.pixiApp.canvas.style.zIndex = "1";

      // убедимся, что video‑overlay действительно выше канваса
      const overlay = document.getElementById("video-overlay");
      if (overlay) overlay.style.zIndex = "10000";

      // create interactive viewport (pixi v8: use "events")
      this.viewport = new Viewport({
        screenWidth: container.clientWidth,
        screenHeight: container.clientHeight,
        events: this.pixiApp.renderer.events, // EventSystem
      });

      this.routeGroup = new PIXI.Container();
      // this.routeGroup.scale.set(2);
      this.viewport.addChild(this.routeGroup);

      // enable typical interactions (restored .decelerate())
      this.viewport.drag().pinch().wheel().decelerate();
      // если к этому моменту уже известны ограничения – сразу применим
      this.updateViewportClampZoom();

      // add viewport to stage
      this.pixiApp.stage.addChild(this.viewport);

      this.routeGraphics = new PIXI.Graphics();
      this.routeGroup.addChild(this.routeGraphics);
      this.passedGraphics = new PIXI.Graphics();
      this.routeGroup.addChild(this.passedGraphics);
      this.remainingGraphics = new PIXI.Graphics();
      this.routeGroup.addChild(this.remainingGraphics);

      this.stationContainer = new PIXI.Container();
      this.routeGroup.addChild(this.stationContainer);
      this.sightsContainer = new PIXI.Container();
      this.routeGroup.addChild(this.sightsContainer);
    },
    // Применяем ограничения масштабирования к viewport
    updateViewportClampZoom() {
      if (
        this.viewport &&
        this.routeScaleMin !== null &&
        this.routeScaleMax !== null
      ) {
        const minScale = this.routeScaleMin / 30;
        const maxScale = this.routeScaleMax / 20;
        // убедимся, что min < max
        if (minScale < maxScale) {
          this.viewport.clampZoom({ minScale, maxScale });
          // apply initial zoom at minimum scale
          this.viewport.setZoom(minScale);
          console.log(
            `%cViewport clampZoom → min=${minScale}, max=${maxScale}`,
            "color: violet;"
          );
        }
      }
    },

    // Рендерим линию маршрута по массиву координат [lat, lng]
    drawRoute(latlngs) {
      if (!Array.isArray(latlngs) || latlngs.length < 2) return;
      // если graphics ещё не готов – запомним и перерисуем, когда он появится
      if (!this.routeGraphics) {
        // пробуем снова через animation frame
        requestAnimationFrame(() => this.drawRoute(latlngs));
        return;
      }

      // очищаем графику и задаём стиль обводки
      this.routeGraphics.clear();
      this.routeGraphics.setStrokeStyle({
        width: 10,
        color: 0xffffff,
        alpha: 1,
      });
      this.routeGraphics.beginPath();

      // Bounding box для нормализации
      const lats = latlngs.map((p) => p[0]);
      const lngs = latlngs.map((p) => p[1]);
      const minLat = Math.min(...lats);
      const maxLat = Math.max(...lats);
      const minLng = Math.min(...lngs);
      const padding = 40;

      const scale = 35000;
      const midLat = (minLat + maxLat) / 2;
      const latFactor = Math.cos((midLat * Math.PI) / 180);
      console.log(
        `Using fixed scale ${scale} and latFactor ${latFactor.toFixed(4)}`
      );

      const toPoint = ([lat, lng]) => {
        const x = (lng - minLng) * scale * latFactor + padding;
        const y = (maxLat - lat) * scale + padding;
        return { x, y };
      };

      // сохраняем функцию трансформации для станций
      this.toCanvasPoint = toPoint;

      const first = toPoint(latlngs[0]);
      this.routeGraphics.moveTo(first.x, first.y);
      for (let i = 1; i < latlngs.length; i++) {
        const p = toPoint(latlngs[i]);
        this.routeGraphics.lineTo(p.x, p.y);
      }
      this.routeGraphics.stroke();
    },

    // Отрисовываем станции: PIXI-кружок + подпись (RU+EN)
    drawStations(stationsRu, stationsEn) {
      console.groupCollapsed(
        "%cdrawStations()",
        "color: dodgerblue; font-weight: bold;"
      );

      // Базовые проверки
      if (!Array.isArray(stationsRu) || !stationsRu.length) {
        console.warn("stationsRu пуст или не массив → ничего не рисуем");
        console.groupEnd();
        return;
      }
      if (!this.toCanvasPoint) {
        console.warn("toCanvasPoint ещё не готов → ждём drawRoute()");
        console.groupEnd();
        return;
      }

      // RU->EN словарь
      const enById = {};
      if (Array.isArray(stationsEn)) {
        stationsEn.forEach((st) => (enById[st.id] = st.name));
      } else {
        console.warn("stationsEn не массив → EN названия пропущены");
      }

      // Очищаем слой
      if (this.stationContainer) this.stationContainer.removeChildren();
      else {
        console.warn("stationContainer ещё не создан");
        console.groupEnd();
        return;
      }

      let drawn = 0;
      stationsRu.forEach((st, idx) => {
        // Проверяем координаты
        if (
          typeof st.latitude !== "number" ||
          typeof st.longitude !== "number"
        ) {
          console.warn(`station[${idx}] (${st.id}) без координат → пропущен`);
          return;
        }

        const { x, y } = this.toCanvasPoint([st.latitude, st.longitude]);
        const ox = (st.offset_x || 0) / 1.25; // смещение по X (½)
        const oy = st.offset_y || 0; // смещение по Y (½)
        // const oy = 1;
        if (isNaN(x) || isNaN(y)) {
          console.warn(`station[${idx}] (${st.id}) координаты NaN → пропущен`);
          return;
        }

        /* 1. Кружок PIXI */
        const node = new PIXI.Container();
        node.x = x;
        node.y = y;
        this.stationContainer.addChild(node);

        const g = new PIXI.Graphics();
        // initially white fill for all stations
        g.fill(0xffffff)
          .stroke({ width: 5, color: 0x000000 })
          .circle(0, 0, 11)
          .fill()
          .stroke();
        node.addChild(g);
        // store station coordinates and graphic for dynamic recoloring
        node._stationLatlng = [st.latitude, st.longitude];
        node._circleGraphic = g;

        const solidWhite = new FillGradient({
          type: "linear",
          colorStops: [
            { offset: 0, color: 0xffffff },
            { offset: 1, color: 0xffffff },
          ],
        });

        const lightGray = new FillGradient({
          type: "linear",
          colorStops: [
            { offset: 0, color: "#CBCBCB" },
            { offset: 1, color: "#CBCBCB" },
          ],
        });

        const labelGroup = new PIXI.Container();
        labelGroup.x = ox;
        labelGroup.y = oy;
        // сохраняем исходные оффсеты, чтобы пересчитывать при повороте
        labelGroup._ox = ox;
        labelGroup._oy = oy;
        node.addChild(labelGroup);

        const stationPoint = { x, y };
        const thresholdX = 15;
        const thresholdY = 30;
        let shiftX = 0,
          shiftY = 0;
        if (this.routeLatlngs && this.routeLatlngs.length > 1) {
          for (let i = 0; i < this.routeLatlngs.length - 1; i++) {
            const p1 = this.toCanvasPoint(this.routeLatlngs[i]);
            const p2 = this.toCanvasPoint(this.routeLatlngs[i + 1]);
            const vx = p2.x - p1.x,
              vy = p2.y - p1.y;
            const len2 = vx * vx + vy * vy;
            const t =
              ((stationPoint.x - p1.x) * vx + (stationPoint.y - p1.y) * vy) /
              (len2 || 1);
            const tClamped = Math.max(0, Math.min(1, t));
            const projX = p1.x + vx * tClamped;
            const projY = p1.y + vy * tClamped;
            const dx = stationPoint.x - projX,
              dy = stationPoint.y - projY;
            // elliptical distance check
            const normDist =
              (dx * dx) / (thresholdX * thresholdX) +
              (dy * dy) / (thresholdY * thresholdY);
            if (normDist < 1) {
              // compute normal to segment
              let nx = -vy,
                ny = vx;
              const nlen = Math.hypot(nx, ny) || 1;
              nx /= nlen;
              ny /= nlen;
              // choose direction based on original offset
              const dot = ox * nx + oy * ny;
              const sign = dot >= 0 ? 1 : -1;
              shiftX = nx * thresholdX * sign;
              shiftY = ny * thresholdY * sign;
              break;
            }
          }
        }
        // apply shift
        labelGroup.x += shiftX;
        labelGroup.y += shiftY;
        labelGroup._ox += shiftX;
        labelGroup._oy += shiftY;

        // RU
        const ruStyle = new TextStyle({
          fill: solidWhite,
          fontSize: 20,
          fontFamily: "Arial",
          align: "right",
          fontWeight: "bold",
        });
        const ruText = new Text({ text: st.name, style: ruStyle });
        ruText.anchor.set(1, 0.5); // bottom-right
        ruText.x = 24;
        ruText.y = -35;
        labelGroup.addChild(ruText);

        // EN
        const enName = enById[st.id];
        if (enName) {
          const enStyle = new TextStyle({
            fill: lightGray,
            fontSize: 12,
            fontFamily: "Arial",
            align: "right",
            fontWeight: "bold",
          });
          const enText = new Text({ text: enName, style: enStyle });
          enText.anchor.set(1, -1); // top-right
          enText.x = 24;
          enText.y = ruText.y + 2;
          labelGroup.addChild(enText);
        }

        drawn++;
      });

      console.log(`✅ Отрисовано станций: ${drawn}/${stationsRu.length}`);
      console.groupEnd();

      // гарантируем, что подписи горизонтальны
      this.updateLabelOrientation();

      if (!this.stationZoomed && this.stationContainer.children.length) {
        this.autoFitViewport();
        this.stationZoomed = true;
      }
    },

    // Поддерживаем горизонтальную ориентацию подписей
    updateLabelOrientation() {
      if (!this.stationContainer || !this.routeGroup) return;

      // угол для компенсации
      const angle = -this.routeGroup.rotation;

      this.stationContainer.children.forEach((node) => {
        node.children.forEach((child) => {
          // Graphics‑объект (кружок) оставляем без изменений
          if (child instanceof PIXI.Text) {
            // просто выравниваем текст
            child.rotation = angle;
          } else if (child instanceof PIXI.Container) {
            child.rotation = angle;

            if (child._ox !== undefined && child._oy !== undefined) {
              const cosA = Math.cos(angle);
              const sinA = Math.sin(angle);
              child.x = child._ox * cosA + child._oy * sinA;
              child.y = -child._ox * sinA + child._oy * cosA;
            }
          }
        });
      });
      if (this.sightsContainer) {
        this.sightsContainer.children.forEach((node) => {
          node.rotation = -this.routeGroup.rotation;
        });
      }
    },

    autoFitViewport() {
      if (this.viewportAdjusted || !this.viewport || !this.routeGroup) return;

      // Глобальные габариты с учётом поворота/скейла
      const b = this.routeGroup.getBounds();

      const sw = this.viewport.screenWidth;
      const sh = this.viewport.screenHeight;

      let scale = Math.min(sw / b.width, sh / b.height);

      let target;
      if (this.tramContainer) {
        // глобальная позиция контейнера с трамваем
        const worldPos = this.tramContainer.getGlobalPosition();
        target = worldPos;
      } else if (this.centerLatitude != null && this.centerLongitude != null) {
        const local = this.toCanvasPoint([
          this.centerLatitude,
          this.centerLongitude,
        ]);
        target = this.routeGroup.toGlobal(new PIXI.Point(local.x, local.y));
      } else {
        target = new PIXI.Point(b.x + b.width / 2, b.y + b.height / 2);
      }
      this.viewport.animate({
        time: 600,
        scale: scale,
        position: { x: target.x, y: target.y },
        easing: "easeInOutSine",
      });
      this.viewportAdjusted = true;
    },
    applyPanClamp() {
      return;
    },
    // ───────────────────────── polling helpers ─────────────────────────
    startRoutePolling() {
      if (this.routePollTimer) return; // already polling
      this.routePollTimer = setInterval(() => {
        console.debug("route poll → fetchContext() + fetchRoute()");
        this.fetchContext().finally(() => this.fetchRoute());
      }, 3000);
    },
    stopRoutePolling() {
      if (this.routePollTimer) {
        clearInterval(this.routePollTimer);
        this.routePollTimer = null;
      }
    },

    // Получаем контекст геолокации, чтобы определить актуальный routeId
    async fetchContext() {
      try {
        const res = await fetch(`${GEO_URL}/v1/geolocation/context`);
        const data = await res.json();

        const ctxRouteId =
          data?.routeId ?? data?.routeProgress?.routeId ?? null;

        if (ctxRouteId) {
          // если routeId изменился – обновим, иначе оставим прежний
          const changed = ctxRouteId !== this.routeId;
          this.routeId = ctxRouteId;

          if (changed) {
            this.fetchRoute();
          } else if (!changed && !this.routeGraphics) {
            // если маршрут ещё не нарисован (первый рендер), всё равно подтянем данные
            this.fetchRoute();
          }
        } else {
          console.warn("fetchContext: routeId не получен из контекста");
        }
      } catch (err) {
        console.error("Ошибка при получении контекста геолокации:", err);
      }
    },

    // Получаем все данные маршрута и выводим их в консоль
    async fetchRoute() {
      if (!this.routeId) return;
      if (this.routePollTimer) this.attemptCount += 1;
      else this.attemptCount = 0;
      this.attemptCount += 1;
      console.log(
        `%cFetchRoute attempt #${this.attemptCount} (routeId=${this.routeId})`,
        "color: lightgreen;font-weight:bold;"
      );
      if (!this.routeId) {
        console.warn("fetchRoute: routeId ещё не известен. Ждём контекст…");
        return;
      }

      try {
        let gotPath = false;
        const [route, stationsRu, stationsEn, sights] = await Promise.all([
          fetch(`${API_URL}/route/${this.routeId}`).then((r) => r.json()),
          fetch(`${API_URL}/route/${this.routeId}/station`).then((r) =>
            r.json()
          ),
          fetch(`${API_URL}/route/${this.routeId}/station?lang=en`).then((r) =>
            r.json()
          ),
          fetch(`${API_URL}/route/${this.routeId}/sight`).then((r) => r.json()),
        ]);
        this.sights = sights;

        console.log(
          `%cFetchRoute attempt #${this.attemptCount}`,
          "color: lightgreen; font-weight: bold;"
        );
        console.log("  Route →", route);
        console.log("  Stations (ru) →", stationsRu.length);
        console.log("  Stations (en) →", stationsEn.length);
        console.log("  Sights →", sights.length);
        if (typeof route.scale_min === "number")
          this.routeScaleMin = route.scale_min;
        if (typeof route.scale_max === "number")
          this.routeScaleMax = route.scale_max;
        if (typeof route.center_latitude === "number")
          this.centerLatitude = route.center_latitude;
        if (typeof route.center_longitude === "number")
          this.centerLongitude = route.center_longitude;
        // обновим ограничения масштабирования в viewport
        this.updateViewportClampZoom();

        if (
          route?.path &&
          Array.isArray(route.path) &&
          route.path.length >= 2
        ) {
          this.drawRoute(route.path);
          // --- Insert tram progress logic here ---
          this.routeLatlngs = route.path;
          if (!this.tramPollTimer) {
            this.tramPollTimer = setInterval(
              () => this.updateTramPosition(),
              500
            );
          }
          // apply rotation before rendering stations and sights
          if (typeof route.rotate === "number") {
            this.routeGroup.rotation = (route.rotate * Math.PI) / 180;
          }
          // draw stations and keep labels horizontal
          this.drawStations(stationsRu, stationsEn);
          this.updateLabelOrientation();
          // draw sights (icons rotate correctly with routeGroup)
          this.drawSights(sights);
          // compensate rotation for sight icons
          this.updateLabelOrientation();
          // сначала обновляем позицию трамвая, чтобы был this.tramContainer
          this.updateTramPosition();
          // затем корректно центрируем и зумим вид
          this.autoFitViewport();
          gotPath = true;
        } else {
          console.warn(
            "fetchRoute: path отсутствует или слишком короткий. Повторяем запрос…"
          );
        }
        if (gotPath) {
          this.stopRoutePolling();
          this.attemptCount = 0;
        } else {
          this.startRoutePolling();
        }
      } catch (err) {
        console.error("Ошибка при получении данных маршрута:", err);
        this.startRoutePolling();
        if (this.attemptCount > 1e6) this.attemptCount = 0;
      }
    },
    setTramDirectionOverride(direction, durationMs) {
      this.manualTramDirection = direction;
      this.manualDirectionUntil = Date.now() + durationMs;
    },
    // ── Update tram position and color route based on progress ──
    async updateTramPosition() {
      if (!this.routeLatlngs || !this.routeLatlngs.length) return;
      try {
        const res = await fetch(`${GEO_URL}/v1/geolocation/context`);
        const data = await res.json();
        const percentage = data.routeProgress?.percentageCompleted;
        if (percentage == null) return;
        const total = this.routeLatlngs.length;
        const passedCount = Math.round(percentage * (total - 1));
        const passed = this.routeLatlngs.slice(0, passedCount + 1);
        const remaining = this.routeLatlngs.slice(passedCount);

        // draw passed segment in red
        this.passedGraphics.clear();
        this.passedGraphics.setStrokeStyle({
          width: 10,
          color: 0xed1c24,
          alignment: 0.5,
          alpha: 1,
        });
        this.passedGraphics.beginPath();
        passed.forEach((pt, i) => {
          const { x, y } = this.toCanvasPoint(pt);
          if (i === 0) this.passedGraphics.moveTo(x, y);
          else this.passedGraphics.lineTo(x, y);
        });
        this.passedGraphics.stroke();

        // draw remaining segment in white
        this.remainingGraphics.clear();
        this.remainingGraphics.setStrokeStyle({
          width: 10,
          color: 0xffffff,
          alignment: 0.5,
          alpha: 1,
        });
        this.remainingGraphics.beginPath();
        remaining.forEach((pt, i) => {
          const { x, y } = this.toCanvasPoint(pt);
          if (i === 0) this.remainingGraphics.moveTo(x, y);
          else this.remainingGraphics.lineTo(x, y);
        });
        this.remainingGraphics.stroke();

        // draw or update tram marker
        const current = this.routeLatlngs[passedCount];
        const { x, y } = this.toCanvasPoint(current);

        let direction;
        const now = Date.now();
        if (this.manualTramDirection && this.manualDirectionUntil > now) {
          direction = this.manualTramDirection;
        } else {
          direction = this.chooseTramDirection([x, y]);
          // clear expired override
          if (this.manualDirectionUntil <= now) {
            this.manualTramDirection = null;
          }
        }

        // this.setTramDirectionOverride("bottom-right", 60000);

        const svgMap = {
          right: tramRight,
          left: tramLeft,
          "top-right": tramTopRight,
          "bottom-right": tramBottomRight,
          "top-left": tramTopLeft,
          "bottom-left": tramBottomLeft,
        };
        const projSrc = svgMap[direction];

        // создаём контейнер с красным кружком и стрелкой, если ещё нет
        if (!this.tramContainer) {
          this.tramContainer = new PIXI.Container();

          // 1) базовый кружок красного маркера
          const circle = new PIXI.Graphics();
          circle
            .beginFill("#FCD500")
            .lineStyle(6, "#000")
            .drawCircle(0, 0, 14)
            .endFill();
          this.tramContainer.addChild(circle);

          // 2) стрелка‑проекция
          // Horizontal arrows flush to red circle, diagonal by anchor
          const CIRCLE_D = 28; // diameter of the red circle (2*14)
          const arrow = PIXI.Sprite.from(projSrc);
          if (direction === "right") {
            arrow.width = 112;
            arrow.height = 82;
            arrow.anchor.set(0, 0.5);
            arrow.x = CIRCLE_D / 2 - 12;
            arrow.y = 0;
          } else if (direction === "left") {
            arrow.width = 112;
            arrow.height = 82;
            arrow.anchor.set(1, 0.5);
            arrow.x = -CIRCLE_D / 2 + 12;
            arrow.y = 0;
          } else {
            // diagonal orientations
            arrow.width = 93;
            arrow.height = 97;
            // bottom-right: top-left corner flush to marker
            if (direction === "bottom-right") {
              arrow.anchor.set(0, 0);
              arrow.x = 0;
              arrow.y = 0;
            }
            // top-right: bottom-left corner flush
            else if (direction === "top-right") {
              arrow.anchor.set(0, 1);
              arrow.x = 0;
              arrow.y = 0;
            }
            // bottom-left: top-right corner flush
            else if (direction === "bottom-left") {
              arrow.anchor.set(1, 0);
              arrow.x = 0;
              arrow.y = 0;
            }
            // top-left: bottom-right corner flush
            else if (direction === "top-left") {
              arrow.anchor.set(1, 1);
              arrow.x = 0;
              arrow.y = 0;
            }
            // Removed any diagonal rotation or scale flips
          }
          // keep arrow upright regardless of route rotation
          arrow.rotation = -this.routeGroup.rotation;
          this.tramContainer.addChild(arrow);
          this.tramArrow = arrow;
          this.tramDirection = direction;
          this.routeGroup.addChild(this.tramContainer);
        }
        // update existing arrow sprite on direction change
        if (this.tramArrow && direction !== this.tramDirection) {
          const arrow = this.tramArrow;
          const projSrc = svgMap[direction];
          arrow.texture = PIXI.Texture.from(projSrc);
          // size & anchor & position logic (same as creation)
          const CIRCLE_D = 28;
          if (direction === "right") {
            arrow.width = 112;
            arrow.height = 82;
            arrow.anchor.set(0, 0.5);
            arrow.x = CIRCLE_D / 2 - 12;
            arrow.y = 0;
          } else if (direction === "left") {
            arrow.width = 112;
            arrow.height = 82;
            arrow.anchor.set(1, 0.5);
            arrow.x = -CIRCLE_D / 2 + 12;
            arrow.y = 0;
          } else {
            arrow.width = 93;
            arrow.height = 97;
            if (direction === "bottom-right") {
              arrow.anchor.set(0, 0);
              arrow.x = 0;
              arrow.y = 0;
            } else if (direction === "top-right") {
              arrow.anchor.set(0, 1);
              arrow.x = 0;
              arrow.y = 0;
            } else if (direction === "bottom-left") {
              arrow.anchor.set(1, 0);
              arrow.x = 0;
              arrow.y = 0;
            } else if (direction === "top-left") {
              arrow.anchor.set(1, 1);
              arrow.x = 0;
              arrow.y = 0;
            }
            // Removed any diagonal rotation or scale flips
          }
          // keep arrow upright regardless of route rotation
          arrow.rotation = -this.routeGroup.rotation;
          this.tramDirection = direction;
        }

        // позиционируем контейнер по маркеру
        this.tramContainer.x = x;
        this.tramContainer.y = y;

        // update station marker colors based on tram progress
        this.stationContainer.children.forEach((node) => {
          const latlng = node._stationLatlng;
          if (!latlng) return;
          // find nearest index on route for this station
          let closestIdx = 0,
            minDist = Infinity;
          this.routeLatlngs.forEach((pt, i) => {
            const dx = pt[0] - latlng[0],
              dy = pt[1] - latlng[1];
            const d = dx * dx + dy * dy;
            if (d < minDist) {
              minDist = d;
              closestIdx = i;
            }
          });
          // red if passed, white otherwise
          const color = closestIdx <= passedCount ? 0xed1c24 : 0xffffff;
          const g = node._circleGraphic;
          if (g) {
            g.clear();
            g.fill(color)
              .stroke({ width: 5, color: 0x000000 })
              .circle(0, 0, 11)
              .fill()
              .stroke();
          }
        });
      } catch (err) {
        console.error("Error updating tram position:", err);
      }
    },
    // Render sight icons with counts
    drawSights(sights) {
      if (!Array.isArray(sights) || !sights.length || !this.toCanvasPoint)
        return;
      // clear any previous sight icons
      this.sightsContainer.removeChildren();
      // group sights by approximate location (2 decimal places)
      const grouped = {};
      sights.forEach((s) => {
        const keyLat = s.latitude.toFixed(2);
        const keyLng = s.longitude.toFixed(2);
        const key = `${keyLat},${keyLng}`;
        if (!grouped[key]) grouped[key] = [];
        grouped[key].push(s);
      });
      // for each group, render an icon and, if multiple, a count badge
      Object.values(grouped).forEach((group) => {
        const first = group[0];
        const { x, y } = this.toCanvasPoint([first.latitude, first.longitude]);
        const node = new PIXI.Container();
        node.x = x;
        node.y = y;
        this.sightsContainer.addChild(node);
        // use a static sight icon
        const texture = PIXI.Texture.from(sightIcon);
        const sprite = new PIXI.Sprite(texture);
        sprite.anchor.set(0.5);
        node.addChild(sprite);

        // ── Route collision avoidance: offset from route segments if too close ──
        if (Array.isArray(this.routeLatlngs) && this.routeLatlngs.length > 1) {
          const canvasPts = this.routeLatlngs.map((pt) =>
            this.toCanvasPoint(pt)
          );
          let minDist = Infinity;
          let normalX = 0,
            normalY = 0;
          let bestDx = 0,
            bestDy = 0;
          // for each segment, find perpendicular distance
          for (let i = 0; i < canvasPts.length - 1; i++) {
            const p1 = canvasPts[i];
            const p2 = canvasPts[i + 1];
            const vx = p2.x - p1.x;
            const vy = p2.y - p1.y;
            const denom = vx * vx + vy * vy || 1;
            const t = ((node.x - p1.x) * vx + (node.y - p1.y) * vy) / denom;
            const tClamped = Math.max(0, Math.min(1, t));
            const projX = p1.x + vx * tClamped;
            const projY = p1.y + vy * tClamped;
            const dx = node.x - projX;
            const dy = node.y - projY;
            const dist = Math.hypot(dx, dy);
            if (dist < minDist) {
              minDist = dist;
              bestDx = dx;
              bestDy = dy;
              // perpendicular normal (one of two)
              const nx = -vy;
              const ny = vx;
              const nlen = Math.hypot(nx, ny) || 1;
              normalX = nx / nlen;
              normalY = ny / nlen;
            }
          }
          const threshold = 50; // pixels
          if (minDist < threshold) {
            // ensure normal points toward the sight (dot > 0), else flip
            if (bestDx * normalX + bestDy * normalY < 0) {
              normalX = -normalX;
              normalY = -normalY;
            }
            const shift = threshold - minDist;
            node.x += normalX * shift;
            node.y += normalY * shift;
          }
        }

        const solidWhite = new FillGradient({
          type: "linear",
          colorStops: [
            { offset: 0, color: 0xffffff },
            { offset: 1, color: 0xffffff },
          ],
        });

        if (group.length > 1) {
          const style = new TextStyle({
            fill: solidWhite,
            fontSize: 14,
            fontWeight: "bold",
          });
          const badge = new Text(String(group.length), style);
          // якорь в правом верхнем углу значка
          badge.anchor.set(1, 0);
          // смещаем к правому верхнему краю спрайта
          badge.x = sprite.width / 2 - 4;
          badge.y = -sprite.height / 2 + 4;
          node.addChild(badge);
        }
      });
    },
    // reset the inactivity timeout
    resetInactivityTimer() {
      console.log("[inactivity] scheduling follow in 15s");
      clearTimeout(this.inactivityTimer);
      this.inactivityTimer = setTimeout(() => {
        this.isFollowingTram = true;
        this.startFollowTram();
      }, 15000);
    },

    startFollowTram() {
      console.log("[follow] startFollowTram() triggered");
      if (this.followTramTimer) return;
      this.followTramTimer = setInterval(() => {
        if (this.tramContainer) {
          const local = new PIXI.Point(
            this.tramContainer.x,
            this.tramContainer.y
          );
          const worldCenter = this.viewport.toLocal(local, this.routeGroup);
          this.viewport.animate({
            time: 600,
            position: { x: worldCenter.x, y: worldCenter.y },
            scale: 1,
            easing: "easeInOutSine",
          });
        }
      }, 500);
    },
    stopFollowTram() {
      console.log("[follow] stopFollowTram() triggered");
      if (this.followTramTimer) {
        clearInterval(this.followTramTimer);
        this.followTramTimer = null;
      }
    },

    // start watching for user interactions
    startInactivityWatcher() {
      if (!this.viewport) return;
      // reset on any user drag, wheel, or pinch
      this.viewport.on("drag-start", this.resetInactivityTimer);
      this.viewport.on("pointerdown", this.resetInactivityTimer);
      this.viewport.on("wheel", this.resetInactivityTimer);
      // initialize the timer
      this.resetInactivityTimer();
    },
  },
  beforeUnmount() {
    this.stopRoutePolling();
    if (this.tramPollTimer) {
      clearInterval(this.tramPollTimer);
      this.tramPollTimer = null;
    }
    if (this.inactivityInterval) {
      clearInterval(this.inactivityInterval);
      this.inactivityInterval = null;
    }
    if (this.followTramTimer) {
      clearInterval(this.followTramTimer);
      this.followTramTimer = null;
    }
    ["mousemove", "mousedown", "keydown", "touchstart", "scroll"].forEach(
      (evt) => window.removeEventListener(evt, this.resetInactivity)
    );
  },
};
</script>
